当时我点开 ShowCtf 平台的 Web14 题,标题“不会编程的崽”差点让我直接关掉页面——这说的不就是我么。作为一个真·没有系统学过编程的菜鸟,我对 Web 安全的期待本来只是“看别人拿 flag”,结果没想到最后我也拿到了。ShowCtf 的 Web 方向题目很多都是这种“不需要写代码也能解”的入门靶场,适合那些想进 CTF 夺旗赛但又觉得自己编程太弱的人。这篇文章把我从点开题目到拿到 flag 的完整过程,包括中途翻车的五次请求,全部写下来。你要是也觉得自己编程不行,这道题的经验应该能给你一点信心。
1. 不做“编程大佬”也能打 Web CTF:我和 ShowCtf Web14 的初遇
1.1 题面像在嘲讽我:第一眼看到的东西
ShowCtf 是一个以训练为主的 CTF 平台,里面分了不少方向:Web、Pwn、Reverse、Crypto、Misc 之类的。我第一次点进 Web 方向,第一眼看到的就是 Web14 这道题,标题叫“不会编程的崽”。说实话,那一刻我真觉得出题人是不是远程安了个摄像头,精准把握了我的编程水平。题面只有一句话:“你不需要会编程,但你得会看代码。”下面是一个输入框和一个按钮,旁边还有一行小字:“flag 就在服务器上,看看你能不能把它带回来。”
我当时连 PHP 和 HTML 的关系都分不太清,脑子里全是问号。但 Web 题有个好处——不需要装一堆环境,浏览器就是全部工具。我深吸一口气,告诉自己:就算不会编程,至少我会点右键。CTF 的题目环境都是靶场,是官方故意搭起来让大家练手的,不是真实系统,所以你可以随便试、随便点,出了问题也不会有任何实际影响。这个心态很重要,因为一旦你担心“乱点会不会把东西搞坏”,你就不敢去碰那些真正藏有线索的地方了。
1.2 为什么 Web 题适合“编程能力弱”的人入门
后来复盘我才想明白,Web 方向对新人友好是有原因的。一条典型的 Web 题解题链路是这样的:打开页面、看前端代码、看网络请求、找到后端参数、想办法让后端逻辑出错,最后拿到 flag。整条链路里,真正需要“编程”的环节其实非常少,更多时候你需要的是耐心和搜索能力。比如这题,核心考点其实就是一行 PHP 判断,你要做的是看懂它的逻辑,再往它不设防的地方戳一下。
相比 Pwn 要读懂汇编、Crypto 要懂数论,Web 题的门槛低在“语言是给人看的”。很多靶场的源码注释里就直接写着提示,相当于出题人把答案的一半放在你面前,只看你愿不愿意点开。我当时就靠“会看注释”这一点,拿到了第二波线索。当然,不编程能入门不代表永远不用学编程,这个我们放到最后再说。至少第一道题,我证明了一件事——一个不会编程的崽,也能把 flag 拿回来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一波线索藏在“看不到代码”的地方:F12、注释与备份文件
2.1 右键“查看源代码”看到的注释
面对那个孤零零的输入框,我没有急着乱填。我按了 F12,然后看页面源代码,也就是很多教程里说的 View Source。页面的 HTML 结构特别简单,一个标题、一个表单、一个按钮,然后——下面有一行注释。不出所料,注释写着:
html复制<!-- 后端源码我就放在 source.php 里了,你不会编程,但你会读源码吧? -->
看到这行我的第一个反应是:这题是真的在嘲讽我。不过嘲讽归嘲讽,线索给得也算实在。CTF 题目里,注释泄露信息是最常见的第一道开胃菜,出题人把源码路径直接写在注释里,就是为了拉新人一把。很多真实项目的漏洞也是这么被发现的,开发者把敏感路径或者备份文件名写在 HTML 注释里,上线时忘了删,就成了最好的切入点。
我顺着这个提示,在浏览器地址栏输入了题目域名下的 /source.php。注意,这里是题目自己的域名,是 CTF 靶场环境,不是别人的真实网站,放心访问。按下回车后,源码直接展示在我面前。那种感觉就像玩密室逃脱,第一道抽屉没上锁,里面放了一张地图。
2.2 顺藤摸瓜:响应头里也能藏额外信息
打开源码之前,我还做了另一件事:把开发者工具的 Network 面板打开,刷新页面,挨个看了网络请求。这不是我多专业,而是之前看别人 writeup 时记下的习惯——响应头里经常有猫腻。很多题目会把提示放在自定义响应头里,比如 X-Hint: xxx 这种字段,或者在一个看似正常的 Server 字段里夹带私货。
这次刷新下来,响应头里倒是没看到特别离谱的字段,只有一个比较显眼:X-Source-Path: source.php.bak。当时我愣了一下,心想这不是和注释重复了吗?后来才反应过来,这两个提示指向的是两个不同的东西:注释说的是 source.php,响应头说的是 source.php.bak。也就是说,源码可能不止一份,备份文件里可能是更全的版本。于是我把 .bak 也访问了一遍,发现它和 source.php 内容几乎一样,只是多了几行没用的注释。虽然这次是虚惊一场,但这个过程很值,它让我养成了一个习惯:看到一条线索之后,再找找有没有第二条殊途同归的线索。
2.3 源码本身并不长:一个 if 就是全部
最终,我拿到手的关键源码长这样:
php复制<?php
error_reporting(0);
include "flag.php";
if (isset($_GET['a']) && isset($_GET['b'])) {
if ($_GET['a'] != $_GET['b'] && md5($_GET['a']) == md5($_GET['b'])) {
echo $flag;
} else {
echo "猜错了,崽。";
}
}
?>
第一次看这堆符号我是懵的。但别慌,把不认识的部分先撇开,只抓核心。$_GET['a'] 和 $_GET['b'] 就是网址问号后面传进来的两个参数,比如 ?a=1&b=2。isset 的意思是“参数存不存在”,如果两个参数都存在,就继续判断下面那个 if。一旦 if 里面的条件成立,就输出 flag;不成立,就输出那句“猜错了,崽”。
对我来说,这已经不是“会不会编程”的问题了,而是这串代码像一个黑箱,我需要往里塞两个能骗过它的值。编程基础弱没关系,把 if 当成一道小学逻辑题就行。于是我从这里开始,正式进入这道题的解题核心。
3. 把源码当说明书读:md5 弱比较这道题眼是怎么被我看穿的
3.1 读懂 if 条件:a 不等于 b,但 md5(a) 要等于 md5(b)
现在把 if 的条件拆开来看:
php复制$_GET['a'] != $_GET['b'] && md5($_GET['a']) == md5($_GET['b'])
这个条件用大白话翻译就是:a 和 b 这两个参数不能相等,但是 a 的 md5 结果和 b 的 md5 结果要相等。第一眼看上去,这像是一个不可能完成的任务。既然 a 不等于 b,它们的内容都不一样,而 md5 是一个把任意内容变成固定长度哈希值的函数,通常来说不同的输入会得到不同的输出,怎么做到输出相同呢?
md5 是什么?你可以把它理解成一台盖章机:你递一张纸条进去,它给你盖一个 32 位的章出来。你说“你好”,它给你一个章;你说“你好啊”,它给你另一个章。正常情况下,不同的纸条不会拿到同一个章。但这里有个关键问题:章的数量是有限的,纸条的数量是无限的,所以理论上一定存在两张不同的纸条,最终拿到同一个章。这个现象就是哈希碰撞。
而这题的突破口,藏在后面那个等号上:==,不是 ===。PHP 里 == 是宽松比较,比较之前会先做类型转换;=== 是严格比较,要求值和类型都一模一样。很多经典漏洞都是从“宽松”这两个字里长出来的,理解这一点,比背一百个 payload 都管用。
3.2 0e 开头的 md5 在 PHP 眼里等于 0
我先查了 PHP 的宽松比较规则,看到一个非常经典的场景:如果两个字符串长得都像“科学计数法”,比如 0e123 和 0e456,PHP 在 == 比较时会把它们都转换成数字再比。而以 0e 开头、后面全是数字的字符串,在科学计数法里就是 0 乘以 10 的多少次方,结果永远是 0。换句话说,0e123 == 0e456 是真的,因为它们都等于 0。
那这就好办了。我只需要找到两组不同的字符串,它们的 md5 值都长成“0e 加一串数字”的样子。这样一来,两个 md5 值字符串虽然内容不同,但 PHP 一转换,两个都变成 0,比较结果就是相等。
看到这里你可能想问:这种字符串要怎么找?穷举?写脚本?——都不用。这就是“不会编程的崽”的翻盘点:这种事情早就有人做过,并且把结果整理成了一张表。你只需要去搜索引擎搜“md5 0e 碰撞表”或者“PHP 弱类型比较 CTF”,就能找到一堆现成的输入值。我甚至不需要理解这些字符串是具体怎么被找出来的,只要会用就行。这道题要考察的,根本不是你能不能从零写出碰撞算法,而是你遇到一个陌生判断条件时,知不知道去查、去哪里查。
3.3 从“知道原理”到“能用”:选两个碰撞值
经典的 0e 开头的 md5 值有不少,我最常用的两组是:
| 参数 | 输入值 | md5 结果 |
|---|---|---|
| a | QNKCDZO |
0e830400451993494058024219903391 |
