文章目录
- 一、概述
- [二、LOW 级别------ XOR + Base64 弱加密](#二、LOW 级别—— XOR + Base64 弱加密)
-
- 1、漏洞描述
- 2、查看网页源代码
- 3、分析网页源代码
- 4、操作步骤
-
- (1)前置准备
- [(2)基线:直接用页面自带的 Decode 工具解(最省事)](#(2)基线:直接用页面自带的 Decode 工具解(最省事))
- [(3)原理验证:用 Python 本地解密](#(3)原理验证:用 Python 本地解密)
- (4)核心攻击:用解出的明文密码登录
- [(5)对照组:错误密码 / "想当然加密后提交" 都失败](#(5)对照组:错误密码 / "想当然加密后提交" 都失败)
- (6)加密可逆性闭环验证
- [5、Python------PoC 脚本](#5、Python——PoC 脚本)
- [6、 AI 视角下的密码学检测](#6、 AI 视角下的密码学检测)
-
- (1)静态特征提取(SAST,无需运行)
- (2)动态/黑盒探测(无源码也能做)
- (3)端到端利用链自动化
- (4)差分对比与误报控制
- [(5)AI 的判定逻辑](#(5)AI 的判定逻辑)
- [(6)AI 相比纯脚本扫描的核心优势](#(6)AI 相比纯脚本扫描的核心优势)
- [7、 LOW 级别------小结](#7、 LOW 级别——小结)
- [三、MEDIUM 级别------ AES-128-ECB 确定性加密](#三、MEDIUM 级别—— AES-128-ECB 确定性加密)
-
- 1、漏洞描述
- 2、查看网页源代码
- 3、分析网页源代码
- 4、操作步骤
-
- (1)前置准备
- [(2)基线:把三段 token 抓下来](#(2)基线:把三段 token 抓下来)
- (3) 用 Python 解密三个 token(验证密钥与模式) 用 Python 解密三个 token(验证密钥与模式))
- [(4)观察 ECB 确定性:密文块重复](#(4)观察 ECB 确定性:密文块重复)
- [(5)对照组:直接重放截获 token 都达不成目标](#(5)对照组:直接重放截获 token 都达不成目标)
- [(6)构造目标明文并 ECB 加密](#(6)构造目标明文并 ECB 加密)
- [(7)核心攻击:提交伪造 token](#(7)核心攻击:提交伪造 token)
- [5、Python------PoC 脚本](#5、Python——PoC 脚本)
- [6、AI 视角下的密码学检测](#6、AI 视角下的密码学检测)
-
- (1)静态规则(SAST)
- [(2)黑盒/动态检测(无源码也能判定 ECB)](#(2)黑盒/动态检测(无源码也能判定 ECB))
- (3)差分对比矩阵
- (4)判定与利用伪代码
- [(5)AI 的关键洞察](#(5)AI 的关键洞察)
- [7、MEDIUM 级别------小结](#7、MEDIUM 级别——小结)
- [四、HIGH 级别:AES-128-CBC 固定 IV、无完整性](#四、HIGH 级别:AES-128-CBC 固定 IV、无完整性)
-
- 1、漏洞描述
- 2、查看网页源代码
- 3、分析网页源代码
- 4、操作步骤
-
- (1)前置准备
- [(2)基线:抓服务端下发的 token](#(2)基线:抓服务端下发的 token)
- (3)强验证:本地复现加密,密文应与服务端逐字节一致
- [(4)基线:重放原 token](#(4)基线:重放原 token)
- [(5)核心攻击:伪造管理员 token(userid:1)](#(5)核心攻击:伪造管理员 token(userid:1))
- (6)验证"无完整性":篡改密文观察响应码
- [(7)对照组:错误 Content-Type / 缺字段 / 不存在用户 都被拒](#(7)对照组:错误 Content-Type / 缺字段 / 不存在用户 都被拒)
- [5、Python------PoC 脚本](#5、Python——PoC 脚本)
- [6、AI 视角下的密码学检测](#6、AI 视角下的密码学检测)
-
- (1)静态规则(SAST)
- [(2)IV 数据流追踪(AI 的核心能力)](#(2)IV 数据流追踪(AI 的核心能力))
- [(3)padding oracle 动态探测(DAST)](#(3)padding oracle 动态探测(DAST))
- (4)越权伪造闭环
- (5)差分对比与关键洞察
- [7、HIGH 级别------小结](#7、HIGH 级别——小结)
- [五、Impossible 级别:AES-256-GCM 认证加密](#五、Impossible 级别:AES-256-GCM 认证加密)
-
- 1、现象观察
- 2、查看网页源代码
- 3、分析网页源代码
-
- [(1)HIGH → Impossible 的逐行差异总览](#(1)HIGH → Impossible 的逐行差异总览)
- (2)六条安全性逐条分析
-
- [4、AI 视角下的密码学检测](#4、AI 视角下的密码学检测)
-
- (1)静态规则(SAST)
- (2)动态篡改矩阵(DAST,黑盒验证)
- (3)四级差分对比矩阵
- (4)判定伪代码
- [(5)AI 关键洞察](#(5)AI 关键洞察)
- [5、Impossible 级别------小结](#5、Impossible 级别——小结)
- 六、四级对比分析
- 七、总结
- [八、AI 增强防御建议](#八、AI 增强防御建议)
-
- 1、智能密码学缺陷检测
- [2、基于数据流的密钥与 IV 分析](#2、基于数据流的密钥与 IV 分析)
- 3、动态篡改矩阵(DAST)
- 4、代码审计集成
- [5、AI 防御架构图](#5、AI 防御架构图)
- 6、实施优先级建议
- 7、总结
- 个人主页 :蒲公英eric
- 专栏传送门 :《DVWA通关全记录:从漏洞复现到安全防御》
- 学习方向:Web安全 / AI安全交叉领域
- 人生格言:安全没有终点,只有不断迭代的防御
📌 本文是专栏「DVWA通关全记录:从漏洞复现到安全防御」的第 18 篇文章。
一、概述
1、什么是密码学漏洞?
大多数 Web 漏洞的本质是"输入没过滤"或"权限没校验",而密码学漏洞的本质是**"保护数据的那把锁本身有缺陷"。它不报错、不崩溃,甚至开发者会觉得"我都用 AES 加密了还能不安全?"------但正因为它发生在数据已经"看起来被保护"的环节,一旦出错危害极深:攻击者拿到的不是一条明文,而是批量解密历史数据、永久伪造任意身份**的能力。
常见的密码学错误可以归纳为五类,本篇四级恰好一一对应:
- 自研弱算法 / 编码冒充加密:用 XOR、Base64、凯撒移位、字符串反转等可逆变换充当加密。它们没有密钥安全可言,甚至根本不需要密钥。
- 硬编码密钥:密钥写死在前端 JS、APP 安装包或可被下载/包含读取的源码里。这种情况下算法再强也等于没锁------钥匙就挂在锁上。
- 确定性加密(错误模式 / 固定 IV):ECB 模式、固定/全零 IV,导致相同明文永远产生相同密文,泄露数据模式,攻击者可直接对比、拼接、重放密文块。
- 只加密不认证(缺失完整性):CBC、ECB、CTR 等只提供机密性,没有 MAC 或认证标签。攻击者可以在不知道密钥的情况下翻转、剪切、拼接密文来操纵解密结果,即"可篡改加密"(malleable encryption)。
- 填充预言机(Padding Oracle) :解密失败时服务端的响应能被攻击者区分出"是 padding 错了还是内容不对",这种可区分性本身就是一个预言机,攻击者据此可逐字节解密任意密文、加密任意明文,无需密钥。
理解本篇的一个核心心智模型:机密性(Confidentiality)和完整性(Integrity)是两个独立的安全目标 。加密提供机密性,认证(MAC/tag)提供完整性。LOW/MEDIUM/HIGH 的问题层层递进,但直到 Impossible 之前,没有任何一级同时做对了这两件事。
2、实验环境
- 靶场:DVWA(本地部署,域名
dvwa.cc指向127.0.0.1) - 账号:
gordonb / abc123(普通用户) - 级别:在
DVWA Security页面切换 low / medium / high / impossible - 脚本依赖:
requests(HTTP)、pycryptodome(AES 加解密),安装:pip install requests pycryptodome - 入口:左侧菜单 Cryptography (
/vulnerabilities/cryptography/)
3、四级挑战一览
这个模块每一级是一个独立的小挑战,目标各不相同:
| 级别 | 算法 | 你拿到的东西 | 目标 |
|---|---|---|---|
| LOW | XOR + Base64 | 一段截获的密文 | 解出密码 Olifant 并登录 |
| MEDIUM | AES-128-ECB | 三个人的会话 token(hex) | 伪造 sweep 的 admin 未过期会话 |
| HIGH | AES-128-CBC(固定 IV、无 MAC) | 一个 userid:2 的 token(base64 JSON) |
解密并伪造 userid:1 管理员 token |
| Impossible | AES-256-GCM(随机 IV + 认证标签) | 一个 userid:2 的 token |
尝试任何篡改------应当全部失败 |
4、防御演进主线
LOW XOR+Base64,硬编码密钥 → 加密可逆,密文=明文
MEDIUM AES-128-ECB,无 MAC → 算法强但模式错,确定性 + 可伪造
HIGH AES-128-CBC,固定IV,无 MAC → 模式对了但无完整性,密钥硬编码,IV 客户端可控
IMPOSSIBLE AES-256-GCM,随机IV+tag → 认证加密(AEAD),机密性+完整性一体
核心启示:加密算法的强度 ≠ 系统的安全性。AES 本身没问题,但 ECB 模式、固定 IV、缺少认证标签,任何一个环节错误都会让"AES-256"形同虚设。
二、LOW 级别------ XOR + Base64 弱加密
1、漏洞描述
LOW 页面分上下两部分:
- 上半部分是一个"安全消息收发工具",你可以在文本框里输入消息,选择 Encode(编码)或 Decode(解码)后提交;
- 下半部分给出一段"你截获的密文",声称里面是某个用户的新密码,要求你解出密码并在 Password 框登录。
它使用的"加密"是:固定密钥的循环逐字节 XOR,再套一层 Base64 编码。这一级的漏洞在于,XOR 是一种对称且自逆的变换,密钥又硬编码在源码里,Base64 更是人尽可解的编码------整套机制不提供任何真正的机密性。
2、查看网页源代码
php
<?php
function xor_this($cleartext, $key) {
// Our output text
$outText = '';
// Iterate through each character
for($i=0; $i<strlen($cleartext);) {
for($j=0; ($j<strlen($key) && $i<strlen($cleartext)); $j++,$i++) {
$outText .= $cleartext[$i] ^ $key[$j];
}
}
return $outText;
}
$key = "wachtwoord";
$errors = "";
$success = "";
$messages = "";
$encoded = null;
$encode_radio_selected = " checked='checked' ";
$decode_radio_selected = " ";
$message = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
try {
if (array_key_exists ('message', $_POST)) {
$message = $_POST['message'];
if (array_key_exists ('direction', $_POST) && $_POST['direction'] == "decode") {
$encoded = xor_this (base64_decode ($message), $key);
$encode_radio_selected = " ";
$decode_radio_selected = " checked='checked' ";
} else {
$encoded = base64_encode(xor_this ($message, $key));
}
}
if (array_key_exists ('password', $_POST)) {
$password = $_POST['password'];
$decoded = xor_this (base64_decode ($password), $key);
if ($password == "Olifant") {
$success = "Welcome back user";
} else {
$errors = "Login Failed";
}
}
} catch(Exception $e) {
$errors = $e->getMessage();
}
}
$html = "
<p>
This super secure system will allow you to exchange messages with your friends without anyone else being able to read them. Use the box below to encode and decode messages.
</p>
<form name=\"xor\" method='post' action=\"" . $_SERVER['PHP_SELF'] . "\">
<p>
<label for='message'>Message:</lable><br />
<textarea style='width: 600px; height: 56px' id='message' name='message'>" . htmlentities ($message) . "</textarea>
</p>
<p>
<input type='radio' value='encode' name='direction' id='direction_encode' " . $encode_radio_selected . "><label for='direction_encode'>Encode</label> or
<input type='radio' value='decode' name='direction' id='direction_decode' " . $decode_radio_selected . "><label for='direction_decode'>Decode</label>
</p>
<p>
<input type=\"submit\" value=\"Submit\">
</p>
</form>
";
if (!is_null ($encoded)) {
echo "
<p>
<label for='encoded'>Message:</lable><br />
<textarea readonly='readonly' style='width: 600px; height: 56px' id='encoded' name='encoded'>" . htmlentities ($encoded) . "</textarea>
</p>";
}
echo "
<hr>
<p>
You have intercepted the following message, decode it and log in below.
</p>
<p>
<textarea readonly='readonly' style='width: 600px; height: 28px' id='encoded' name='encoded'>Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY=</textarea>
</p>
";
if ($errors != "") {
echo '<div class="warning">' . $errors . '</div>';
}
if ($messages != "") {
echo '<div class="nearly">' . $messages . '</div>';
}
if ($success != "") {
echo '<div class="success">' . $success . '</div>';
}
echo "
<form name=\"ecb\" method='post' action=\"" . $_SERVER['PHP_SELF'] . "\">
<p>
<label for='password'>Password:</lable><br />
<input type='password' id='password' name='password'>
</p>
<p>
<input type=\"submit\" value=\"Login\">
</p>
</form>
";
?>
3、分析网页源代码
(1)代码概述
LOW 级别的后端逻辑可以拆成"加解密原语 + 两个 POST 分支"三部分,核心代码不多,但每一处都踩在密码学的红线上:
| 行 | 代码 | 作用 |
|---|---|---|
| 3~14 | function xor_this($cleartext, $key) |
双重循环,把明文字节与循环复用的密钥字节逐字节 XOR |
| 16 | $key = "wachtwoord"; |
硬编码密钥(荷兰语"密码"),10 字节 |
| 30~37 | $encoded = xor_this(base64_decode($message,$key)) / base64_encode(xor_this(...)) |
上半部分"加解密工具":decode 先 base64 解码再 XOR,encode 先 XOR 再 base64 |
| 40 | $decoded = xor_this(base64_decode($password), $key); |
登录分支:把提交的密码"解"了一遍(但结果从未被使用) |
| 41 | if ($password == "Olifant") |
真正的登录判断:比较的是用户提交的原始明文 |
逻辑流程:
- 页面上半部分是一个任何人都能用的"加解密工具":POST
message+direction=encode/decode,服务端原样返回变换结果; - 页面下半部分是登录框:POST
password,服务端先"解密"了一下(结果丢弃),然后直接拿原始提交值和字符串Olifant做明文比较; - 页面还在只读
<textarea>里直接给出了"截获的密文"Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY=。
(2)漏洞分析
逐段分析,可以提炼出五个环环相扣的问题:
① XOR 不是加密算法,是可逆的布尔运算。
问题分析 :XOR(异或)有一个基本数学性质------(A XOR B) XOR B = A。也就是说加密和解密是同一个操作 :密文再和密钥 XOR 一次就还原明文,它根本不存在"单向""不可逆"这类密码学该有的属性。XOR 流加密( Vernam )唯一安全的情形是"一次性密钥本"(key 与明文等长、真随机、绝不复用),而这里密钥只有 10 字节还循环复用,安全性 100% 依赖密钥保密------可密钥 "wachtwoord" 偏偏直接写在源码第 16 行,任何人都能读到。
② Base64 是编码不是加密。
问题分析 :Base64 的设计目的是让二进制数据能在只支持文本的通道(邮件、HTML 文本框)里安全传输,它没有密钥、算法完全公开 ,任何人看到 Base64 字符串都能一键解码。这里 base64_encode(xor(...)) 的组合,仅仅是把 XOR 产生的不可见/控制字符变成可打印字符,避免在 HTML 里显示乱码,不增加任何机密性。把 Base64 当加密,是初学者最常见的误解之一。
③ 密钥硬编码且循环复用。
问题分析 :密钥 wachtwoord 只有 10 个字节,比明文短时内层循环的 $j 就在密钥长度内回绕($j < strlen($key)),相当于密钥被周期性地重复使用。这带来两个后果:一是密钥写在源码里、公开可得,等于没有密钥;二是循环 XOR 在密钥短于明文时会产生周期性,理论上即使不知道密钥,也能通过已知明文攻击或频率分析还原内容(密钥长度可通过"重合指数"等统计方法推断)。
④ 加密 = 解密同一函数,页面还免费送了一个"加解密 oracle"。
问题分析 :上半部分工具允许任何人提交任意消息做 encode、提交任意密文做 decode,而且不需要任何特权。这等于把一台加解密机白送给攻击者------连密钥都不用知道,直接把截获密文贴进 Decode 框就能拿到明文(这是最省事的解法,连脚本都不用写)。在真实系统里,"未授权的加解密预言机"本身就是一个严重漏洞:它让攻击者可以离线试探、对照、验证自己的猜测。
⑤ 死代码导致真实校验逻辑异常宽松。
问题分析 :看登录分支第 40~44 行:$decoded = xor_this(base64_decode($password), $key) 把提交值"解密"了一遍,但这个 $decoded 变量之后再也没有被读取或参与任何判断 。真正决定登录成败的是下一行 if ($password == "Olifant")------比较的是用户提交的原始字符串 是否等于 Olifant。这是典型的"做了安全处理,但处理结果没有接入授权判断"的逻辑断层:开发者大概以为自己在校验解密后的密码,实际生效的却是一句明文比较。哪怕你完全不懂加密,只要解出密码是 Olifant,直接明文提交即可登录。
把密文 Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY= 还原的完整过程:
text
第 1 步:Base64 解码
Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY=
→ 32 个二进制字节(含大量不可见字符,所以才需要 base64 包一层)
第 2 步:逐字节与循环密钥 "wachtwoord" XOR
明文字节 i = 密文字节 i XOR 密钥字节 (i mod 10)
→ "Your new password is: Olifant"
密码就是 Olifant (荷兰语"大象")。顺带一提,密钥 wachtwoord(密码)、明文里的 Olifant(大象)都是荷兰语------这是 DVWA 作者的小彩蛋,也从侧面说明"密钥+提示"全都摊在明面上。
4、操作步骤
(1)前置准备
- 浏览器访问
http://dvwa.cc/login.php,用gordonb / abc123登录; - 左侧菜单 DVWA Security → 下拉选
low→Submit; - 进入左侧 Cryptography 菜单。页面分上下两部分:上半部分是"消息加解密工具"(Message 文本框 + Encode/Decode 单选 + Submit),下半部分是只读的"截获密文"文本框和最下方的 Password 登录框。
页面下半部分只读文本框里就是截获密文:
text
Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY=
(2)基线:直接用页面自带的 Decode 工具解(最省事)
把上面这段密文复制到页面上半部分 的 Message 文本框,单选选 Decode,点 Submit。
预期结果:页面回显解码后的明文。
实测结果(浏览器回显):
text
Your new password is: Olifant
这一步甚至不需要任何工具------页面自己就是一台解密机。这直观证明了"加解密 oracle 公开"的危害:攻击者连密钥都不用知道。
(3)原理验证:用 Python 本地解密
新建 low.py,逻辑是 Base64 解码后与循环密钥 XOR(对应源码 xor_this):
python
import base64
key = b"wachtwoord"
ct = base64.b64decode("Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY=")
pt = bytes(b ^ key[i % len(key)] for i, b in enumerate(ct))
print(pt.decode())
预期结果:输出与页面 Decode 完全一致的明文。
实测结果:
text
Your new password is: Olifant
两条独立路径(页面 oracle、本地脚本)得到同一结果,互相印证密钥与算法推断正确。
(4)核心攻击:用解出的明文密码登录
回到页面,在最下方 Password 输入框输入 Olifant,点 Login。
预期结果 :成功时页面出现绿色提示 Welcome back user;失败时是红色 Login Failed。
实测结果(浏览器回显):
text
✅ 绿色横幅:Welcome back user
实测结果 (HTTP 层捕获,POST 到 /vulnerabilities/cryptography/,表单字段 password=Olifant):
text
请求: POST /vulnerabilities/cryptography/ Content-Type: application/x-www-form-urlencoded
Body: password=Olifant
响应正文含: <div class="success">Welcome back user</div>
判定: 登录成功
注意:这里提交的是明文 Olifant,而不是"加密后的密码"------原因见踩坑 ②(源码里解密结果是死代码,真正判断是明文比较)。
(5)对照组:错误密码 / "想当然加密后提交" 都失败
为了确认"真正生效的是明文比较",做两个对照:
- 在 Password 框输入错误密码(如
wrong)→ 红色Login Failed; - 想当然地把
Olifant用 XOR+Base64 "加密"成密文再提交 → 同样Login Failed(因为服务端根本不解密登录输入,它直接拿提交串和Olifant比)。
实测结果(HTTP 层):
text
Body: password=wrong -> 响应含 Login Failed
Body: password=<Olifant 的 XOR+base64 密文> -> 响应含 Login Failed
Body: password=Olifant -> 响应含 Welcome back user
对照组清晰地说明:服务端只认明文 Olifant,加解密那行代码在登录流程里是"摆设"。
(6)加密可逆性闭环验证
在页面工具里选 Encode,输入 secret,提交得到一段 Base64;再把这段 Base64 贴回去选 Decode,能还原出 secret。
实测结果:
text
Encode: "secret" -> BAQAGhED
Decode: BAQAGhED -> "secret" (完全还原)
这证明 XOR+Base64 完全对称可逆,不存在任何单向性------它压根不是密码学。
踩坑 ①:靶场是本机部署,服务没启动会"连接被拒"
笔者第一次跑脚本时遇到:
textConnectionError: HTTPConnectionPool(host='dvwa.cc', port=80): Max retries exceeded with url: /login.php Caused by NewConnectionError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。一开始以为是网络抖动或靶场挂了,反复重试无果。后来用 Python 排查:
pythonimport socket print(socket.getaddrinfo('dvwa.cc', 80)) # 解析出 127.0.0.1发现
dvwa.cc解析到的 IP 居然是127.0.0.1------这个靶场根本不在远程,而是跑在本机 ,当时 Apache 没启动。打开 phpStudy 启动 Apache/MySQL 后立即恢复。教训:脚本连不上时先解析域名、确认靶场到底在哪,别把本机服务当远程故障。
踩坑 ②:LOW 登录根本不需要加密密码笔者最初想当然地设计成"解出密码 → 重新用 XOR+Base64 加密 → 提交密文",结果怎么都不成功。回头精读源码第 40~44 行才发现:
$decoded = xor_this(base64_decode($password), $key)这行把提交值解了一遍,但$decoded变量之后完全没被使用 ;真正的判断是if ($password == "Olifant"),比较的是未经任何处理的原始提交值。直接 POSTpassword=Olifant立刻登录成功。教训:审计加密代码不能只看"它有没有解密",必须跟踪"解密出来的变量到底有没有进入授权判断"。 这类"算了但没用"的死代码在真实系统里并不罕见,往往意味着真正生效的校验比开发者以为的更宽松。
5、Python------PoC 脚本
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DVWA Cryptography - LOW Level PoC
验证: XOR + Base64 自研弱加密
1) 从页面抓取"截获密文"
2) Base64 解码 + 与硬编码循环密钥 XOR,还原出新密码 Olifant
3) 直接用明文密码 POST 登录(源码里解密结果是死代码,真正判断是明文比较)
4) 本地加密 oracle 自测,证明 XOR+Base64 完全对称可逆
依赖: pip install requests
"""
import re, base64, requests
requests.packages.urllib3.disable_warnings() # 忽略本机 http 的 InsecureRequestWarning
BASE = "http://dvwa.cc"
LOW_KEY = b"wachtwoord" # 源码第 16 行硬编码密钥(荷兰语"密码"),10 字节,循环复用
def xor_this(data: bytes, key: bytes) -> bytes:
"""逐字节与循环密钥 XOR ------ 对应 PHP low.php 的 xor_this()。
XOR 自逆:(A^B)^B = A,所以加密解密是同一个函数。
key[i % len(key)] 等价于 PHP 双重循环里 $j 走到密钥长度就回绕。"""
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
def login(s, user="gordonb", pw="abc123"):
"""DVWA 登录带 CSRF user_token:先 GET 登录页正则取 token,再 POST 凭据。"""
r = s.get(f"{BASE}/login.php", timeout=15)
m = re.search(r"name='user_token'\s+value='([a-f0-9]+)'", r.text)
t = m.group(1) if m else ""
s.post(f"{BASE}/login.php",
data={"username": user, "password": pw, "Login": "Login", "user_token": t},
timeout=15)
def set_level(s, level):
"""切换安全级别:GET security.php 取 token,POST 提交级别(写入 security cookie/session)。"""
r = s.get(f"{BASE}/security.php", timeout=15)
m = re.search(r"name='user_token'\s+value='([a-f0-9]+)'", r.text)
t = m.group(1) if m else ""
s.post(f"{BASE}/security.php",
data={"security": level, "seclev_submit": "Submit", "user_token": t}, timeout=15)
def main():
s = requests.Session(); s.verify = False
login(s)
print("[OK] 登录 gordonb 成功")
set_level(s, "low")
# 1) 从页面只读 <textarea> 抓取截获密文;抓不到则用源码里的默认密文兜底
html = s.get(f"{BASE}/vulnerabilities/cryptography/", timeout=15).text
m = re.search(r"<textarea[^>]*>([A-Za-z0-9+/=]+)</textarea>", html)
intercepted = m.group(1) if m else "Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY="
print(f"[1] 页面截获密文: {intercepted}")
# 2) 还原链路与页面 Decode 工具完全一致:base64_decode -> xor_this(密文, key)
plain = xor_this(base64.b64decode(intercepted), LOW_KEY).decode("utf-8", "replace")
print(f"[2] 解码明文: {plain}")
# 3) 明文结构固定为 "Your new password is: Olifant",取最后一个冒号后的词
password = plain.split(":")[-1].strip()
print(f"[3] 提取密码: {password}")
# 4) 直接用明文密码登录。
# 注意:源码 $decoded = xor_this(base64_decode($password),$key) 的结果从未被使用,
# 真正判断是 if($password == "Olifant"),所以必须提交明文而非密文(踩坑 ②)。
r = s.post(f"{BASE}/vulnerabilities/cryptography/",
data={"password": password}, timeout=15)
ok = "Welcome back user" in r.text # 成功标志写死在源码 $success 里
print(f"[4] 提交 password={password} -> {'Welcome back user 登录成功' if ok else '失败'}")
# 5) 加密 oracle 自测:先加密 secret 再解密,能还原才说明本地原语实现正确
msg = "secret"
enc = base64.b64encode(xor_this(msg.encode(), LOW_KEY)).decode()
dec = xor_this(base64.b64decode(enc), LOW_KEY).decode()
print(f"[5] 加密 oracle 自测: '{msg}' -> {enc} -> 解回 '{dec}' ({'OK' if dec == msg else 'FAIL'})")
print(f"\n[结论] LOW: XOR 硬编码短密钥 + base64,密文可直接还原 -> {'VULNERABLE' if ok else '??'}")
if __name__ == "__main__":
main()
脚本说明
这个 PoC 的核心设计思路是:把"我的密码学实现对不对"和"靶场到底有没有漏洞"拆成两件可独立验证的事------先用本地自测确认加解密代码与 PHP 语义一致,再去打靶场验证业务逻辑缺陷。下面逐块讲清每段代码是根据源码哪一行、出于什么考虑写的。
编写逻辑详解:
| 功能模块 | 对应源码依据 | 说明 |
|---|---|---|
xor_this(data, key) |
low.php 第 3~14 行双重循环 | 逐字节与循环密钥 XOR,加密解密同一函数 |
login(s) |
DVWA 登录表单 + user_token |
带 CSRF token 的登录 |
set_level(s, level) |
security.php 表单 |
切换安全级别 |
| 抓取截获密文 | 页面 <textarea readonly> |
正则提取 Base64,抓不到用源码默认值兜底 |
| 还原明文 | base64_decode + xor_this |
与页面 Decode 工具同一条链路 |
| 登录判定 | $success="Welcome back user" |
用成功标志字符串判定,不依赖 HTML class |
| oracle 自测 | 页面 encode/decode 工具 | 本地加密再解密,验证原语实现正确 |
(1)密码学原语为什么直接照搬 PHP 语义?
xor_this 对应 low.php 第 3~14 行的双重循环:PHP 用 $i(明文索引)和 $j(密钥索引)两个指针,$j 走到密钥长度就回绕。Python 用一行生成器表达同样的"密钥循环":
python
def xor_this(data: bytes, key: bytes) -> bytes:
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
这里 key[i % len(key)] 就是 PHP 内层循环 $j 回绕的等价写法。写这一步时,我先在本地用页面的 encode/decode 工具对拍------页面加密 secret 得到 BAQAGhED,脚本算出的结果必须一致,才确认原语实现正确。跨语言复现密码学逻辑,对拍(differential test)是最可靠的验证手段。
(2)密文为什么用正则从页面抓,而不是写死?
依据是页面把密文放在 <textarea readonly ...>...</textarea> 里:
python
m = re.search(r"<textarea[^>]*>([A-Za-z0-9+/=]+)</textarea>", html)
intercepted = m.group(1) if m else "Lg4WGlQZChhSFBYSEB8bBQtPGxdNQSwEHREOAQY="
字符类 [A-Za-z0-9+/=] 正好覆盖 Base64 全部合法字符(含 + / =),不会误抓 HTML 标签。抓不到时回退到源码里的默认密文,保证脚本在页面结构微调时仍能运行------这是一种"优先动态获取、静态兜底"的健壮性设计。
(3)"提取密码"为什么按明文结构取,而不是硬编码下标?
python
password = plain.split(":")[-1].strip()
明文固定是 Your new password is: Olifant,密码在最后一个冒号之后。用 split(":")[-1].strip() 取值,而不是写死"第 25 个字符开始",这样即使消息文案微调(多一个词、改个措辞),只要"密码在冒号后"这个结构不变就能取到。
(4)登录判定为什么用成功标志字符串?
依据源码:成功时 $success = "Welcome back user" 被放进 class="success" 的 div,失败时 $errors = "Login Failed" 放进 class="warning" 的 div。脚本用:
python
ok = "Welcome back user" in r.text
直接在响应正文里找成功标志,比解析 HTML class 或写复杂正则更稳------class 名可能随主题变化,而这句成功文案是源码里写死的。
(5)为什么要在打靶场前做"加密 oracle 自测"?
python
enc = base64.b64encode(xor_this(msg.encode(), LOW_KEY)).decode()
dec = xor_this(base64.b64decode(enc), LOW_KEY).decode()
# dec == msg 才说明加解密代码没写错
这是写密码学脚本的好习惯:先在本地用同一函数加密 secret 再解密,能还原才说明我的实现正确。如果这一步都过不了,那后面"靶场返回失败"就可能是我脚本写错了,而不是靶场安全------把两件事分开验证,才能避免"脚本错了却以为靶场安全"的误判,也能避免"靶场有洞却因为脚本 bug 没打出来"。
(6)测试步骤的排列逻辑
脚本的 5 步按"取证据 → 还原 → 利用 → 自检"排列:先抓密文(取证据),再本地还原出明文(还原),再用明文密码登录(利用),最后做加密可逆自测(自检原语)。其中第 4 步登录是真正的漏洞验证,第 5 步是实现正确性的回归测试,两者目的不同但都不可少。

6、 AI 视角下的密码学检测
AI 自动化检测:自研弱加密的"高置信度指纹"与端到端利用链
在 AI 自动化安全测试中,自研弱加密(XOR/Base64 冒充加密)是最容易被机器命中的一类密码学问题------它的特征极其明显、几乎没有误报:标准密码库一个都没调用,却出现了密钥常量、位运算和"加密/解密"命名。以下是 AI 自动化检测的完整流程设计。
(1)静态特征提取(SAST,无需运行)
AI 在代码审计阶段扫描源码,命中以下任意一条即可高危标记:
| 检测项 | 命中特征(LOW 实例) | 置信度 |
|---|---|---|
| 硬编码密钥 | 字符串字面量赋值给密钥变量并流入加密函数($key = "wachtwoord") |
高 |
| 自研弱算法 | 出现按位异或 ^ + 循环,函数名/注释含 xor/cipher,且全文件无 openssl_*/hash_hmac 等标准密码库调用 |
高 |
| 编码误用 | base64_encode(...)、strrev()、str_rot13() 被当作加密层(典型嵌套 base64_encode(xor(...))) |
高 |
| 加解密 oracle 暴露 | 同一未鉴权端点既能加密任意输入又能解密任意输入(direction=encode/decode) |
高 |
| 死代码/无效校验 | 解密结果变量($decoded)被赋值后从未进入任何条件判断,授权判断直接用了原始用户输入 |
中高 |
其中"自研弱算法识别"是一条高置信度、低误报的组合规则:AI 构建"密钥变量 → 加密函数参数"的数据流图,只要密钥来自字面量(literal)而非环境变量/KMS,且加密体是位运算而非标准库,就直接判定为"自研混淆而非加密"。
(2)动态/黑盒探测(无源码也能做)
即使拿不到源码,AI 也能从页面行为判断:
- oracle 探测 :向"加解密工具"端点提交已知明文(如
AAAA)做 encode,再把结果做 decode,若能原样还原且无需任何密钥,即判定"可逆变换 + 加解密机公开"; - 熵与编码识别 :截获"密文"字符集仅含
[A-Za-z0-9+/=]且长度是 4 的倍数 → Base64 特征;解码后字节分布不符合分组密码(AES 密文近似随机高熵),反而呈现低熵周期 → 指向 XOR 类流加密; - 密钥恢复 :对循环 XOR,AI 可尝试密钥长度 1~32 逐一对拍已知明文/字典(
wachtwoord、password、secret等高频词),命中即还原。
(3)端到端利用链自动化
AI 一旦识别出"算法 + 密钥 + 密文位置 + 成功标志",即可全自动闭环,无需人工介入:
text
抓 <textarea> 截获密文
→ Base64 解码
→ 与恢复/读到的循环密钥 XOR 还原明文
→ 正则/结构化提取凭据(冒号后的密码 Olifant)
→ POST password=Olifant
→ 校验响应是否含 "Welcome back user"
→ 命中则判定 VULNERABLE
(4)差分对比与误报控制
AI 把同一套"可逆性 + 密钥来源 + 是否接入授权判断"的检查在四个级别上分别执行,形成差分:
| 检查项 | LOW | MEDIUM | HIGH | Impossible |
|---|---|---|---|---|
| 调用标准密码库 | ❌ 无 | ✅ openssl | ✅ openssl | ✅ openssl |
| 密钥来自字面量 | ❌ 是 | ❌ 是 | ❌ 是 | ❌ 是(运维残留) |
| 加解密 oracle 公开 | ❌ 是 | 仅解密 | 仅解密 | 仅解密 |
| 解密结果接入授权判断 | ❌ 否(死代码) | ✅ 是 | ✅ 是 | ✅ 是 |
这张差分表让 AI 能自动定位 LOW 的两个独有问题:用自研可逆变换冒充加密 、以及解密结果根本没接入校验。这类问题特征明显、模式固定、利用链清晰,在 AI 辅助审计里属于送分题------真正考验 AI 的是后三级那种"用了真算法但模式/完整性出错"的隐蔽缺陷。
(5)AI 的判定逻辑
把上面的检测固化成机器可执行的规则,LOW 的判定伪代码如下:
text
# 静态判定
IF 文件中出现位运算(^)/循环 且 函数名含 xor/cipher
且 全文件未调用任何标准密码库(openssl_*/hash_hmac/Crypto API)
THEN verdict = SELF_BUILT_CRYPTO(自研弱加密,高危)
IF 存在字符串字面量赋值给密钥变量 且 该变量流入加密函数
THEN verdict += HARDCODED_KEY(硬编码密钥)
IF 解密结果变量被赋值后 从未出现在任何 if/授权判断中
且 授权判断直接使用了原始用户输入
THEN verdict += DEAD_CRYPTO_CHECK(解密结果未接入校验)
# 动态判定
IF encode(decode(x)) == x 且全程无需密钥
THEN verdict += PUBLIC_ORACLE(加解密预言机公开)
IF POST password=<解出的明文> 后响应含成功标志
THEN exploit = CONFIRMED
这套规则对 LOW 的误报率极低------"无标准密码库 + 位运算 + 密钥字面量"三者同时出现,几乎不可能是正常的安全实现。
(6)AI 相比纯脚本扫描的核心优势
纯脚本扫描器可能只报一条"使用了 XOR",而 AI 能进一步做语义级判断 :它能读懂"解密结果 $decoded 被赋值后从未使用"这种数据流断层,能把"页面上半部分的 encode/decode 工具"识别为"公开的加解密预言机",还能把这两个看似无关的点串成一条完整利用链------从"这段代码很可疑"推进到"这样发包就能登录成功"。这种从代码缺陷到可利用证明的自动闭环,正是 AI 辅助安全测试的价值所在。
7、 LOW 级别------小结
LOW 级别的根本问题不是"算法太简单",而是它根本不构成密码学:XOR 是自逆布尔运算、Base64 是公开编码、密钥硬编码、加解密机公开、登录判断甚至没用解密结果。
核心问题:
- 无机密性:XOR 自逆 + 密钥公开 + Base64 公开,密文等同于明文,任何人都能还原;
- 加解密 oracle 公开:页面工具允许任意人加密/解密,攻击者连密钥都不需要;
- 密钥硬编码 :
wachtwoord写在源码里,钥匙就挂在锁上; - 死代码导致授权失效 :解密结果
$decoded未参与判断,真正生效的是明文比较$password == "Olifant"。
攻击成本几乎为零:不用写脚本,光用页面自带 Decode 框贴一下截获密文就能拿到密码 Olifant,再明文提交即登录成功。
一句话总结:base64_encode(xor_this($msg, $key)) 这一行不是加密,是编码 + 可逆运算的叠叠乐;而"解密结果不进判断"的死代码,让本就形同虚设的"加密"连形式上的作用都没起到。 修复方向是彻底抛弃自研方案,改用标准库的认证加密(见 Impossible),永远不要在客户端/可下载源码里存放密钥,也不要把解密结果与授权判断割裂。
三、MEDIUM 级别------ AES-128-ECB 确定性加密
1、漏洞描述
MEDIUM 升级成了真正的 AES------但用的是 ECB(Electronic Codebook)模式 ,并且没有任何完整性校验(MAC)。
剧情设定是:你截获了某个应用的三个会话令牌(session token),分别属于 Sooty(admin,已过期)、Sweep(普通 user,已过期)、Soo(普通 user,仍有效)。页面"好心"地告诉你:token 是用 aes-128-ecb 加密的,明文是一个 JSON,并给出了 JSON 格式模板。目标是操纵截获的令牌,以 Sweep 的身份、admin 权限、且会话不过期的状态登录 ,成功标志是 Welcome administrator Sweep。
这一级的漏洞核心是:ECB 是确定性 模式(相同明文块 → 相同密文块),且整个机制只加密不认证,攻击者一旦掌握密钥(或利用 ECB 特性)就能加密任意伪造的 JSON。
2、查看网页源代码
php
<?php
function decrypt ($ciphertext, $key) {
$e = openssl_decrypt($ciphertext, 'aes-128-ecb', $key, OPENSSL_PKCS1_PADDING);
if ($e === false) {
throw new Exception ("Decryption failed");
}
return $e;
}
$key = "ik ben een aardbei";
$errors = "";
$success = "";
$messages = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
try {
if (!array_key_exists ('token', $_POST)) {
throw new Exception ("No token passed");
} else {
$token = $_POST['token'];
if (strlen($token) % 32 != 0) {
throw new Exception ("Token is in wrong format");
} else {
$decrypted = decrypt(hex2bin ($token), $key);
$user = json_decode ($decrypted);
if ($user === null) {
throw new Exception ("Could not decode JSON object.");
}
if ($user->user == "sweep" && $user->ex > time() && $user->level == "admin") {
$success = "Welcome administrator Sweep";
} else {
$messages = "Login successful but not as the right user.";
}
}
}
} catch(Exception $e) {
$errors = $e->getMessage();
}
}
$html = "
<p>
You have managed to get hold of three session tokens for an application you think is using poor cryptography to protect its secrets:
</p>
<p>
<strong>Sooty (admin), session expired</strong>
</p>
<p>
<textarea style='width: 600px; height: 56px'>e287af752ed3f9601befd45726785bd9b85bb230876912bf3c66e50758b222d0837d1e6b16bfae07b776feb7afe576305aec34b41499579d3fb6acc8dc92fd5fcea8743c3b2904de83944d6b19733cdb48dd16048ed89967c250ab7f00629dba</textarea>
</p>
<p>
<strong>Sweep (user), session expired</strong>
</p>
<p>
<textarea style='width: 600px; height: 56px'>3061837c4f9debaf19d4539bfa0074c1b85bb230876912bf3c66e50758b222d083f2d277d9e5fb9a951e74bee57c77a3caeb574f10f349ed839fbfd223903368873580b2e3e494ace1e9e8035f0e7e07</textarea>
</p>
<p>
<strong>Soo (user), session valid</strong>
</p>
<p>
<textarea style='width: 600px; height: 56px'>5fec0b1c993f46c8bad8a5c8d9bb9698174d4b2659239bbc50646e14a70becef83f2d277d9e5fb9a951e74bee57c77a3c9acb1f268c06c5e760a9d728e081fab65e83b9f97e65cb7c7c4b8427bd44abc16daa00fd8cd0105c97449185be77ef5</textarea>
</p>
<p>
Based on the documentation, you know the format of the token is:
</p>
<pre><code>{
\"user\": \"example\",
\"ex\": 1723620372,
\"level\": \"user\",
\"bio\": \"blah\"
}</code></pre>
<p>
You also spot this comment in the docs:
</p>
<blockquote><i>
To ensure your security, we use aes-128-ecb throughout our application.
</i></blockquote>
<hr>
<p>
Manipulate the session tokens you have captured to log in as Sweep with admin privileges.
";
if ($errors != "") {
echo '<div class="warning">' . $errors . '</div>';
}
if ($messages != "") {
echo '<div class="nearly">' . $messages . '</div>';
}
if ($success != "") {
echo '<div class="success">' . $success . '</div>';
}
echo "
<form name=\"ecb\" method='post' action=\"" . $_SERVER['PHP_SELF'] . "\">
<p>
<label for='token'>Token:</lable><br />
<textarea style='width: 600px; height: 56px' id='token' name='token'></textarea>
</p>
<p>
<input type=\"submit\" value=\"Submit\">
</p>
</form>
";
?>
3、分析网页源代码
(1)代码概述
MEDIUM 的处理流程是"取 token → 长度/格式校验 → hex 解码 → AES-128-ECB 解密 → JSON 解析 → 字段判定",核心代码如下:
| 行 | 代码 | 作用 |
|---|---|---|
| 2~7 | openssl_decrypt($ciphertext,'aes-128-ecb',$key,OPENSSL_PKCS1_PADDING) |
ECB 模式解密,PKCS7 填充;无 IV、无 MAC |
| 9 | $key = "ik ben een aardbei"; |
硬编码密钥,17 字符,AES-128 实际静默截断为前 16 字节 |
| 16 | strlen($token) % 32 != 0 |
hex 长度校验:一个 AES 块 = 16 字节 = 32 个 hex 字符 |
| 18 | decrypt(hex2bin($token), $key) |
hex2bin 把 hex 转二进制,再 ECB 解密 |
| 19 | json_decode($decrypted) |
解密结果当 JSON 解析 |
| 22 | $user->user=="sweep" && $user->ex>time() && $user->level=="admin" |
成功三条件:用户 sweep + 未过期 + admin |
逻辑流程:
- 服务端只接收一个 POST 参数
token(hex 字符串); - 先校验长度是 32 的倍数(保证是整块 hex),再
hex2bin转字节,用 AES-128-ECB + 硬编码密钥解密,去 PKCS7 填充,json_decode成对象; - 然后逐字段判断 :
user必须是sweep、过期时间ex必须大于当前时间、level必须是admin,三者同时满足才回Welcome administrator Sweep。
(2)漏洞分析
① ECB 是确定性模式,没有 IV。
问题分析 :AES 是分组密码,每块处理 16 字节。ECB(Electronic Codebook,电子密码本)模式下每一块独立加密,相同的 16 字节明文永远得到相同的 16 字节密文,且全程不使用 IV(初始化向量)。这意味着:相同前缀的明文会产生相同前缀的密文;攻击者可以脱离上下文直接对比、剪切、粘贴、重排密文块。最经典的例证是"ECB 企鹅"------一张 BMP 图片用 ECB 加密后,企鹅轮廓依然清晰可辨,因为颜色相同的区域加密后仍然相同。在本模块里,这个特性让我们无需密钥就能在两个 token 里发现一模一样的密文块。
② ECB 无完整性,密文可任意伪造。
问题分析 :openssl_decrypt(..., 'aes-128-ecb', ...) 只负责"解密",不做任何来源校验------它既不验证密文是不是服务端自己签发的,也没有 MAC/签名。ECB 模式本身不含任何认证步骤。因此攻击者只要知道密钥(密钥就在源码里),就能用 ECB 加密任意一段 JSON,服务端会照单解密、照单执行。这里连"防篡改"的概念都不存在,是典型的"只加密、不认证"(confidentiality without integrity)。
③ 算法、格式、密钥全部对攻击者透明。
问题分析 :页面文档直接写明 we use aes-128-ecb,给出明文 JSON 模板(连字段名 user/ex/level/bio 和示例值都给了),密钥又硬编码在源码中。攻击者拥有伪造令牌所需的全部信息:用什么算法、明文长什么样、密钥是什么、token 用什么编码(hex)、成功条件是什么。这相当于把加密机和说明书一起交给了攻击者。
④ 判定条件精确可构造。
问题分析 :成功需要同时 满足三个条件:user == "sweep"、ex > time()(过期时间在未来)、level == "admin"。这三个字段都在我们能自由控制的伪造 JSON 里:user 写 sweep,ex 写一个未来时间戳(如 time()+86400),level 写 admin 即可。bio 字段源码根本不检查,随便填。判定条件越精确、字段越可控,伪造就越简单。
⑤ 密钥长度陷阱(跨语言复现第一大坑)。
问题分析 :$key = "ik ben een aardbei" 数一下是 17 个字符 (含空格),而 AES-128 的密钥必须是 16 字节。PHP 的 openssl_decrypt 对此不报错 ,而是静默取前 16 字节,即 "ik ben een aardb"(丢掉最后的 ei)。用 Python pycryptodome 复现时,直接传 17 字节会抛"密钥长度必须是 16/24/32 字节",手动截断又容易数错字节。这是复现 MEDIUM 时最容易踩的坑(见踩坑 ③)。
实测解密三个 token,能清楚看到 ECB 的确定性------Sooty 和 Sweep 的密文里有完全相同的密文块 b85bb230876912bf3c66e50758b222d0,因为它们的明文在某个 16 字节块对齐后内容相同:
text
Sooty: {"user":"sooty","ex":1723620672,"level":"admin","bio":"Izzy wizzy let's get busy"}
Sweep: {"user":"sweep","ex":1723620672,"level": "user","bio": "Squeeeeek"}
Soo : {"user" : "soo","ex":1823620672,"level": "user","bio": "I won The Weakest Link"}
把三段 hex 按 32 字符(16 字节)切块对比,Sooty 与 Sweep 第二块均为 b85bb230876912bf3c66e50758b222d0------这正是 ECB"相同明文块→相同密文块"的指纹。注意三个明文的 JSON 空格风格都不一样(有的 "user":"x"、有的 "user" : "x"、有的 "level": "user")------这是 DVWA 作者故意构造的,用来制造"块边界不同"的对比,也解释了为什么恰是 Sooty 和 Sweep 共享一个块(它们的 ,"ex":1723620672,"level": 片段在相同块偏移上对齐)。
4、操作步骤
(1)前置准备
DVWA Security设为 medium 并 Submit;- 进入左侧 Cryptography 菜单。页面上有三段很长的 hex token,分别标注 Sooty(admin,session expired) 、Sweep(user,session expired) 、Soo(user,session valid) ,下方给出明文 JSON 模板和一句
we use aes-128-ecb提示。
(2)基线:把三段 token 抓下来
三段 token(hex)即页面给出的截获数据(完整值见 3.2)。先明确每段的"身份标签":
text
Sooty : admin 权限,但会话已过期(ex 是过去时间)
Sweep : 普通 user,会话已过期
Soo : 普通 user,会话仍有效(ex 是未来时间)
目标 : 以 Sweep 身份 + admin 权限 + 会话不过期登录 -> Welcome administrator Sweep
注意:三段 token 单独看都不能直接达成目标------Sooty 是 admin 但用户不对且过期、Sweep 用户对但权限不够且过期、Soo 未过期但用户和权限都不对。必须伪造一段全新的 token,这正是本题意图。
(3) 用 Python 解密三个 token(验证密钥与模式)
关键点有两个:AES-128 密钥取前 16 字节 "ik ben een aardb";token 是 hex (不是 base64),先 bytes.fromhex 再 ECB 解密、去 PKCS7 填充。下面用 完整的 Sweep token 演示(注意:bytes.fromhex 只接受 0-9a-fA-F,任何非 hex 字符都会报错):
python
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
KEY = b"ik ben een aardbei"[:16] # PHP 静默截断到 16 字节
hx = "3061837c4f9debaf19d4539bfa0074c1b85bb230876912bf3c66e50758b222d083f2d277d9e5fb9a951e74bee57c77a3caeb574f10f349ed839fbfd223903368873580b2e3e494ace1e9e8035f0e7e07"
c = AES.new(KEY, AES.MODE_ECB)
print(unpad(c.decrypt(bytes.fromhex(hx)), 16).decode())
⚠️ 容易踩的坑:示例里的省略号不是密文的一部分
如果你把代码片段里的
"3061837c4f9debaf19d4539bfa0074c1..."直接复制运行,会报:
textValueError: non-hexadecimal number found in fromhex() arg at position 32原因:
...是排版用的省略号占位符 ,不是十六进制字符。bytes.fromhex只认0-9a-fA-F,遇到.就抛错。position 32指的是第 33 个字符(0 起算),正好是第一个.的位置。正确做法 :把
...替换成完整的 token 字符串(如上面那段 160 个字符的 Sweep token),或直接用第(1)步页面上给出的三段完整 token。自检 :AES-128 密文是 16 字节的整数倍,对应 hex 长度必须是 32 的倍数 (
32/64/96/128/160/...)。可以先print(len(hx))确认,再bytes.fromhex。
预期结果:解出可读 JSON。
实测结果(三段全部解出):
text
Sooty: {"user":"sooty","ex":1723620672,"level":"admin","bio":"Izzy wizzy let's get busy"}
Sweep: {"user":"sweep","ex":1723620672,"level": "user","bio": "Squeeeeek"}
Soo : {"user" : "soo","ex":1823620672,"level": "user","bio": "I won The Weakest Link"}
能正确解出三段,就证明密钥(前 16 字节)、模式(ECB)、编码(hex)、填充(PKCS7)推断全部正确。
(4)观察 ECB 确定性:密文块重复
把每个 hex token 按 32 个字符(16 字节)切块,对比 Sooty 和 Sweep:
python
blocks = [hx[i:i+32] for i in range(0, len(hx), 32)]
实测结果:
text
共享密文块: b85bb230876912bf3c66e50758b222d0
(Sooty 与 Sweep 的第 2 个块完全相同)
两个不同用户的令牌里出现一模一样的密文块,这就是 ECB"相同明文块→相同密文块"的指纹------无需源码,仅凭密文就能判定模式是 ECB(或固定 IV 的确定性加密)。
(5)对照组:直接重放截获 token 都达不成目标
在伪造之前先做对照,验证"不改 token 直接提交"的后果:
- 重放 Sooty(admin 但过期)→ 解密成功但
ex < now,回Login successful but not as the right user.(非目标); - 重放 Sweep(user、过期)→ 用户对但
level=user且过期,非目标; - 重放 Soo(未过期但 user)→
user=soo,非目标。
实测结果 (HTTP 层,POST 表单字段 token=<hex>):
text
token=<Sooty> -> 响应含 "Login successful but not as the right user."(过期/admin 但非 sweep)
token=<Sweep> -> 响应含 "Login successful but not as the right user."(user 且过期)
token=<Soo> -> 响应含 "Login successful but not as the right user."(user=soo)
对照组说明:光靠重放截获数据不够,必须主动伪造满足三条件的明文。
补充:为什么随手输一个
fake_token会报 "Token is in wrong format"?如果你在 Token 文本框里直接输入
fake_token(或任何非 hex 字符串)提交,页面会返回:
textToken is in wrong format这不是解密失败,而是在解密之前 就被
check_token_med.php的格式校验拦下了。对应源码:
php// 校验 1:长度必须是 32 的倍数(32 个 hex 字符 = 16 字节 = 一个 AES 块) if (strlen($token) % 32 != 0) { throw new Exception("Token is in wrong format"); } // 校验 2:只能包含十六进制字符 [0-9a-fA-F] if (!preg_match('/^[0-9a-f]+$/i', $token)) { throw new Exception("Token is in wrong format"); }字面量
fake_token两条都不满足:
校验项 要求 fake_token的情况结果 长度 len % 32 == 0长度 10, 10 % 32 = 10 ≠ 0❌ 字符集 [0-9a-fA-F]+含 _(下划线不是 hex)❌ 所以任何非 hex 或长度不是 32 倍数的输入,都会在这一步被拒,根本到不了 AES 解密。这也说明:要伪造 token,必须用密钥 ECB 加密后转 hex(第 5 步),得到的字符串天然满足"长度是 32 的倍数 + 全 hex"这两个条件,才能通过校验。
(6)构造目标明文并 ECB 加密
严格对照成功条件 user=="sweep" && ex>now && level=="admin",构造紧凑 JSON:
json
{"user":"sweep","ex":1788942731,"level":"admin","bio":"pwned"}
其中 ex 用当前时间 +86400(一天后)保证未过期,bio 字段源码不检查,填 "pwned" 占位。用同一密钥做 PKCS7 填充后 AES-128-ECB 加密,再转 hex:
python
import json, time
from Crypto.Util.Padding import pad
fake = json.dumps({"user":"sweep","ex":int(time.time())+86400,
"level":"admin","bio":"pwned"}, separators=(",",":"))
fake_token = AES.new(KEY, AES.MODE_ECB).encrypt(pad(fake.encode(), 16)).hex()
(7)核心攻击:提交伪造 token
把 fake_token(hex)贴进页面 Token 文本框点 Submit(或用脚本 POST 表单 token=<hex>)。
预期结果 :绿色 Welcome administrator Sweep。
实测结果(浏览器回显):
text
✅ 绿色横幅:Welcome administrator Sweep
实测结果(HTTP 层):
text
请求: POST /vulnerabilities/cryptography/ Content-Type: application/x-www-form-urlencoded
Body: token=<伪造的 ECB hex>
响应正文含: Welcome administrator Sweep
判定: 以 sweep + admin + 未过期 身份越权登录成功
踩坑 ③:密钥是 17 字符,AES-128 只用前 16 个
笔者第一次直接把整串
"ik ben een aardbei"当密钥传给AES.new(),pycryptodome 直接报错"密钥长度必须是 16/24/32 字节";后来手动切成 16 字节却切成了"ik ben een aard"(漏了b),结果解出来全是乱码。反复对拍后才确认正确密钥是前 16 字节"ik ben een aardb"(ik ben een aardb+ 被丢弃的ei)。根因是 PHP 的
openssl_decrypt对超长密钥静默截断到算法所需长度 而不报错。教训:跨语言复现 PHP openssl 时,密钥必须按算法长度归一化------AES-128 取前 16 字节、AES-256 不足则补\0(见 Impossible 踩坑 ⑦)。 脚本里显式写b"ik ben een aardbei"[:16]并加注释,就是为了把这个坑固化下来。
踩坑 ④:token 是 hex 不是 base64,长度校验是 32 的倍数MEDIUM 的 token 用 hex 编码(每个字节两个十六进制字符),源码里
hex2bin($token)且校验strlen($token) % 32 != 0(一个 AES 块 16 字节 = 32 个 hex 字符)。笔者一开始想当然用 base64 解码,直接抛错。对照源码hex2bin改成bytes.fromhex后正常。教训:编码方式必须以源码为准(hex / base64 / 原始 JSON),三个级别各不相同。
5、Python------PoC 脚本
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DVWA Cryptography - MEDIUM Level PoC
====================================
验证: AES-128-ECB 确定性加密 + 无完整性校验
【ECB 为什么是错误的模式】
- AES 本身没问题,问题出在 ECB(电子密码本)模式:它把明文按 16 字节分块,
每块独立加密、互不影响。于是【相同明文块 -> 永远相同密文块】。
- 后果一(模式泄露):攻击者通过密文块的重复就能推断明文结构。本脚本实测
sooty 与 sweep 两段 token 共享密文块 b85bb230...,正是 ECB 的指纹。
- 后果二(可任意伪造):ECB 不使用 IV、不产出任何认证标签,服务端也不校验完整性。
攻击者只要拿到密钥(这里硬编码在源码),就能加密任意自选 JSON 冒充任何用户。
- 对比:安全做法应使用带随机 IV 的 CBC 并配 MAC,或直接用 GCM 这类 AEAD。
【攻击链路】
1) 把三段截获 token 按 16 字节切块,找出重复密文块(ECB 指纹)
2) 用硬编码密钥解密三个 token,还原明文 JSON
3) 伪造满足 user=sweep / ex 未过期 / level=admin 的 JSON
4) ECB 加密成 hex 提交(注意 token 是 hex 不是 base64,见踩坑 ④),
拿到 Welcome administrator Sweep
依赖: pip install requests pycryptodome
"""
import re, json, time, requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
requests.packages.urllib3.disable_warnings()
BASE = "http://dvwa.cc"
MED_KEY = b"ik ben een aardbei"[:16] # aes-128:PHP openssl 静默截断到 16 字节(踩坑 ③)
def login(s, user="gordonb", pw="abc123"):
"""带 CSRF user_token 登录。"""
r = s.get(f"{BASE}/login.php", timeout=15)
m = re.search(r"name='user_token'\s+value='([a-f0-9]+)'", r.text)
s.post(f"{BASE}/login.php",
data={"username": user, "password": pw, "Login": "Login",
"user_token": m.group(1) if m else ""}, timeout=15)
def set_level(s, level):
"""切换安全级别。"""
r = s.get(f"{BASE}/security.php", timeout=15)
m = re.search(r"name='user_token'\s+value='([a-f0-9]+)'", r.text)
s.post(f"{BASE}/security.php",
data={"security": level, "seclev_submit": "Submit",
"user_token": m.group(1) if m else ""}, timeout=15)
# 页面给出的三段截获 token(hex 编码,见踩坑 ④:不是 base64)
GIVEN = {
"sooty(admin,过期)": "e287af752ed3f9601befd45726785bd9b85bb230876912bf3c66e50758b222d0837d1e6b16bfae07b776feb7afe576305aec34b41499579d3fb6acc8dc92fd5fcea8743c3b2904de83944d6b19733cdb48dd16048ed89967c250ab7f00629dba",
"sweep(user,过期)": "3061837c4f9debaf19d4539bfa0074c1b85bb230876912bf3c66e50758b222d083f2d277d9e5fb9a951e74bee57c77a3caeb574f10f349ed839fbfd223903368873580b2e3e494ace1e9e8035f0e7e07",
"soo(user,有效)": "5fec0b1c993f46c8bad8a5c8d9bb9698174d4b2659239bbc50646e14a70becef83f2d277d9e5fb9a951e74bee57c77a3c9acb1f268c06c5e760a9d728e081fab65e83b9f97e65cb7c7c4b8427bd44abc16daa00fd8cd0105c97449185be77ef5",
}
def main():
s = requests.Session(); s.verify = False
login(s); print("[OK] 登录 gordonb 成功")
set_level(s, "medium")
# 1) ECB 确定性:按 16 字节(=32 hex 字符)切块,求 sooty/sweep 的公共密文块
blocks = {n: [hx[i:i+32] for i in range(0, len(hx), 32)] for n, hx in GIVEN.items()}
common = set(blocks["sooty(admin,过期)"]) & set(blocks["sweep(user,过期)"])
print(f"[1] ECB 确定性: sooty 与 sweep 共享密文块 {len(common)} 个 -> {common}")
# 2) 解密三个 token:hex2bin -> AES-128-ECB 解密 -> 去 PKCS7 -> JSON 明文
for n, hx in GIVEN.items():
c = AES.new(MED_KEY, AES.MODE_ECB)
pt = unpad(c.decrypt(bytes.fromhex(hx)), 16).decode("utf-8", "replace")
print(f"[2] 解密 {n}: {pt}")
# 3) 伪造明文:严格对照成功三条件 user=sweep / ex>now / level=admin
# 紧凑分隔符 separators=(",",":") 让块对齐可控、结果可复现
fake = json.dumps({"user": "sweep", "ex": int(time.time()) + 86400,
"level": "admin", "bio": "pwned"}, separators=(",", ":"))
print(f"[3] 伪造明文: {fake}")
c = AES.new(MED_KEY, AES.MODE_ECB)
fake_token = c.encrypt(pad(fake.encode(), 16)).hex() # PKCS7 填充 -> ECB 加密 -> hex
# 4) 提交(MEDIUM 是普通表单,字段 token,值为 hex;与 HIGH/Impossible 的 JSON 不同)
r = s.post(f"{BASE}/vulnerabilities/cryptography/",
data={"token": fake_token}, timeout=15)
ok = "Welcome administrator Sweep" in r.text
print(f"[4] 提交伪造 token -> {'Welcome administrator Sweep 越权成功' if ok else '未成功'}")
print(f"\n[结论] MEDIUM: ECB 确定性 + 无完整性校验,可解密/伪造任意会话 -> {'VULNERABLE' if ok else '??'}")
if __name__ == "__main__":
main()
脚本说明
这个 PoC 的设计思路是:先用"能解密"证明密钥/模式/编码推断正确,再用"密文块重复"用实测数据坐实 ECB 的确定性,最后"加密一段任意明文"证明无完整性------三步层层递进,每一步都对应源码里的一个具体事实。 下面逐块讲清依据。
编写逻辑详解:
| 功能模块 | 对应源码依据 | 说明 |
|---|---|---|
MED_KEY = b"..."[:16] |
$key="ik ben een aardbei"(17 字符) |
AES-128 静默截断到前 16 字节 |
块切分 hx[i:i+32] |
AES 块 = 16 字节 = 32 hex 字符 | 用于检测重复密文块 |
| 解密链 | hex2bin → openssl_decrypt(ECB) → json_decode |
bytes.fromhex → ECB decrypt → unpad |
| 公共块检测 | ECB"相同明文块→相同密文块" | 集合交集找重复块 |
| 伪造明文 | $user->user/ex/level 三条件 |
构造 sweep + 未来 ex + admin |
| 加密提交 | openssl_encrypt(ECB) + 表单 token |
PKCS7 pad → ECB encrypt → hex → POST |
(1)密钥归一化为什么是第一要务?
依据 PHP openssl_decrypt 对 AES-128 的密钥处理,脚本显式写:
python
MED_KEY = b"ik ben een aardbei"[:16] # 结果是 b"ik ben een aardb"
这是踩坑 ③ 的直接落地。不写这一步,pycryptodome 会因密钥长度不是 16/24/32 而直接报错;写错截断位置(漏一个 b)则解出乱码。把归一化逻辑写在常量定义处并加注释,能让"跨语言密钥长度差异"这个坑永远不会再坑到第二次。
(2)编码链为什么严格对照源码一步步来?
源码的解密链是 hex2bin($token) → openssl_decrypt(..., OPENSSL_PKCS1_PADDING) → json_decode。Python 严格对应:
python
pt = unpad(AES.new(MED_KEY, AES.MODE_ECB).decrypt(bytes.fromhex(hx)), 16)
bytes.fromhex() 对应 hex2bin,.decrypt() 对应 openssl_decrypt,unpad(..., 16) 对应去掉 PKCS7(OPENSSL_PKCS1_PADDING 对分组密码即 PKCS7)。每一步都能在源码里找到出处,任何一步用错(比如踩坑 ④ 用了 base64 解码)都会立刻抛异常------这种"严格对拍"让脚本行为可预期。
(3)为什么把"ECB 确定性检测"独立成一步?
python
blocks = {n: [hx[i:i+32] for i in range(0, len(hx), 32)] for n, hx in GIVEN.items()}
common = set(blocks["sooty(admin,过期)"]) & set(blocks["sweep(user,过期)"])
不停留在"ECB 不安全"的理论结论,而是把每个 hex token 按 32 字符切块、求集合交集,真的在两个不同用户的令牌里找到了相同密文块 b85bb...222d0。这种"拿证据证明模式缺陷"的写法比空泛结论有说服力得多,而且它是黑盒可做的------即使没有源码,仅凭密文块重复也能判定 ECB。
(4)伪造明文为什么逐字段对照判定条件?
源码要求三个条件同时成立:
php
if ($user->user == "sweep" && $user->ex > time() && $user->level == "admin")
脚本就把这三个字段设成达标值:
python
fake = json.dumps({"user": "sweep", "ex": int(time.time()) + 86400,
"level": "admin", "bio": "pwned"}, separators=(",", ":"))
user="sweep" 对应用户名、ex=time()+86400 保证过期时间在未来、level="admin" 提权;bio 字段源码根本不检查,填 "pwned" 占位。判定条件里检查什么我们就喂什么,没检查的字段随意------这是伪造类 exploit 的通用思路。
(5)JSON 为什么用紧凑分隔符?
python
json.dumps(..., separators=(",", ":"))
ECB 按 16 字节块加密,虽然任意合法 JSON 都能被 json_decode 接受,但紧凑输出(无多余空格)让块对齐可控、密文长度可心算校验,也保证每次运行生成的明文字节一致、结果可复现。顺带一提,DVWA 作者故意让三段截获明文的空格风格不同,正是为了演示"空格影响块边界"------我们伪造时用紧凑格式反而是最稳的。
(6)为什么这一步能成功,恰恰证明"无完整性"?
攻击者能在不知道任何服务端秘密之外的东西 (密钥本就硬编码)的情况下,加密一段服务端从未签发过的 JSON 并被接受,这本身就说明 ECB 解密端没有任何来源校验 。脚本第 4 步提交伪造 token 拿到 Welcome administrator Sweep,就是"只加密、不认证"最直接的利用证明。

6、AI 视角下的密码学检测
AI 自动化检测:危险模式静态命中 + 密文块黑盒指纹 + 伪造闭环
MEDIUM 与 LOW 最大的不同是:它用了真算法 AES ,简单的"有没有调用密码库"规则已经失效。AI 必须进一步识别"算法对,但模式和完整性错了"。这类缺陷的检测分静态和动态两条腿走路。
(1)静态规则(SAST)
| 检测项 | 命中特征(MEDIUM 实例) | 置信度 |
|---|---|---|
| 危险模式识别 | openssl_encrypt/decrypt 的算法参数为 aes-*-ecb |
高(ECB 几乎不该出现在新代码中) |
| 完整性缺失 | 解密点附近没有 hash_hmac/hash_equals,也没有 GCM/CCM 的 tag 处理 |
高 |
| 硬编码密钥 | 字符串字面量流入 openssl_* 的 key 参数($key="ik ben een aardbei") |
高 |
| 密钥长度异常 | 密钥字面量长度与算法要求不符(17 字符给 aes-128)→ 提示静默截断风险 | 中 |
| 加密元信息泄露 | 页面/注释写明 we use aes-128-ecb、给出明文 JSON 模板与字段语义 |
中 |
AI 维护一张"危险模式清单":ECB、以及任何"只加密不认证"的模式(裸 CBC/CTR 无 MAC)都直接高危。这条规则置信度高、误报低------生产代码里几乎没有正当理由使用 ECB。
(2)黑盒/动态检测(无源码也能判定 ECB)
即使拿不到源码,AI 仅凭密文就能识别 ECB:
- 收集样本:抓取多个 token(本题给了 3 个);
- 按块长切分:AES 块长 16 字节,hex 即 32 字符/base64 即约 22 字符一块;
- 统计重复块:若不同 token(或同一 token 内)出现完全相同的密文块,即 ECB(或固定 IV 的确定性加密)指纹;
- 确定性复测:多次请求/加密同一明文,若密文逐字节不变,也指向 ECB/固定 IV。
本题实测 sooty 与 sweep 共享块 b85bb230876912bf3c66e50758b222d0,AI 仅凭这一条即可标记"确定性加密"。
(3)差分对比矩阵
AI 把同一组探测在四个级别上执行,形成差分:
| 探测 | LOW | MEDIUM | HIGH | Impossible |
|---|---|---|---|---|
| 调用标准密码库 | ❌ | ✅ AES | ✅ AES | ✅ AES |
| 模式 | XOR | ❌ ECB | CBC | GCM(AEAD) |
| 有完整性校验 | ❌ | ❌ 无 MAC | ❌ 无 tag | ✅ GCM tag |
| 密文块可重复 | N/A | ❌ 是(实测公共块) | 固定 IV 时是 | ✅ 随机 nonce,否 |
| 可加密任意伪造内容 | ✅ | ✅ | ✅(知密钥) | ❌ 无合法 tag |
差分让 AI 自动归纳出演进规律:LOW 是"没有加密",MEDIUM 是"加密但模式确定性 + 无完整性",两者在"伪造任意身份"这个目标下危害等价。
(4)判定与利用伪代码
text
# 静态
IF 算法参数匹配 aes-*-ecb -> ECB_MODE(高危)
IF 存在 openssl_decrypt 但全程无 hmac/gcm-tag -> NO_INTEGRITY(高危)
IF key 参数来自字符串字面量 -> HARDCODED_KEY
# 动态
收集 >=2 个密文样本; 按 16 字节切块
IF 存在跨样本/块内重复的密文块 -> DETERMINISTIC_CIPHER(ECB 指纹)
# 利用闭环
IF 已知(或泄露)算法+密钥+明文模板+成功字段:
构造越权明文(提权/改身份/续期) -> ECB 加密 -> 提交
IF 响应含成功标志(Welcome administrator Sweep) -> EXPLOIT_CONFIRMED
(5)AI 的关键洞察
MEDIUM 给 AI 审计的最大启示是:"用了 AES"不等于"安全" 。纯脚本扫描器看到 openssl_encrypt 可能就放过了,而 AI 必须读懂第二个参数(模式)和周边有没有完整性校验。更进一步,AI 能把"页面泄露 aes-128-ecb + 明文模板 + 硬编码密钥 + 无 MAC"这四个弱信号聚合成一个高置信度结论,并自动产出伪造 token 完成端到端验证------从"代码里有个 ECB"推进到"这样发就能以管理员登录"。
7、MEDIUM 级别------小结
MEDIUM 用上了强算法 AES,但三个错误叠加让它彻底失效:ECB 模式的确定性 (密文块可对比拼接,实测公共块 b85bb...222d0)、无完整性校验 (可加密任意伪造内容)、设计信息全泄露(算法/格式/密钥透明)。
核心问题:
- 模式错误:AES-128-ECB 确定性加密,相同明文块永远产生相同密文块,泄露数据模式;
- 无完整性:只加密不认证,攻击者用硬编码密钥即可加密任意 JSON,服务端无法分辨真伪;
- 信息全透明:页面直接给出算法名、明文模板、字段语义,密钥还在源码里;
- 跨语言密钥陷阱:17 字符密钥被 PHP 静默截断到 16 字节,复现时极易踩坑。
攻击成本依然极低:三段 token 全部可解,伪造一段满足 sweep + admin + 未过期 的 JSON 加密提交即可,成功标志 Welcome administrator Sweep。
一句话总结:这一级最深刻的教训是------密码学的安全性是"模式 × 完整性 × 密钥管理"的乘积,任何一个因子为零,整体就是零。 用 AES-128-ECB 并不比 XOR 安全多少:在"伪造任意会话"这个目标下,两者都让攻击者得偿所愿。修复必须换掉 ECB(改用带随机 IV 的 CBC/GCM)并加上认证。
四、HIGH 级别:AES-128-CBC 固定 IV、无完整性
1、漏洞描述
HIGH 把模式换成了更"正确"的 CBC(Cipher Block Chaining) ,但仍有两个致命问题:IV 固定硬编码、且由客户端随 token 一起提交 ,以及依然没有认证标签(MAC/tag)。
这一级改成了前后端分离的交互:页面通过 JavaScript 用 fetch 把 token POST 到 check_token_high.php,token 是一段 JSON,含 token(base64 密文)和 iv(base64 IV)两个字段,明文是 userid:N。服务端解密后用正则 ^userid:(\d+)$ 取出 id,查用户表:id=1 是 Geoffery(admin),id=2 是 Bungle(user)等。目标:解密截获的 userid:2 token,并伪造一个 userid:1 的管理员 token。
2、查看网页源代码
php
<?php
require ("token_library_high.php");
$message = "";
$token_data = create_token();
$html = "
<script>
function send_token() {
const url = 'source/check_token_high.php';
const data = document.getElementById ('token').value;
console.log (data);
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: data
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log(data);
message_line = document.getElementById ('message');
if (data.status == 200) {
message_line.innerText = 'Welcome back ' + data.user + ' (' + data.level + ')';
message_line.setAttribute('class', 'success');
} else {
message_line.innerText = 'Error: ' + data.message;
message_line.setAttribute('class', 'warning');
}
})
.catch(error => {
console.error('There was a problem with your fetch operation:', error);
});
}
</script>
<p>
You have managed to steal the following token from a user of the Prognostication application.
</p>
<p>
<textarea style='width: 600px; height: 23px'>" . htmlentities ($token_data) . "</textarea>
</p>
<p>
You can use the form below to provide the token to access the system. You have two challenges, first, decrypt the token to find out the secret it contains, and then create a new token to access the system as a other users. See if you can make yourself an administrator.
</p>
<hr>
<form name=\"check_token\" action=\"\">
<div id='message'></div>
<p>
<label for='token'>Token:</lable><br />
<textarea id='token' name='token' style='width: 600px; height: 23px'>" . htmlentities ($token_data) . "</textarea>
</p>
<p>
<input type=\"button\" value=\"Submit\" onclick='send_token();'>
</p>
</form>
";
?>
3、分析网页源代码
(1)代码概述
HIGH 改成了前后端分离:页面 JS 把 token 以 JSON POST 到 check_token_high.php,服务端在 token_library_high.php 里做加解密。核心要素如下:
| 位置 | 代码 | 作用 / 问题 |
|---|---|---|
| 令牌库 | define("KEY","rainbowclimbinghigh") |
硬编码密钥,19 字符 ,AES-128 静默截断为前 16 字节 rainbowclimbingh |
| 令牌库 | define("ALGO","aes-128-cbc") |
模式从 ECB 换成 CBC(方向正确) |
| 令牌库 | define("IV","1234567812345678") |
固定 IV 常量,16 字节,硬编码 |
encrypt() |
openssl_encrypt($pt,ALGO,KEY,RAW,$iv,$tag) |
CBC 加密;$tag 传入但 CBC 不产生 tag,始终为空 |
decrypt() |
openssl_decrypt($ct,ALGO,KEY,RAW,$iv) |
无 tag 校验;解密/padding 失败抛异常 |
create_token() |
base64_encode(IV) 随 token 返回 |
IV 被 base64 放进 JSON 发给客户端 |
check_token() |
$iv = base64_decode($data_array['iv']) |
直接用客户端提交的 IV 解密 |
check_token() |
preg_match("/^userid:(\d+)$/",$d,$m) |
解密后用正则取 id,查用户表 |
| 错误分支 | 解密失败→526;正则不匹配→527 |
两类错误响应可区分(padding oracle) |
| 端点 | $_SERVER['CONTENT_TYPE'] != "application/json" → 527 |
强制 JSON;读 php://input |
用户表:id=1 Geoffery/admin、id=2 Bungle/user、id=3 Zippy/user、id=4 George/user。
逻辑流程:服务端下发 userid:2 的 token(含固定 IV)→ 客户端原样或篡改后 POST → 服务端用客户端给的 IV + 硬编码密钥 CBC 解密 → 正则提 id → 查表返回身份。
(2)漏洞分析
① 密钥硬编码,且 AES-128 截断到前 16 字节。
问题分析 :KEY = "rainbowclimbinghigh" 是 19 个字符,AES-128 实际只用前 16 字节 "rainbowclimbingh"(与 MEDIUM 同源的静默截断坑)。密钥白盒可见,意味着攻击者可离线加密任意明文------伪造 userid:1 不需要任何服务端配合。
② IV 是固定常量,还随 token 发给客户端、校验时直接用客户端值。
问题分析:这是三重问题叠加:
- 固定 IV 导致确定性 :IV 写死为
"1234567812345678",CBC 下相同明文 + 相同 IV 永远产生相同密文。实测每次刷新页面,服务端下发的userid:2密文都一模一样:PhQwGVA3q+T2mT+L3Pe5Vg==。CBC 本想用 IV 避免 ECB 的确定性,但固定 IV 让这个好处归零; - IV 对攻击者可见 :IV 被 base64 放进返回 JSON(
MTIzNDU2NzgxMjM0NTY3OA==解码即1234567812345678); - IV 客户端可控 :
check_token里$iv = base64_decode($data_array['iv'])直接采信客户端提交的 IV。攻击者不仅能看到 IV,还能任意指定 IV------这为 CBC 位翻转攻击(通过修改前一密文块/IV 来精确操纵下一明文块)打开了大门。
③ CBC 无认证标签,密文可伪造。
问题分析 :encrypt 里虽然 $tag = "" 并把 $tag 传给了 openssl_encrypt,但 tag 是 GCM/CCM 这类 AEAD 模式才有的输出,CBC 根本不产生 tag ,返回值里也不含任何认证信息。因此服务端解密时无从判断密文是不是自己签发的。攻击者用已知密钥加密 userid:1 即可冒充管理员 Geoffery。这与 MEDIUM 的"只加密不认证"本质相同------换了模式,没补完整性。
④ 更严重的是 padding oracle 侧信道(无密钥也能打)。
问题分析 :看错误码分支------解密/padding 失败抛异常 → 526 "Unable to decrypt token";解密成功但明文不匹配 ^userid:\d+$ → 527 "No user specified"。这两个响应可以被攻击者稳定区分 。在 CBC + PKCS7 填充下,这种"能区分 padding 错还是内容错"的响应差异本身就是一台填充预言机(padding oracle) :攻击者即使完全不知道密钥 ,也能通过反复提交"精心篡改过一个字节"的密文、观察每次返回 526 还是 527,逐字节解出任意密文(解密预言机),甚至构造出能正确解密的任意明文密文(加密预言机)。模块 "More Information" 里列的一堆 padding oracle 资料,指向的正是这里。这意味着 HIGH 的攻击面比 MEDIUM 还大------MEDIUM 至少需要知道密钥,HIGH 连密钥都不需要。
⑤ 一个强验证点:本地加密结果应与服务端密文逐字节一致。
问题分析 :用推断出的密钥 rainbowclimbingh + IV 1234567812345678 加密 userid:2,得到的 base64 应当正好等于 服务端下发的 PhQwGVA3q+T2mT+L3Pe5Vg==。实测完全一致。这比"能解密"更强地证明了密钥、IV、模式、填充全部推断正确------能解密可能是部分正确,密文逐字节相同则是全对。
4、操作步骤
(1)前置准备
DVWA Security设为 high 并 Submit;- 进入左侧 Cryptography 菜单。页面外观与前两级类似(一个 token 文本框 + Submit),但提交动作由页面 JS 用
fetchPOST 到source/check_token_high.php。
(2)基线:抓服务端下发的 token
文本框里是一段 JSON。注意在 HTML 源码里双引号被转义成了 "(见踩坑 ④),反转义后的实际内容是:
json
{"token": "PhQwGVA3q+T2mT+L3Pe5Vg==", "iv": "MTIzNDU2NzgxMjM0NTY3OA=="}
把 iv 字段 base64 解码:MTIzNDU2NzgxMjM0NTY3OA== → 1234567812345678,正好是源码里写死的固定 IV。再把 token 用任意方式多刷新几次页面------密文每次都一样,这就是固定 IV → 确定性的直接证据。
这里有两个值得记录的观察。其一,服务端主动把 IV 随 token 一起返回给客户端 ,而解密时又直接采信客户端回传的 iv 字段 ------也就是说 IV 不仅固定,还完全处于攻击者可控范围,这为后续的位翻转 / 操纵明文埋下伏笔。其二,密文 PhQwGVA3q+T2mT+L3Pe5Vg== 解码后是 16 字节、恰好一个 AES 块,对应明文 userid:2(7 字节)加 PKCS7 填充到 16 字节;这解释了为什么本例篡改首字节会让整块 padding 失效(详见踩坑 ⑤)。作为对照,第五章 Impossible 同样明文 userid:2 每次刷新密文都不同------差别正是"固定 IV" vs "随机 nonce"。
(3)强验证:本地复现加密,密文应与服务端逐字节一致
用密钥前 16 字节 rainbowclimbingh、IV 1234567812345678,CBC 加密 userid:2(PKCS7 填充):
python
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
KEY = b"rainbowclimbinghigh"[:16]
IV = b"1234567812345678"
ct = AES.new(KEY, AES.MODE_CBC, IV).encrypt(pad(b"userid:2", 16))
print(base64.b64encode(ct).decode()) # 期望 PhQwGVA3q+T2mT+L3Pe5Vg==
预期 / 实测结果 :输出 PhQwGVA3q+T2mT+L3Pe5Vg==,与服务端下发密文逐字节相同。这一步实锤密钥、IV、模式、填充全部正确。
(4)基线:重放原 token
把服务端下发的 JSON 原样 POST 到 check_token_high.php(Content-Type: application/json,body 为原始 JSON 字符串):
HTTP 层请求 / 响应实测如下:
text
POST /vulnerabilities/cryptography/source/check_token_high.php HTTP/1.1
Host: dvwa.cc
Content-Type: application/json
Cookie: PHPSESSID=<已登录会话>; security=high
{"token":"PhQwGVA3q+T2mT+L3Pe5Vg==","iv":"MTIzNDU2NzgxMjM0NTY3OA=="}
text
HTTP/1.1 200 OK
Content-Type: application/json
{"status": 200, "user": "Bungle", "level": "user"}
合法 token 正常放行,确认请求通道(JSON 头 + php://input 原始 body)走通。注意:body 必须是一整段原始 JSON 字符串,不能像 LOW/MEDIUM 那样发表单字段。
(5)核心攻击:伪造管理员 token(userid:1)
用同一密钥/IV 加密 userid:1,构造 JSON 提交:
python
ct1 = AES.new(KEY, AES.MODE_CBC, IV).encrypt(pad(b"userid:1", 16))
forged = {"token": base64.b64encode(ct1).decode(),
"iv": base64.b64encode(IV).decode()}
# POST 到 source/check_token_high.php,Content-Type: application/json
HTTP 层实测(伪造请求/响应):
text
POST /vulnerabilities/cryptography/source/check_token_high.php HTTP/1.1
Host: dvwa.cc
Content-Type: application/json
Cookie: PHPSESSID=<已登录会话>; security=high
{"token":"<本地CBC加密userid:1的base64>","iv":"MTIzNDU2NzgxMjM0NTY3OA=="}
text
HTTP/1.1 200 OK
Content-Type: application/json
{"status": 200, "user": "Geoffery", "level": "admin"}
垂直越权成功:普通访客拿到管理员身份,页面显示 Welcome back Geoffery (admin)。不需要知道任何服务端秘密之外的东西(密钥本就硬编码、IV 固定可见),就能伪造任意 userid。 对照用户表:id=1 是 Geoffery(admin),id=2 是 Bungle(user),id=3 Zippy、id=4 George 均为 user------把明文里的数字换成 1 即完成提权。
(6)验证"无完整性":篡改密文观察响应码
把伪造密文(base64 解码后)的第一个字节翻转 1 bit,重新编码提交:
实测结果:
json
{"status": 526, "message": "Unable to decrypt token"}
返回 526(解密/padding 失败)而非 200。这说明密文没有完整性保护、可被任意篡改(只是篡改后解不出合法明文);而 526/527 响应可区分,正是 padding oracle 信号。
这里值得展开 padding oracle 的原理:CBC 解密第 N 块时,明文块 = decrypt(密文块N) XOR 密文块N-1。攻击者翻转密文块 N-1 的某个字节 ,就能精确控制明文块 N 的对应字节 ,而代价是密文块 N-1 解密出来的整块明文变成乱码 。配合 padding 校验的"错/对"反馈(526 vs 527),攻击者可以逐字节探测,把任意密文解出来、或把任意明文加密进去------全程不需要密钥。这正是 HIGH 比 MEDIUM 更危险的地方。
(7)对照组:错误 Content-Type / 缺字段 / 不存在用户 都被拒
为确认端点契约,做一组对照,把端点的状态码语义摸清楚:
- 用普通表单(
Content-Type: application/x-www-form-urlencoded)提交 → 返回 527Content type must be application/json; - 提交的 JSON 缺
token或iv字段 → 返回 523/524(缺 token / 缺 IV); - 明文能解密、格式也对,但 userid 在用户表里不存在(如
userid:99)→ 返回 525(用户不存在)。
实测结果:
text
表单提交(非 JSON) -> status=527 Content type must be application/json
JSON 缺 token -> status=523
JSON 缺 iv -> status=524
解密成功但 userid 不存在 -> status=525
篡改/padding 失败 -> status=526 Unable to decrypt token
解密成功但正则格式不符 -> status=527
合法 JSON -> status=200
端点状态码语义汇总如下(这也是 padding oracle 判定的依据):
| status | 触发条件 | 语义 |
|---|---|---|
| 200 | 解密 + 格式 + 用户都合法 | 成功 |
| 523 | JSON 缺 token |
参数缺失 |
| 524 | JSON 缺 iv |
参数缺失 |
| 525 | 解密/格式 OK,但用户表无此 id | 用户不存在 |
| 526 | openssl_decrypt 返回 false(含 padding 错) |
解密失败 |
| 527 | Content-Type 非 JSON,或解密成功但 preg_match 格式不符 |
契约/格式错 |
对照组确认两件事:其一,这一级必须严格按"JSON body + 正确 Content-Type"发包(踩坑 ⑥);其二,526 与 527 是两类不同的失败------前者是"没解出来",后者是"解出来了但不对",这个差异就是 padding oracle 赖以工作的反馈通道。
踩坑 ④(本模块):页面 token 被 htmlentities 转义,直接 json.loads 报错
笔者用正则抓
<textarea>内容后直接json.loads,报:
textjson.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)打印原文发现是
{"token":"..."}------PHPhtmlentities()把双引号转义成了"。必须先html.unescape()反转义再json.loads。教训:从 HTML 里抠结构化数据,先过 HTML 实体反转义。 脚本里专门封装了extract_token()处理这个。
踩坑 ⑤:以为篡改 1 bit 只乱一个字节,实际整块 padding 失效返回 526笔者本以为 CBC 下改密文只影响对应明文的某些字节(位翻转),结果实测返回 526 而非 527。原因是分组密码雪崩效应 :改动一个密文块,解密该块得到的整块明文都伪随机化,PKCS7 的 padding 字节几乎必然被破坏,于是
openssl_decrypt返回 false → 526。这个现象反而是个"好消息"给攻击者:526(解密失败)和 527(解密成功但格式不对)响应不同,就构成了 padding oracle 预言机,可用于无密钥解密/加密。笔者最初在脚本注释里写"解出乱码 userid",实测后修正为"padding 失效返回 526",并补上 oracle 定性。
踩坑 ⑥:Content-Type 必须是 application/json,且 body 是原始 JSON端点
check_token_high.php强校验$_SERVER['CONTENT_TYPE'] != "application/json",用普通表单data={...}提交会返回 527 "Content type must be application/json";而且它读的是php://input(原始 body),不是$_POST。脚本必须用data=json.dumps(obj)+headers={"Content-Type":"application/json"}。这和 LOW/MEDIUM 的表单提交完全不同,是这一级(及 Impossible)独有的坑。
5、Python------PoC 脚本
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DVWA Cryptography - HIGH Level PoC
==================================
验证: AES-128-CBC 固定 IV、无认证标签、IV 客户端可控
【CBC 模式对了,但三个致命缺口】
- 缺口一(固定且可控的 IV):CBC 用 IV 让相同明文产生不同密文,但这里 IV 写死成
常量 1234567812345678,还随 token 发给客户端,解密时直接采信客户端提交的 iv 字段。
于是确定性问题没解决,攻击者还能通过操纵 IV 精确翻转第一块明文。
- 缺口二(无认证标签):CBC 只加密不认证,不产出 tag。攻击者知道密钥即可加密任意
明文(userid:1)冒充 admin;即使不知道密钥,位翻转也能改内容。
- 缺口三(padding oracle 侧信道):服务端对"解密/padding 失败"返回 526、对"解密成功
但格式不符"返回 527,两类响应可被稳定区分。攻击者据此可逐字节解密/加密任意密文,
连密钥都不需要。
【攻击链路】
1) 抓取服务端下发的 userid:2 token(JSON,含 iv 字段)
2) 本地解密确认明文;并用"本地加密 == 服务端密文逐字节一致"实锤密钥/IV
3) 重放原 token 作为基线
4) 用硬编码密钥+固定IV 伪造 userid:1,拿到 Geoffery(admin) 垂直越权
5) 篡改密文 1 bit,观察 526;526/527 可区分 = padding oracle 侧信道
注意: 端点强制 Content-Type: application/json,body 为原始 JSON(读 php://input)。
依赖: pip install requests pycryptodome
"""
import re, json, html, base64, requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
requests.packages.urllib3.disable_warnings()
BASE = "http://dvwa.cc"
HIGH_KEY = b"rainbowclimbinghigh"[:16] # aes-128:19 字符静默截断到前 16 字节
HIGH_IV = b"1234567812345678" # 固定 IV,硬编码常量(确定性 + 客户端可见)
EP = f"{BASE}/vulnerabilities/cryptography/source/check_token_high.php"
def login(s, user="gordonb", pw="abc123"):
"""带 CSRF user_token 登录。"""
r = s.get(f"{BASE}/login.php", timeout=15)
m = re.search(r"name='user_token'\s+value='([a-f0-9]+)'", r.text)
s.post(f"{BASE}/login.php",
data={"username": user, "password": pw, "Login": "Login",
"user_token": m.group(1) if m else ""}, timeout=15)
def set_level(s, level):
"""切换安全级别。"""
r = s.get(f"{BASE}/security.php", timeout=15)
m = re.search(r"name='user_token'\s+value='([a-f0-9]+)'", r.text)
s.post(f"{BASE}/security.php",
data={"security": level, "seclev_submit": "Submit",
"user_token": m.group(1) if m else ""}, timeout=15)
def extract_token(page_text):
"""HIGH/Impossible 的 token JSON 经 htmlentities() 转义("),需先反转义再解析(踩坑 ④)。"""
m = re.search(r"<textarea[^>]*>(\{.*?\})</textarea>", page_text, re.S)
if not m:
return None
return json.loads(html.unescape(m.group(1)).replace("\n", "").replace("\r", ""))
def post_token(s, obj):
"""端点读 php://input + 强制 application/json:必须发原始 JSON 字符串,不能用表单(踩坑 ⑥)。"""
return s.post(EP, data=json.dumps(obj),
headers={"Content-Type": "application/json"}, timeout=15).json()
def main():
s = requests.Session(); s.verify = False
login(s); print("[OK] 登录 gordonb 成功")
set_level(s, "high")
# 1) 抓服务端下发的 token(userid:2)
page = s.get(f"{BASE}/vulnerabilities/cryptography/", timeout=15).text
legit = extract_token(page)
print(f"[1] 服务端下发 token(userid:2): {json.dumps(legit)[:70]}...")
# 2) 本地解密确认。IV 直接取自 JSON 的 iv 字段(服务端不自己存 IV,用客户端提交值)
ct = base64.b64decode(legit["token"]); iv = base64.b64decode(legit["iv"])
pt = unpad(AES.new(HIGH_KEY, AES.MODE_CBC, iv).decrypt(ct), 16).decode()
print(f"[2] 解密下发 token: {pt!r}(IV 由客户端随 token 一起提交)")
# 强校验:本地用 HIGH_KEY/HIGH_IV 加密 userid:2 应与服务端密文逐字节一致
local_ct = AES.new(HIGH_KEY, AES.MODE_CBC, HIGH_IV).encrypt(pad(b"userid:2", 16))
assert base64.b64encode(local_ct).decode() == legit["token"], "密钥/IV 推断与服务端不一致!"
# 3) 重放原 token(基线)
j = post_token(s, legit)
print(f"[3] 重放原 token -> status={j.get('status')} user={j.get('user')} level={j.get('level')}")
# 4) 伪造 userid:1(id=1 = Geoffery = admin)。密钥已知、IV 固定、无 tag,直接加密即可
forged_ct = AES.new(HIGH_KEY, AES.MODE_CBC, HIGH_IV).encrypt(pad(b"userid:1", 16))
forged = {"token": base64.b64encode(forged_ct).decode(),
"iv": base64.b64encode(HIGH_IV).decode()}
j = post_token(s, forged)
ok = j.get("status") == 200 and j.get("level") == "admin"
print(f"[4] 伪造 userid:1 -> status={j.get('status')} user={j.get('user')} level={j.get('level')}")
# 5) 篡改密文首字节 1 bit:CBC 雪崩致整块 padding 失效 -> 526
# 526(解密失败) 与 527(解密成功但格式不符) 响应可区分 => padding oracle(无密钥也可解密/加密)
bad = bytearray(base64.b64decode(forged["token"])); bad[0] ^= 0x01
j2 = post_token(s, {"token": base64.b64encode(bytes(bad)).decode(), "iv": forged["iv"]})
print(f"[5] 篡改密文 1 bit -> status={j2.get('status')} msg={j2.get('message')}")
print(f" 注: 526(解密失败)/527(格式不符) 可区分响应 = padding oracle 信号")
print(f"\n[结论] HIGH: CBC 无认证标签 + IV 客户端可控,可伪造任意用户 -> {'VULNERABLE' if ok else '??'}")
if __name__ == "__main__":
main()
脚本说明
HIGH 是全模块攻击面最丰富的一级,脚本的设计思路是:先把"通信契约"(JSON + Content-Type + HTML 反转义)全部踩对,再用"本地密文 == 服务端密文"做密码学强校验,最后用一次伪造 + 一次篡改分别证明"无完整性"和"padding oracle"。
编写逻辑详解:
| 功能模块 | 对应源码依据 | 说明 |
|---|---|---|
post_token() |
端点 CONTENT_TYPE != application/json → 527;读 php://input |
发原始 JSON 字符串 + JSON 头,不用表单 |
extract_token() |
页面 htmlentities() 转义 |
html.unescape 还原 " 再 json.loads |
HIGH_KEY ...[:16] |
KEY="rainbowclimbinghigh"(19 字符) |
AES-128 截断到前 16 字节 |
HIGH_IV 常量 |
define("IV","1234567812345678") |
固定 IV |
| 解密取 iv | $iv = base64_decode($data_array['iv']) |
IV 来自客户端提交的 JSON 字段 |
| 密文一致性 assert | 本地加密 userid:2 == 服务端 PhQw... |
密钥/IV/模式/填充全对的强证明 |
| 伪造 userid:1 | CBC 无 tag,密钥已知 | 直接加密越权明文提交 |
| 篡改 1 bit → 526 | catch 异常→526;正则不匹配→527 | 无完整性 + padding oracle 定性 |
(1)请求方式为什么必须照抄前端 JS?
high.php 里的 send_token() 用 fetch('source/check_token_high.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: data}),端点又强校验 Content-Type 且读 php://input:
python
def post_token(s, obj):
return s.post(EP, data=json.dumps(obj),
headers={"Content-Type": "application/json"}, timeout=15).json()
data=json.dumps(obj) 发的是原始 JSON 字符串 (对应 php://input),配合 Content-Type: application/json 才能通过端点校验。如果沿用 LOW/MEDIUM 的表单写法(data={"token":...}),requests 会发 application/x-www-form-urlencoded,端点直接回 527。这是踩坑 ⑥ 的落地。
(2)extract_token() 为什么要先反转义?
这是踩坑 ④ 的修复。页面把 token JSON 放进 <textarea> 时经过 PHP htmlentities(),双引号变成 ":
python
return json.loads(html.unescape(m.group(1)).replace("\n", "").replace("\r", ""))
html.unescape() 把 " 还原成 ",去掉折行换行后再 json.loads。没有这一步,脚本在解析页面 token 时直接抛 JSONDecodeError。这个函数 HIGH 和 Impossible 共用。
(3)为什么用"本地加密 == 服务端密文"做强校验?
脚本第 2 步先用服务端给的 IV 解密 token;紧接着加了一条 assert:
python
local_ct = AES.new(HIGH_KEY, AES.MODE_CBC, HIGH_IV).encrypt(pad(b"userid:2", 16))
assert base64.b64encode(local_ct).decode() == legit["token"]
本地用推断密钥/IV 加密 userid:2 的结果必须与服务端下发的 PhQwGVA3q+T2mT+L3Pe5Vg== 逐字节相同 。"能解密"可能是巧合或部分正确(比如 IV 错了但恰好解出可读片段),而密文完全一致则说明密钥、IV、模式、填充全部正确。这是写密码学 exploit 时最硬的一条正确性锚点。
(4)伪造逻辑为什么刻意写得很短?
python
forged_ct = AES.new(HIGH_KEY, AES.MODE_CBC, HIGH_IV).encrypt(pad(b"userid:1", 16))
因为密钥已知(硬编码)、IV 固定(常量)、无 tag(CBC 不认证),伪造只需"换明文 userid:1 → 同样参数加密 → 提交"。脚本把这一步写得很短,正是为了凸显:当完整性和 IV 这两道防线都缺失时,越权的代码量小到几乎是一行加密。防护缺失的程度,直接体现在利用的简单程度上。
(5)第 5 步篡改测试为什么是"画龙点睛"?
python
bad = bytearray(base64.b64decode(forged["token"])); bad[0] ^= 0x01
翻转密文首字节观察到 526。脚本同时打印"526/527 可区分 = padding oracle",把一个简单现象上升到侧信道漏洞定性:526 对应源码 catch 分支(解密/padding 异常),527 对应正则不匹配分支(解密成功但格式错)。这两类响应能被稳定区分,意味着攻击者无需密钥即可逐字节解密/加密。脚本虽然只演示了 1 bit 篡改,但注释明确点出了它背后完整的 padding oracle 攻击面。

6、AI 视角下的密码学检测
AI 自动化检测:CBC 的"三连坑"------未认证、IV 可控、padding oracle
HIGH 是最能体现 AI 审计价值的一级:它用了"看起来很专业"的 AES-CBC,简单规则会放过它。AI 需要同时做模式/完整性静态分析 、IV 数据流追踪 和响应侧信道动态探测,才能把三个隐藏问题都挖出来。
(1)静态规则(SAST)
| 检测项 | 命中特征(HIGH 实例) | 置信度 |
|---|---|---|
| 未认证加密 | aes-*-cbc/ecb/ctr 且全程无 hash_hmac/hash_equals/GCM tag |
高 |
| 固定 IV | IV 来自字面量常量 define("IV","1234567812345678") |
高 |
| IV 客户端可控 | 解密用的 IV 来自 $data_array['iv'](用户输入) |
高 |
| 密钥硬编码 | define("KEY",...) 常量流入 openssl_encrypt/decrypt |
高 |
| 非随机化 | 未出现 openssl_random_pseudo_bytes/random_bytes 作为 IV 来源 |
中高 |
(2)IV 数据流追踪(AI 的核心能力)
AI 追踪 IV 变量从"来源"到"解密函数参数"的完整路径,并给出来源分类:
text
IV 来源判定:
字面量常量 (define("IV",...)) -> 固定 IV ❌
用户输入 ($_POST/JSON 的 iv 字段) -> 客户端可控 ❌
CSPRNG (openssl_random_pseudo_bytes/random_bytes) -> 随机 ✅
HIGH 的 IV 路径:
create_token(): 用常量 IV 加密 → 还把常量 IV base64 发给客户端
check_token(): $iv = base64_decode($data_array['iv']) ← 直接采信客户端
结论:同时踩中"固定 IV"(确定性)+"IV 客户端可控"(可操纵)两条红线
(3)padding oracle 动态探测(DAST)
AI 自动发送一组探测请求,比较响应状态码/文案是否可区分:
text
探测矩阵:
(a) 合法密文 -> 期望 200
(b) 翻转密文某一字节 -> 观察返回码
(c) 截断密文 -> 观察返回码
(d) 解密后格式非法(如 userid:x) -> 观察返回码
判定:
IF (b)/(c) 返回"解密失败"(526)
AND (d) 返回"解密成功但格式错"(527)
AND 两者能被稳定区分
THEN -> PADDING_ORACLE(高危,可无密钥逐字节解密/加密)
实测 HIGH 中 (b)=526、(d)=527,响应可区分 → 判定存在 padding oracle。AI 还会提示可进一步接入成熟的 CBC padding oracle 利用脚本(如 PadBuster)实现全自动解密。
(4)越权伪造闭环
识别明文格式(userid:N)与权限映射(id=1=admin)后,AI 用泄露密钥加密越权明文并提交,用响应 level=admin 确认 exploit:
text
识别明文模板 userid:{id} + 用户表(id=1 admin)
-> 加密 userid:1(用硬编码 KEY + 固定 IV)
-> POST JSON
-> IF status==200 && level==admin -> PRIV_ESCALATION_CONFIRMED
(5)差分对比与关键洞察
| 探测 | MEDIUM | HIGH | Impossible |
|---|---|---|---|
| 模式 | ECB | CBC | GCM |
| IV | 无 | ❌ 固定 + 客户端可控 | ✅ 随机 12B |
| 完整性 | ❌ | ❌ | ✅ tag |
| 无密钥能否攻击 | 否(需密钥) | ❌ 是(padding oracle) | 否 |
| 确定性 | 是(块重复) | 是(固定 IV) | 否 |
AI 对 HIGH 的关键洞察是:"半吊子修复"可能比不修复更危险 ------MEDIUM 还需要攻击者拿到密钥,HIGH 的 padding oracle 却让攻击者连密钥都不需要。这解释了为什么 HIGH 是真实系统里最该警惕的状态:开发者因为"我都用上 AES-CBC 了"而放松警惕,却同时留下了可控 IV、无 MAC、可区分错误响应三个缺口。
7、HIGH 级别------小结
HIGH 把模式从 ECB 换成了 CBC,方向正确,但只修了一半,三个致命问题依旧:
- 固定且客户端可控的 IV :IV 写死为
1234567812345678还随 token 发给客户端、解密时直接采信客户端值------确定性问题没解决,反而给了攻击者操纵 IV 的能力; - 缺失认证标签 :CBC 不产 tag,密文可被任意伪造(知道硬编码密钥即可加密任意身份,实测伪造
userid:1拿到 Geoffery/admin); - padding oracle 侧信道 :526(解密失败)与 527(格式不对)响应可区分,攻击者甚至无需密钥就能逐字节解密/加密。
核心问题:模式对了,但完整性、IV 随机性、错误响应统一性三件事一件都没做对。
一句话总结:这一级是真实系统里最常见、也最危险的状态------"用了 AES-CBC 就以为安全了"。 它用实测告诉我们:CBC 必须同时配合随机不可预测的 IV、完整性校验(HMAC,或干脆直接用 GCM)、以及统一不可区分的错误响应,三者缺一不可。 少任何一个,"AES"这个金字招牌都救不了你。
五、Impossible 级别:AES-256-GCM 认证加密
1、现象观察
设 security 为 impossible,页面外观与 HIGH 几乎一致(同样一个 token 文本框 + Submit,JS 同样 fetch 到 check_token_impossible.php)。但当你系统性地测试时:
| 操作 | 服务端响应 |
|---|---|
| 原样提交服务端下发的 token | {"status":200,"user":"Bungle","level":"user"} ✅ 成功 |
| 篡改密文 1 bit | {"status":526,"message":"Unable to decrypt token"} ❌ |
| 篡改 IV 1 字节 | {"status":526,"message":"Unable to decrypt token"} ❌ |
| 截断 1 字节(破坏 tag) | {"status":526,"message":"Unable to decrypt token"} ❌ |
规律很清楚:合法 token 能用;对密文、IV、tag 的任何改动都被拒绝;攻击者没有有效 tag 就无法自己造 token。 这和 HIGH 形成鲜明对比------HIGH 里你能伪造 userid:1 拿到 admin,这里同样的手法全部失败。
2、查看网页源代码
令牌库 token_library_impossible.php(与 HIGH 逐行对比):
php
define ("KEY", "rainbowclimbinghigh");
define ("ALGO", "aes-256-gcm"); // ① 算法:CBC → GCM(认证加密)
function encrypt ($plaintext, $iv) {
if (strlen ($iv) != 12) { // ② IV 长度:16(CBC) → 12(GCM 推荐 nonce)
throw new Exception ("IV must be 12 bytes, " . strlen ($iv) . " passed");
}
$e = openssl_encrypt($plaintext, ALGO, KEY, OPENSSL_RAW_DATA, $iv, $tag);
if ($e === false) { throw new Exception ("Encryption failed"); }
return $e . $tag; // ③ 输出:密文拼接 16 字节认证标签 tag
}
function decrypt ($ciphertext, $iv) {
if (strlen ($iv) != 12) { throw new Exception ("IV must be 12 bytes ..."); }
$tag = substr($ciphertext, -16); // ④ 解密前取出末 16 字节 tag
$text = substr($ciphertext, 0, -16);
$e = openssl_decrypt($text, ALGO, KEY, OPENSSL_RAW_DATA, $iv, $tag); // ⑤ 带 tag 校验解密
if ($e === false) { throw new Exception ("Decryption failed"); }
return $e;
}
function create_token () {
$token = "userid:2";
$iv = openssl_random_pseudo_bytes(12, $cstrong); // ⑥ IV:固定常量 → 每次随机 12 字节
$e = encrypt ($token, $iv);
return json_encode(array(
"token" => base64_encode ($e),
"iv" => base64_encode ($iv)
));
}
// check_token 的用户表、JSON 解析、错误码结构与 HIGH 完全相同
校验端点 check_token_impossible.php 与 HIGH 版本逻辑一致(同样要求 application/json、读 php://input、返回 status 200/526 等),区别只在于它 require 的是 token_library_impossible.php。
3、分析网页源代码
(1)HIGH → Impossible 的逐行差异总览
把 token_library_high.php 和 token_library_impossible.php 摆在一起,差异只有寥寥几处,但每一处都精准补上了 HIGH 的一个攻击面:
| 维度 | HIGH(有漏洞) | Impossible(安全) | 消除的攻击面 |
|---|---|---|---|
| 算法/模式 | aes-128-cbc |
aes-256-gcm |
换成 AEAD,自带完整性 |
| IV 来源 | 常量 "1234567812345678" |
openssl_random_pseudo_bytes(12) |
消除确定性 |
| IV 长度 | 16 字节 | 12 字节(GCM 推荐 nonce) | 符合 GCM 规范 |
| 认证标签 | 无($tag 恒为空) |
16 字节 tag,密文+tag 一起输出 |
消除伪造/位翻转 |
| 解密校验 | 无 tag 校验 | openssl_decrypt(..., $tag) 验 tag |
篡改即失败 |
| 填充 | PKCS7(有 padding) | GCM 无 padding | 免疫 padding oracle |
| 密钥 | 截断到 16 字节 | \0 填充到 32 字节(AES-256) |
---(密钥仍硬编码,见残留风险) |
(2)六条安全性逐条分析
① GCM 是认证加密(AEAD),同时提供机密性 + 完整性。
问题分析 :这是最根本的变化。CBC/ECB 只加密不认证,攻击者改密文服务端"照单解密";GCM(Galois/Counter Mode)在加密的同时用 GHASH 计算一个 16 字节认证标签 tag ,解密时先验证 tag 再输出明文 ------密文/IV 任何一位被改动,tag 校验都失败,openssl_decrypt 直接返回 false。这从根本上消除了 HIGH 的"密文可伪造"。实测:篡改密文、篡改 IV、截断 tag,三种攻击全部返回 526。AEAD(Authenticated Encryption with Associated Data)把"加密"和"认证"在一个算法内原子完成,避免了开发者自己用 CBC+HMAC 拼装时容易出错(先加密后 MAC 还是先 MAC 后加密、MAC 覆盖范围等)。
② 随机 IV(nonce)消除确定性。
问题分析 :HIGH 的 IV 是写死的 "12345678...",相同明文永远得到相同密文;Impossible 每次 openssl_random_pseudo_bytes(12, $cstrong) 生成新的密码学随机 IV 并随 token 返回($cstrong 用于确认底层用了强算法)。即使明文都是 userid:2,每次刷新页面拿到的密文都不同------ECB/固定 IV 的"模式泄露"和"可离线复现"被彻底消除。需要强调:GCM 的 nonce 绝不要求保密 (它随密文公开),但要求同一把密钥下绝不重复;用 12 字节随机 nonce,重复概率可忽略。
③ GCM 没有 padding,天然免疫 padding oracle。
问题分析 :GCM 基于 CTR 流密码 + GMAC,明文按字节异处理、不需要 PKCS7 填充,因此根本不存在"padding 正不正确"这回事。HIGH 里 526(padding/解密失败)与 527(格式不对)那种可区分的填充错误信号,在 GCM 下统一为"tag 校验失败"这一种结果------无论你改密文、改 IV 还是截断,返回的都是同一个 526,没有任何差异可供侧信道利用。padding oracle 攻击面直接消失。
④ tag 与密文、IV 全绑定。
问题分析 :GCM 的 tag 同时对密文和 IV(nonce)(以及可选的 AAD 附加认证数据)做认证。实测单独改 IV 也返回 526------这证明 IV 的完整性也在 tag 保护范围内,攻击者无法通过换 IV 来操纵明文。HIGH 里"CBC 位翻转攻击"(改前一密文块/IV 精确控制下一明文块)的路子,在 GCM 下完全走不通:任何位翻转都会让 tag 校验失败。
⑤ 输出格式 密文 + tag,解密前正确分离。
问题分析 :encrypt 返回 $e . $tag(密文后拼 16 字节 tag),decrypt 用 substr($ciphertext, -16) 取末 16 字节当 tag、substr($ciphertext, 0, -16) 取其余当密文,再把 tag 传给 openssl_decrypt 校验。这个"拼接/分离"约定保证了 tag 始终随密文一起被验证、不会被漏掉。这是一个容易写错的细节------如果开发者只加密、解密时忘了把 tag 传进去,GCM 也会退化成不认证;Impossible 正确地完成了这一步。
⑥ 安全边界被正确收窄到"密钥保密"这一件事。
问题分析 :笔者做了一个白盒对照实验:用源码里的 KEY(AES-256 会把它用 \0 填充到 32 字节)在本地加密 userid:1 并附带正确 tag,提交后确实 返回 status=200 level=admin。这说明 GCM 并非"魔法"------它的安全前提是密钥不泄露 :只要密钥保密,攻击者就算能任意篡改/重放/操纵 IV,也算不出正确的 tag(GHASH 需要密钥)。但在真实黑盒攻击中攻击者拿不到密钥,就无法生成合法 tag,因而 5.1 的所有篡改/伪造尝试都失败。Impossible 把"系统安全"正确地简化为"密钥管理"这一个可聚焦的运维问题。这是好密码学设计的标志:把复杂度留给经过广泛审查的算法库,把唯一的信任假设留给密钥。
4、AI 视角下的密码学检测
AI 审计安全实现:不是"找不到漏洞就报安全",而是主动验证安全属性是否成立
对 Impossible 这类"看起来很安全"的代码,AI 最容易犯的错是默认放行。正确做法是把"安全"拆成可断言的子属性(AEAD、tag 真被使用、nonce 随机、篡改全拒、响应不可区分),逐条用静态规则 + 动态探测去证实,任何一条不满足都降级。
(1)静态规则(SAST)
| 检测项 | 命中特征(Impossible 实例) | 置信度 |
|---|---|---|
| AEAD 算法白名单 | aes-256-gcm / chacha20-poly1305 / aes-*-ccm |
通过 |
| tag 真被接收 | 加密时第 7 参数 &$tag 被写入、与密文一起返回 |
通过 |
| tag 真被校验 | 解密时第 6 参数传入 tag,openssl_decrypt(...) === false 有处理 |
通过 |
| nonce 随机 | IV = openssl_random_pseudo_bytes(12) / random_bytes,长度 12 |
通过 |
| 反例:ECB | 模式含 ecb |
高危(本级未命中) |
| 反例:无 MAC 的 CBC | cbc 且无 hash_hmac/GCM tag |
中危(本级未命中) |
| 残留:密钥硬编码 | define("KEY",...) 流入加密函数 |
运维建议(非算法缺陷) |
关键陷阱:只看算法名会误判。若开发者写了 GCM 却在解密时没把 tag 传进去(第 6 参数留空),等于不认证------AI 必须沿数据流确认 tag 从"加密产生"到"解密校验"整条链路都被正确使用。
(2)动态篡改矩阵(DAST,黑盒验证)
AI 自动发送一组 token,断言响应:
text
探测集:
(a) 合法 token 期望 200
(b) 改密文 1 bit 期望 统一失败码 526
(c) 改 IV 1 byte 期望 统一失败码 526
(d) 截断/破坏 tag 期望 统一失败码 526
(e) 黑盒无密钥伪造 期望 统一失败码 526
判定 SECURE 当且仅当:
(a)==200 且 (b)==(c)==(d)==(e)==526 且 四者响应文案完全不可区分
这正是 poc_crypto_impossible 做的事。
(3)四级差分对比矩阵
| 探测 | LOW | MEDIUM | HIGH | Impossible |
|---|---|---|---|---|
| 算法 | XOR+Base64 | AES-128-ECB | AES-128-CBC | AES-256-GCM |
| 完整性 | ❌ | ❌ | ❌ | ✅ tag |
| IV/nonce | --- | 无(ECB) | ❌ 固定+可控 | ✅ 随机 12B |
| 篡改响应 | 明文/无保护 | 526 无侧信道 | 526 vs 527 可区分(oracle) | 统一 526 不可区分 |
| 黑盒越权 | ✅ 直接 | ✅ 需密钥 | ✅ 无需密钥(oracle) | ❌ 无法伪造 |
| AI 判定 | VULNERABLE | VULNERABLE | VULNERABLE | SECURE |
(4)判定伪代码
text
function audit_crypto(code):
algo = parse_cipher(code)
if algo.mode in {ecb}: return HIGH("ECB 确定性")
if algo.mode in {cbc, ctr, ...} and not has_mac(code):
return HIGH("未认证加密")
if algo.mode in {gcm, chacha20-poly1305, ccm}:
if not tag_received_on_encrypt(code): return HIGH("GCM 未取 tag")
if not tag_verified_on_decrypt(code): return HIGH("GCM 未验 tag")
if not csprng_nonce(code): return HIGH("nonce 不随机")
# 动态确认
r = tamper_matrix() # 合法 200;改密文/IV/tag/伪造 全失败且不可区分
if r.pass_all:
verdict = SECURE
if hardcoded_key(code): verdict += NOTE("密钥硬编码,迁移 KMS/环境变量并轮换")
return verdict
else:
return HIGH("存在可利用篡改面 / 侧信道")
(5)AI 关键洞察
AI 对这一级的结论是:密码学层面已无懈可击(AEAD + 随机 nonce + tag 全链路校验 + 无 padding oracle),剩余风险仅在密钥管理这一运维边界。 源码里 define("KEY", ...) 硬编码密钥在纯黑盒威胁模型下不可利用,但在"源码可被获取"(开源、LFI 文件包含、备份文件泄露、反编译)场景下仍是命脉------应迁移到环境变量 / 密钥管理服务(KMS)并定期轮换。
对 AI 审计系统而言,能输出这种**"算法层 SECURE + 运维层一条建议"的分级结论**,比简单的"通过/不通过"更有价值:它既不会因为"没发现漏洞"而漏掉 tag 未使用之类的隐患,也不会把安全实现误报成漏洞。这与对前三级"算法/模式/完整性存在可利用缺陷"的结论形成清晰对照。
5、Impossible 级别------小结
Impossible 用 AES-256-GCM 一次性补齐了前三季所有短板:
- 认证标签 tag 解决完整性(密文/IV/tag 改一 bit 即拒绝);
- 随机 12 字节 nonce 解决确定性(每次密文不同);
- GCM 无 padding 免疫 padding oracle;
- 并把信任假设正确地压缩到密钥保密这一个点上。
它示范了安全设计的正确姿势:不要自研、不要用 ECB、不要裸 CBC 自己拼 MAC,而是直接选用经过广泛审查的 AEAD 原语(AES-GCM / ChaCha20-Poly1305),让成熟的密码库来处理完整性,把密钥放进 KMS。 做对了这些,攻击者即使能任意篡改、重放、操纵 IV,也无法在不知道密钥的情况下伪造出一个能通过 tag 校验的 token。
攻击成本对照:LOW 几乎零成本(XOR 可逆)、MEDIUM 需拿到硬编码密钥、HIGH 甚至无需密钥(padding oracle),而 Impossible 在黑盒下成本为无穷大(无密钥即无法构造合法 tag)。
一句话总结:从 XOR 到 GCM,DVWA 用四级代码讲清了同一件事------机密性(加密)和完整性(认证)必须同时满足,且唯一的信任假设应当是"密钥保密"。选对 AEAD、用随机 nonce、让密码库处理 tag,就是这一章给出的标准答案。
六、四级对比分析
1、防护策略演进对比
| 维度 | LOW | MEDIUM | HIGH | Impossible |
|---|---|---|---|---|
| 算法 | XOR + Base64 | AES-128-ECB | AES-128-CBC | AES-256-GCM |
| 密钥 | 硬编码 wachtwoord(10B) |
硬编码(截断16B) | 硬编码(截断16B) | 硬编码(补\0到32B)⚠️唯一残留 |
| IV/nonce | 无 | 无(ECB 特性) | 固定常量 + 客户端可控 | 每次随机 12 字节 |
| 完整性校验 | 无 | 无(无 MAC) | 无(CBC 不产 tag) | GCM 16 字节认证 tag |
| 确定性 | 是 | 是(密文块重复) | 是(固定 IV) | 否(随机 nonce) |
| 篡改密文后果 | 可逆 | 可任意伪造 | 可伪造 / padding oracle | tag 校验失败,拒绝 |
| padding oracle | 无分组概念 | 无区分 | 存在(526 vs 527) | 无(GCM 无 padding) |
| 攻击者能力 | 解出密码登录 | 解密+伪造任意会话 | 解密+伪造任意用户 | 黑盒无法解密/伪造 |
| 实测结论 | VULNERABLE | VULNERABLE | VULNERABLE | SECURE(黑盒) |
2、攻击面变化分析
沿着四个级别走,攻击者需要的前置条件在发生微妙变化。最反直觉的是:攻击面并不是随级别升高而单调减小,HIGH 反而比 MEDIUM 多出一条"无需密钥"的攻击路径。
- LOW :无需任何密码学知识,用页面自带解码框即可还原明文。攻击者既不需要密钥(XOR 密钥在页面里、且加密 oracle 公开可反复调用),也不需要理解算法。攻击门槛≈0。
- MEDIUM :需要知道"ECB + 密钥"两个要素。密钥硬编码在源码里、算法和 token 格式页面直接给出,门槛依然极低;攻击者真正需要的核心能力只是"会用加密库加密一段 JSON"。此外 ECB 的块确定性(sooty/sweep 共享密文块
b85bb...)还额外暴露了模式指纹。 - HIGH :知道密钥(源码)即可伪造任意用户;更严重的是,即使不知道密钥 ,526/527 的可区分响应构成 padding oracle,攻击者仍能逐字节解密/加密任意密文。攻击面不降反升------这是"半吊子修复"的典型反效果:换上 CBC 让人放松警惕,却同时引入了可控 IV 和侧信道两个新缺口。
- Impossible:黑盒下无论怎么篡改密文、IV、tag 都被统一的 526 拒绝,也没有任何侧信道差异可供利用。攻击者唯一的前提是拿到密钥------攻击面被成功压缩到"密钥是否泄露"这一个纯运维问题。
| 级别 | 攻击者需要的前提 | 关键攻击能力 | 侧信道 |
|---|---|---|---|
| LOW | 无(密钥公开) | 页面解码 / 调用加密 oracle | --- |
| MEDIUM | 硬编码密钥 | 用库加密伪造 JSON | 块重复指纹 |
| HIGH | 密钥或仅网络可达 | 加密伪造 或 padding oracle 无密钥攻击 | 526/527 可区分 |
| Impossible | 必须持密钥(黑盒不可得) | 黑盒无法伪造 | 无(响应统一) |
从这张表能读出一条重要规律:衡量一个加密实现是否安全,不能只看"用了什么算法",而要看"攻击者最少需要什么前提"。HIGH 的问题正在于它把这个前提从"需要密钥"降低到了"只需能发请求"。
3、防御策略演进路径
DVWA 这四级恰好对应真实项目里密码学改造的四个阶段,每个阶段都"修了一个问题、但又暴露/遗留了下一个问题":
text
LOW ──换成标准算法──▶ MEDIUM ──换成带IV的模式──▶ HIGH ──加认证+随机IV──▶ IMPOSSIBLE
(假加密) (真算法/错模式ECB) (CBC/无完整性/固定IV) (GCM/AEAD)
修复"有没有加密" 修复"算法强度" 修复"模式正确性" 修复"完整性+随机化"
第一阶段:LOW → MEDIUM,解决"有没有用真加密"。 LOW 的 XOR+Base64 连密码算法都算不上,密钥长度只有 10 字节、可逆、加密 oracle 公开。换成 AES 算是从"玩具"跨进了"标准算法"的门槛。但这一步只解决了算法强度,开发者随手选了 ECB 模式。
第二阶段:MEDIUM → HIGH,解决"模式对不对"。 ECB 把每块独立加密,相同明文块产生相同密文块(sooty/sweep 的公共块 b85bb... 实锤),完全不隐藏数据模式。换成 CBC 引入了 IV 和分组链接,方向正确。但这一步只盯着"机密性模式",既没给 IV 用随机值(写死成常量还发给客户端),也没加任何完整性校验。
第三阶段:HIGH → IMPOSSIBLE,解决"完整性 + 随机化"。 这是最关键、也最容易被跳过的一步。换上 GCM 这类 AEAD,一次性补齐三件事:认证 tag(完整性)、随机 12 字节 nonce(消除确定性)、无 padding(免疫 padding oracle)。同时把密钥归一化到 AES-256。
这条路径的三点启示:
- 每一步都在补洞,但只有走到 IMPOSSIBLE 才同时满足机密性与完整性。 前三级始终在"只加密、不认证"的状态里打转。
- 真实项目最容易停在 HIGH。 "我们都用上 AES-CBC 了"是极常见的自我安慰,而 HIGH 恰恰同时埋着可控 IV、无 tag、padding oracle 三个问题------它甚至比 MEDIUM 更危险,因为 padding oracle 让攻击者连密钥都不需要。
- 与其逐级打补丁,不如一步到位选 AEAD。 AES-GCM / ChaCha20-Poly1305 把"加密 + 认证 + 随机 nonce 处理"打包成一个不易用错的原语,能让团队直接从 LOW 思维跳到 IMPOSSIBLE 结果,避开 MEDIUM/HIGH 这两个"半吊子"中间态。
这也解释了为什么现代安全规范(NIST、OWASP)都在推动"默认使用 AEAD"------不是因为 CBC 本身有错,而是因为正确使用 CBC 需要开发者同时做对太多件事,而 AEAD 把这些正确性内置进了算法。
4、密码学防御的核心原则
把四个级别的教训提炼成可落地的工程原则,每一条都对应本次实测中看到的具体漏洞:
- 永远不要自研密码算法,也不要用 XOR/Base64/移位冒充加密。 LOW 证明了这类"伪加密"在加解密 oracle 公开时形同虚设------密钥就在页面里、密文可一键还原。加密要用经过公开审查的标准算法。
- 需要机密性 + 防篡改时,直接用 AEAD(AES-GCM、ChaCha20-Poly1305),不要用裸 CBC/ECB 再手工拼 MAC。 MEDIUM 的 ECB 和 HIGH 的无 tag CBC 都栽在"只加密不认证"。AEAD 在算法内部原子地完成加密与认证,避免了"先加密后 MAC 还是先 MAC 后加密""MAC 覆盖哪些字段"等极易出错的手工拼装。
- IV/nonce 必须随机、不可预测、绝不能由攻击者控制。 HIGH 把 IV 写死成
1234567812345678还回传给客户端、解密时直接采信,是确定性和可控性的双重失败。规范做法:GCM 用 12 字节 CSPRNG nonce、CBC 用 16 字节 CSPRNG IV,随密文公开无妨,但同一密钥下绝不复用。 - 错误响应要统一、不可区分,从根上消除 padding oracle。 HIGH 的 526(解密失败)与 527(格式不对)可被稳定区分,等于给了攻击者一台"解密预言机"。最彻底的解法不是"把错误文案改一样"这种治标手段,而是直接使用无 padding 的 AEAD------根本不存在填充对错这回事。
- 密钥绝不能硬编码在前端或可下载源码里,要放进环境变量 / KMS 并定期轮换。 四级全部硬编码密钥,Impossible 也不例外------这是全篇唯一在 Impossible 残留的风险。黑盒下不可利用,但一旦源码泄露(开源、LFI、备份文件、反编译),密钥就是最后的命门。
- 解密/校验结果必须真正接入授权判断,杜绝"解了但没用"的死代码。 LOW 里
$decoded解出了明文密码却根本没参与登录判断,真实校验竟是$password == "Olifant"明文比较。密码学代码不能只"看起来在工作",其输出必须真正驱动安全决策。
这六条里,第 2、3、4 条是"选对原语就能一次满足"的------这正是推荐 AEAD 的根本原因;第 5、6 条则属于工程与运维纪律,再好的算法也救不回硬编码密钥和断线的授权逻辑。
七、总结
1、漏洞全景回顾
- LOW :XOR+Base64 自研弱加密,密钥硬编码、加解密 oracle 公开、解密结果不参与校验,密文可直接还原成密码
Olifant。 - MEDIUM :AES-128-ECB 确定性模式 + 无 MAC,相同明文块产生相同密文块(实测公共块
b85bb...),攻击者可加密任意伪造 JSON,以 sweep/admin 身份登录。 - HIGH :AES-128-CBC 固定 IV 且客户端可控、无认证标签,可伪造
userid:1拿到 Geoffery(admin);526/527 可区分响应构成 padding oracle。 - Impossible:AES-256-GCM 认证加密,随机 nonce + tag 校验,密文/IV/tag 任一篡改均被拒;黑盒不可伪造,唯一信任假设是密钥保密。
2、核心防御建议优先级
下表按"修复收益 ÷ 实施成本"排序,P0 是必须立刻做、且改动量小的项,P1/P2 可纳入迭代。每条建议都能在本次实测中找到对应的漏洞级别和实证数据:
| 优先级 | 建议 | 对应修复的级别 | 实测依据 |
|---|---|---|---|
| P0 | 淘汰自研/XOR/Base64,改用标准 AEAD(AES-GCM/ChaCha20-Poly1305) | LOW | 密文一键还原出密码 Olifant |
| P0 | 禁止 ECB;加密必须配套完整性校验(MAC/tag) | MEDIUM | sooty/sweep 共享块 b85bb...;可伪造 admin |
| P0 | IV/nonce 使用 CSPRNG 随机生成,禁止固定/客户端可控 | HIGH | IV 写死且客户端回传,确定性实锤 |
| P1 | 统一解密错误响应,消除 padding oracle(根治用 AEAD) | HIGH | 526 vs 527 可稳定区分 |
| P1 | 密钥外置到环境变量/KMS,禁止硬编码,定期轮换 | 全部(Impossible 残留) | 四级密钥均 define("KEY",...) |
| P2 | 解密/校验结果必须真实接入授权逻辑,杜绝死代码 | LOW | $decoded 解了却不参与判断 |
落地顺序建议:先做三条 P0------它们都能通过"替换加密调用 + 调整 IV 生成"在小范围内完成,且一举消灭 LOW/MEDIUM/HIGH 三级的可利用漏洞;再做 P1 中的错误响应统一(若已切到 GCM,padding oracle 自然消失,此项可合并进 P0 的 AEAD 迁移);密钥外置(P1)和授权逻辑梳理(P2)涉及运维与业务代码,可排进后续迭代,但密钥轮换机制应尽早建立。
一个判断标准:做完 P0 后,重新跑一遍本文的 PoC 篡改矩阵,应当看到"合法 token 200、其余篡改全部统一失败码"------这就是从 VULNERABLE 转向 SECURE 的可验证信号。
3、关键启发
- 算法强度 ≠ 系统安全:AES-256 配错模式(ECB)、缺完整性(裸 CBC)、IV 固定,照样被攻破。密码学的安全性取决于"整条链路最弱的一环",而不是密钥位数。
- 机密性和完整性是两个独立目标,只加密不认证等于"锁了门却没插销"------攻击者看不到内容,但可以随意改内容。AEAD 的价值就在于把两者绑在一起。
- 半吊子修复可能比不修复更危险:HIGH 的 padding oracle 是 LOW/MEDIUM 都没有的新攻击面,它让攻击者连密钥都不需要。"用了 AES-CBC"带来的虚假安全感,往往比明显的弱加密更致命。
- 安全设计应把信任假设最小化:像 GCM 那样,把系统唯一的秘密收敛到"密钥"这一个点上,其余(IV 可公开、算法可公开、密文可公开)都交给经过广泛审查的算法库。需要保密的东西越少,系统越容易被证明是安全的。
- 可验证比"我觉得安全"重要:本文每一级结论都建立在 PoC 实测响应上。防御方也应当建立自己的"篡改断言矩阵",用自动化测试持续证明安全属性成立,而不是依赖代码评审时的主观判断。
八、AI 增强防御建议
1、智能密码学缺陷检测
AI 静态扫描(SAST)应内置一套密码学规则引擎,在代码提交阶段自动识别危险用法。与通用 bug 扫描不同,密码学缺陷的特点是"代码能正常运行、结果看起来是乱码",因此必须靠模式匹配 + 数据流分析,而不是靠崩溃或异常:
- 危险算法黑名单 :自研 XOR/移位、
aes-*-ecb、DES、3DES、RC4,以及把 MD5/SHA1 用于密码哈希或完整性保护等用途 → 直接高危。这些在本文对应 LOW(XOR)和 MEDIUM(ECB)。 - 安全算法白名单 :
aes-256-gcm、chacha20-poly1305、aes-*-gcm/ccm等 AEAD → 通过机密性 + 完整性初检(对应 Impossible)。 - 完整性配对检查 :追踪每个
openssl_encrypt/ 加密调用,确认其产物在解密侧能配对到 tag 或 HMAC 的生成与校验 ;若加密用了 CBC/CTR/ECB 却全程没有hash_hmac、没有 GCM tag、没有hash_equals比对 → 判定"未认证加密"(对应 HIGH/MEDIUM)。 - IV 来源追踪 :沿数据流回溯 IV 变量,若来自字面量常量(
define("IV",...))或客户端输入($_POST/JSON 字段)→ 告警;若来自openssl_random_pseudo_bytes/random_bytes→ 通过(对应 HIGH 的固定/可控 IV vs Impossible 的随机 nonce)。 - 密钥来源检查:识别硬编码密钥常量直接流入加密函数 → 告警并建议外置;本文四级全部命中此项。
- 弱用途识别 :检测"解密结果变量未被后续使用"(LOW 的
$decoded死代码)、把密码可逆加密而非做单向哈希等逻辑断层。
规则引擎的价值在于:这些缺陷在 code review 中极易被"AES 看起来很专业"的表象骗过,而正则 + 数据流可以零成本、全量、一致地拦截。
2、基于数据流的密钥与 IV 分析
密码学漏洞的根因往往不在"调用了哪个函数",而在"密钥和 IV 从哪里来、到哪里去"。AI 应构建跨函数、跨文件的数据流图:
- 密钥数据流 :构建"密钥变量 → 加密/解密函数参数"的完整路径,标注来源是硬编码字面量、配置文件、环境变量还是 KMS。硬编码
define("KEY",...)直接流入openssl_encrypt是最典型的告警(本文四级皆是)。 - IV/nonce 数据流 :追踪 IV 变量的三类来源并分别处置------
define字面量常量(固定 IV ❌)、$_POST/php://input等客户端输入(可控 IV ❌)、openssl_random_pseudo_bytes/random_bytes等 CSPRNG 输出(随机 ✅)。HIGH 同时踩中前两类,Impossible 命中第三类。 - 非随机化指纹:若加密路径上完全不出现 CSPRNG 调用,或 IV 在多次请求间保持不变(可结合动态确认),标记为"确定性加密"。
- 逻辑断层识别 :识别"解密/验签结果变量赋值后从未被条件判断使用"(LOW 的
$decoded)、或校验结果不影响授权分支等控制流缺陷------这类问题纯数据流看不出来,需要结合污点分析与控制流分析。
数据流分析的关键优势是能穿透封装 :即便开发者把加密包在 token_library_*.php 这类库里、调用点看起来很干净,AI 仍能顺着函数进入库内部,看到 IV 到底是常量还是随机数、tag 到底有没有被校验。这正是人工审计容易遗漏、而工具擅长的地方。
3、动态篡改矩阵(DAST)
AI 对加密端点自动执行:
- 抓 token:从页面/响应提取加密凭据(自动 HTML 实体反转义、自动识别 hex/base64/JSON 编码)。
- 熵与块分析:按块长切分,重复块 → ECB/固定 IV 指纹;多次请求同明文密文不变 → 确定性。
- 篡改矩阵:发送 合法 / 改密文 / 改 IV / 截断 四类请求,断言响应。
- oracle 探测:若"解密失败"与"格式失败"响应可稳定区分(如 526 vs 527)→ padding oracle 高危。
- 越权伪造 :识别明文权限字段(
level、userid),自动构造越权密文并验证。
4、代码审计集成
把上述静态 + 动态能力嵌入研发流水线,才能让密码学检查从"偶尔做一次的安全评审"变成"每次提交都强制经过的门禁":
- 提交期 lint(pre-commit / PR 检查) :当 diff 中出现
openssl_encrypt/openssl_decrypt、hash_hmac、random_bytes等加密相关调用时,自动触发密码学规则检查------模式是否为 AEAD、IV 是否来自 CSPRNG、tag 是否被生成并校验、密钥是否硬编码。 - 阻断式门禁 :对
aes-*-ecb、自研 XOR/移位、硬编码密钥、无 MAC 的裸 CBC 等确定性高危项,直接阻断合并并在 PR 上评论给出修复建议(推荐替代写法 + 代码片段),而不是仅出报告。 - 定期全量扫描:门禁只管增量,存量代码需要定期全仓扫描,输出"密码学债务清单",按"是否暴露在网络端点 + 是否处理身份/权限数据"排序整改。
- 结果可追溯:每条告警关联到 CWE(如 CWE-327 使用破损/风险算法、CWE-347 不当签名验证、CWE-330 不充分随机值)和对应的修复 PR,形成闭环。
- 与 DAST 联动:SAST 标记出的加密端点自动加入 DAST 篡改矩阵的探测目标列表,让静态发现的疑点在运行时被实证确认或排除。
集成的核心原则是**"左移 + 自动化"**:密码学错误一旦写进代码就很难靠事后测试发现(因为程序不会报错),必须在最早、成本最低的提交阶段用规则拦下来。
5、AI 防御架构图
下图给出一套"静态门禁 + 运行时探测 + 风险处置"三层联动的密码学防御体系,覆盖从代码提交到线上运行的完整生命周期:
text
┌─────────────────────────────────────────────────────────────────┐
│ 代码提交阶段(SAST 静态门禁) │
│ │
│ 代码提交 ──▶ 密码规则引擎 │
│ ├─ 算法黑白名单 (ECB/XOR/DES/RC4 拦截;GCM 放行) │
│ ├─ IV 来源数据流 (CSPRNG? 固定常量? 客户端可控?) │
│ ├─ tag/MAC 配对 (加密↔认证是否成对、是否真校验) │
│ ├─ 密钥硬编码检测 (define(KEY) / 字面量 → 告警) │
│ └─ 逻辑断层 (解密结果未接入授权 → 告警) │
│ │ │
│ 命中高危 ──▶ 阻断合并 + PR 评论修复建议 │
└──────────────────────────┼──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 运行时阶段(DAST 动态探测,针对加密端点) │
│ │
│ 运行时 ──▶ 加密端点探测器 │
│ ├─ 抓 token + 编码自动识别(hex/base64/JSON/HTML反转义)│
│ ├─ 密文熵与块分析(重复块=ECB指纹;同明文同密文=确定性) │
│ ├─ 篡改矩阵(合法 / 改密文 / 改IV / 截断tag) │
│ ├─ padding oracle 探测(失败响应是否可区分 526/527) │
│ └─ 越权伪造(识别 level/userid,构造越权密文验证) │
│ │ │
│ 发现可利用 ──▶ 生成工单 + 定级 + 推送负责人 │
└──────────────────────────┼──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 处置与放行 │
│ │
│ 命中风险 ──▶ 阻断构建 / 生成工单 / 输出修复建议 │
│ (标准修复:AEAD + 随机 nonce + tag 校验 + 密钥外置) │
│ │
│ 通过 ──▶ AEAD(AES-GCM/ChaCha20-Poly1305) │
│ + 12B 随机 nonce + tag 全链路校验 │
│ + 密钥进 KMS/环境变量并轮换 │
│ + 统一不可区分的错误响应 ──▶ 放行 │
└─────────────────────────────────────────────────────────────────┘
这张图的设计要点是纵深防御:SAST 在源头拦下绝大多数低级错误(ECB、硬编码、无 MAC),DAST 在运行时实证那些"静态看不出、只有发请求才能暴露"的问题(padding oracle、篡改响应可区分、越权闭环),最后由处置层把结论收敛成统一的修复范式。三层互为补充------静态漏的动态补,动态确认的反哺静态规则库。
6、实施优先级建议
密码学防御改造不必一步到位,可按"见效速度 ÷ 落地成本"分三批推进:
- 第一批:静态规则(成本最低、见效最快,1~2 个迭代即可上线)。 在 CI 中加入密码学 lint,拦截 ECB、自研 XOR、硬编码密钥、无 MAC 的 CBC。这一步几乎不需要改业务逻辑,却能立刻阻止新的高危写法进入主干,并通过存量扫描给出债务清单。
- 第二批:动态篡改矩阵(覆盖黑盒场景)。 把本文 PoC 中的"合法 / 改密文 / 改 IV / 截断 / oracle 探测"固化为自动化 DAST 用例,纳入定期扫描和上线前验收。它能发现静态规则漏掉的运行时配置问题(如错误响应可区分、IV 实际被回传客户端),并给出可复现的实证。
- 第三批:密钥管理改造(长期、跨团队)。 推动密钥从源码迁移到环境变量 / KMS、建立轮换与泄露应急流程。这是唯一需要运维、研发、安全多方协调的项,也是 Impossible 级别唯一残留风险的根治手段,应尽早立项、分步实施。
推进过程中可遵循一条经验:凡是能用"替换为 AEAD"一次性解决的(机密性、完整性、随机 nonce、padding oracle),就不要拆成多个独立任务去分别打补丁------前者改一处、后者改四处还容易遗漏。
7、总结
AI 在密码学防御中的角色,本质是把"资深安全工程师脑子里的检查清单"固化成可全量、一致、重复执行的规则与探测。它不会替代密码学专家做算法设计,但能在三个层面放大团队能力:在提交期用静态规则零成本拦截低级错误、在运行时用篡改矩阵实证安全属性、在审计期用数据流穿透封装揪出硬编码密钥和可控 IV。最终目标是让"用对 AEAD、随机 nonce、tag 校验、密钥外置"成为默认且被自动强制的工程习惯,而不是依赖个人经验的临场发挥。
免责声明:本文所述内容仅供安全研究与学习交流使用,所有测试均在本地授权靶场(DVWA)环境中进行。未经授权,严禁将文中技术用于任何非法目的。
📌 本文是专栏「DVWA通关全记录:从漏洞复现到安全防御」的第 18 篇文章。
- 上一篇 :从直接重定向到白名单验证:DVWA 开放重定向模块完整漏洞分析教程
- 下一篇 :将进入 API(应用程序接口) 模块,带你完整理解 API 安全漏洞的攻防全貌。
👉 点击订阅专栏,第一时间收到更新通知!
如果你在阅读过程中有任何疑问,欢迎在评论区留言交流。😊