从 assert 动态调用报错到临时文件链路:PHP 5.6 与 7.3 授权靶场中的短 payload 复盘 | (17字符绕过)

目录

背景与复盘范围

[1. 先让错误可见:配置检查与 FPM 重启](#1. 先让错误可见:配置检查与 FPM 重启)

[2. 从 500 错误定位参数与类型约束](#2. 从 500 错误定位参数与类型约束)

[3. 版本差异:为什么转向 PHP 5.6 测试](#3. 版本差异:为什么转向 PHP 5.6 测试)

[4. PHPStudy 中重现 PHP 5.6 行为](#4. PHPStudy 中重现 PHP 5.6 行为)

[5. "能执行"不等于"连接成功":一键连接与 Base64 差异](#5. “能执行”不等于“连接成功”:一键连接与 Base64 差异)

[6. 抓包还原 base64_decode、eval 与 assert 的执行顺序](#6. 抓包还原 base64_decode、eval 与 assert 的执行顺序)

[7. eval 为什么不能像函数一样动态调用](#7. eval 为什么不能像函数一样动态调用)

[8. 回调后门案例:usort、变长参数与 assert](#8. 回调后门案例:usort、变长参数与 assert)

[9. 语法错误与修正:eval 代码必须有分号](#9. 语法错误与修正:eval 代码必须有分号)

[10. 输入长度从十七字符压缩到七字符](#10. 输入长度从十七字符压缩到七字符)

[11. 为什么观察 PHP 文件上传临时文件](#11. 为什么观察 PHP 文件上传临时文件)

[12. 从临时文件内容推导短输入匹配思路](#12. 从临时文件内容推导短输入匹配思路)

[13. 抓取文件上传数据包并验证临时文件出现](#13. 抓取文件上传数据包并验证临时文件出现)

[14. 讲师经验与方法总结](#14. 讲师经验与方法总结)

按证据推进,而不是按结果倒推

复盘结论


摘要:本文复盘一组只在课程授权实验环境中进行的 PHP 代码审计与调试实验。案例从 display_errorsFPM 配置开始,逐步解释 assert 在 PHP 5.6 与 7.3 中的动态调用差异,再通过抓包还原 base64_decodeevalusort 回调链路。最后,实验转向七字符限制下的文件上传临时文件观察,记录临时文件的生成、消失、匹配思路及验证失败点,强调版本、调用类型、错误信息和实际复现之间的关系。

背景与复盘范围

本文只整理附件中出现的课程实验过程。所有请求、代码片段、抓包和版本切换均发生在讲义所示的本地 PHPStudy、Windows 或 Linux 测试环境中,目标是理解旧版 PHP 行为并完成授权靶场验证,不应直接套用到未知站点或未获授权的系统。

这次复盘的核心问题有三个:第一,为什么同一段动态调用在 PHP 7.3 中报错,而在 PHP 5.6 中能够运行;第二,为什么给参数增加 Base64 编码后,连接结果反而发生变化;第三,当输入长度从十七个字符压缩到七个字符时,如何从常规参数传递转向观察文件上传临时文件。课程没有把这些问题当作一次性成功的操作,而是通过错误、抓包和环境切换逐层定位原因。

1. 先让错误可见:配置检查与 FPM 重启

实验的第一步不是直接尝试执行,而是把 PHP 的错误显示打开。配置界面中重点确认了 display_errors 和错误级别相关选项,先只打开确定需要的项目,保留其他选项作为后续排错手段。这样做的目的,是让页面能够返回 500 错误背后的具体原因,而不是只看到空白或泛化的失败提示。

图 01:截图展示了课程环境里与错误显示有关的配置项,后续判断以页面实际返回的错误文本为依据。

图 02:截图记录了 display_errors 等配置被启用后的状态,配置修改后需要重新加载服务才能生效。

课程中再次确认了另一个配置位置,避免只修改了一个页面却没有覆盖实际运行的 PHP 配置。若后续仍然没有错误回显,才继续扩大开启范围;当前先保持最小修改,减少其他变量对实验的影响。

图 03:截图展示了需要核对的第二处配置,说明同一运行环境可能存在多层配置来源。

修改完成后重启 PHP-FPM。重启并不是"顺手做一下",而是为了让进程重新读取配置;如果跳过这一步,页面仍可能沿用旧进程中的设置,导致后面的错误判断失真。

图 04:终端截图记录了 FPM 重启过程,后续请求均建立在重新加载配置后的进程上。

重启后再次请求测试页面,确认页面可以正常返回;随后故意保留缺失参数的状态,以便观察程序如何报告问题。此时的目标不是马上得到"成功连接",而是把每个缺失参数和类型要求分离出来。

2. 从 500 错误定位参数与类型约束

首次请求返回 500。错误信息指出,参数 0 没有传入,同时某处要求接收到 string 类型的函数名。两个错误同时出现,说明问题还没有进入 assert 的核心逻辑,程序在读取参数和检查调用类型时就已经停止。

图 05:页面错误信息对应两个缺失值,截图用于确认错误并非网络层超时或服务未启动。

随后补上第一个参数,再观察错误数量变化。错误少了一项,说明参数确实被程序读到了,剩余报错可以继续作为下一步定位依据。这个过程体现了一个重要的排查习惯:每次只改变一个输入,利用错误数量或错误文本判断程序执行到了哪一层。

图 06:截图展示了只补充部分参数后的响应,结果表明参数读取已经向前推进。

实验把参数 1 临时设置为 phpinfo,再把它交给 assert 相关逻辑执行。此时页面不再提示"未定义参数",而是明确指出不能通过动态方式执行 assert。这条报错把问题从"有没有传参"推进到了"运行时是否允许把变量当作可调用对象"。

图 07:截图对应 PHP 7.3 环境中的动态执行错误,实验据此开始比较不同版本的语言行为。

图 08:终端输出进一步确认了限制来自运行时,而不是请求格式或页面显示问题。

这时得到的第一条阶段性结论是:在 PHP 7.3 环境下,把 assert 作为变量函数动态调用会失败;因此,不能只看代码表面是否包含 assert,还必须确认目标 PHP 版本及其调用规则。

3. 版本差异:为什么转向 PHP 5.6 测试

课程随后回到兼容性问题。讲解中提到,当前 PHP 已经发展到 8.x,但互联网上仍可能存在 PHP 7.3、7.4 或 5.6 的遗留服务。这里的"仍可能存在"只是版本背景判断,不代表可以对任何外部服务进行测试;在本次实验中,版本差异通过本地环境重现。

图 09:截图展示了测试环境中可切换的 PHP 版本,实验据此选择旧版本进行行为对照。

在 Ubuntu 环境中,apt list 能否列出旧版 PHP 包,取决于是否添加了课程中提到的 PHP 软件源。默认源提供的版本较新,未配置额外源时看不到 5.6 等旧版本。因此,版本切换本身也是一个环境依赖问题。

图 10:截图保留了课程安装文档中关于软件源的说明,解释旧版本包为什么能够被列出。

如果直接在当前 Ubuntu 上安装 PHP 5.6,需要同时安装对应的 FPM 和依赖。课程尝试核对了图片处理库、MySQL 依赖等选项,发现完整安装可能牵动已有 PHP 7.3 环境;因此没有继续在同一系统上覆盖安装,而是把对照实验转移到 Windows 的 PHPStudy。

图 11:界面截图显示了安装旧版本时需要考虑的扩展和依赖,部分库在当前案例中并未实际使用。

图 12:截图保留了依赖检查结果,说明强行补齐全部依赖可能影响已有版本。

这一取舍体现了课程中的环境经验:当旧版本只是为了验证一个语言行为时,优先准备隔离的对照环境,避免为了复现单个现象破坏当前可用的 PHP 7.3 环境。

4. PHPStudy 中重现 PHP 5.6 行为

在 Windows 的 PHPStudy 中,实验选取一个本地测试入口,把原来的参数结构改成课程前面讨论的形式,并选择 PHP 5.6.9。请求仍先保持缺少参数,以确认环境切换后错误是否一致。

图 13:截图展示了 Windows 本地环境中的测试页面,目标是隔离版本差异。

图 14:界面中可见本地站点和 PHP 版本的对应关系,实验在此切换到 5.6 系列。

图 15:截图用于确认本次请求实际由 PHP 5.6.9 处理。

切换到 5.6.9 后,第一次请求仍因没有传入参数而报错。这一步很关键:它说明版本变化没有消除基本的输入校验,后续能够观察到的差异确实来自调用规则,而不是因为测试页面被换成了另一套代码。

图 16:截图展示版本切换后仍然存在的参数错误,验证测试入口保持一致。

然后补入 assert 以及 phpinfo,再次发送请求。PHP 5.6 没有再返回"不能动态执行"的错误,说明该版本仍把 assert 当作可以动态调用的函数处理。

图 17:请求界面记录了对照实验使用的两个参数。

图 18:截图显示旧版本没有出现 PHP 7.3 中的动态调用错误。

5. "能执行"不等于"连接成功":一键连接与 Base64 差异

完成单次函数调用后,课程继续尝试把同样的参数用于工具的一键连接。参数 0 保持为 assert,连接参数只给出一个简单值。页面返回"数据为空"的警告,随后确认连接失败。

图 19:截图展示了尝试一键连接时使用的参数,结果尚未形成有效响应。

图 20:代码截图用于对照单次执行和工具连接两种请求格式。

工具连接失败后,实验没有直接把结果归因于 PHP 版本,而是先关闭代理、重新连接,再确认返回数据仍为空。随后把参数增加 Base64 编码,连接结果反而变为成功。

图 21:截图展示了编码形式变化后出现的成功响应,但此时原因尚未确定。

课程特别强调,"改成 Base64 就成功"只能算观察到的现象,不能算解释。 为了知道差异来自哪里,必须抓取两次请求的连接流量,分别保存默认形式和 Base64 形式,再逐字段比较。

6. 抓包还原 base64_decodeevalassert 的执行顺序

抓包时先记录默认参数,再关闭相关窗口重新发送编码形式。初次抓取过程中界面被关闭,导致需要重新采集;这属于实验过程中的无效尝试,但它提醒复盘者不要只保留最终截图,抓包失败本身也会影响判断。

图 22:截图记录默认请求的第一次抓包结果。

图 23:第二次抓包保留了更完整的请求内容,用于与编码版本进行比较。

图 24:截图将两次请求放在同一工具中比较,差异集中在参数值和解码链路。

把编码后的字段直接按纯 Base64 解码并不能得到完整代码,原因是字段中还混有不属于 Base64 的字符。实验先把原始字段复制出来,再单独提取可解码部分。

图 25:终端截图显示了从抓包结果中提取参数的过程。

图 26:截图展示了去除外围字符后对 Base64 内容进行解码的操作。

解码结果与前面看到的盘符遍历代码一致。代码会从 C 盘到 Z 盘逐个检查,因为实验代码不预设系统实际安装在哪个盘符。连接成功后工具界面能够列出目录和文件,正是因为这段逻辑执行了盘符遍历。

图 27:代码截图展示了从 CZ 的遍历思路。

图 28:抓包界面对比了直接交给执行入口的代码与先经函数处理的代码。

图 29:截图保留了请求中 eval、编码参数及调用参数的位置关系。

两次请求的关键差异不在最终字符串内容,而在传入 assert 的对象类型。第一种形式把代码字符串直接交给动态 assert;第二种形式先由 eval 调用 base64_decode,再把包含函数调用的整体表达式交给 assert。在 PHP 5.6 中,后者可以满足动态 assert 的调用要求。

图 30:代码截图展示了直接提交参数的路径。

图 31:代码截图展示了把解码函数嵌入动态调用的写法。

图 32:截图标注了请求参数、解码函数和执行入口之间的层次。

课程将执行过程拆成四步:首先,POST 参数被读取;其次,eval 执行包含 base64_decode 的表达式;再次,Base64 字段被解码;最后,解码结果作为函数调用链的一部分进入 assert。因此,真正执行解码后代码的是 eval,而动态 assert 负责调用包含函数的表达式。

图 33:流程截图展示了直接传入代码字符串时未能通过动态调用约束。

图 34:代码截图展示了"先调用函数,再交给 assert"的结构。

图 35:截图用于确认真正执行字符串的是 eval,而不是 assert 本身。

7. eval 为什么不能像函数一样动态调用

在理解上述链路后,实验尝试把参数 0 直接改成 eval,希望让动态调用入口直接执行 eval。请求返回"eval 未定义"之类的错误。若它是普通 PHP 函数,错误表现应与 assert 类似;实际结果说明两者的语言类别不同。

图 36:截图展示了直接把 eval 作为变量函数目标的尝试。

图 37:错误响应说明 eval 不能按普通函数名解析。

课程随后查阅 PHP 资料确认:eval 是语言构造器,不是标准函数,因此不能通过可变函数形式动态调用;PHP 5 中的 assert 曾被当作函数,可以走变量函数路径,而 PHP 7 以后 assert 也转为语言构造器,动态调用随之失效。

图 38:资料截图给出了 eval 属于语言构造器的解释。

图 39:代码截图说明函数名由变量决定时才构成可变函数调用。

这也解释了版本差异:PHP 5.6 中,assert 仍能作为动态函数调用;PHP 7.3 中,它已经不能通过变量函数方式调用;eval 从始至终都不是可变函数目标。两者的共同点是都不能被当作普通函数名任意替换,差异则在于 PHP 5.6 对 assert 的兼容行为。

图 40:截图将不同 PHP 版本中的执行结果并列展示。

图 41:代码截图保留了 base64_decode 结果进入 eval,再由外层表达式满足 assert 调用的关系。

【小白通俗注解】可以把这段差异理解为:普通函数像"可以通过名字查找的工具",语言构造器则由 PHP 语法直接识别,不能仅凭一个变量名把它替换出来。这里的注解只解释实验中已经出现的调用现象,不延伸到其他 PHP 特性。

8. 回调后门案例:usort、变长参数与 assert

第二个独立案例转向一个回调函数链路。课程先指出,示例使用 usort,其第二个参数是比较函数;只要 PHP 文档中出现 callback,就需要把它理解为"运行过程中由函数内部回头调用外部提供的函数"。usort 会把数组中的两个元素交给比较函数,根据返回值 0-1 或正数决定排序关系。

图 42:截图展示了回调案例的函数结构和第二参数位置。

图 43:代码图展示了 usort 接收来自 $_GET 的变长参数。

图 44:截图对照了函数文档和课程中的实验写法。

图 45:代码截图展示比较函数如何接收数组中的两个值。

图 46:截图保留了三类返回值对应的比较结果。

实验进一步关注 PHP 5.6 的变长参数语法 ...usort 在该版本中可以接收数量可变的参数,代码把 $_GET 中的数组和值传入函数。第一个参数是包含 testphpinfo 的数组,第二个参数是 assert,于是回调过程会依次把数组元素交给 assert

图 47:截图说明 ... 代表参数数量可变,实验限定在 PHP 5.6 环境。

图 48:截图展示了 GET 参数进入 usort 的位置。

图 49:代码图保留了数组参数位于第一个位置的结构。

图 50:截图展示了用于比较的两个数组元素。

图 51:截图记录了回调案例仍在 PHP 5.6 本地环境中验证。

为了观察请求如何进入回调函数,课程把请求抓到重放模块。原始请求使用 GET,重放时改成 POST,并在请求中补上 usort 所需的参数。PHP 允许同一个请求同时携带 GET 和 POST 数据:POST 用来承载入口参数,GET 仍可被代码读取。

图 52:截图展示了抓包前的本地访问地址和请求准备。

图 53:抓包界面展示了请求进入重放模块的过程。

图 54:截图记录了重放时修改请求方法的操作。

图 55:代码截图展示了 POST 参数与 GET 参数的配合位置。

图 56:截图对应回调函数的两个核心参数。

图 57:代码图显示数组元素将按顺序传入回调函数。

回调链的执行顺序可以还原为:请求先进入外层执行入口;usort 读取变长参数;数组 [test, phpinfo] 作为待比较数据;assert 作为比较函数;test 执行时没有明显结果,phpinfo 执行时才出现可观察输出。这个顺序解释了为什么同一请求需要同时准备数组和回调函数,而不是简单提交一个字符串。

9. 语法错误与修正:eval 代码必须有分号

首次重放回调请求时,错误落在 eval 附近,原因是传入的 PHP 代码缺少分号。课程反复强调,eval 接收的是 PHP 代码字符串,代码语法仍需完整;即使调用链已经正确,少一个分号也会让执行在语法解析阶段失败。

图 58:截图展示了错误发生在语法解析阶段,而不是回调参数读取阶段。

图 59:截图记录了修正分号后请求继续执行的结果。

修正后,执行顺序与前面的推理一致:先进入 eval,再得到回调函数,最后依次处理数组中的元素。课程据此把这个案例归纳为三个条件的组合:PHP 5.6 的变长参数、usort 提供的回调能力,以及回调函数中可被执行的表达式。

图 60:总结图把本案例拆成变长参数、usort 回调和代码执行三个组成部分。

讲师在这一段的经验判断是:代码短并不代表逻辑简单。如果只看见 usort(...$_GET) 而不理解参数展开、数组元素传递和比较函数回调,就很难还原它的执行过程。基础语法、错误信息和请求结构必须结合起来看,才能判断一个看似普通的排序函数为什么会形成复杂调用链。

10. 输入长度从十七字符压缩到七字符

前面的案例仍有较长的参数和函数名。课程随后引入新的限制:可用输入从十七个字符缩短到七个字符。长度一旦降到七个字符,前面依赖较长函数名的方案大多无法直接使用,问题从"能否执行"转为"如何在有限字符内保留必要的执行链"。

图 61:截图保留了课程中从常规代码链路转向短字符限制的过渡。

图 62:代码截图显示现有函数名和参数组合已经超过长度上限。

图 63:截图继续标记了七字符限制下可用空间的变化。

图 64:示意图展示了长度减少后需要重新寻找执行入口的原因。

课程没有立即给出"七字符一定可行"的结论,而是指出前面三种常规绕过方式大多被长度限制淘汰,需要转向文件上传产生的临时文件。这个方向的前提仍然是授权实验:验证 PHP 文件上传生命周期和临时文件内容,不是为未知目标设计攻击路径。

11. 为什么观察 PHP 文件上传临时文件

在 Linux 环境中,课程先确认 PHP 上传文件的临时目录。页面代码使用 POST,但临时文件的生成发生在 PHP 解析请求的过程中,即使业务代码没有显式读取该参数,也可能已经完成了上传数据的暂存。临时文件通常在请求处理结束后被 PHP 回收,因此平时很难在目录中直接看到。

图 65:终端截图展示了实验环境里对系统临时目录的检查。

图 66:代码截图记录了为观察文件生命周期而加入的 sleep(30) 思路。

图 67:界面截图展示了重新发起 POST 请求的步骤。

课程特别验证了一点:页面是否读取 POST 参数,并不决定 PHP 是否先生成上传临时文件。只要请求按照文件上传格式到达 PHP,运行时就可能先创建临时文件,业务代码可以不接收该字段,区别只是后续没有业务结果。

图 68:截图说明页面没有对应业务处理,也不妨碍 PHP 先处理上传数据。

图 69:终端截图记录了提交后检查临时文件是否出现的过程。

第一次检查没有立即看到文件。课程没有据此判断"临时文件不存在",而是转向检查 PHP 配置和系统临时目录。Ubuntu 环境的目录结构、运行用户和临时目录设置可能与预期不同,单次 ls 没有结果只能说明当前观察窗口没有捕获到文件。

图 70:终端输出展示了第一次查找未发现目标文件的情况。

图 71:截图展示了从 PHP 配置文件中查找上传临时路径。

图 72:终端截图用于比较系统临时目录和 PHP 运行时配置。

重新提交后仍没有稳定捕获到临时文件,课程转而记录其预期命名格式:文件名通常以 php 开头,后面接随机字符。截图中的随机串只是一次实验结果,不能当作固定值;它的意义在于说明匹配时不能依赖完整文件名。

图 73:代码编辑器中准备了一个简单文本文件作为上传内容。

图 74:截图展示了测试文件中用于确认内容写入的最小代码。

图 75:终端截图记录了上传请求与临时目录检查同时进行的过程。

12. 从临时文件内容推导短输入匹配思路

当临时文件能够被观察到后,问题被拆成两个条件。第一,临时文件内容由上传者决定,因此可以在授权实验中放入课程准备的测试文本;第二,临时文件名包含固定前缀和随机部分,不能写死完整名称。课程据此讨论了用短模式匹配临时文件,再把匹配结果交给已有执行链的思路。

图 76:代码截图展示了从临时目录开始构造匹配条件的实验。

图 77:截图记录了模式长度与随机文件名之间的关系。

图 78:代码图展示了把不同请求参数组合进匹配逻辑的方向。

文件上传请求本身有完整的数据格式,上传文件内容位于 multipart 数据包中。课程准备一个普通文本文件,抓取上传请求,再把整个请求送到重放模块中分析。这里关注的是请求格式和临时文件生命周期,仍然限定在本地靶场。

图 79:截图说明上传内容会成为临时文件的文件内容。

图 80:代码截图展示了匹配到临时文件后再交给执行组件的理论链路。

课程在这里给出一个谨慎结论:理论链路可以打通,但理论成立不代表实践必然成功。 真实复现还受 PHP 版本、运行用户、临时目录、回收时机、上传格式和服务配置影响。讲师还联系到前一日 filter 伪协议实验中"文件写不进去"的经历,说明即使备课时没有遇到权限或进程限制,现场环境也可能暴露新的失败原因。

13. 抓取文件上传数据包并验证临时文件出现

为了验证文件上传请求的具体格式,课程重新打开上传页面,选择一个普通的 .txt 文件,先让浏览器生成 multipart 请求,再将请求发送到重放模块。上传字段名称必须前后一致:表单中的字段名、请求体中的 name 属性和重放时保留的字段名只要有一个字母不同,上传就可能失败。

图 81:示意图展示了短模式与上传字段之间的对应关系。

图 82:终端截图记录了上传测试前的请求准备。

图 83:截图展示了浏览器提交文件后生成的上传数据包。

图 84:重放界面保留了上传请求的原始结构。

图 85:截图记录了把抓到的请求复制到重放窗口的操作。

图 86:界面截图展示了重放前核对字段名称的步骤。

图 87:截图强调表单字段和请求字段必须使用相同名称。

图 88:请求界面展示了字段不一致时的失败风险。

上传请求中可以删除与当前验证无关的多余字段,保留最小可用结构。课程同时指出,服务器端页面是否声明接收文件并不改变 PHP 先生成临时文件的事实;区别只在于业务代码是否继续处理这个文件。

图 89:截图展示了删除非必要字段后的上传请求。

图 90:请求截图保留了文件内容与边界标记的对应关系。

最后一次验证没有立即执行整条链路,而是先观察临时文件是否出现。这样做可以把"上传是否成功"和"后续匹配是否成功"拆开,避免两个问题同时失败却无法定位。

图 91:截图展示了上传提交后暂停观察的状态。

图 92:终端截图记录了临时文件生成时的文件名形态。

在观察窗口结束后,临时文件消失。课程解释这是 PHP 的正常回收流程:请求处理完成后,临时文件会被删除,通常不会保留到下一次目录检查。因此验证窗口必须覆盖"文件已生成但请求尚未结束"的时间段,不能等程序结束后才寻找。

图 93:最终截图展示了临时文件消失后的目录状态,说明回收机制已经生效。

14. 讲师经验与方法总结

这组实验的价值不在于记住某个固定字符串,而在于建立一条可复用的排查顺序。首先确认错误可见,再根据每次错误减少或变化判断参数是否进入下一层;其次固定代码和请求格式,只切换 PHP 版本;再次用抓包比较成功与失败请求,而不是把编码当成"魔法开关";最后把复杂链路拆成函数、参数、回调和生命周期四类因素。

课程中的多个失败都说明了同一件事:成功结果只能说明某个条件组合在当前环境下成立,不能自动解释原因。 一键连接在未编码时失败、加入 Base64 后成功,只有抓包并解码后才能看出函数调用顺序;PHP 5.6 能运行而 PHP 7.3 报错,只有对照语言构造器和普通函数的差异才能解释;临时文件第一次未被看到,也不能直接推出"没有生成",还要核对目录、回收时机和观察窗口。

讲师还强调基础能力的重要性。面对短小的后门或回调代码,若不了解 $_GET$_POST、变长参数、usort 回调、evalbase64_decode 和 PHP 版本变化,很难判断真实执行顺序。AI 可以帮助解释陌生代码,但不能替代抓包、版本确认和现场验证。面试或实战复盘时,应同时说明尝试过什么、返回了什么、为什么失败、如何排除,以及最终结论适用于哪个版本和授权环境。

按证据推进,而不是按结果倒推

这次实验还提供了一套更细的证据链。配置阶段的证据是 display_errors 已打开、FPM 已重启;请求阶段的证据是 500 错误从两个未定义参数减少到一个动态调用错误;版本阶段的证据是同一组参数在 PHP 7.3 与 PHP 5.6.9 中出现不同结果;抓包阶段的证据是默认值与 Base64 值对应两种不同的请求体;文件阶段的证据则是临时文件在请求未结束时出现、结束后消失。每一阶段都只回答一个问题,避免把不同层面的现象混在一起。

如果页面没有返回错误,排查顺序也应保持克制。首先确认当前请求确实到达了测试页面,其次确认配置改动对应正在运行的 PHP 进程,再检查 FPM 是否已重启。如果错误出现,则先读错误本身,不要立刻修改多个参数。课程中补入第一个参数后错误数量减少,正是因为这个变化提供了明确证据;如果同时修改函数名、请求方法和编码方式,就无法判断到底是哪一个变化带来了结果。

版本切换也需要保留边界。Ubuntu 上的 PHP 软件源、FPM 包和扩展依赖属于环境条件,不能把"能安装 5.6"误写成"所有系统都能无冲突安装 5.6"。课程发现完整依赖可能影响现有 7.3,于是改用 Windows 的 PHPStudy 做隔离测试。这个决定没有改变案例对象,反而让对照实验更可控:入口、参数和观察目标保持不变,只有 PHP 运行版本发生变化。

抓包分析时,默认请求和编码请求必须成对保存。默认请求用于确认没有额外编码时服务器收到什么;编码请求用于确认工具所谓"成功"到底改变了哪些字段。把编码值直接粘贴到解码器失败,说明外围存在非 Base64 字符;提取字段后得到与盘符遍历相同的代码,才证明两次请求的业务内容相关。这个过程不能用一句"Base64 解码后成功"代替,因为真正的转折点在于 eval 是否先执行了解码函数。

回调案例的排查同样可以拆成独立验证。先确认 usort 接收了数组,再确认数组中有两个元素;接着确认第二个参数是 assert,最后观察两个元素依次进入回调时的结果。第一次报语法错误时,回调关系其实已经成立,失败原因只在传入 eval 的 PHP 字符串缺少分号。补充分号后请求继续执行,说明前面的参数展开和回调推理没有被推翻。

临时文件案例则要求把"目录没看到文件"和"没有生成文件"严格区分。文件可能位于 PHP 配置指定的目录,也可能因为请求已经结束而被回收;sleep(30) 的作用是延长观察窗口,而不是改变 PHP 的上传机制。即便页面代码不读取 POST 字段,PHP 仍可能先处理 multipart 数据并创建临时文件。只有在请求暂停期间看到带 php 前缀和随机字符的文件,才能确认观察到了运行时的中间状态。

最后,七字符限制改变的是工程约束,不是语言语义。前面较长的 asserteval 或回调组合无法原样搬到七字符空间,因此课程把注意力转到可由运行时自动生成的临时文件名。这个思路仍需经历字段名一致性、上传包格式、临时目录和回收时机等验证;只要任意一个条件不成立,理论上的匹配链就不能被写成已经成功。对外发布时保留这些限制,比只展示最终截图更能说明复盘价值。

复盘结论

本次课程按照"配置可见性 → 参数错误 → PHP 版本差异 → 抓包对比 → 函数与语言构造器 → 回调链 → 短输入限制 → 临时文件生命周期"的顺序推进。结论不是一个脱离环境的通用利用方案,而是几条受实验事实支持的判断:

  1. display_errors 和 FPM 重启决定了后续错误是否可观察。

  2. 参数补齐后,错误变化可以用来判断程序进入了哪一层。

  3. PHP 5.6 与 7.3 对动态 assert 调用的处理不同,版本必须作为复现条件记录。

  4. eval 是语言构造器,不能按普通函数名动态调用;assert 在不同版本中的类别变化会直接影响结果。

  5. usort 的变长参数和回调机制可以形成复杂执行链,但语法细节如分号仍决定最终成败。

  6. 文件上传临时文件由 PHP 运行时管理,生成和删除之间存在很短的观察窗口;第一次没有看到文件并不等于文件没有生成。

  7. 理论推导、抓包结果和实际复现必须分开记录,任何"看起来能行"的方案都需要在授权环境中再次验证。

相关推荐
sbjdhjd1 小时前
从 7 字符文件名拼接到二维数组取值:PHP 无参函数限制下的受控靶场复盘 | (7字符绕过)
网络·nginx·安全·网络安全·云计算·php·apache
haon11221 小时前
树模型在信贷风控怎么用——从决策树到 XGBoost
大数据·人工智能·算法·决策树·机器学习·数据挖掘
浅安的邂逅1 小时前
260910-DeepSeek 发布 V4.1 Flash:1M 上下文、552B MoE,KV 缓存降到上一代 1/4,同步开源
人工智能·开源·ai大模型·deepseek·ai日报
Mr.朱鹏1 小时前
科技周报(第2026-09-10期):模型激战量子降温
人工智能·科技·chatgpt·机器人·开源
万物智能信息科技2 小时前
LVDS屏幕输出桌面—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
人工智能·华为·开源·harmonyos·鸿蒙
隐擎fox2 小时前
网络传输安全剖析:WebRTC 本地真实 IP 泄漏机制(STUN/TURN)与 Python 防护检测实战
网络·网络协议·tcp/ip·安全·webrtc
fengkai45452 小时前
七、华为云网络综合实验 & 运维类及其他服务总结
服务器·开发语言·php
vipjx12 小时前
百度网盘PanDownload最新版评测:搭配直链助手实现高速传输
开源
PHP实战开发录2 小时前
PHP日志写满磁盘后接口为什么异常
开发语言·系统架构·php·开发