ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PICO CTF 2013 php2:从strcmp数组绕过看PHP弱类型审计入门

PICO CTF 2013 php2:从strcmp数组绕过看PHP弱类型审计入门 简介PICO CTF 2013 php2 是一份针对早期 CTF 赛题的实操资料面向 CTF 入门选手、Web 安全初学者及希望巩固 PHP 漏洞利用与防御的开发者。赛题聚焦 PHP 代码审计资料围绕完整解题过程展开帮助读者学会从入口参数出发追踪数据流识别 SQL 注入、XSS、文件包含、命令注入、文件上传校验不足等典型风险点并理解对应的修复思路。压缩包共 4 个文件总大小 18.85MB包含解题操作录屏、赛题原始 PHP 源码、技术说明文档与讲解 PPT既能对照视频重演分析过程也可从文档中快速查阅漏洞原理与防御措施。目前已有 159 人学习下载。通过这份资源读者可掌握 PHP 代码审计的基本步骤包括定位用户可控输入、查找危险函数调用、构造有效攻击载荷以及总结加固方案为后续参加同类型 CTF 题目或进行 Web 安全开发打下基础。1. 一份 2013 年的 CTF 压缩包为什么 php2 至今仍是 PHP 审计入门首选如果你手头正好有一份 PICO CTF 2013 的 php2 压缩包大概率是照着某篇 writeup 找到它的。网上关于它的记录往往就三行——一个 PHP 页面、一个密码参数、一串 flag但真正动手复现时卡点全在题目之外的工程细节上环境版本不对、解压路径错了、URL 里的方括号被终端吃掉、PHP 8 直接白屏。这份资源的价值不在于题目本身有多难而在于它把 PHP 弱类型比较、strcmp 返回值陷阱、GET 参数类型混淆三个考点浓缩在一个十几行的 index.php 里。适合刚入 CTF 的 web 方向新手用来建立代码审计的基本动作也适合写过几年 PHP 但没从安全视角审视过语言特性的后端开发。题目不大但麻雀虽小五脏俱全。2. 落地到本地环境解压 php2.rar 与 PHP 版本选型2.1 环境选型为什么我先开 phpstudy 而不是 docker复现这类题目的第一道坎是 PHP 环境。我一般会打开 phpstudy 而不是 docker原因很直接题目是 2013 年的PHP 版本跨度带来的行为差异比题目本身的考点更折磨人。phpstudy 集成了 Apache 和 PHP 5.x/7.x/8.x 多个版本解压到 WWW 目录就能跑切换版本在面板上点两下调试速度比 docker 快很多。docker 不是不行而是不划算。PICO CTF 2013 是本地文件直接访问的 PHP 页面不需要数据库、不需要端口编排、不需要容器网络。为了一个 index.php 起一个容器再把题目目录挂载进去光排查挂载路径和文件权限就能浪费半小时。常见做法是直接在 phpstudy 的 WWW 目录下新建一个 php2 文件夹把 rar 解压进去浏览器访问 http://localhost/php2/index.php 就完事。这套流程对一个十几行的题目来说足够轻量。如果你问我 XAMPP 行不行答案是行但不推荐。XAMPP 默认带的 PHP 版本通常只有一个主线切换版本要改配置文件、改 PATH比 phpstudy 的图形界面麻烦。phpstudy 这类面板对老题目的另一个友好之处是 Apache 和 Nginx 可以随时切换——有些题目依赖 .htaccess 重写规则Apache 下开个 AllowOverride 就行Nginx 下还得手动写伪静态。2013 年的题目基本没有 .htaccess 依赖但以后跑别的资源包时能省不少事。版本选择上我给两条硬建议。第一优先 PHP 7.x其次 5.6不要用 PHP 8.0 以上的版本。原因后面在 payload 部分会展开——PHP 8 对 strcmp 传数组直接抛 TypeError页面会白屏而 PHP 7 只给一个 Warning 然后照常执行这正是我们需要的绕过的定式。第二在 php.ini 里把 display_errors 打开错误显示开了你在浏览器里看到 Warning 不是坏事反而是判断代码执行路径的信号灯。Warning 和 TypeError 的差别后面避坑章会专门讲。提示phpstudy 里切换 PHP 版本后记得重启 Apache不然面板显示版本变了实际跑的进程还是旧的。这个坑我踩过不止一次。把三个版本对 strcmp 数组参数的响应放在一起看最清楚PHP 版本strcmp(x, []) 的行为结果值能否绕过5.6返回 NULL无警告NULL 0 为 true能7.4返回 NULL产生 WarningNULL 0 为 true能8.2抛 TypeError脚本中断无不能这张表看着简短实际是这份资源能不能复现的分水岭。我第一次复现时用了 PHP 8.1怎么提交都是白屏后来切到 7.4 才恍然大悟。选版本时别觉得新版一定更好老题目有自己的语言时代背景我们得顺着它来。2.2 解压后的目录结构先核对文件再碰代码解压之后别急着点开浏览器先把目录结构过一遍。我拿到一份 php2 压缩包会先在终端里看一眼文件清单。Windows 下用 dir /smacOS 或 Linux 下用 find . -type f目的是确认入口脚本到底是 index.php 还是别的文件名以及有没有附带 hint 或 readme 文件。很多 writeup 只讲 payload不讲题目还藏了提示文件但提示文件往往比源码先暴露考点。这类题目最常见的布局是这样的按题目的常见形式整理不代表每份包都一样文件 / 目录作用审计优先级index.php入口脚本接收参数并做判断最高通读hint.txt / readme.txt出题人给的提示常有 flag 格式或过滤规则高先读img/ 或 css/静态资源与逻辑无关低跳过为什么会先核对目录2013 年的题目和现在的自动化部署风格差很多那个年代的题目经常是导师手写的脚本入口文件名不一定叫 index.php有的叫 login.php有的叫 auth.php。如果只按惯性访问 index.php可能直接撞上 404然后开始怀疑人生。我一般会先把目录清单打印出来用排除法锁定接收参数的那个文件。确认入口文件后还要顺手确认一件事页面报错是关着的还是开着的。常见做法是直接在 URL 后面拼一个不存在的参数访问一次比如访问 http://localhost/php2/index.php?probe1如果页面输出 PHP Warning说明 error_reporting 开着后面调试数组 payload 时能直接看到警告文本如果页面一片空白就去 php.ini 里手动打开 display_errors 并重启服务。这一步是后面所有调试的前提。最后一步是确认环境真的通了。我会在 php2 目录里放一个临时 probe.php内容就一行?php phpinfo(); ?然后在浏览器访问它确认页面能出 phpinfo 再删掉。原因很简单如果这个最基础的文件都跑不起来说明问题在 Apache 或 PHP 配置根本还没轮到题目本身。这套「先探活、再审计」的顺序能帮你把环境问题、题目问题、payload 问题三者隔离不会混在一起排查。顺着这个思路把 URL 和文件路径的对应关系也捋清楚WWW 目录是 web 根php2 子目录对应路径 /php2/index.php 对应 /php2/index.php。访问时大小写也要注意Linux 服务器区分大小写本地如果用的是 Windows 倒不区分如果在 macOS 上复现目录名必须和 URL 完全一致否则直接 404。这种底层的路径对应关系新手最容易漏漏了之后所有调试都在错误目录里打转。3. 进入代码审计index.php 里的 strcmp 与松散比较3.1 入口代码拆解一个写死的密码变量跑通环境之后就该打开 index.php 看源码了。这类题目核心代码通常不长十几行就能读完。我在这类资源包里见到最多的写法是页面定义了一个密码变量用 strcmp 和用户输入比较相等就放行。下面是一份典型的示意代码和 2013 年前后的题目风格一致?php $password Th1s_1s_A_Fake_Flag; // 出题人写死的密码 if (isset($_GET[password])) { if (strcmp($password, $_GET[password]) 0) { echo Access granted: the flag is ...; } else { echo Access denied; } } else { echo Please provide ?passwordxxx; } ?逻辑说明在代码之后核心只有两个点。一是 isset 检查确保 URL 里有 password 这个参数才进入比较二是strcmp($password, $_GET[password]) 0strcmp 的返回约定是两个字符串相等时返回 0第一个参数大时返回正数小时返回负数。所以外层判断把它和 0 做松散比较等于 0 就算通过。参数说明$_GET[password]来自 URL 查询串比如访问 http://localhost/php2/index.php?passwordabc那么$_GET[password]的值就是字符串 abc。如果不传 passwordisset 为 false走 else 分支页面只会提示给参数看不到任何判断逻辑。这一步看来废话但它决定了后面 payload 的写法——所有尝试都要挂在 password 这个键上。新手最容易犯的错是盯着 strcmp 本身看以为考点是字符串比较算法。实际上考点藏在这个运算符上是松散比较比较前会做类型转换才是严格比较要求类型和值都相等。题目故意用就是为了给类型混淆留后门。把换成这道题就死了这个翻车案例我放到避坑章再细说。3.2 考点定位松散比较到底松在哪PHP 的松散比较表是这道题的底层逻辑。动手前先把这张表过一遍它解释了为什么数字字符串能混过判断也解释了为什么有些老 writeup 在新时代下失效比较PHP 5 结果PHP 7 结果0 0truetrue0 abctrueabc 转数字 0false1e3 1000truetrueNULL 0truetrue这张表的前三行揭示了 PHP 在比较字符串和数字时的转换规则字符串如果能转换成数字就按数字比较不能转换时PHP 5 和 PHP 7 走了不同的路径。很多早期 PHP CTF 题围绕这个差异出题不过 php2 这道题的正统解法不靠这个它靠的是 strcmp 碰到数组时的特殊返回。这里你要记住的一点是NULL 0在松散比较下为 true这是后面整条攻击链的基石。把注意力拉回 strcmp。PHP 手册对 strcmp 的签名要求两个参数都是字符串但当第二个参数$_GET[password]被传成数组时函数不会去按字符串比较——它直接返回 NULL并抛一个 Warning。NULL 和 0 做松散比较时PHP 把 NULL 视为 false 值而 0 在布尔上下文里也是 false于是NULL 0成立放行。整个攻击链只有两步把 password 从字符串变成数组然后让数组避开 strcmp 的正常比较路径。到这里考点就清楚了题目考的不是「猜密码」而是「制造类型混淆」。出题人写死密码字符串你不可能猜中但你可以改变输入参数的类型让 strcmp 不再执行真正的字符串比较。这个思路能延伸到很多现代 PHP 题目里——凡是代码里出现函数参数直接取自$_GET或$_POST且没有is_string校验都值得往类型混淆方向试一下。顺带辨析一个高发误区很多人会把 strcmp 题和 MD5 题混在一起。MD5 题是md5($_GET[a]) md5($_GET[b])靠的是 md5 返回值参与松散比较和数组传参使 md5 返回 NULL而 strcmp 题靠的是函数自身对数组参数返回 NULL。两者最后的 payload 都长成[]1但应对的校验代码完全不同。如果题目把 strcmp 换成 hash_equals数组绕过直接失效必须回到别的攻击路径。这些区别在后续避坑章会落到具体现象上。还有一种常用的审计辅助手段在本地复制一份 index.php在比较前插入一行var_dump(strcmp($password, $_GET[password]));然后退出。这样能把函数真实返回值打出来亲眼确认它到底是 0 还是 NULL避免被页面上的「Access denied」带偏判断。这是审计任何函数型考点都通用的手法——先看函数返回什么再谈怎么绕。如果你拿到的压缩包里没给源码只有线上地址也没关系。常见做法是先按?password[]1试一次看页面是报 Warning 还是直接放行再按?passwordadmin试一次确认存在 Access denied 响应。两个响应对比一眼就能判断考点属于 strcmp 型还是纯字符串比较型。这种「黑盒两步探测」是我在 CTF 里验证 PHP 考点的默认动作比打开审计工具扫描快得多。4. 构造 payload数组绕过与 URL 编码细节4.1 数组绕过的原理一个 Warning 换一个放行现在把 payload 落到具体参数上。目标很单纯让 strcmp 的第二个参数变成一个数组。PHP 的$_GET解析规则天然支持这一点——URL 里出现password[]1时PHP 会把 password 解析成数组数组里只有一个元素 1。也就是说你不需要任何工具浏览器地址栏就能构造这个类型的输入payload 的门槛比想象中低。当strcmp($password, [1])执行时PHP 内部发现第二个参数不是字符串不会去做逐字节比较而是返回 NULL。同时错误系统抛出一条 Warningstrcmp() expects parameter 2 to be string, array given。关键点在于Warning 只是警告PHP 默认继续执行后面的代码于是NULL 0成立页面进入放行分支。这就是「用一个 Warning 换一个放行」的由来。我建议你在本地把 display_errors 打开亲眼看一次这条 Warning。很多人第一次复现时被浏览器上的黄色警告吓到以为失败了其实那是成功的标志。当你能区分「致命的 TypeError」和「警告性 Warning」的时候PHP 审计就算入门了。这条经验在后面所有 PHP 题目里都成立不管题目升级成什么形态报错信息的类型永远是第一手线索。注意PHP 8 里同样的请求会变成 TypeError 并中断脚本页面直接白屏。这不是 payload 写错了是语言版本把「警告」升级成了「致命错误」。解决方案只有一个——换到 PHP 7.x 或 5.6别在 8.x 上死磕死磕解决不了语言演进带来的破坏性变更。4.2 完整请求示例与参数说明下面给三种提交方式按你的调试习惯选一种。第一种是浏览器地址栏最直接http://localhost/php2/index.php?password[]1浏览器会自动把方括号放进查询串PHP 收到后解析为数组。这里有一个编码细节把 URL 复制到别处时方括号有时会被转成%5B和%5D写成http://localhost/php2/index.php?password%5B%5D1两种写法 PHP 都能正确解析但前者更直观后者在 Burp 或写脚本时更安全能避免某些中间设备把方括号当非法字符处理。如果你发现某种写法下页面没有反应先用另一种写法交叉验证排除编码干扰。第二种是 curl适合验证命令和写自动化脚本curl -v http://localhost/php2/index.php?password[]1逻辑说明curl 不加 -d 时默认走 GET查询串原样携带。加 -v 是为了看 HTTP 状态码和响应头——如果页面返回 200 且响应体里有 Access granted说明放行成功如果返回 302 或 500再去查服务器的访问日志看是被重定向了还是 PHP 进程崩了。参数说明password[]1中的 1 可以是任意值重点是方括号这个语法标记它决定了参数类型是数组写成password[]x效果完全一样值本身不参与比较。第三种是 Burp Suite 的 Repeater适合实战环境。在请求行里写GET /php2/index.php?password[]1 HTTP/1.1Host 填 localhost。在 Repeater 里还能顺手观察响应时间差异——如果出题方在放行分支里加了 sleep 或耗时逻辑响应时间会是重要的旁路信号。这道题本身没有这种设计但养成看响应时间差的习惯不吃亏很多盲注题的核心判断依据就是时间差。提交之后的预期结果分两种情况页面输出 Access granted 和 flag说明整条链路通了如果还是 Access denied别急着改 payload先按避坑章的排查顺序走。这里我不建议继续盲试passwordflag之类的字符串因为写死密码的题目里暴力猜中的概率约等于零问题一定出在链路的某个环节上而不是密码没猜对。有些魔改版题目会在入口加一道过滤比如禁止参数名里出现[]。绕过也不复杂常见做法是换一个数据类型制造差异有的版本用password0配合代码里的if ($password $_GET[password])通过弱类型直接绕有的版本利用strcmp的第二个参数传对象。做题顺序永远是「先确认比较函数再确认操作符最后选输入类型」。如果资源包里遇到带过滤版本的 php2先看过滤代码写在判断之前还是之后——很多过滤只挡了字符串没挡数组payload 原样就能过。再填一个真实踩过的编码坑在浏览器地址栏手输?password[]1没问题但从终端复制到浏览器、或从浏览器复制到终端时有的系统会自动转义更隐蔽的是 Windows 的 cmd 终端会把[当通配符处理curl 在 cmd 里执行时会去匹配同名文件导致请求失败。解决方法一是加双引号包住整个 URL二是干脆用 POST 方式提交既躲开方括号被 shell 解析的问题又能顺便看一眼$_POST和$_GET在题目里的差别。5. 避坑与常见问题复现 php2 的四次翻车记录5.1 翻车一解压后目录乱码页面直接 404现象把 php2.rar 用系统自带压缩工具解压后目录名变成一串乱码访问 http://localhost/php2/ 直接 404换成各种猜测路径也打不开浏览器始终提示文件不存在。原因这个压缩包生成于 2013 年前后当时的打包工具和现在的 UTF-8 环境不兼容压缩包内部的文件名和注释用的是本地编码常见于 GBK 系系统现代解压工具默认按 UTF-8 解码两边一对就乱码了。另外很多人解压时是双击直接释放到压缩包当前文件夹导致实际结构变成 php2/php2/index.php多套了一层目录访问路径自然对不上。解决换 WinRAR 或 7-Zip 解压在解压选项里指定按 UTF-8 处理文件名解压后立刻在文件管理器和终端里各确认一次目录层级确保 index.php 就在访问路径的根目录下。我在本地会把解压出来的文件夹强制重命名为纯英文的 php2省掉后续所有编码麻烦。这种「先重命名、再启动服务」的顺序在处理老资源包时永远不会错。5.2 翻车二PHP 8.x 白屏payload 怎么传都没反应现象照着 writeup 提交?password[]1页面白屏什么都不显示。关掉 display_errors 后更惨连错在哪都不知道浏览器控制台里也只有一堆 500 状态码。原因PHP 8.0 起把 strcmp 的数组参数从 Warning 升级为 TypeError脚本直接终止。网上流传的 writeup 大多是 2015 到 2018 年之间写的当时默认环境是 PHP 5.x 或 7.x作者根本没想到语言版本升级会改变函数行为。这不是题目变了是运行时的语言规格变了。解决在 phpstudy 里切到 PHP 7.4重启 Apache 再试。如果项目必须用 8.x就不要用 strcmp 数组绕过改用?password0配合弱类型比较前提是题目代码里用的是而不是或者找别的攻击入口。我的习惯是本地专门留一个 PHP 7.4 的站点来跑 2015 年以前的题目跟现代项目彻底隔离互不污染。这样老题目的复现成功率会高非常多。5.3 翻车三cmd 里 curl 请求失败提示找不到文件现象在 Windows cmd 里执行curl http://localhost/php2/index.php?password[]1curl 报错说找不到文件或语法错误而不是返回页面内容换到 PowerShell 里好像好一点但时好时坏。原因cmd 把[和]当作通配符去匹配磁盘文件URL 里的方括号被本地 shell 吃掉了一部分。这个问题在 PowerShell 里较少出现在 cmd 里非常典型。很多新手在这步开始怀疑 payload 写错了其实 payload 一点问题都没有是终端在捣乱。解决Windows 下给 URL 加双引号仍然可能不够保险做法是改用 POST 形式curl --data-urlencode password[]1 http://localhost/php2/index.php逻辑说明--data-urlencode会把password[]1完整做一次 URL 编码后再提交方括号变成%5B和%5D彻底躲开 cmd 的通配符解析。参数说明这里明确告诉 curl 用 POST 方法提交后端$_GET[password]收不到这个参数所以如果题目读的是$_GET这个命令会失败——先确认题目入口是 GET 还是 POST 再选参数传递方式盲目的 POST 同样会翻车。5.4 翻车四顺手把 改成 题目直接失效现象本地调试时觉得不够严谨把判断条件顺手改成了strcmp(...) 0然后任何人提交都绕不过去包括 writeup 里验证过的数组 payload页面永远返回 Access denied。原因是严格比较要求两边类型和值都相同。strcmp 返回 NULL 时NULL 0为 false因为 NULL 是 null 类型0 是整数只有NULL 0这种松散比较才会把二者都视为 false 值。数组绕过整个建立在的松散类型转换上一旦换成这道题的漏洞就被「修好」了payload 自然全部失效。解决审计题目代码时把、、strcmp、hash_equals这几个关键字圈出来逐一确认再动手。给题目的任何「顺手优化」都可能改变漏洞语义如果为了本地验证确实要改代码也要先跑通原版再改而不是先改后跑。从那以后我拿到 PHP 审计题的第一件事就是先把所有操作符标记出来确认哪个是松散比较、哪个是严格比较这决定了后续所有攻击路径的可行性。6. 验证与扩展把一次巧合变成可复用的审计习惯复现到最后我建议你做一次独立的验证而不是看到 Access granted 就关掉页面。用一个本地 test.php 把 strcmp 的行为直接打出来?php $password local_test_flag; var_dump(strcmp($password, local_test_flag)); // int(0) var_dump(strcmp($password, [x])); // NULL Warning var_dump(strcmp($password, [x]) 0); // bool(true) ?这个文件在 PHP 7.4 环境下跑完你会更理解刚才那几步不是玄学而是返回值的类型转换。前两行的输出差异就是整道题成立的基础字符串相等时返回 int(0)数组传入时返回 NULL而NULL 0在松散比较下为 true。亲眼看到这三个输出比背十遍 writeup 都管用。验证通过后把经验沉淀成一套 check list拿到任何 PHP CTF 题目先过三关。第一关看入口文件里有没有直接可用的输出函数比如 echo、print_r、var_dump它们决定了漏洞能不能被观测到。第二关标记所有比较操作符和比较函数区分松散与严格尤其注意、、strcmp、in_array的默认行为差异。第三关凡是函数参数来自$_GET或$_POST且没有类型校验就尝试传入数组、对象、布尔值观察响应差异。php2 只是这个流程的最小样本同样的套路迁移到文件包含、反序列化这些后来的 PHP 考点上一样成立——原理都是先找输入、再找比较、最后制造类型差异。我第一次复现这道题时也翻了车以为是环境版本问题折腾了一下午后来才发现是自己在 cmd 里忘了处理方括号同一份 payload 在浏览器里十秒就过了。从那以后我每次跑老 CTF 题目都强制走一遍「环境版本 → 目录结构 → 操作符标记 → payload 编码」四步再没在环境上耽误过时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表