经典老洞,一直没有复现过,趁现在闲着,回味经典。
环境:
xdebug helper(浏览器插件)
phpstorm
php7.2(开启xdebug)
MAMP
复现
由于ThinkPHP是一个很经典的MVC结构,即模型、视图、控制器。这种结构的CMS(如PHPCMS)通俗理解就是可以通过参数来访问不同文件夹下的不同文件,然后实例化对应文件的类并对其进行调用,而在这其中,最重要的就是控制器。通过调用不同的控制器类可以做到不同的功能,而审计该结构相对来说也比较简单,首先是搞清楚如何通过参数调用控制器(即路由),然后是看控制器。
了解了MVC结构之后再来说说ThinkPHP,ThinkPHP5.0.22是通过参数s来实现的,而参数中是通过路由来进行调用相应的类的,所以首先得大致了解它的路由是如何运行的。
路由
首先是查看路由。这个有三种方法,一种是从开头插断点嗯跟代码跟过去(牛一点也可以大概看一看代码然后人脑+PHPstorm的快捷键跟过去),另一种就是通过改参数匹配框架下对应的文件夹,找到对应文件夹后就基本了解了大概运行方式,第三种是看开发文档之类的文件来了解如何进行开发。
在thinkphp中,比较好找跟着代码很块就能找到,在thinkphp/library/think/App.php
下。在617行的routeCheck
函数中对路由进行检查
首先它会检测是否开启路由,开启后读取缓存的路由,如果没读取到缓存的路由信息则会导入配置文件中的路由并将路由储存在$rules
中,然后对路由进行检测,若路由无效则会进入Route::parseUrl
中对其进行切割。之后一路跟进,在route.php的parseUrl中,有这么一个操作:
1 | if (isset(self::$bind['module'])) { |
可以看到,它将index/\think\app/invokefunction
替换成了index|\think\app|invokefunction
,再用self::parseUrlPath
进行了切割之后直接当成了模块、控制器等,最后在1263行封装成为路由。
在图里我们可以看到模块为index
,控制器为\thinkphp\app
,函数为invokefunction
最后在thinkphp/library/think/App.php
下的445行exec函数一路向下进入494行的module
函数,在其中通过获取控制器名和函数名且没有任何过滤,最终执行控制器下的指定函数。
原理
其实已经没什么好说的了
通过thinkphp/library/think/App.php
中的invokeFunction
函数,可以通过反射实例化任意函数。加上上面说的控制路由执行任意函数。只要利用/
将模型、控制器、函数分开,然后再将参数传入即可。
最终payload如下:
1 | GET /public/?s=index/\think\app/invokefunction&function=phpinfo&vars[0]=1 |
防御
最后看看从5.0.22到5.0.23如何进行了防御:
它在设置控制器的位置进行了正则匹配:
- 本文作者: Sn1pEr
- 本文链接: https://sn1per-ssd.github.io/2022/07/26/thinkphp5022RCE复现/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!