phpwind 利用哈希长度扩展攻击进行getshell

一哥新发的漏洞,还是蛮屌的: http://www.wooyun.org/bugs/wooyun-2016-0210850。分析补丁( http://www.phpwind.net/read/3709549 )加上一些风闻,我得知利用的是哈希长度扩展攻击。之前CTF中经常出境的MD5 Length Extension Attack,终于在实战中露了一次面。

今晚基本没睡觉,一直在调试这个漏洞,虽说不知道一哥后续getshell用的是什么方法,但我这个文章基本把该漏洞的原理讲清楚了。至于getshell的话,等漏洞公开了再看看吧。

0x01 漏洞点分析

phpwind逻辑太冗杂了,一看就是java程序员开发的。

补丁文件修补了src/windid/service/base/WindidUtility.php的appKey函数。之前的appKey函数如下:

<?php
public static function appKey($apiId, $time, $secretkey, $get, $post) {
    // 注意这里需要加上__data,因为下面的buildRequest()里加了。
    $array = array('windidkey', 'clientid', 'time', '_json', 'jcallback', 'csrf_token',
                   'Filename', 'Upload', 'token', '__data');
    $str = '';
    ksort($get);
    ksort($post);
    foreach ($get AS $k=>$v) {
        if (in_array($k, $array)) continue;
        $str .=$k.$v;
    }
    foreach ($post AS $k=>$v) {
        if (in_array($k, $array)) continue;
        $str .=$k.$v;
    }
    return md5(md5($apiId.'||'.$secretkey).$time.$str);
}

可见,这里其实是一个『签名』函数,将GET、POST变量使用secrectkey进行签名,但签名的算法比较古老,直接用md5。

我们可以看看道哥在09年发的一篇博客: http://blog.chinaunix.net/uid-27070210-id-3255947.html

其中详尽的说明了哈希长度扩展攻击的原理及利用方法(所以我文章就不细说了),总结起来其实就一句话:

