
1. 从一道CTF题看PHP代码执行的艺术最近在复盘一些经典的CTF题目发现BUUCTF上的“[极客大挑战 2019]RCE ME”这道题很有意思。它表面上是一个简单的远程代码执行RCE挑战但内核却涉及了PHP环境下对代码执行限制的多种绕过思路。很多刚接触安全测试的朋友一看到RCE可能就想着直接上system或exec但这道题恰恰告诉你路被堵死了该怎么办。它模拟了一个真实环境中开发者已经做了一些基础防护但防护并不彻底的情景。通过解这道题我们不仅能学到几种具体的绕过技巧更能理解PHP代码执行的底层逻辑和常见的“防御-绕过”博弈。无论你是CTF爱好者还是想深入理解Web安全中代码执行漏洞的开发者这道题都是一个绝佳的切入点。题目通常只给你一个输入框暗示你需要通过它来执行命令。但当你输入system(‘ls’);之类的payload时往往会发现毫无反应。这就是题目的第一道门槛它禁用了绝大多数常见的命令执行函数。这时你需要从“代码执行”而不仅仅是“命令执行”的角度去思考。PHP提供了丰富的函数和语言结构用来执行一段字符串形式的PHP代码比如eval()、assert()、create_function()或者利用preg_replace()的/e修饰符已废弃等。题目往往在禁用这些函数上做文章我们的任务就是找到那个未被禁用的、或者可以通过特殊方式激活的“后门”。接下来我们就深入这道题用两种截然不同的方法拆解它。第一种方法偏向于“正面突破”利用PHP内置的、可能未被完全过滤的函数组合拳第二种方法则更“迂回”利用PHP序列化、魔术方法等特性从另一个维度触发代码执行。这两种方法代表了两种不同的安全攻防思维模式。2. 环境侦察与核心限制分析在开始真正的攻击之前我们必须像侦探一样先摸清“现场”的环境。盲目地发送payload是低效的。对于这道题我们的第一步就是进行代码审计和情报收集。虽然题目通常不会直接给出源码但我们可以通过一些技巧来获取关键信息。最直接的方式就是尝试让目标“自我介绍”。在PHP中phpinfo()函数能泄露大量服务器配置信息包括PHP版本、加载的扩展、以及最重要的——禁用函数列表disable_functions。我们可以构造一个简单的payload来尝试执行它phpinfo();。但题目通常会对这类敏感函数调用进行拦截。一个常见的绕过方式是使用字符串拼接、编码或利用其他函数来动态构造函数名。例如使用.连接符$a‘php’;$b‘info’;$a.$b();。或者利用$_GET参数本身来传递函数名和参数再通过可变函数variable function来调用。假设我们通过某种方式成功获取了phpinfo的输出或者通过报错信息、题目描述推断出了环境。我们会发现关键点systemshell_execexecpassthruproc_openpopen等直接调用系统命令的函数很可能被列入了disable_functions。同时直接执行代码的eval()和assert()也可能被过滤或禁用。这就是题目的核心防御。但这并不意味着无路可走。PHP的世界很广阔。我们需要寻找那些不在禁用列表里却又能够间接达成代码执行或命令执行效果的函数。例如文件操作类file_put_contents()可以写文件如果我们可以写入一个Webshell就能获得执行权限。代码执行类除了eval还有create_function()注意它在PHP 7.2.0后已废弃但在老环境或特定题目中仍可用或者通过include/require包含一个我们可控内容的文件需要配合文件写入。反射与回调类call_user_func()array_map()等函数可以调用回调函数如果我们能控制回调函数和其参数就可能构成代码执行。特殊扩展如果安装了Imagick、GD等图形处理扩展它们的某些方法也可能存在风险但这道题更可能考察通用性。我们的侦察结论是这是一个典型的“黑名单”过滤场景。管理员认为禁用了危险函数就安全了但黑名单永远无法穷尽所有可能性。我们的攻击思路就是找到那个被遗漏的“白名单”函数链。3. 方法一利用PCNTL扩展与pcntl_exec的奇袭第一种方法也是相对直接的一种它利用了PHP的一个不太常用的扩展——PCNTLProcess Control。这个扩展提供了进程控制功能通常用于Unix风格的fork系统调用。而其中的pcntl_exec()函数正是执行指定程序的关键。为什么pcntl_exec可能成为漏网之鱼在常见的WAFWeb应用防火墙或管理员配置的disable_functions列表中system、exec、shell_exec这些是头号通缉犯。但pcntl_exec由于其使用场景相对专业多用于命令行脚本、守护进程在Web环境下的普及度不如前者因此容易被防御规则忽略。这就给了我们可乘之机。但请注意使用这个函数的前提是服务器安装了并启用了PCNTL扩展。在Linux环境下这个扩展比较常见但在Windows或某些精简的Docker镜像中可能没有。pcntl_exec的工作方式这个函数类似于C语言中的exec系列函数。它用指定的程序文件替换当前进程的映像。这意味着调用pcntl_exec后当前的PHP脚本进程将结束取而代之的是我们指定的新程序。它的基本语法是pcntl_exec(string $path, array $args [], array $envs [])$path要执行的程序的绝对路径。这是最关键的一点你不能只给一个命令名如ls而必须给出ls这个二进制文件在系统中的完整路径例如/bin/ls。$args一个数组表示传递给程序的参数。数组的第一个元素通常是程序名本身虽然不强制但更常见的做法是$args[0]设为程序名$args[1]开始是真正的参数。$envs一个关联数组用于设置新进程的环境变量。构造攻击Payload假设我们已经通过其他方式比如scandir(‘.’)知道了当前目录下有一个名为flag的文件。我们的目标是读取它。使用pcntl_exec我们不能直接执行cat flag而需要执行/bin/cat这个程序并将flag作为参数传递。因此我们的PHP代码需要这样写pcntl_exec(‘/bin/cat’, array(‘flag’));或者更完整地将程序名也放入参数数组虽然pcntl_exec的$path参数已经指定了程序pcntl_exec(‘/bin/cat’, array(‘cat’, ‘flag’));但是题目给我们的通常只是一个输入点我们需要将这段代码“注入”到服务器端执行。如果输入点直接是eval($_POST[‘cmd’])这种形式那很简单。但题目往往有过滤。我们需要考虑如何绕过对pcntl_exec函数名的直接书写过滤。这里就可以用到PHP的可变函数和字符串操作。字符串拼接$a‘pcntl’ $b‘_exec’ $f$a.$b $f(‘/bin/cat’, array(‘flag’));利用$_GET或$_POST传参假设我们的输入点是?cmdxxx我们可以传递?cmdpcntl_exec(‘/bin/cat’,array(‘flag’))但函数名可能被过滤。我们可以尝试传递函数名部分?funcpcntl_execarg/bin/cat然后在后端构造$_GET[‘func’]($_GET[‘arg’])但这需要后端代码支持这种危险的动态调用在CTF题中有时会这样设计。十六进制或ASCII码转换使用chr()函数拼接出函数名。例如pcntl_exec的ASCII码可以拼接出来但这通常用于绕过简单的字符串匹配对于复杂的WAF可能效果有限。实战中的注意事项与局限路径问题你必须知道目标系统上命令的绝对路径。Linux下常见命令路径如/bin/cat/bin/ls/usr/bin/whoami/bin/sh等。如果不知道可能需要先进行信息搜集比如用phpinfo()查看$_SERVER[‘PATH’]环境变量或者尝试常见的路径。无回显问题pcntl_exec执行成功后原PHP进程就结束了不会有任何输出返回到当前的HTTP响应中。这意味着即使你执行了cat flag你也看不到结果。为了解决这个问题我们通常需要将输出重定向到一个Web目录下可访问的文件中然后通过浏览器访问该文件来读取结果。例如pcntl_exec(‘/bin/sh’, array(‘sh’, ‘-c’, ‘cat flag /var/www/html/result.txt’));执行后再访问http://target.com/result.txt就能看到flag。这需要你对Web目录有写权限并且知道绝对路径。扩展依赖如果服务器没有安装PCNTL扩展这个方法会直接失败提示未定义函数。因此它并非通用解法但在特定环境下是一把犀利的“手术刀”。这个方法教会我们在代码审计和渗透测试中要时刻关注那些“非主流”但功能强大的函数和扩展它们往往是绕过常规防御的关键。4. 方法二PHP反序列化漏洞的链式利用第二种方法与第一种截然不同它不直接寻找命令执行函数而是利用PHP的反序列化漏洞。这是一种更高级、也更常见的攻击手法在真实世界的漏洞如ThinkPHP Laravel反序列化RCE中屡见不鲜。题目“[极客大挑战 2019]RCE ME”很可能就包含了这样一个反序列化的入口。什么是PHP反序列化漏洞简单来说就是当程序使用unserialize()函数对一个用户可控的、恶意的序列化字符串进行还原时如果该字符串精心构造就可以在反序列化过程中自动调用对象的一些“魔术方法”如__wakeup()__destruct()__toString()等并最终导致任意代码执行。攻击链的起点寻找unserialize首先我们需要在题目中找到哪里使用了unserialize()。它可能出现在Cookie、POST数据、GET参数或者存储在Session、数据库的某个字段中。题目可能会给你一个看起来像base64编码的字符串解码后发现是序列化格式。例如一个Cookie可能叫user其值是一串O:4:“User”:2:{s:4:“name”;s:5:“admin”;s:5:“token”;s:32:“xxx”;}这样的字符串。一旦发现这样的点就可以尝试注入我们构造的序列化数据。构造POP链Property-Oriented Programming单纯的unserialize一个对象不会直接导致代码执行除非这个对象的类定义中存在危险的魔术方法。攻击者需要找到一条从反序列化入口到危险函数如evalsystem 或者能触发文件包含、写文件的方法的调用链这就是POP链。假设题目源码或通过其他方式推断出的源码中有如下类class Example { public $cache_file; function __destruct() { unlink($this-cache_file); } }这个类的__destruct方法对象销毁时自动调用会尝试删除$cache_file属性指定的文件。这本身不是代码执行但如果我们能控制$cache_file就可以让它删除一个关键文件比如index.php造成破坏。但这还不是我们想要的RCE。我们需要寻找更强大的链。一个经典的简单链条可能涉及两个类class FileHandler { public $filename; function __toString() { return file_get_contents($this-filename); } } class Main { public $obj; function __wakeup() { echo $this-obj; // 这里会触发$obj对象的__toString方法 } }攻击思路我们构造一个Main对象$m。将$m-obj设置为一个FileHandler对象$f。将$f-filename设置为php://filter/convert.base64-encode/resourceflag.php这是一个PHP流包装器可以读取文件并base64编码输出常用于读取源码。序列化$m得到字符串。当这个字符串被unserialize()时会创建Main对象并自动调用其__wakeup()方法。__wakeup()中执行echo $this-obj因为$obj是一个FileHandler对象PHP会尝试将其转换为字符串从而自动调用FileHandler的__toString()方法。__toString()方法执行file_get_contents($this-filename)读取了flag.php文件的内容并以base64编码形式返回避免了直接执行。这样我们通过控制对象的属性引导程序在执行过程中自动调用一系列方法最终达到了读取文件的目的。如果__toString方法里是include($this-filename)那就可以直接造成代码执行。结合本题的实战利用对于本题我们需要猜测或发现题目中存在的类。一种常见的方法是结合PHP内置类。PHP有些内置类在反序列化时很有用比如SplFileObject可以用于读取文件Error/Exception类的__toString会包含调用栈信息有时会泄露路径。更直接的方法是如果题目允许我们上传或写入文件我们可以先写入一个包含恶意类的PHP文件然后利用反序列化触发__autoload或spl_autoload去包含这个文件从而将我们的类定义加载进来然后再触发我们自定义类中的危险方法。这就是所谓的“phar://反序列化”攻击的一种变体思路。具体到本题的绕过题目禁用了很多函数但反序列化漏洞的终点不一定非要是system。我们可以让终点是一个能写文件的函数比如file_put_contents。写一个一句话木马Webshell到Web目录然后访问这个Webshell就可以执行任意命令了。假设我们构造的POP链最终能调用到如下代码file_put_contents(‘/var/www/html/shell.php’, ‘?php eval($_POST[“cmd”]);?’);那么通过触发反序列化我们就在服务器上生成了一个Webshell。之后用蚁剑、中国菜刀等工具连接http://target.com/shell.php密码为cmd就可以在图形化界面下执行命令了。即使system等被禁用我们还可以在Webshell里尝试其他PHP代码执行的方式或者利用这个shell进一步探索服务器寻找flag。注意在实际CTF比赛中通常不允许也不会提供这种直接写Webshell然后连接的工具环境更多的是要求你通过漏洞直接输出flag内容。但理解这个完整的攻击链对于理解真实世界的反序列化RCE至关重要。这种方法的技术门槛更高需要对目标代码结构有一定了解并且要精心构造利用链。但它也更能体现攻击者的技术水平并且是一种极其通用的漏洞利用方式。5. 两种方法的对比与深层防御思考通过以上两种方法的详细拆解我们可以清晰地看到它们代表了两种不同的攻击维度。方法一PCNTL属于正面利用“未禁用函数”。它的思路直接寻找黑名单的遗漏点。这种方法成功的关键在于攻击者对PHP函数库的熟悉程度和想象力。它不依赖于特定的应用程序代码只依赖于PHP环境本身的配置。防御这种攻击的方法相对简单但需彻底在php.ini中使用disable_functions指令时要尽可能全面不仅要禁用system、exec还要考虑pcntl_exec、shell_exec、passthru、proc_open、popen、dl加载扩展等所有能执行外部命令或代码的函数。甚至可以考虑将PHP放入一个禁止执行任何外部命令的“监狱”环境如Docker容器配合严格的Seccomp策略。方法二反序列化属于利用应用程序逻辑缺陷。它的突破口是开发者不安全地使用了unserialize()函数并且程序中存在可以被串联起来的类和方法POP链。防御这种攻击需要从开发层面入手根本解决永远不要对用户输入的数据使用unserialize()。如果必须序列化存储数据请使用json_encode()/json_decode()等更安全的格式。白名单验证如果确实需要使用unserialize()应确保反序列化的数据来源可信如来自服务器自身加密存储的数据并且在进行反序列化前对数据进行严格的完整性校验如数字签名。魔术方法审查在代码审计时仔细检查所有类的__wakeup、__destruct、__toString、__call等魔术方法确保其中没有危险操作或者危险操作的参数完全不可被外部控制。使用安全的反序列化库对于复杂对象可以考虑使用专门设计、经过安全审计的序列化库。从这道题目中我们可以提炼出更普适的代码审计和漏洞挖掘思路1. 信息收集是第一步永远不要假设。用phpinfo()、报错信息、扫描目录scandir()、读取源码highlight_file()、php://filter等方式尽可能多地了解服务器环境、目录结构、配置文件。2. 思维要发散当直接执行命令被阻思考代码执行当代码执行被阻思考如何写文件当写文件被阻思考如何利用已有文件如日志文件包含或如何触发反序列化、SSRF等二次漏洞。3. 熟悉PHP的“特性”PHP有很多为了便捷而设计但安全性欠佳的“特性”比如可变函数$func()、preg_replace的/e修饰符已移除、assert()用于代码执行、create_function()创建匿名函数、以及各种流包装器php://inputphp://filterdata://等。攻击者利用它们防御者则需要了解并关闭或避免使用它们。4. 黑名单的局限性这道题完美诠释了黑名单的失败。安全设计应遵循“最小权限原则”和“默认拒绝原则”。对于执行外部命令这种高危操作如果不是绝对必要就应该完全禁止如果必要则应该通过严格的白名单只允许执行特定的、经过校验的命令来控制而不是试图列一个永远不全的黑名单。6. 从CTF到实战代码执行漏洞的防御纵深解CTF题的目的是为了将知识应用于实战提升真实系统的安全性。针对PHP代码执行漏洞我们可以构建一个多层次的防御纵深体系。第一层安全开发规范这是最根本的一层。在编码阶段就杜绝漏洞。输入验证与过滤对所有用户输入进行严格的类型、长度、格式检查。使用白名单而非黑名单。避免危险函数在项目规范中明确禁止使用eval()assert()用于代码执行system()exec()shell_exec()passthru()popen()proc_open()pcntl_exec()create_function()等函数。如果必须使用必须经过严格的参数过滤和上下文审查。安全使用反序列化如前所述避免unserialize()用户数据。使用JSON等安全格式。禁用危险特性在php.ini中设置allow_url_fopen Off和allow_url_include Off防止远程文件包含RFI漏洞。第二层服务器与环境加固即使代码有瑕疵坚固的环境也能增加攻击难度。配置disable_functions在php.ini中用逗号分隔禁用所有不必要的函数。一个相对全面的列表可以参考systemexecshell_execpassthruproc_openpopenpcntl_execevalassertcreate_functiondlmailini_setputenvsymlinklink等。具体需要根据业务需求调整。配置open_basedir将PHP可访问的文件限制在网站根目录及其子目录下防止跨目录访问敏感文件如/etc/passwd。非Root权限运行PHP-FPM或Apache的PHP模块应以独立的、低权限用户如www-datanobody运行避免一旦被攻破就获得root权限。定期更新及时更新PHP版本和所有扩展修复已知的安全漏洞。第三层WAF与运行时防护这一层用于检测和阻断已经发生的攻击行为。部署WAFWeb应用防火墙可以基于规则库拦截常见的攻击payload如包含eval(system(base64_decode(等特征的请求。RASP运行时应用自我保护这是一种更高级的防护技术它在应用程序运行时内部进行检测。例如可以监控eval()、system()等危险函数的调用如果调用栈不符合预期的业务逻辑例如从用户输入直接调用了eval()则立即中断执行并告警。RASP能有效防御未知的、绕过WAF的攻击手法。第四层监控与应急响应没有绝对的安全假设防线被突破需要能快速发现和响应。日志审计详细记录PHP错误日志、Web访问日志。监控日志中是否出现大量包含特殊字符如${()|;的请求这可能是攻击尝试。文件完整性监控监控Web目录下文件的变化特别是新增的.php、.jsp、.asp等可执行文件这很可能是攻击者上传的Webshell。入侵检测系统在服务器层面部署HIDS监控异常进程、异常网络连接。回到我们这道题它主要考察的是绕过第一层防御黑名单禁用函数和第二层防御disable_functions的能力。在实际工作中作为开发者或安全工程师我们需要做的是把这四层防御都尽可能做好让攻击者从“简单题”变成“无从下手”的难题。通过不断研究这些绕过技巧我们才能更好地理解攻击者的思维从而设计出更有效的防御措施。安全是一个动态对抗的过程而CTF正是这个过程中最好的练兵场。