极客大挑战 2023 · Web easy_php —— Writeup
| 项目 | 内容 |
|---|---|
| 靶场 | 极客大挑战 2023(GEEK 2023)Web 题 easy_php |
| 题型 | Web / PHP 源码审计 · 多关卡逻辑绕过 |
| 目标 | http://80-ce718975-a4ad-4e08-ad97-d2280287b3d2.challenge.ctfplus.cn/ |
| 入口 | GET 参数 syc、lover;POST 参数 qw、yxx、SYC_GEEK.2023 |
| 服务端 | Apache/2.4.10 (Debian),实测 PHP 5.x(intval 表现为 PHP 5 特性) |
| 工具 | Burp Suite Community v2026.8(新版 UI,Repeater / Decoder / Comparer) |
| 结果 | 拿到 GEEK{b8ff48c3-018c-448f-a9ae-6671266c7b0d} |
平台下发的实例是临时的。
一、题目:源码全文
打开靶机,页面直接把源码打印了出来。核心是四层嵌套 if:
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__FILE__);
include_once('flag.php');
if(isset($_GET['syc'])&&preg_match('/^Welcome to GEEK 2023!$/i', $_GET['syc']) && $_GET['syc'] !== 'Welcome to GEEK 2023!') {
if (intval($_GET['lover']) < 2023 && intval($_GET['lover'] + 1) > 2024) {
if (isset($_POST['qw']) && $_POST['yxx']) {
$array1 = (string)$_POST['qw'];
$array2 = (string)$_POST['yxx'];
if (sha1($array1) === sha1($array2)) {
if (isset($_POST['SYC_GEEK.2023'])&&($_POST['SYC_GEEK.2023']="Happy to see you!")) {
echo $flag;
} else {
echo "再绕最后一步吧";
}
} else {
echo "好哩,快拿到flag啦";
}
} else {
echo "这里绕不过去,QW可不答应了哈";
}
} else {
echo "嘿嘿嘿,你别急啊";
}
}else {
echo "不会吧不会吧,不会第一步就卡住了吧,yxx会瞧不起你的!";
}
?>
二、源码逐行翻译
2.1 开头四行 —— “舞台布置”
| 行 | 代码 | 大白话 |
|---|---|---|
| 2 | header('Content-type:text/html;charset=utf-8'); | 给浏览器发个响应头:内容是 HTML、按 UTF-8 解码。只影响"怎么显示",不影响逻辑。 |
| 3 | error_reporting(0); | 关掉所有 PHP 报错和警告。后面 intval("1e4")、数组转字符串都会产生 warning,但页面上一句都不会显示,脚本也不会中断 —— 所以别指望靠报错找线索。 |
| 5 | highlight_file(__FILE__); | 把当前这个 PHP 文件自己的源码高亮打印到页面上。→ 这就是"页面直接把源码给你"的原因,题目是 100% 白盒。 |
| 6 | include_once('flag.php'); | 引入同目录的 flag.php(里面定义了变量 $flag),_once 表示只引一次。→ flag 早就在内存里了,只是没被打印;我们要做的是让脚本走到 echo $flag; 那一行。 |
2.2 第 1 关 · 参数 syc
| 片段 | 代码 | 大白话 |
|---|---|---|
| 存在性 | isset($_GET['syc']) | URL 查询串里必须带 syc 这个参数 |
| 正则 | preg_match('/^Welcome to GEEK 2023!$/i', $_GET['syc']) | syc 的值必须能匹配这个正则。^…$ 是整串锚定,结尾的 /i 表示不区分大小写 |
| 反条件 | $_GET['syc'] !== 'Welcome to GEEK 2023!' | 但值又必须严格不等于原文(!== 同时比类型和值) |
→ 两个条件取交集:改任意一个字母的大小写即可。
2.3 第 2 关 · 参数 lover
| 片段 | 代码 | 大白话 |
|---|---|---|
| 第一次转换 | intval($_GET['lover']) | 把 lover 当字符串从左往右读,读到非数字就停,转成整数 |
| 第二次转换 | intval($_GET['lover'] + 1) | 先做算术:把 lover 当数字加 1,再取整 |
| 比较 | < 2023 / > 2024 | 要一个 “它自己 < 2023,但它 +1 之后 > 2024” 的值 —— 正常整数做不到 |
→ 突破口:两次转换走的不是同一条路,去找一个"当字符串转"和"当数字算"结果差很大的字符串 → 科学计数法。
2.4 第 3 关 · 参数 qw / yxx
| 片段 | 代码 | 大白话 |
|---|---|---|
| 存在性 | isset($_POST['qw']) && $_POST['yxx'] | body 里要有 qw、yxx 两个参数(且 yxx 不能为假值) |
| 强转 | $array1 = (string)$_POST['qw']; | 强制转成字符串。若传来的是数组,转字符串会得到 "Array" |
| 比较 | sha1($array1) === sha1($array2) | 两串的 sha1 必须严格相等。注意:没有要求两串本身不相等 |
→ 直接传相同值即可;或都传数组,(string) 后都是 "Array",sha1 自然相同。
2.5 第 4 关 · 参数名 SYC_GEEK.2023
| 片段 | 代码 | 大白话 |
|---|---|---|
| 存在性 | isset($_POST['SYC_GEEK.2023']) | body 里要有这个参数名(注意里面带一个点号) |
| 赋值 | ($_POST['SYC_GEEK.2023'] = "Happy to see you!") | ⚠️ 这里是 =(赋值)不是 ==(比较)→ 只要参数存在,赋值结果恒为真,传什么值都行 |
| 成功 | echo $flag; | 四关全过,打印 flag |
| 失败 | echo "再绕最后一步吧"; | 第 4 关没过时的话 |
2.6 else 分支 —— 每句话就是一层进度条
| 回显文案 | 含义 |
|---|---|
| 不会吧不会吧,不会第一步就卡住了吧,yxx会瞧不起你的! | 第 1 关(syc)没过 |
| 嘿嘿嘿,你别急啊 | 第 2 关(lover)没过 |
| 这里绕不过去,QW可不答应了哈 | 第 3 关外层(qw/yxx 缺失)没过 |
| 好哩,快拿到flag啦 | sha1 比较没过 |
| 再绕最后一步吧 | 第 4 关(SYC_GEEK.2023)没过 |
GEEK{…} | 全通 |
调试时这五句话就是进度指纹,比看源码猜快得多。
三、四关分析与绕过
第 1 关 · syc:正则不区分大小写,但比较是强等于
preg_match('/^Welcome to GEEK 2023!$/i', $_GET['syc']) && $_GET['syc'] !== 'Welcome to GEEK 2023!'- 正则末尾的
i表示大小写不敏感;!==是强比较(值 + 类型都相同才算等)。 - 只要把任意一个字母改大小写,就同时满足"能匹配"和"不等于原串"。
Payload:syc=welcome to geek 2023!
(另一种写法:syc=Welcome to GEEK 2023!%0a —— PCRE 里 $ 允许结尾有一个换行,也满足"匹配但不相等"。)
第 2 关 · lover:intval 弱转换(PHP 5 特性)
intval($_GET['lover']) < 2023 && intval($_GET['lover'] + 1) > 2024- 对同一个数,
intval(x) < 2023而intval(x+1) > 2024在数学上矛盾 —— 必须让两次转换走不同的路径。 - 关键:
intval()与+对科学计数法字符串的处理不同。- PHP 5:
intval("1e4")从左往右读,遇到非数字字符e就停,返回 1; - 而
"1e4" + 1会先把字符串整体当浮点数(10000)再相加 → 10001,最后才被intval()取整。 - 结果:
1 < 2023✓ 且10001 > 2024✓。
- PHP 5:
Payload:lover=1e4(等价写法:2022e2、2e9 等,指数 ≥ 4 即可。)
⚠️ 这是 PHP 5 的特性。在 PHP 7+ 中
intval("1e4")会返回 10000,本关就绕不过去了 —— 所以这题也能反过来判断靶机的 PHP 大版本(实测本靶机为 PHP 5)。
第 3 关 · qw / yxx:sha1 强相等
$array1 = (string)$_POST['qw']; $array2 = (string)$_POST['yxx'];
if (sha1($array1) === sha1($array2))- 源码只要求两串 sha1 相等,没有要求两串本身不相等 —— 所以最简单是给两个参数传相同的真值。
- 更"教科书"的写法是数组绕过:给
qw、yxx传数组,(string)强转后都变成字符串"Array",sha1 自然相同。
Payload:qw=1&yxx=1(数组版:qw[]=1&yxx[]=2)
第 4 关 · 参数名 SYC_GEEK.2023:PHP 会清洗参数名
if (isset($_POST['SYC_GEEK.2023']) && ($_POST['SYC_GEEK.2023'] = "Happy to see you!"))- 后半句是
=(赋值)而不是==(比较)—— 只要参数存在,赋值结果恒为真,所以传什么值都行。 - 真正的坑在参数名里的点号:PHP 在接收外部变量时,会把名字里的
.和空格自动换成下划线_。- 直接发
SYC_GEEK.2023=1→ PHP 收到的键名是SYC_GEEK_2023→isset($_POST['SYC_GEEK.2023'])为假 → 卡住。
- 直接发
- 绕过:PHP 的这次"清洗"在遇到第一个
[时会停手([自身占掉那个被替换的位置,其后的字符原样保留)。- 发
SYC[GEEK.2023=1→ 实际得到的键名正好是SYC_GEEK.2023✓
- 发
Payload:SYC[GEEK.2023=1
实测对照(用于确认上面两条规则)
| 发送的参数名 | 服务端实际键名 | 结果 |
|---|---|---|
SYC_GEEK.2023 | SYC_GEEK_2023(.→_) | 卡住 |
SYC.GEEK.2023 | SYC_GEEK_2023(.→_,两个点都换) | 卡住 |
SYC[GEEK[2023 | SYC_GEEK[2023([→_ 但后面残留 [) | 卡住 |
SYC[GEEK.2023 | SYC_GEEK.2023(首个 [ 挡刀,点号保住) | ✅ 出 flag |
四、Burp Suite 完整操作流程(新版 UI)
0. 起手:用内置浏览器抓包
- Proxy → Intercept 页,点右上角 Open browser(免配代理、免装证书)。
- 在浏览器里访问靶机地址。回到 Burp,Intercept is on 会拦到这条
GET /请求。 - 先点 Intercept is off,把拦截关掉,避免后面每次都要放行。或直接在 HTTP history 里找这条请求右键使用。
1. 送进 Repeater 改 GET 参数
右键该请求 → Send to Repeater(快捷键 Ctrl+R),切到 Repeater 页签。
把请求行改成(空格在请求行里必须写成 %20):
GET /?syc=welcome%20to%20geek%202023!&lover=1e4 HTTP/1.1- 手敲空格容易出错 → 用 Decoder 更稳:把
welcome to geek 2023!粘进 Decoder,选 Encode as → URL,再把结果贴回请求行。 - 点 Send:若仍是
不会吧不会吧…说明syc写错;若回嘿嘿嘿…说明lover没生效。
2. 改方法为 POST 并加 Body
- 在请求行右键 → Change request method(GET ↔ POST 一键切换),或直接把首行
GET改成POST。 - 确认请求头里有:
Content-Type: application/x-www-form-urlencoded - 加上空行后写 Body:
qw=1&yxx=1&SYC[GEEK.2023=1[直接敲即可,不用编码(它在 Body 里,不是 URL 里)。Content-Length必须精确等于 body 字节数:这段 body 正好 26 字节。 写大了(例如 28)服务端会一直等差的字节数 → 请求挂死、收不到响应(实测:CL=28 vs 实际 26 → 收到 0 字节)。 稳妥做法:右键 → Update Content-Length,或干脆先删掉这一行让 Burp 自动补。- 点 Send,响应里应出现:
GEEK{b8ff48c3-018c-448f-a9ae-6671266c7b0d}3. 用 Comparer 验证"第 4 关"的差异(可选,但很能说明问题)
把两次请求各发一份:一次 Body 用 SYC_GEEK.2023=1,一次用 SYC[GEEK.2023=1,分别选中响应 → 右键 Send to Comparer → 在 Comparer 里对比,能直接看到前者是"再绕最后一步吧"、后者是 flag,验证点号确实被 PHP 改掉了。
3.4 五步对照表:每加一段,命中的源码是哪句、为什么要这么写
| 步 | Burp 里做什么 | 请求变成什么样 | 命中的源码 | 逐段解释 |
|---|---|---|---|---|
| 0 | 抓到请求,先原样 Send 一次 | GET / | — | 拿"基准回显",确认这页确实在执行源码里那条 if 链 |
| 1 | 请求行末尾加 ?syc=… | GET /?syc=welcome%20to%20geek%202023! | preg_match('/^…$/i') && $_GET['syc'] !== '…' | /i 让它"能匹配",!== 又要求"不能相等" → 改大小写;请求行里空格必须写 %20 |
| 2 | 再追加 &lover=1e4 | GET /?syc=…&lover=1e4 | intval($_GET['lover']) < 2023 && intval($_GET['lover'] + 1) > 2024 | 普通整数无解;PHP 5 里 intval('1e4')=1,而 '1e4' + 1 = 10001 |
| 3 | 改方法为 POST,加 Content-Type 头 | POST /?syc=…&lover=1e4 + Content-Type: application/x-www-form-urlencoded | isset($_POST['qw']) 等 | 第 3、4 关读的是 $_POST,所以必须放 body;URL 里那两个 GET 参数原地不动(换方法 ≠ 换位置) |
| 4 | 补上 body 三兄弟 | body:qw=1&yxx=1&SYC[GEEK.2023=1(26 字节) | sha1(...) === sha1(...) / isset($_POST['SYC_GEEK.2023']) | qw、yxx 传同值 → sha1 必然相等;SYC[GEEK.2023 用 [ 挡掉 PHP 的参数名清洗,保住点号 |
每步发完都看回显:
不会吧不会吧…→ 卡第 1 关;嘿嘿嘿…→ 卡第 2 关;这里绕不过去…→ 卡第 3/4 关;出现GEEK{…}→ 通关。
五、知识点整合
5.1 PHP 解析"外部输入"的规则(本题半数坑都在这)
| 现象 | 规则 |
|---|---|
参数名里有 . 或空格 | 自动换成 _(SYC_GEEK.2023 → SYC_GEEK_2023) |
参数名里出现 [ | 清洗遇到第一个 [ 就停手,[ 本身占掉那个被替换的位置(SYC[GEEK.2023 → SYC_GEEK.2023) |
参数名写成 xxx[] | 该参数的值变成数组(qw[]=1 → $_POST['qw'] = ['1']) |
$_GET / $_POST 的来源 | $_GET 永远读 URL 查询串;$_POST 只读 body(且需 Content-Type: application/x-www-form-urlencoded)。换请求方法不会把参数从 URL 挪到 body |
5.2 弱类型转换(PHP 5)
| 表达式 | 结果 | 说明 |
|---|---|---|
intval("1e4") | 1 | 当字符串读,遇 e 停 |
"1e4" + 1 | 10001 | 先当浮点算,再取整 |
(string)['1'] | "Array" | 数组转字符串是固定串 |
sha1((string)$a) === sha1((string)$b)(两者都是数组) | true | 因为都变成 "Array" |
5.3 比较运算符陷阱
| 写法 | 含义 | 本题里的位置 |
|---|---|---|
= | 赋值,不是比较 | 第 4 关:isset(...) && ($x = "Happy to see you!") 恒为真 |
== | 弱比较,会做类型转换 | (本题未用,但同类题常见) |
=== | 强比较(类型 + 值) | 第 3 关 sha1(...) === sha1(...) |
!== | 强不等 | 第 1 关用它卡"照抄原文" |
preg_match(...) | 只是"能不能匹配上",跟"相等"是两套判断 | 第 1 关的缝就在这 |
5.4 正则小知识
| 点 | 说明 |
|---|---|
/i | 不区分大小写 |
$ | PCRE 里允许结尾有一个 \n(所以 原串%0a 也能"匹配但不相等") |
^ … $ | 整串锚定,避免子串匹配 |
5.5 HTTP 层必备
| 点 | 说明 |
|---|---|
| 请求行里的空格 | 必须写成 %20(query 里也可用 +);裸空格会把"方法 空格 URI 空格 版本"切坏,请求发不出去 |
Content-Length | 必须精确等于 body 字节数。写大了服务端会一直等剩余字节 → 请求挂死(实测 CL=28 / 实际 26 → 收到 0 字节) |
| 参数分隔符 | query 里用 & 连接多个参数 |
[ 在 body 里 | 无需编码,可直写 |
六、解题思路(可迁移到同类题)
- 源码直给就先通读:出现
highlight_file就是白盒。先数清有几层if、每层的输入从哪来($_GET还是$_POST)。 - 给每层配一句"回显指纹":本题五句中文提示就是进度条,改动后一眼看出推进到第几关。
- 逐层找"缝",按三个层次扫:
- 类型层:弱转换(
intval、==、sha1(array)) - 运算符层:
=误写成==、!==与正则错位 - 解析层:参数名清洗、URL 编码、
[]变数组
- 类型层:弱转换(
- 按来源分配位置:
$_GET的放 URL、$_POST的放 body,别混;换方法 ≠ 换位置。 - 一次只改一处、发一次看回显:不要一次把 payload 全糊上去,否则出错时无从定位。
七、和之前讲过的内容有什么联系
| 本题考点 | 之前的对应内容 | 关系 |
|---|---|---|
第 4 关:参数名里的 . 被 PHP 换成 _ | HelloCTF RCE「SHELL 运算符」关:; 在 URL 里是 PHP 的参数分隔符,必须写 %3B | 同一大类:你写的 payload ≠ 服务端实际收到的 payload。前者坑在参数值/分隔符,本题坑在参数名 |
第 3 关:qw[]=1 数组绕过 | 之前用自建回显服务实测过 ip[]=hello → PHP 里 $_GET['ip'] 变数组 | 同一手法:[] 变数组是 PHP 特有的解析行为,可直接复用来打 sha1(array) 之类的弱逻辑 |
第 1 关:preg_match + !== | RCE labs「空字符/通配符」关:preg_match("/flag/") 无 /i,大写直接过 | 同属正则/比较绕过;本题是"正则能匹配但强比较不相等"的逆向思维 |
页面直接 highlight_file 给源码 | RCE labs 各关同样是源码直给 | 同一入口习惯:先读源码,找四个 if 对应四句回显 |
第 2 关:intval 科学计数法 | —— | 新知识点,之前没讲过 |
第 4 关:[ 截断参数名清洗 | —— | 新知识点,之前没讲过 |
三条可以带走的通用结论
- 回显文案就是进度条。 多关卡题每层失败给不同话术时,先给每句话建映射表,爆破/调试时一眼就知道卡在哪一关。
- PHP 对外部输入做了两层"预处理":① 参数名清洗(
./空格 →_,遇[停手);② 参数值只在+/intval等具体运算时才转换。写 payload 前先问一句"PHP 收到时它长什么样"。 - 弱逻辑题的三种破法:类型不匹配(
1e4、数组、0e...)、运算符陷阱(=写成==)、比较层级错位(正则匹配 ≠ 强等于)。
八、汇总表
| 关卡 | 参数 / 位置 | 判断条件 | 绕过原理 | Payload |
|---|---|---|---|---|
| 1 | GET syc | preg_match('/^…$/i') && !== | 正则不区分大小写,!== 区分 → 改大小写 | syc=welcome to geek 2023! |
| 2 | GET lover | intval(x)<2023 && intval(x+1)>2024 | PHP5 intval 遇 e 截断,+ 先算浮点 | lover=1e4 |
| 3 | POST qw / yxx | sha1(str1)===sha1(str2) | 未要求两串不同 → 传同值 / 数组转 "Array" | qw=1&yxx=1 |
| 4 | POST 参数名 | isset($_POST['SYC_GEEK.2023']) | PHP 清洗参数名遇 [ 停手 → [ 挡刀保住点号 | SYC[GEEK.2023=1 |
完整 payload
GET /?syc=welcome%20to%20geek%202023!&lover=1e4 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
qw=1&yxx=1&SYC[GEEK.2023=1(body 长度 26 字节,对应 Content-Length: 26)
Flag:GEEK{b8ff48c3-018c-448f-a9ae-6671266c7b0d}
- 本文链接: https://quinc.cn/posts/geek-challenge-2023-easy-php-writeup/
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。