当知道 MD5(secret) 时,在不知道secret的情况下,可以很轻易的推算出 MD5(secret||padding||m')

当前phpwind的签名算法刚好符合上述的格式 md5(md5($apiId.'||'.$secretkey).$time.$str)。我们虽然不知道md5($apiId.'||'.$secretkey),但$time$str是可控的,可以进行哈希长度扩展攻击。

0x02 寻找已知哈希

根据哈希长度扩展攻击的原理,也就是 MD5(secret) --> MD5(secret||padding||m'),我们就需要先找到一个已知的secret。

src/windid/service/user/srv/WindidUserService.php showFlash函数:

<?php
public function showFlash($uid, $appId, $appKey, $getHtml = 1) {
    $time = Pw::getTime();
    $key = WindidUtility::appKey($appId, $time, $appKey, array('uid'=>$uid, 'type'=>'flash', 'm'=>'api', 'a'=>'doAvatar', 'c'=>'avatar'), array('uid'=>'undefined'));
    $key2 = WindidUtility::appKey($appId, $time, $appKey, array('uid'=>$uid, 'type'=>'normal', 'm'=>'api', 'a'=>'doAvatar', 'c'=>'avatar'), array());

    $postUrl = "postAction=ra_postAction&redirectURL=/&requestURL=" . urlencode(Wekit::app('windid')->url->base . "/index.php?m=api&c=avatar&a=doAvatar&uid=" . $uid . '&windidkey=' . $key . '&time=' . $time . '&clientid=' . $appId . '&type=flash') . '&avatar=' . urlencode($this->getAvatar($uid, 'big') . '?r=' . rand(1,99999));
    ...
}

这里调用了WindidUtility::appKey生成key,并将这个key放入了url中。其实这个功能是前台用户头像上传,我们来到 http://10.211.55.3/phpwind/index.php?m=profile&c=avatar&_left=avatar 右键源码即可查看到当前的key:

14640455257307.jpg

这个key,实际上是通过如下计算获得:

<?php
$get = some_sort(array('uid'=>$uid, 'type'=>'flash', 'm'=>'api', 'a'=>'doAvatar', 'c'=>'avatar'));
$post = some_sort(array('uid'=>'undefined'));
md5( md5($apiId.'||'.$secretkey) . time() . $get . $post ) 

也就是说,此时的secret其实是 md5($apiId.'||'.$secretkey) . time() . $get . $post,大概类似md5(...) + 1464044063adoAvatarcavatarmapitypeflashuid2uidundefined
在哈希长度扩展攻击中,我们是不需要知道这个secret的值的,只需要知道它的长度,上面这个字符串的长度很好算,md5固定32位,time时间戳10位,get和post两个数组也是固定的。
所以,经过计算,uid=2的用户获得的secret长度为55位。

0x03 进行扩展攻击

公式: MD5(secret) --> MD5(secret||padding||m'),我们已经知道了MD5(secret)和secret的长度(55),现在可以推MD5(secret||padding||m')了。

因为secret的值为md5(...) + 1464044063adoAvatarcavatarmapitypeflashuid2uidundefined,所以下次增加的padding和m'必须放在1464044063adoAvatarcavatarmapitypeflashuid2uidundefined后面。

我们需要推导并绕过的是如下函数 src/applications/windidserver/api/controller/OpenBaseController.php beforeAction函数:

<?php
public  function beforeAction($handlerAdapter) {
    parent::beforeAction($handlerAdapter);
    $charset = 'utf-8';
    $_windidkey = $this->getInput('windidkey', 'get');
    $_time = (int)$this->getInput('time', 'get');
    $_clientid = (int)$this->getInput('clientid', 'get');
    if (!$_time || !$_clientid) $this->output(WindidError::FAIL);
    $clent = $this->_getAppDs()->getApp($_clientid);
    if (!$clent) $this->output(WindidError::FAIL);
    if (WindidUtility::appKey($clent['id'], $_time, $clent['secretkey'], $this->getRequest()->getGet(null), $this->getRequest()->getPost()) != $_windidkey)  $this->output(WindidError::FAIL);

    $time = Pw::getTime();
    if ($time - $_time > 1200) $this->output(WindidError::TIMEOUT);
    $this->appid = $_clientid;
}

我们看到,这里计算出 WindidUtility::appKey($clent['id'], $_time, $clent['secretkey'], $this->getRequest()->getGet(null), $this->getRequest()->getPost()) 的结果,与传入的windidkey进行比较,如果不相等,则函数报错。

我们需要先构造出1464044063adoAvatarcavatarmapitypeflashuid2uidundefined,才能够在后面增加我们需要的GET和POST值。

所以,我传入time=1464044063,而adoAvatarcavatarmapitypeflashuid2uidundefined作为一个GET数组的键传入。

这样,在appKey函数构造的过程中,就可以人为构造出secret,然后后面的内容就是可以利用的padding,如下图(s是未知的key,t是获取md5时的时间戳,R就是adoAvatarcavatarmapitypeflashuid2uidundefined):

14640478360329.jpg

所以,我们构造出类似如下的数据包:

14640481720300.jpg

此时后端进行的加密如下:

MD5( key + 1464048076 + adoAvatarcavatarmapitypeflashuid2uidundefined + %80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%B8%02%00%00%00%00%00%00 + sort(a=get&c=app&m=api&id=1) )

我之前获取的secret如下:

MD5( key + 1464048076 + adoAvatarcavatarmapitypeflashuid2uidundefined )

正好满足哈希长度扩展攻击的条件,成功绕过beforeAction中的验证。

0x04 获取敏感信息 + 修改管理员密码 + getshell

我绕过了beforeAction的验证,其实效果类似于绕过了discuz中UC_KEY的验证过程。拿到了UC_KEY,就可以构造为任意用户做其权限(甚至是管理员权限)下的很多事情。
比如,获取敏感信息:

14640484775768.jpg

获得所有系统配置信息(包括cookie加密的密钥):

14640486498469.jpg

我写了一个脚本用来生成payload,我就不公开了。看懂的人自然会写,看不懂的人好好琢磨琢磨:

14640495312072.jpg

另外,利用该方法可以修改所有系统配置、增删改APP、增删改查用户、修改用户密码等等。phpwind有个奇怪的逻辑,其管理员分为『创始人』和『管理员』,而创始人如果要登录后台,需要一个保存在文件中的账号密码,而管理员登录后台需要的是数据库中的账号密码。

这个漏洞只能修改数据库中的账号密码,所以无法修改创始人的后台账号,但管理员权限也就够了,配合我之前发的phpwind后台getshell( http://www.wooyun.org/bugs/wooyun-2010-0175815 ),即可完美拿下。

赞赏

喜欢这篇文章,扫码和我成为赞友!

评论

xxlegend 回复

请教:我使用md5 length Extension attack poc的时候校验的md5值不对,这里面有什么限制因素吗?我在chrome上的输出如下:
=== MD5 Length Extension Attack POC ====== by axis ===
[+] secret is :0.7104842912105351[+] length is :18
[+] message want to append is :axis is smart!
[+] Start calculating secret's hash
[+] Calculate secret's md5 hash: E1DD22A37AC6D24F6F1650DE1D5C7125
================================
[+] Start calculating new hash
[+] theory: h(m||p||m1)
[+] that is: md5_compression_function('E1DD22A37AC6D24F6F1650DE1D5C7125', 'secret's length', 'axis is smart! ')
run times: 1h3: -549672650h2: 1411096006h1: 377315664h0: 979963726run times: 1
h3: -549672650
h2: 1411096006
h1: 377315664
h0: 979963726
[+] padding(urlencode format) is: %80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00
[+] guessing new hash is: 4e0f693a50617d16c69d1b5436a93cdf
================================
[+] now verifying the new hash
[+] new message(urlencode format) is: 0.7104842912105351%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00axis%20is%20smart%21%0A
md5 of the new message is: 93342C3CCD05FBD7091C2287863278F8

xxlegend 回复

@xxlegend:手工用python库检测了一下md5值的输出,预测值是准确的,但是调用faultylabs.MD5(x1)的值却是有问题的

phithon 回复

@xxlegend:可能是faultylabs.MD5这库使用方法不对吧。
建议用 https://github.com/bwall/HashPump ,可以直接生成

xxlegend 回复

@phithon:不错,我当时还想python怎么调起js库的呢?

JoyChou 回复

@xxlegend:刺文章中的js在验证md5有问题,建议有py版本的,可读性比js强。

菜鸟 回复

phithon求源码:xiaoqiu19820@163.com

iant 回复

请教:我写了一个测试脚本
<?php
$key = 'safwefdsfsacxww';

//假设a=11,b=22,k=b12a3ff0b11a5ea852840a632f533218
//根据提示我算出了padding=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%98%00%00%00%00%00%00%00

$k = md5($key.$_GET['a'].$_GET['b']);

if($_GET['b'] === '123456' && $k ===$_GET['k']){
print '我成功绕过了</br>';
}else{
print '我没有绕过</br>';
}
?>
我请求:?a=1122%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%98%00%00%00%00%00%00%00&b=123456&k=b12a3ff0b11a5ea852840a632f533218
根据我自己测试,始终不能成功,我哪里写错了吗?还是我压根就理解错误了?

phithon 回复

@iant:第二次计算的md5怎么会和第一次算出来一样呢

iant 回复

@phithon:这里是我忽略了....
第二次的K我算出来是26ea8c899d259a02f1ae4b56d6a9213e
但是还是不对,能帮我瞅瞅是哪错了吗?
这个跟双向MD5加密应该没关系吧?

phithon 回复

@iant:$_GET['a'] = urldecode("1122%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%98%00%00%00%00%00%00%00");
$_GET['b'] = urldecode("123456");
echo md5($key.$_GET['a'].$_GET['b']);
我算出来可不是26ea8c899d259a02f1ae4b56d6a9213e。

iant 回复

@phithon:感谢,明白了!

黑皮 回复

为什么我利用每次返回的都是 0

phithon 回复

@黑皮:返回0说明没做对。

黑皮 回复

@phithon:但是我完全复现的博客上的数据,返回还是0

黑皮 回复

@黑皮:楼上的回复用自己id呀,借用我的id是个什么鬼????

phithon 回复

@黑皮:完全复现博客里的数据包肯定不对。我讲的是方法,数据包每个网站都不一样。

阿呆 回复

文章中secret的长度应该是87
请教:假设POST参数a=get&c=app&m=api&id=1,后台padding我得到是agetcappid1mapi,但是自己扩展后得到的md5和后台生成的不一样?

phithon 回复

@阿呆:嗯,忘记加md5长度了,利用程序里是自动算的。
生成的一样的,不一样怎么绕过验证。。

captcha