PHP eval 型 RCE:flag 正则过滤与通配符绕过实操 | 01

目录

[从 eval 入口到命令函数差异:一次 PHP RCE 实验环境的技术复盘](#从 eval 入口到命令函数差异:一次 PHP RCE 实验环境的技术复盘)

[1. 从概念与实验环境开始](#1. 从概念与实验环境开始)

[2. 用 VS Code 连接实验服务器](#2. 用 VS Code 连接实验服务器)

[3. 第一条链路:eval 接收输入,再调用 system](#3. 第一条链路:eval 接收输入,再调用 system)

[4. 过滤出现后:先理解匹配,再验证通配符](#4. 过滤出现后:先理解匹配,再验证通配符)

[5. system 之外:从函数文档进入输出差异](#5. system 之外:从函数文档进入输出差异)

[6. 调试停住时,如何向 AI 提问并校正输出位置](#6. 调试停住时,如何向 AI 提问并校正输出位置)

[7. 继续核验 AI 方案:路径、反引号与命令替换](#7. 继续核验 AI 方案:路径、反引号与命令替换)

[8. 第三个函数:proc_open 与管道读写](#8. 第三个函数:proc_open 与管道读写)

[9. 第四个函数:passthru 的直接回显](#9. 第四个函数:passthru 的直接回显)

[10. 第五个函数:exec 的数组输出](#10. 第五个函数:exec 的数组输出)

[11. 收束:escapeshellcmd 是过滤,而不是命令执行](#11. 收束:escapeshellcmd 是过滤,而不是命令执行)

[12. 本次复盘的顺序性结论](#12. 本次复盘的顺序性结论)


eval 入口到命令函数差异:一次 PHP RCE 实验环境的技术复盘

本文基于一次仅用于授权教学环境的 PHP RCE 演示整理而成。文中的目标文件、地址和代码均属于隔离靶场,不应直接迁移到生产系统或未获授权的目标上。

1. 从概念与实验环境开始

RCE 是 Remote Command Execution 的缩写,中文通常称为远程命令执行。从目标服务器的视角看,访问者经由 Web 应用把命令交给远程主机执行,因此"远程"描述的是访问关系,"命令执行"描述的是最终能力。

本次演示选择 Linux 作为实验系统。这不是说其他系统不存在类似问题,而是教学与渗透测试场景中的服务器常见 Linux,后续命令、目录和函数验证也都围绕该环境展开。实验前需要准备 PHP 与 Nginx;课程不再展开完整安装过程。Nginx 可以通过源码编译部署,也可使用系统包管理器安装。示例使用 PHP 7.3,并提示需要为 PHP-FPM 调整监听端口,避免与已有服务冲突。

这段准备过程本身也展示了一个真实的排错习惯:配置文件位置一时记不清并不罕见,先回到服务目录、查看配置,再结合实际进程与访问结果确认即可。课程中的几张终端截图依次记录了搜索、进入目录、重载服务和观察进程的过程。单看其中任意一张都不足以证明环境完成,串起来才构成"配置已改动、服务已重载、进程在运行、页面可访问"的闭环。

图 1:终端检索 PHP-FPM 相关配置时出现参数错误,记录了部署阶段需要回到实际配置路径继续确认的过程。

图 2:访问 phpinfo() 页面,作为 PHP 与 Web 服务联通性的第一项验证。

图 3:检查 Nginx 目录结构,确认 Web 根目录的位置。

图 4:请求页面失败后回到目录检查,体现部署验证需要把浏览器结果与服务端文件位置一起排查。

图 5:定位 Nginx 配置文件,为后续确认服务配置做准备。

图 6:调整服务配置后重载 Nginx,说明配置变更必须落实到运行中的服务。

图 7:通过进程列表验证 Nginx 已运行,避免仅凭配置文件存在就判断环境已就绪。

图 8:环境启动后再次访问服务,作为连通性验证的一环。

图 9:实验入口代码的整体视图,后续所有请求均围绕该页面展开。

2. 用 VS Code 连接实验服务器

接下来进入实际操作。课程没有要求直接在服务器终端用 Vim 编辑,因为对不熟悉 Vim 的学习者来说,频繁编辑会增加无关的操作成本。更合适的做法是使用自己熟悉的编辑器,并通过 VS Code 的 Remote SSH 扩展连接远程实验机。

讲师的经验判断是:不是不能在终端里做,而是不熟悉 Vim 时会明显影响操作流畅度;优先选择熟悉的编辑器,更有利于把注意力放在漏洞验证本身。

图 10:在 VS Code 中安装并打开 Remote SSH 扩展后的远程资源入口。

在连接命令框中输入 SSH 连接信息,例如课程环境中的 ssh root@192.168.68.187 -A;实际环境应使用自己的授权主机与账户。提交后,编辑器会要求输入密码。

连接动作的价值在于把远端目录直接纳入本地熟悉的编辑流程:文件树、代码编辑、保存与终端反馈都在同一个窗口中完成。课程由此把注意力从"如何记住编辑器命令"转回"PHP 页面究竟接收了什么输入、输出在哪里消失"。

图 11:填写远程 SSH 连接命令,演示目标是课程中的内网实验主机。

图 12:SSH 连接建立过程,等待远程窗口完成初始化。

连接成功后,选择 Nginx 的 Web 根目录。若未改动默认路径,课程按 /usr/local/nginx/html 进行操作。打开目录时还会出现工作区信任提示;当前场景是自有实验服务器,因此选择信任。

图 13:定位并打开远程服务器上的 html 目录。

图 14:远程工作区连接完成,后续直接在该目录创建和修改 PHP 文件。

此时创建第一个 PHP 文件,例如 1.php。这里的任务不是直接堆叠大量技巧,而是先回到 RCE 的定义:PHP 代码若把外部输入交给能够执行系统命令的函数,就可能形成由 Web 请求到系统命令的执行链。

3. 第一条链路:eval 接收输入,再调用 system

示例代码把查询参数 c 交给 eval,同时通过正则表达式尝试拦截某个敏感词。eval 的作用是把字符串按 PHP 代码解释执行,因此它本身就是本例最重要的入口。演示的目标是读取靶场当前目录内名为 flag 的测试文件,因此先在 Web 根目录创建该文件并写入测试内容。

图 15:演示页面的核心结构:参数 c 进入判断逻辑,并可能被 eval 解释。

图 16:创建用于验证读取效果的测试文件;它仅是本地靶场中的教学占位文件。

图 17:确认当前命令执行位置与测试文件所在目录一致。

图 18:目录清单验证测试文件与 PHP 页面均在当前实验目录。

图 19:对比多种文本查看命令;课程认为读取单个短文件时 cat 最直观。

参数 c 最终进入 eval,所以思路是让它解析一段调用 PHP 系统函数的表达式,再由该函数执行 Linux 命令。课程首先使用 system,并通过 cat 读取测试文件。这里的关键不是记住某个固定字符串,而是理解两层执行关系:eval 解释 PHP 代码,PHP 的命令函数再调用系统命令。

在最初的命令行验证中,课程依次看过 catmoreless 一类读取方式。它们都能用于查看内容,但这个测试文件很短,cat 的结果最直接,适合观察是否已成功读到测试字符串。这个选择只是为了让浏览器侧的回显更容易判定,并不是对所有文本查看任务的通用优劣结论。

【小白通俗注解】可以把第一层理解成"页面先读懂你交来的 PHP 语句",第二层理解成"这条 PHP 语句再让服务器终端去做事"。两层都可控时,风险才会连起来。

图 20:将系统命令封装进 PHP system 调用,并准备交给 eval 的写法。

图 21:通过查询参数把待解释的 PHP 表达式提交给实验页面。

图 22:请求成功后页面输出靶场测试文件的内容,证明命令执行与回显链路已建立。

图 23:回到源码可见,页面仍有针对敏感词的正则检查。

4. 过滤出现后:先理解匹配,再验证通配符

刚才的成功并不意味着过滤不存在。课程说明,前一次能读到内容是因为演示代码临时多写了一行;恢复正常逻辑后,正则会对完整敏感词进行限制。因此问题变成:能否在不完整出现被过滤词的前提下,让 shell 在文件名匹配时仍定位到目标?

课程从 Linux 的通配符开始验证。问号 ? 可以匹配一个任意字符;如果当前目录只有那个四字符文件名,多个问号可能仍能匹配到它。星号 * 则匹配长度可变的一段字符。需要注意的是,通配符匹配高度依赖当前目录内容;目录中候选文件变多时,结果未必唯一。

课程特意把"当前目录"作为前提反复强调:四个问号之所以在演示里可行,是因为当前目录中恰好只有对应长度的候选文件。换到别的目录,或者目录里出现多个同长度文件,单靠这一写法就不再具有同样确定性。后面涉及目录列出和筛选的调试,也正是在验证这个前提有没有被忽略。

图 24:在目标目录中使用问号替代完整文件名的一部分,验证 shell 会完成文件名匹配。

图 25:以星号替代不确定的字符片段,展示另一种通配匹配形式。

图 26:将通配匹配思路带回 Web 请求参数进行验证。

图 27:课程引用的资料页列出通配、复制文件和重定向等备选思路。

图 28:过滤检查针对的是输入文本,而实际文件定位发生在后续 shell 解释阶段。

图 29:补充示例强调此处绕过的依据是匹配规则,而非简单改变字母大小写。

课程还特别指出,把敏感词改为大写在这个例子中无效,因为正则带有忽略大小写标志。相比先复制文件再读、或把内容重定向到新文件再读,通配符方案步骤更短,更贴近这里"绕过文本匹配"的核心。

资料中还出现了把目标文件复制为其他名称后再读取、或先重定向到新的文本文件再读取的做法。课程没有否认这些思路能够描述一种处理路径,但指出它们都增加了额外动作:先产生新文件,再去读取新文件。对当前只有一个简单词语限制的演示而言,它们没有比通配符带来更直接的验证价值。

讲师的评价是:这类只过滤一个明确词语的规则非常基础,问号或星号即可完成验证。优先理解最短的直接路径,没必要把简单问题人为拆成更多命令。

这一段也明确了一个学习前提:命令执行题依赖函数,也依赖 Linux 基础。知道 PHP 函数能调起命令只是开始,能够识别当前目录、文件名匹配和命令输出,才会让验证过程真正闭环。

5. system 之外:从函数文档进入输出差异

确认 system 能工作后,课程没有停在单一函数上,而是转向"还有哪些函数可以执行命令"。这一步的目的不是背诵清单,而是观察各函数对返回值、回显和参数的不同要求。

图 30:查看 system 文档,确认它的参数与输出行为。

第一个对比对象是 shell_exec。文档将它描述为通过 shell 执行命令并返回完整输出。课程用一个简单目录列出命令作为测试,表面上看它同样可以执行;但直接把调用交给页面后,页面没有自动展示结果。

图 31:shell_exec 的官方函数说明与示例入口。

图 32:文档示例显示,函数结果需要先接收,再由输出语句展示。

图 33:突出"赋值给变量并 echo"这一回显步骤。

【小白通俗注解】system 更像是"边执行边直接把结果说出来";shell_exec 更像是"把结果交到你手里",你还要主动把它打印到页面上。

图 34:直接调用 shell_exec 后页面不显示结果,暴露出缺少输出语句的问题。

6. 调试停住时,如何向 AI 提问并校正输出位置

页面无回显后,课程刻意没有马上给出答案,而是示范如何向 AI 描述问题。提问时需要写清楚前提:这是一个 CTF 或隔离实验题、正在使用 PHP 的 shell_exec 执行命令、页面没有输出;再请求解释函数用法与提供多个清晰例子。这样 AI 才有足够上下文定位问题,而不是只对一个孤立函数名泛泛而谈。

讲师的观点是:很多"看不懂 AI 回答"的情况,根源可能是问题没有问完整。带上目标、当前代码、现象和希望得到的解释,答案通常会比只翻一段文档更具可读性。

课程还将 AI 与官方文档的关系说得很明确:官方文档仍是核对函数签名和示例的重要来源;AI 的优势是可以基于当前代码补出解释、给多个对照例子,并针对"为什么没有回显"这类现象重新组织答案。调试时应先用实际执行结果检验建议,而不是因为答案看似完整就跳过验证。

图 35:调试时先提供完整的最小代码与现象,而不是只抛出一个函数名。

图 36:问题中明确场景、目标和"页面未输出"的现象。

AI 的分析指出,输出位置写错了:结果必须在函数调用之后被输出。课程承认此前把赋值或输出故意放在不正确的位置,正是为了展示这个常见错误。真正要展示的是函数返回的结果,而不是误把表达式结构写进输出语句内部。

图 37:正确做法是在 shell_exec 执行后,对其返回值进行 echo

图 38:AI 同时给出替代函数,说明函数选择会影响回显处理方式。

图 39:AI 将过滤特点与问号、星号等匹配思路关联起来。

图 40:额外方案包括命令拼接等,课程将其作为可核验的参考而非直接照搬的答案。

课程随后把输出语句移到正确位置并重新测试,页面即出现结果。

图 41:调整输出位置后,shell_exec 的结果成功显示在页面上。

7. 继续核验 AI 方案:路径、反引号与命令替换

AI 的后续方案尝试先列出文件名,再用筛选命令找到目标,最后把结果交给读取命令。课程没有直接认定它正确,而是逐步实测。首先发现示例使用根目录,而测试文件实际在当前目录,因此原命令的路径前提不成立;去掉错误路径或改为当前目录后继续验证。

图 42:参考方案先获取文件名,再筛选出匹配项。

图 43:将示例中的根目录假设改为当前目录,避免路径与实验环境不一致。

接着,课程把注意力放到反引号。反引号在 Linux shell 中可以先执行内部命令,再把其输出替换到外层命令的位置。此处不能只看字符长得像不像,更要分清哪一段由反引号执行,哪一段由 PHP 函数调用的 shell 执行。

具体到该例,内层命令先列出当前目录内容,再通过筛选得到目标名称;外层读取命令再把这个名称作为参数。课程把两种执行来源拆开说明:反引号承担内部命令替换,shell_exec 承担包裹整段字符串的 shell 调用。若把反引号放在错误范围内,或保留与实验目录不符的路径,页面就不会得到预期结果。

图 44:独立验证反引号的命令替换行为。

图 45:修正反引号包裹范围,使内部筛选命令与外部读取命令职责分开。

【小白通俗注解】这里像是先让"内层小命令"找出文件名,再把找到的名字填到"外层读取命令"的空位中。两层命令先后执行,不能把它们混作一个步骤理解。

图 46:修正后页面有回显,表明内层命令结果已被外层读取命令使用。

图 47:继续测试不同组织方式,确认哪些写法能在当前 PHP 表达式中成立。

图 48:课程回到最短路径,强调在本例中通配符比复杂拼接更直接。

讲师最终不推荐把这个简单案例写成多层命令替换。若 system 可用,优先使用它;若受限,再考虑其他函数与更复杂的组合。复杂度上升并不等于方案更优。

8. 第三个函数:proc_open 与管道读写

接下来课程查看 proc_open。文档中它的参数明显多于前两个函数:除了要执行的命令,还需要描述符规范、管道数组、工作目录和环境变量等。因此它提供了更强的进程控制能力,但理解和调用成本也更高。

图 49:proc_open 的函数签名,参数数量显示其用途比简单命令函数更复杂。

图 50:文档解释命令、描述符数组、管道、工作目录与环境变量等参数。

图 51:进一步展示描述符规范对父子进程输入输出关系的定义。

Linux 中常用 012 分别表示标准输入、标准输出和错误输出。proc_open 通过描述符数组把这些通道连接到管道或文件。课程的示例中,0 被配置为可写入子进程的管道,1 用于读取子进程输出,2 则可把错误输出写入指定文件。

示例先建立描述符数组,再调用 proc_open 获得进程资源;其中工作目录被设置到测试目录,环境变量参数也被放入调用中。课程说明,工作目录会影响命令相对路径的解析,而环境变量在该基础案例中可以暂时不作为理解重点。重要的是不要把"函数已调用"误认为"命令一定成功":后续的资源检查、标准输出读取和错误输出读取,分别承担了验证启动状态、观察正常结果和定位异常的职责。

图 52:示例先定义标准输入、输出和错误输出的处理方式,再创建进程。

图 53:示例中的工作目录与环境变量参数;课程认为环境变量在本次基础验证中通常不是重点。

图 54:调用结果被保存为进程资源,后续以资源是否有效判断启动是否成功。

【小白通俗注解】管道可以看成一根数据管:一端写进去,另一端读出来。这里命令的输入、正常结果和报错结果走的是不同的"管子"。

示例中,进程资源存在时,先向标准输入对应的管道写入内容,再关闭写入端;随后从标准输出对应的管道读取内容,并展示到页面。这样做的重点不在于记住每行代码,而在于明确数据方向:写入端负责交给子进程,读取端负责拿回子进程结果。

课程借 stream_get_contents 的名称帮助理解这一步:stream 可以按"流"来理解,get contents 是获取其中内容。形象地说,管道像一根装载数据的管子,命令执行后的结果在输出端流出;程序调用这个函数就是从管子里把结果取回来。读取完成后关闭句柄,才是这一小段流程的结束。

图 55:标准输入管道用于向子进程传递数据,写完后关闭对应句柄。

图 56:按顺序完成写入、关闭输入端、读取输出端。

图 57:从输出管道取得内容并关闭读取句柄。

理解复杂函数时,课程再次让 AI 给出更清晰的例子,而不是仅依赖原始文档。

图 58:针对复杂函数,提出"执行命令并给出清晰例子"的定向问题。

图 59:最简示例保留标准输入、标准输出和错误输出三个描述符。

图 60:分别读取正常输出和错误输出,便于定位命令执行失败的原因。

图 61:页面成功显示命令结果,验证管道读取逻辑有效。

图 62:将 proc_open 的完整流程放回代码上下文复核。

图 63:正常输出和错误输出均被显式处理,避免失败时只有空白页面。

课程指出,虽然 proc_open 能完成命令执行和回显,但放回最初只有单个 c 参数的页面时并不适合直接使用:需要将描述符、判断、读写及关闭等大量内容压缩到请求参数中,空格、引号、换行等字符都会增加传参难度。理论上可以把内容改成一行,但在这个案例里得不偿失。

讲师的结论是:第三个函数的重点在于理解管道与输入输出控制。它不是这个单参数靶场最省事的选择;为了用它而把大量代码塞入地址栏,没有实际收益。

9. 第四个函数:passthru 的直接回显

课程随后查看 passthru。它的使用方式与 systemshell_exec 接近,先用简单命令验证即可。在当前测试中,它能够正常执行并把结果回显到页面,无须额外把返回值手动输出,因此操作上较为直接。

图 64:查阅 passthru 的官方说明和参数形式。

图 65:在实验页面中以简单目录命令验证 passthru 的执行与回显。

10. 第五个函数:exec 的数组输出

最后一个命令执行函数是 exec。课程先阅读官方文档,再把命令输出接入一个变量。与直接回显的函数不同,exec 的输出可以放进数组;因此仅调用函数不会让页面自动出现结果,也不能简单把数组当普通字符串用 echo 打印。

课程的测试顺序也保留了一个常见的调试过程:先给 exec 传入输出变量,页面依然没有显示;再意识到变量里装的是数组;最后分别用 var_dump 和文档常见的 print_r 查看。这个过程说明,"已经接收输出"与"已经把输出渲染到页面"是两件不同的事。

图 66:exec 文档展示命令、输出数组和返回状态等参数。

图 67:exec 把多行命令结果收集到输出数组。

图 68:使用 var_dump 检查数组内容,适合调试阶段观察结构。

图 69:print_r 同样可以显示数组,符合文档中的常见示例。

【小白通俗注解】数组可以理解为一个装了多项内容的盒子。echo 擅长直接显示一段文本,但不会自动把盒子里的每一项都展开;var_dumpprint_r 才能把盒子的内容展示出来。

讲师个人更常用 var_dump 做这类调试输出;print_r 也能完成同样的验证。关键不是偏好哪一个,而是先认识到 exec 的输出类型是数组。

11. 收束:escapeshellcmd 是过滤,而不是命令执行

课程最后查看 escapeshellcmd。它的功能不是执行命令,而是转义字符串中可能影响 shell 解释的元字符。也就是说,它会在某些特殊字符前加入反斜杠,使这些字符不再按原先的 shell 语义参与解析。

图 70:escapeshellcmd 的文档说明,强调其职责是字符转义而非命令执行。

这正好解释了为什么通配符验证会受过滤函数影响:如果问号、星号等字符先被转义,shell 就不会再把它们按通配规则理解,前面依赖匹配的方式也就无法奏效。课程将它归为过滤函数,而不是第六个命令执行函数。

12. 本次复盘的顺序性结论

至此,课程按顺序完成了五类 PHP 命令执行函数的基础验证:system 的直接输出、shell_exec 的返回值输出、proc_open 的管道读写、passthru 的直接回显,以及 exec 的数组承载与打印。随后又区分了 escapeshellcmd 这类转义过滤函数。

讲师的总体建议是:在实验题中,能用 system 时优先用 system;当它不可用或被禁用,再依据回显需求、参数空间和函数行为选择其他方式。后续内容将继续在这一基础上分析更具体的过滤绕过。

从这次过程可以看到,真正重要的不是记住一段固定请求,而是按原始链路逐项确认:环境是否正常、编辑是否落到正确目录、输入到达了哪里、函数是否会回显、返回值是什么类型、路径假设是否成立、复杂方案是否真的比简单方案更合理。只有每一步都能用代码、终端或浏览器结果验证,调试才不是猜测。

相关推荐
举个栗子。36 分钟前
OpenMontage:首个开源的 Agent 化视频制作系统,用自然语言一键出品影片
开源·音视频
科技小登41 分钟前
重大活动安全保障方案怎么选?重保服务、攻防演练、应急响应与安全意识培训实战与边界
数据库·人工智能·安全
windliang1 小时前
Agent Loop 的 while 循环什么时候开始不够用
人工智能·面试·开源
数据知道1 小时前
静态分析入门——PE 结构、字符串、导入表快速定性
网络·安全·web安全·网络安全
智码看视界1 小时前
Day66-开源模型vs闭源模型:技术决策框架
开源·私有化部署·开源模型·模型选型·gpu推理·闭源模型·决策框架
code 小楊2 小时前
Agent间如何高效协作?深度拆解A2A(Agent-to-Agent)协议
人工智能·架构·开源
黄乐荣2 小时前
系统分析师-2025-11
安全
code 小楊3 小时前
腾讯开源 WeKnora 深度解析:RAG 问答 + ReAct Agent 推理 + 自动 Wiki 图谱,三位一体的企业级知识中台
前端·人工智能·开源·知识图谱
微擎应用市场3 小时前
开源赋能丨微擎面板(W7Panel)产品介绍说明
开源