[toc] 工作需要,又分析了一遍。之前理解的比较粗略,这次认真的跟了一遍。
漏洞复现
漏洞分析
代码执行
/thinkphp/library/think/Request.php
中 1077行filterValue
方法 可以看到如果我们可以控制$filter
和$value
即可构成一个单参数任意代码执行,而这两个变量都来源于filterValue
的参数,因此我们需要找到一个调用这个方法的地方进而回溯变量的来源。 在此文件的第1026-1029行的
input
方法中,无论$data
是否是数组都调用了这个方法。
1 | public function input($data = [], $name = '', $default = null, $filter = '') |
array_walk_recursive()
函数会对第一个数组参数中的每个元素应用第二个参数的函数。在函数中,数组的键名和键值是参数,且键名是第二个参数,键值是第一个参数。因此我们需要控制的是$data
数组的键值以及$filter
。 对于$filter
来说,需要跟进一下$this->getFilter($filter, $default);
当$filter
为空时,返回$this->filter;
1 | protected function getFilter($filter, $default) |
继续寻找调用input
的方法,同文件634行的param
方法最终返回了一个$this->input($this->param, $name, $default, $filter);
。 $this->param
对应上面的$data
,它等于array_merge($this->param, $this->get(false), $vars, $this->route(false));
,也就是把get参数、当前方法的参数以及路由参数合并到一起,我们是可以控制其值的。因此我们可以控制上面$data
的值。
1 | public function param($name = '', $default = null, $filter = '') |
但是对于$filter
我们需要控制Request类的$this->filter
。
任意方法调用
继续看该文件518行method
方法 $this->{$this->method}($_POST);
中,如果$this->method
可控我们就可以调用该类的任意方法。 首先mehod方法的参数需要为false,不过其默认就是false。然后需要存在$_POST[Config::get('var_method')])
,Config::get
从配置参数中取值, 在配置文件中可以看到其默认值为
_method
然后
$this->method
也等于该值post过来的参数值,因此我们可以POST一个_method=方法名
进行任意方法调用。 虽然进行了大写转换,但是对于php来说,是不影响的。
变量覆盖
通过上面的分析,我们知道我们需要控制Request对象的$this->filter
属性。 看其构造方法,存在一个任意属性赋值操作。 因此我们可以配合上面的任意方法调用去覆盖
$this->filter
的值。 即_method=__construct&filter[]=system
上面我们提到了还需要控制$data数组的键值,也就是$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
这里,跟进$this->get(false)
可以看到当$this->get
为空时,$this->get = $_GET;
,否则直接返回$this->get
。
1 | public function get($name = '', $default = null, $filter = '') |
因此payload中get[]=whoami
其实也用到了上面的变量覆盖,把$this->get
覆盖为值为whoami
的array数组。 我们也可以改下payload,让其从$_GET
中传值也是可以的。
触发流程
上面只是漏洞产生原理的分析,我们还需要了解怎么调用的Request
类的method
方法以及param
方法。 thinkphp/library/think/Route.php
836行中的check方法
此处调用了Request对象的method方法,且没有传入参数值,因此其默认参数值为false,符合漏洞利用条件。 在
/thinkphp/library/think/App.php
617行routeCheck
方法调用了Route::check
方法。 想要进入if条件的话,
$check
需要为true,也就是开启了路由。默认情况下其为true。 而在应用程序启动类
App
的run
方法中116行,调用了routeCheck
,不过需要$dispatch
为空。
1 | // 监听 app_dispatch |
跟进Hook::listen
,默认没有app_dispatch
行为,因此$dispatch
默认为空。 接下来是
Request
对象param
方法的触发流程。 /thinkphp/library/think/App.php
当$dispatch['type']
为method
或controller
时,也就是说路由会路由到类的方法,会调用Request::instance()->param()
。 而
App
类的run
方法中也调用了该exec
方法,$dispatch
参数来源于routeCheck
。 因此我们还要继续跟
self::routeCheck
,只需要关注返回值中type
的值。
1 | public static function routeCheck($request, array $config) |
不断跟进,最终 看一下调用栈
关键其实就是
$route
的值,回溯调用看一下$route
从哪里来的 可以看到是checkRoute
方法的$rules
参数的键值 继续回溯
$rules
的值,在check
方法中可以看到,他从self::$rules[$method]
中取值,这里的method其实就是我们payload中method=get
的值,利用上面Request
对象的__construct
方法覆盖了返回值$this->method
。 ThinkPHP5 中自带的验证码组件captcha注册了一个
get
路由规则,路由到类的方法,满足case条件。这里可以知道method=get
是为了正确获取captcha的路由规则。 最终我们可以构造出poc 5.0.02-5.0.23
1 | http://127.0.0.1/index.php?s=captcha |
5.0.0-5.0.12
1 | http://127.0.0.1/index.php?s=index/index |
为什么有两个poc 5.0.1 App
类没有exec
方法,且switch的每个条件中都没有调用Request
对象的param
方法。 不过跟进module条件中,一步步跟入可以看到间接调用了
Request::instance()->param();
因此我们不用依赖captcha去进入到
method
流程中,默认index.php?s=index/index
会进入module
流程中并且$method = $request->method();
没有了转换为小写字母的函数,且rules
数组键名默认为大写,5.0.23是小写,所以我们payload中method=GET
必须使用大写GET
。 在后续版本中,代码可能也有略微不同,不过我们依然可以构造一个5.0.0-5.0.23通用poc
1 | http://127.0.0.1/index.php?s=captcha |
POC2
还有一个poc2,不过仅限于特定版本5.0.21-5.0.23
1 | http://127.0.0.1/index.php?s=captcha |
回头看Request
对象的param
方法,他也调用了method
,不过参数为true 因此会进入
$this->server
里面也调用了input方法。
$this->server
对应input($data = [], $name = '', $default = null, $filter = '')
的$data
参数,且$name
为REQUEST_METHOD
不为空,会进入if条件中,获取$data[$name]
的赋值给$data
,也就是$this->server[REQUEST_METHOD]
。 此时
$data
不是数组,会进入$this->filterValue($data, $name, $filter);
再结合之前的
__construct
覆盖$filter
属性,同样可以造成RCE。
任意文件包含
1 | http://127.0.0.1/index.php?s=captcha |
在大多数情况下网站都会禁用system等危险函数,而上面的payload都是单参数的,无法调用file_put_contents进行任意文件写。 不过/thinkphp/library/think/Loader.php
中有一个__include_file
函数,且Thinkphp基础文件base.php
一开始就包含了它。
1 | namespace think; |
因此我们可以用call_user_func
直接去调用他。 call_user_func("think\__include_file","/etc/passwd");
然后可以配合文件上传或者日志文件进行包含getshell。
pathinfo与兼容模式
几乎所有的框架(ThinkPHP,Zend Framework,CI,Yii,laravel等)都会使用URL重写或者pathinfo模式,使URL看起来更美观,比如可以隐藏掉入口文件,并且有利于搜索引擎优化。 几种访问模式
1 | 普通模式。如:http://localhost/index.php?m=模块&a=方法 |
当服务器上面不支持pathinfo模式的时候,可以用兼容模式来使用pathinfo格式的url。 thinkphp5默认的var_pathinfo
为s
。 因此我们可以使用
http://localhost/index.php?s=captcha
来访问验证码模块 在某些情况下,如果http://localhost/index.php?s=captcha
访问不到验证码模块时,可能是没有安装captcha,也可能是开发人员把默认的PATHINFO
变量名给改了。此时可以尝试访问http://localhost/index.php/captcha
或者http://localhost/captcha
漏洞修复
设置了$method
白名单为['GET', 'POST', 'DELETE', 'PUT', 'PATCH']
,从而限制了其调用本类中的方法。