在 8 字符、字母数字过滤与 PHP 版本差异下复盘临时文件命令执行链 | (8字符绕过)

目录

背景与复盘范围

[第一阶段:8 字符限制下的临时文件执行](#第一阶段:8 字符限制下的临时文件执行)

先确认临时文件是否真的生成

权限匹配成功,但直接执行失败

用点号和空格绕过执行位缺失

[URL 中的加号如何恢复空格语义](#URL 中的加号如何恢复空格语义)

第二阶段:禁止字母数字后的匹配困境

新过滤器让原来的短写法失效

[逐步收窄 glob 范围仍然不够](#逐步收窄 glob 范围仍然不够)

从字符集合转向"非"关系

[先问 AI,再回到官方 glob 文档](#先问 AI,再回到官方 glob 文档)

[利用 ASCII 表定位大写字母区间](#利用 ASCII 表定位大写字母区间)

[第三阶段:eval、反引号与 PHP 标签解析](#第三阶段:eval、反引号与 PHP 标签解析)

[从 shell_exec 转向 eval](#从 shell_exec 转向 eval)

第一次尝试失败:匹配并不等于可执行

[对照官方文档修正 PHP 标签](#对照官方文档修正 PHP 标签)

[第四阶段:PHP 7 取反语法与不可见字符](#第四阶段:PHP 7 取反语法与不可见字符)

[PHP 5 与 PHP 7 的解析差异](#PHP 5 与 PHP 7 的解析差异)

[取反结果不可见,必须经过 URL 编码](#取反结果不可见,必须经过 URL 编码)

[浏览器解码与 eval 二次取反](#浏览器解码与 eval 二次取反)

两类绕过的对照与边界

[从实验绕过延伸到 WAF 验证](#从实验绕过延伸到 WAF 验证)

讲师经验与学习建议

复盘结论


摘要:本文依据一段授权靶场授课转录,重构 PHP 受限命令执行案例的完整排查过程。复盘从临时文件权限为 644 的失败开始,依次处理 8 字符长度、URL 空格编码、通配符误匹配、禁止字母数字的正则,以及 eval 对 PHP 标签的解析要求,最后对比 PHP 5 与 PHP 7 的取反语法,并将问题延伸到宝塔 WAF 的待验证防御场景。

背景与复盘范围

这组实验围绕一个授权的 PHP 代码执行靶场展开。服务端会把请求内容写入 PHP 临时文件,再由后续逻辑匹配并执行。实验过程中,限制条件不断收紧:先限制输入长度,随后禁止命令中出现字母、数字、下划线和 $,再引入 PHP 版本差异。每次限制变化后,前一轮有效的方法都会失效,排查重点因此从"能否执行"转移到"文件如何生成、如何被匹配、如何被解释"。

本文只复述原稿中出现的授权实验与排查结论。文中涉及命令执行、eval、临时文件和 WAF 的内容,均应理解为隔离靶场中的技术复盘,不延伸为针对未知目标的操作指南。

第一阶段:8 字符限制下的临时文件执行

先确认临时文件是否真的生成

实验一开始,服务端要求提交的代码长度小于 8 个字符。表面上看,123456 已经满足长度条件,但长度合规并不代表代码链路能够执行。排查先回到文件本身:请求提交后等待约 30 秒,再查看服务端生成的临时文件内容,而不是立即根据页面是否有回显下结论。

图 01 展示了通过 cat 1.txt 读取生成文件的结果。文件中保留了请求写入的内容,说明写入环节已经完成,后续问题应集中在文件权限和执行方式。

为了让目录更容易观察,实验中先删除了无关文件,再重新列出目录和内容。这个清理动作没有改变服务逻辑,只是减少了排查时的干扰。

图 02 对应的是清理后再次检查文件的过程,重点是确认当前目录中留下的文件确实来自本次提交。

权限匹配成功,但直接执行失败

文件内容正确后,下一步是核对权限。临时文件只有读写权限,没有执行位。也就是说,正则匹配可以找到它,但把路径交给 shell 直接执行时,操作系统会拒绝请求。

图 03 中的 ls -al 输出显示,文件权限为 644,所有者是 root,但权限字符串没有 x。这解释了为什么"找到文件"与"执行文件"是两个独立问题。

随后使用 ls -al 再次确认文件状态,并对照请求内容检查生成结果。权限问题没有因为匹配成功而自动消失。

图 04 进一步印证了临时文件缺少执行权限。此时最直接的修复思路是调用 chmod +x,但实验代码运行在受限服务端,输入长度又被严格控制,无法把完整的权限修改命令写入并执行;同时,测试者并不拥有可以任意修改服务端逻辑的条件。

一次提交尝试表明,文件名已经被匹配出来,但执行仍然失败。这个结果把问题定位得更具体:过滤器方向没有错,失败原因是目标文件不可执行。

用点号和空格绕过执行位缺失

在 Linux shell 中,点号加空格可以让 shell 读取并解释一个没有执行位的脚本。实验先在独立文件上验证这个行为:直接执行 ./1.txt 返回 Permission denied,改用 . 1.txt 后,文件内容被当前 shell 解释。

图 05 是直接执行失败的对照组,错误信息明确指向执行位缺失,而不是文件不存在或内容损坏。

图 06 展示了实验请求的 POSTmultipart/form-data 和边界字段。它说明临时文件由一次上传式请求产生,文件内容和后续 GET 参数属于同一条业务链路。

如果把点号和空格直接加入原有短输入,长度会从 7 个字符增加到 8 个字符,触发"小于 8"的限制。于是排查转向文件名匹配:删除命令中显式写出的 p,改为依靠临时目录中的通配符找到生成的 PHP 文件。临时目录通常只放置本次实验产生的临时文件,因此通配符的匹配范围在靶场中相对可控。

第一次把点号和空格放进 GET 参数时,浏览器将空格编码,服务端得到的字符串已经不是 shell 所需的空格,页面上的参数也出现了编码后的变化。

图 07 反映了短输入在页面中的提交状态,长度检查仍然是第一道约束。

URL 中的加号如何恢复空格语义

GET 查询参数不能直接依赖裸空格。排查时回顾了 SQL 注入资料中常见的 --+ 写法:MySQL 真正的双连字符注释需要后接空格,而 URL 查询字符串中,加号会在解码阶段转换为空格。因此,这里的加号并不是 SQL 注释本身,而是传输层对空格的替代表达。

将点号后的空格替换为加号后,输入长度降为 7 个字符。此时,正则匹配临时文件,点号加空格让 shell 读取该文件,文件内容再由 shell_exec 执行,完整链路重新闭合。

图 08 展示了通过通配符扩展目标路径的实验过程,核心变化是把显式文件名交给 shell 的路径匹配完成。

图 09 显示了通配符误匹配的风险:范围一旦扩大,shell 会将很多无关路径一起展开,目标文件并不一定位于结果首位。

图 10 对应的是缩小匹配范围的中间尝试,目的是避免一次展开整个文件系统。

图 11 记录了长度降到 7 个字符后的请求状态。它验证了"匹配临时文件"和"绕过执行位"可以在同一条短输入中同时完成。

这里不存在依赖时序的竞争条件。GET 参数和 POST 文件提交在同一轮处理过程中完成,临时文件只有在整个请求执行结束后才会删除。因此,只要服务端开始执行 shell_exec,文件就仍然存在;成功并不是靠在删除动作前抢时间,而是靠请求生命周期本身保证文件可用。

经验判断: 处理短字符限制时,应把问题拆成文件生成、文件名匹配、权限处理和解释器执行四层。只证明其中一层成功,不能代表整条链路成功。

从调试顺序看,第一阶段实际完成了三轮验证。第一轮只提交短字符串,用于确认服务端是否创建文件;第二轮保留同样的文件内容,改为观察 ls -alcat 的结果,确认失败不是写入失败;第三轮才把文件路径交给 shell,比较 ./1.txt. 1.txt 的差异。这样的分层让每一个结论都有对应证据,而不是把页面无回显笼统归因于"过滤器拦截"。

文件权限还带来一个容易被忽略的边界:root 身份并不会让 ./1.txt 自动获得执行位。权限字符串是文件元数据,调用者身份与文件是否可执行是两个维度。实验中即使当前终端提示符显示为 root,直接执行仍返回 Permission denied;只有改变 shell 的调用方式,才绕开了对执行位的要求。

加号替代空格也不是对所有传输方式都成立的抽象规则。这里之所以有效,是因为参数位于 URL 查询字符串,浏览器和服务端会按照表单查询语义解码;同一字符若放入原始请求体或其他编码上下文,不能直接假定会得到相同结果。因此,复盘时必须把"字符在地址栏中的形态"和"程序收到的字节"分开记录。

临时文件的删除时序同样需要证据支撑。若文件在 GET 匹配前已经删除,现象会表现为 glob 无结果;若文件在 shell 读取前删除,现象则更像路径存在但内容为空。原稿通过同一请求中先提交 POST、再让服务端处理 GET 的关系,排除了这两种猜测,说明文件删除发生在请求整体结束之后。

第二阶段:禁止字母数字后的匹配困境

新过滤器让原来的短写法失效

第二个案例将输入长度放宽到 35 个字符,但新增了更严格的字符过滤:执行命令过程中不能出现英文字母、数字、下划线和 $。原先用于匹配临时文件的 pphpshell_exec 都含有被禁止字符,第一阶段的 7 字符思路不能直接复用。

图 12 展示了匹配目录中同时存在脚本、二进制和普通文件的情况。临时文件并不是目录中唯一的候选目标,匹配策略必须继续收窄。

图 13 说明仅依靠点号、横杠等符号并不能排除大部分干扰文件。

逐步收窄 glob 范围仍然不够

第一种尝试使用过宽的通配符。单独的 * 可以覆盖几乎所有名称,虽然理论上包含临时文件,却会优先匹配以 ab 等字符开头的其他文件,最终执行到错误目标。把通配符拆成多个片段后,范围有所收窄,但候选仍然很多。

继续利用临时文件名的结构,尝试匹配前 3 个字符,再匹配后 10 个字符。这个方案看起来更接近目标,但观察目录后发现,很多 Python 脚本和二进制文件同样满足长度条件。原稿随后纠正了一个细节:临时文件随机部分是 9 位,不是 10 位。修正长度后,候选集合仍然过大。

图 14 记录了逐位排除特殊字符的尝试。它可以去掉少量带横杠或点号的名称,却无法覆盖所有普通文件。

排除法的问题在于,目录中有大量完全由字母组成、没有特殊符号的文件名。即使把第 3、5、7、8 位出现的横杠和点号逐一排除,剩余候选仍然很多。目标临时文件的真正特征不是某个固定位置的标点,而是随机字符串中可能出现大写字母。

从字符集合转向"非"关系

为了确认正则中方括号的含义,实验使用 Regulex 观察 [abc]。方括号内部表达的是"其中一个字符",不是完整字符串;如果在开头加入尖角号 ^,则变成"除这些字符之外的任意一个字符"。

图 15 将 [abc] 可视化为 one of,说明它会匹配 abc 中的一个。

图 16 展示了 [^abc]none of 语义,即排除集合内字符。这个知识解释了为什么"排除特殊字符"在语法上成立,却在实际目录中不够用:可排除的字符有限,而没有特殊符号的候选仍然占多数。

先问 AI,再回到官方 glob 文档

由于过滤器禁止输入英文字母,常规的 [A-Z] 范围表达式本身无法提交。实验先咨询 DeepSeek,尝试寻找不直接写出英文字母、但能匹配大写字符的 glob 表达式。模型先后给出字母范围、快捷类和 find 命令等建议,但这些回答要么仍包含被禁止字符,要么已经脱离 glob 的语境,无法放进当前输入。

图 17 记录了查询条件,限制重点是"需要识别大写字符,但提交内容不能出现英文字母"。

图 18 展示了更换模型后的对比过程。回答仍然无法给出同时满足字符过滤和大写匹配的可用表达式。

图 19 体现了答案与过滤器之间的冲突:写出 A-Z 能匹配目标,却会在进入执行逻辑前被正则拦截。

排查期间还遇到代理模式切换缓慢的问题。规则模式访问海外站点时响应不稳定,全局模式速度更快,但不愿作为长期实验配置;百度又无法提供需要的 Linux 手册内容。这个插曲没有产生技术结论,却说明工具可用性本身会影响排查节奏。

随后转向 Linux 官方文档,直接核对 glob 的定义,而不是继续依赖模型猜测。

图 20 展示了从搜索入口查找官方资料的过程。

图 21 对应的是官方文档页面,后续判断都以该手册对通配符和字符范围的说明为依据。

文档明确了几个基础规则:? 匹配单个字符,* 匹配任意数量字符(包括空白),方括号可以表达字符范围,方括号开头的 ^ 表示排除集合。文档还列出大写、小写、空白、可打印字符和数字等快捷类别,但这些类别的字面表示仍会触碰禁止字符,因此不能直接使用。

图 22 记录了范围、字符集合和否定集合的手册说明。

利用 ASCII 表定位大写字母区间

官方说明让排查得到一个新的方向:glob 的范围不仅可以写成字母区间,也可以按照字符编码连续区间表达。ASCII 表中,大写字母连续位于 @[ 之间。这个区间的边界字符不是英文字母,因而有机会在过滤器中传递。

图 23 展示了 KZ 等大写字符在 ASCII 表中的连续排列。

图 24 补充了 @[ 等边界位置,为构造不直接写出字母的范围提供依据。

把临时文件随机部分的最后一位限制为这个 ASCII 区间后,其他候选文件都无法满足条件。第一次测试没有列出结果,反而成为有价值的信号:没有输出意味着没有普通文件的末位落在该大写区间,而不是命令本身一定失败。

图 25 展示了过滤后的空结果。结合目录观察,临时文件最后一位出现大写时,glob 能够把它单独挑出。

推理转折: 目标不是把整个随机文件名写出来,而是找到"临时文件有、其他候选没有"的最小特征。这里的特征是末位可能出现大写字符,ASCII 连续区间让这个特征可以在不直接输入字母的情况下表达。

这一阶段还体现了"观察样本再写模式"的必要性。最初把随机部分当成 10 位,是根据视觉印象做出的判断;重新读取文件名后才确认是 9 位。长度纠正并没有立即带来成功,却排除了一个具体误差。随后对目录进行横向比较,才发现大写字符是临时文件与普通脚本之间更稳定的差异。模式不是从语法表直接推出来的,而是由样本特征和语法能力共同决定。

排除法之所以逐渐失去价值,是因为它要求预先知道所有干扰项。目录中每增加一种没有特殊字符的文件,原有排除列表就需要扩展;而"末位属于大写区间"只描述目标的正向特征,不需要枚举其他文件。对于动态目录,这种正向匹配比堆叠负向条件更容易维护,也更接近实际调试时的可验证假设。

AI 查询过程还提醒了一个核对原则:回答中的"能匹配"必须同时满足语法、输入过滤和运行环境三个条件。[A-Z] 在 glob 语法层面合理,但在本案例的输入层面会被过滤;find 可能完成查找,却不再是原问题要求的 glob 表达式。只有把答案放回真实脚本逐字检查,才能判断建议是否可用。

第三阶段:eval、反引号与 PHP 标签解析

shell_exec 转向 eval

第二个脚本不再调用 shell_exec,而是使用 eval 解释用户提交的 PHP 代码。要让 eval 间接执行系统命令,实验依赖 Linux 反引号语法:反引号负责执行命令并返回结果,eval 负责把这段 PHP 代码交给解释器。

图 26 中可见的核心结构包括 strlen($code)>35 的长度判断、preg_match("/[A-Za-z-z0-9_$]+/",$code) 形式的字符过滤、die("NO.") 拦截,以及最后的 eval($code)。截图中的字母范围存在手写或录入差异,复盘保留其实际作用:拦截字母、数字、下划线和 $

最初的构想是把临时文件路径、通配符、反引号和分号串起来,再通过 POST 生成临时文件,由 GET 参数完成匹配和执行。

图 27 展示了反引号和临时文件路径拼接时的中间代码。

图 28 对应的是把分号加入表达式的版本。eval 解析 PHP 语句时需要明确的结束符,否则即使文件匹配成功,也可能在语法阶段失败。

由于临时文件仍然没有执行位,反引号内部的命令继续使用点号加空格的形式读取文件。

图 29 记录了权限绕过与反引号组合后的命令形态。

图 30 展示了为适配 9 位随机名而调整通配符的过程。

第一次尝试失败:匹配并不等于可执行

完成拼接后,实验抓包并提交请求。输入长度、文件生成和通配符匹配看起来都符合预期,但服务端仍然没有执行结果。排查没有直接修改所有部分,而是沿链路逐项确认:临时文件是否生成、glob 是否返回目标、点号加空格是否被 shell 解释、反引号是否被 eval 当作完整 PHP 代码。

图 31 是第一次提交后的请求界面,显示参数已经发出。

图 32 记录了将通配符表达式放进 GET 参数的过程。

图 33 展示了 POST 内容与临时文件路径之间的对应关系。

图 34 对应的是完整拼接后的请求文本,长度仍在 35 字符上限内。

图 35 展示了提交前对 PHP 表达式的检查,重点是反引号、临时文件匹配和分号是否同时存在。

图 36 记录了第二次发送请求时的参数变化。

图 37 的无回显说明,失败点还没有被定位,不能仅凭页面空白判断是文件未生成。

抓包后继续验证:临时文件大概率已经生成,glob 也能匹配到候选,权限绕过方法单独验证过,真正可疑的环节转向 eval 的语法要求。

图 38 将两部分请求并列显示,为后续排除竞争条件和文件删除时序提供依据。

对照官方文档修正 PHP 标签

eval 的官方说明指出,它会把字符串当作 PHP 代码执行,但传入字符串不能直接包含完整的 <?php ?> 开闭标签。若要从已有 PHP 上下文切换出去再切换回来,需要先结束当前代码,再重新进入 PHP 模式。原稿中的第一次表达式只把单个字符或片段交给 eval,没有满足这个边界条件,因此即使文件匹配成功,也可能在解析阶段失败。

图 39 表明临时文件确实存在,排查可以从文件系统转向 PHP 解析。

图 40 对应"生成、匹配、权限均正常但无结果"的状态,是定位 eval 语法问题的关键证据。

图 41 展示了 eval 将字符串作为 PHP 代码解释,以及不应直接嵌入完整 PHP 标签的文档内容。

修正方案分成三部分。第一,在前面先用结束标记关闭当前 PHP 片段;第二,在要执行的内容前重新进入 PHP 模式;第三,用 PHP 7 的短标签 <?=echo 把表达式结果输出。这个结构不是任意拼接,而是对照文档把代码放回解释器能够接受的上下文。

图 42 说明了结束现有代码、重新打开 PHP 片段的必要性。

图 43 展示了短标签将表达式结果直接输出的语法。

在短标签后补上等号并使用 echo,表达式最终需要以分号结束。缺少分号时,eval 会报解析错误;原稿明确引用了"所有语句必须以分号结尾"的要求。

图 44 是按照官方解析边界重构后的代码形态,包含闭合、重新进入、输出和分号。

请求参数中的问号和 @ 可能被 URL 编码。理论上应先确认编码结果,再决定是否手工编码;但实验中即使不额外编码 @,第二次提交仍然得到正确执行。

图 45 记录了修正标签结构后的成功结果。第一次失败与第二次成功之间的差异,不在临时文件生成,而在 eval 能否把输入解析为合法 PHP。

图 46 展示了最终生效的表达式和字符过滤条件。

这次成功也暴露了防御缺口:只过滤字母数字,不能阻止通过临时文件和 glob 找到代码;只限制长度,也不能阻止点号加空格读取无执行位文件;只依赖 eval,还会把用户可控数据送入解释器。

图 47 突出显示了 eval($code) 所处的代码路径。

官方建议是:没有特殊理由时不要使用 eval;如果必须使用,待执行代码应由程序固定生成,不能由用户控制。原稿中的漏洞恰好违反了这条原则:$code 来自请求参数,过滤后仍然被交给 eval

图 48 对应的是用户输入流入解释器的路径,也是本阶段最重要的安全结论。

经验判断: 复盘解析器类问题时,不能只盯着过滤器。需要同时查阅解释器官方文档,确认标签、分号、编码和上下文边界;一个看似"匹配成功"的输入,可能在下一层解析时被当成非法语法。

从失败到成功的差别可以按检查表复现:首先确认 $code 长度没有超过 35;其次确认正则没有返回 NO.;再次确认 POST 产生了 9 位随机文件;然后确认 glob 只返回目标路径;接着用点号加空格读取文件;最后检查反引号是否处于 eval 能解析的 PHP 代码上下文。第一次尝试在前五项上看似成立,却在最后一项失败。第二次只调整标签闭合、短标签输出和分号,成功结果因此具有明确的因果关系。

这个顺序也解释了为什么"再提交一次"本身不能算调试方法。重复请求只能告诉我们现象是否稳定,不能指出是哪一层出错。只有在每次提交之间改变一个变量,并保留抓包、终端和页面结果,才能把随机文件名、大小写变化、URL 编码和解析错误分别归类。

第四阶段:PHP 7 取反语法与不可见字符

PHP 5 与 PHP 7 的解析差异

原稿最后引入了一个只适用于 PHP 7 的思路:利用 PHP 7 的表达式解析规则对字符串做取反,再通过两次取反还原目标函数名。PHP 5 与 PHP 7 在间接调用和表达式括号方面存在差异,截图中的官方迁移文档对比了两种版本的解析顺序。PHP 7 支持把取反结果放入括号后继续调用,PHP 5 不支持同样的写法。

图 49 展示了取反字符串后再作为可调用表达式处理的代码片段。

图 50 是官方版本迁移资料中的对照表,说明同一表达式在 PHP 5 和 PHP 7 中的解析顺序不同。

实验目标仍然是调用 phpinfo,但过滤器不允许直接出现函数名。取反运算符 ~ 可以把字符串逐字节转换为另一组字符。只要把结果再次取反,就会回到原始字符串,因此最终执行目标仍然是 phpinfo()

图 51 展示了资料中"先得到取反字符串,再在表达式中还原并调用"的思路。

取反结果不可见,必须经过 URL 编码

直接在 PHP 中执行 echo (~'phpinfo();'); 时,输出并不是可读的字母,而是一串不可见或难以显示的字节。这些字节无法直接复制到浏览器地址栏,也不能稳定地判断每一位是否正确。

图 52 展示了 echo (~'phpinfo();'); 的代码,输出结果在终端或编辑器中不可读。

为解决传输问题,实验把取反结果交给 urlencode,将不可见字节转换为 URL 可识别的 %XX 形式。

图 53 展示了 echo urlencode((~'phpinfo();')); 的处理方式。

图 54 显示了编码后的 %8F%97... 字符串。编码结果虽然包含数字和字母,但它们只存在于传输层;浏览器提交后会先进行 URL 解码。

取反字符串中用于闭合或调用的部分需要保留,后面的某些字符如果本身不含英文字母,可以根据实验结果省略。原稿强调,真正需要关注的是进入过滤器和进入 eval 时的字符形态,而不是地址栏里暂时可见的编码文本。

图 55 同时呈现了取反 payload 和 PHP 版本页面,证明还原后的表达式能够调用 phpinfo

浏览器解码与 eval 二次取反

第二个脚本的关键过滤代码仍然是 strlen($code)>35preg_match。请求进入浏览器后,百分号编码被自动解码为不可见字节,这些字节既不是字母、数字,也不是下划线或 $,因此不会触发 NO. 分支。

图 56 展示了完整的 PHP 7 测试脚本,以及注释中记录的临时文件路径和取反思路。

图 57 是生成编码字符串的最小代码版本。

图 58 用注释标出了不可见字节、取反结果和最终 phpinfo() 之间的对应关系。

图 59 展示了将编码结果嵌入请求的中间状态。

服务端接收到解码后的不可见字节后,代码前部再次使用 ~。第一次取反产生不可见字节,第二次取反把它们还原为 phpinfo(),随后 PHP 7 的表达式规则允许继续调用。

图 60 展示了最终脚本中二次取反的位置。

图 61 对应的是解释链路:浏览器解码、过滤器放行、eval 执行取反、字符串还原并调用函数。

这条路径与上一阶段使用 glob 的方法不同,但目标仍是同一个:在输入阶段不出现被禁止字符,在解释阶段恢复真正的 PHP 代码。PHP 7 可以直接使用取反表达式;PHP 5 不支持相同的括号和间接调用方式,因此需要回到前一阶段的 glob 匹配临时文件方案。

两类绕过的对照与边界

原稿将前两种方案归纳为两类。第一类面向"长度小于 8,但允许字母"的场景:利用临时文件匹配、点号加空格绕过执行位,再通过加号恢复 URL 空格语义,最终交给 shell_exec。第二类面向"禁止字母数字和 $,长度放宽到 35"的场景:PHP 5/7 通用方案使用 glob 的 ASCII 区间匹配临时文件末位大写字符,再配合点号加空格和 eval 标签边界;PHP 7 专用方案则使用字符串取反和 URL 编码。

两类方案都依赖临时文件机制,但失败原因不同。第一类最初失败在执行位缺失和 GET 空格编码;第二类最初失败在 glob 范围过宽、候选文件过多,以及 eval 对 PHP 标签和分号的要求。把这些失败混为一个"调参后成功"会丢掉真正的排查价值。

复盘方法: 每一次尝试都应记录输入长度、过滤器是否触发、文件是否生成、glob 是否命中、权限是否满足、解释器是否报错以及请求完成后文件何时删除。只有把这些状态串起来,才能区分"没有匹配到"和"匹配到了但没有执行"。

这里的"解释器边界"还包括输出方式。eval 执行的是 PHP 代码字符串,不会自动把任意字符串当作完整页面;短标签 <?= 的作用是把表达式结果交给输出通道,而不是替代 eval。因此,闭合标签、重新进入 PHP 模式、使用 echo、补上分号,分别解决的是上下文、输出和语句结束问题,不能把它们合并成一个模糊的"语法修正"。

URL 编码的讨论也必须分两层:地址栏中看到的是 %XX 文本,程序收到的是解码后的字节。过滤器检查的是后者,eval 处理的也是后者。实验中 @ 是否需要显式编码,最终通过实际请求验证;不能因为某个字符在浏览器中看起来安全,就推断它在所有客户端和服务器组合中都会保持原样。

从实验绕过延伸到 WAF 验证

在课程结尾,问题进一步延伸到安全工具:如果 PHP 可以通过编码、标签、取反和临时文件组合绕过简单过滤,常见运维面板的 WAF 是否能识别这种变形?原稿点名了宝塔面板,并说明其在国内使用量较大,产品内置免费的 WAF,可用于观察对 PHP 一句话木马的防护逻辑。

图 62 展示了宝塔面板官网页面,原稿以此作为后续搭建测试环境的入口。

图 63 展示了下载页面和安装命令区域。原稿建议新建一台虚拟机,在隔离环境中安装宝塔面板,再观察其 WAF 如何处理前述 PHP 代码变形。

这个验证在原稿结束时尚未执行完毕,因此本文不声称宝塔 WAF 已被绕过,也不补写任何拦截或放行结果。可以明确的只有实验设计:不要在原始虚拟机中堆积大量安装文件,应新建虚拟机,安装面板和 WAF,再用授权测试样本逐项观察规则命中情况。

讲师经验与学习建议

原稿反复强调,解决问题的价值不在于记住某一条现成 payload,而在于形成可迁移的推理路径。临时文件并不是只为一道题准备的技巧,它同时解释了 8 字符限制和字母数字过滤两类真实渗透中可能出现的场景;但这句话的适用前提是授权测试环境,不能脱离边界理解。

讲师还指出,过度依赖讲师或 AI 的现成思路会削弱独立分析能力。今天能够照着提示完成,并不等于下一次过滤器换了位置、版本换了实现、临时文件特征发生变化后仍能复现。更可靠的训练方式是先写出约束清单,再逐项验证文件生成、匹配、权限、编码和解释器行为,最后才选择最短表达式。

对 AI 工具的评价也来自实际过程:模型可以帮助定位文档方向,但在"不能输入英文字母、却要匹配大写字符"这种组合约束下,回答可能重复使用被禁止的字母范围,甚至把 glob 问题改写成 find 命令。遇到这种情况,应回到 Linux 官方手册和 ASCII 表核对,而不是把模型回答当作结论。

在学习和面试准备中,建议把失败过程讲清楚:为什么 * 会匹配到错误文件,为什么 9 位和 10 位的判断会改变结果,为什么直接写空格会被 URL 编码,为什么 eval 需要先闭合再重新进入 PHP 模式,为什么 PHP 7 能取反调用而 PHP 5 需要另一条路径。这些因果关系比单独背诵某个字符串更能体现排查能力。

复盘结论

这组实验最终形成了一条清晰的技术链:长度限制并不会自动消除文件机制,字符过滤也不等于解释器安全 。当临时文件可写入但没有执行位时,点号加空格可以改变 shell 的执行方式;当 GET 参数不能承载裸空格时,加号在 URL 解码后恢复为空格;当字母数字全部被拒绝时,临时文件的随机命名特征和 glob 的 ASCII 区间提供了新的匹配方向;当 eval 解析失败时,官方文档中的标签边界和分号要求决定了修正方式;当运行环境是 PHP 7 时,取反表达式与 URL 编码又把不可见字节变成了可传输、可还原的中间表示。

真正值得保留的不是某个"万能绕过",而是按层排查的顺序:先看输入限制,再看文件生成;先证明路径匹配,再证明权限处理;随后确认 URL 解码结果,最后核对 PHP 解释器的语法和版本差异。原稿结尾提出的宝塔 WAF 实验仍待在隔离虚拟机中验证,这个未完成状态本身也应保留在复盘中,避免把计划误写成结果。

相关推荐
我命由我123451 小时前
Compose Codelab 学习 - Jetpack Compose 中的状态
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
土司大王1 小时前
LeetCode hot100——全排列
java·算法·leetcode
zhojiew1 小时前
让 Agent 安全地“借用别人的钥匙“:AgentCore 上用 Authentik 打通 OAuth 出站的一次实战
安全·ai·aws·agentcore
小杨不想秃头1 小时前
信息安全工程师第六章
网络·安全·web安全
万联WANFLOW1 小时前
技术演进与安全博弈中的 OpenAI GPT-6 Astra
网络·人工智能·gpt·安全·业界资讯
秋饼1 小时前
Spring AI 2.0 多模型路由网关:Astra/Sol 到国产模型的智能调度与降级
java·ai·技术分享·后端开发
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之密码存储安全进化与实践
java·安全·spring
yydbjx1 小时前
安全感不是“被害妄想“:如何区分合理的预防和过度的焦虑?
安全
小七在进步1 小时前
C++入门(3)
android·java·c++