flask攻击相关笔记
Flask
SSTI
Flask SSTI,即模板注入,旨在利用相关模板引擎的特点,通过沙箱逃逸等技术进行攻击服务,获取隐私信息。
点击这里。
PIN 码计算
简介
PIN是 Werkzeug(它是 Flask 的依赖项之一)提供的额外安全措施,以防止在不知道 PIN 的情况下访问调试器。
调试器 PIN 只是一个附加的安全层,以防您无意中在生产应用程序中打开调试模式,从而使攻击者难以访问调试器。
Werkzeug 不同版本以及 Python 不同版本都会影响 PIN 码的生成。
但是 PIN 码并不是随机生成,当我们重复运行同一程序时,生成的 PIN 一样,PIN 码生成满足一定的生成算法。
源码分析
Werkzeug 生成 PIN 码的核心代码如下
1 | def get_pin_and_cookie_name( |
其中 rv
为 PIN 码值。
从代码中分析得知,PIN 码的组成由以下几部分组成:
username
:通过getpass.getuser()
获取,是启动 Flask 的用户modname
:模块名,一般默认为flask.app
appname
:通过getattr(app, "__name__", type(app).__name__)
获取,app 名,一般默认为Flask
moddir
:模块路径,flask目录下的一个app.py
的绝对路径mac
:通过str(uuid.getnode())
获取,当前网络的 MAC 地址的十进制数machine_id
:通过get_machine_id()
获取,/etc/machine-id
或者/proc/sys/kernel/random/boot_i
中的值
信息获取
username
一般通过读取 /etc/passwd
文件寻找可能的用户进行爆破。
modname
默认 flask.app
。
appname
默认为 Flask
。
moddir
可以通过 DEBUG 模式下的报错信息获取。
mac
读取 /sys/class/net/eth0/address
获取 MAC 的 16
进制,然后转化为十进制即可。
不一定是
eth0
,可能需要尝试爆破。
machine_id
在 Linux 平台下,为 /etc/machine-id
或者
/proc/sys/kernel/random/boot_i
中的值
在 Win 平台下,为注册表中
SOFTWARE\Microsoft\Cryptography
的值。
在 Docker 中,为 /proc/self/cgroup
的
docker
行后的十六进制字符串,例如
1:name=systemd:/docker/4e2d4390ee2a9b57df253521f44301973efc74e35a300a02b4e509d60989543b
中的
4e2d4390ee2a9b57df253521f44301973efc74e35a300a02b4e509d60989543b
。
计算代码
1 | import hashlib |
在 Python 不同版本中,<=3.7 h
采用 md5 算法;>3.7
h
采用 sha1 算法。在以上代码可以给予值
high_version=True
采用 sha1 算法。
session 构造
Flask 中的 session 是通过 secret_key 进行签名的,所以在构造 session 之前需要获取 secret_key。
可以使用脚本 flask-session-cookie-manager 加密解密 session 的 jwt。
也可以试图自己运行代码获取伪造 session 值。
一些巧妙利用
SECRET_KEY
SECRET_KEY 会被存放在环境变量和 config
中,可以考虑读取
/proc/self/environ
或者使用 env
命令。
其他的获取方式可以查看 SSTI 的文章内容。
os.path.join()
os.path.join()
函数用于拼接两个字符串作为路径,分隔符以系统指定分隔符为主。
但当两个目录为同级目录时,只会默认返回后一个字符串作为结果,例如
1
os.path.join('static', '/abc/cba')
需要注意,第二个目录需要是根目录才能被识别为同级目录。
还有一种利用是目录穿越,但需要注意的是 Python 的目录穿越只有
../../
的方式。
1 | # filename = '../../flag' |
pickle 反序列化注入
基本原理
Python 的 __reduce__
魔术方法正是 Python
类的反序列化方法,我们可以通过注入命令到 __reduce__
方法中。
Python 要求 __reduce__
方法返回一个函数和需要调用的参数((callable, [para1, para2, ...])
),Python
希望的是通过 callable
和其参数生成该对象,但实际上我们可以利用这点变相调用漏洞函数。
1 | import pickle |
当运行 pickle.loads(payload)
时,我们将进入 sh。
opcode
pickle 的 opcode 详解如下
opcode | 描述 | 具体写法 | 栈上的变化 | memo上的变化 |
---|---|---|---|---|
c | 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) | c[module] | 获得的对象入栈 | 无 |
o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 | 无 |
i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module] | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 | 无 |
N | 实例化一个None | N | 获得的对象入栈 | 无 |
S | 实例化一个字符串对象 | S'xxx'(也可以使用双引号、'等python字符串形式) | 获得的对象入栈 | 无 |
V | 实例化一个UNICODE字符串对象 | Vxxx | 获得的对象入栈 | 无 |
I | 实例化一个int对象 | Ixxx | 获得的对象入栈 | 无 |
F | 实例化一个float对象 | Fx.x | 获得的对象入栈 | 无 |
R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 | 无 |
. | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 | 无 |
( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 | 无 |
t | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
) | 向栈中直接压入一个空元组 | ) | 空元组入栈 | 无 |
l | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
] | 向栈中直接压入一个空列表 | ] | 空列表入栈 | 无 |
d | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
} | 向栈中直接压入一个空字典 | } | 空字典入栈 | 无 |
p | 将栈顶对象储存至memo_n | pn | 无 | 对象被储存 |
g | 将memo_n的对象压栈 | gn | 对象被压栈 | 无 |
0 | 丢弃栈顶对象 | 0 | 栈顶对象被丢弃 | 无 |
b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 | 无 |
s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 | 无 |
u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 | 无 |
a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 | 无 |
e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 | 无 |
此外, TRUE
可以用 I
表示:
b'I01\n'
; FALSE
也可以用 I
表示: b'I00\n'
,其他 opcode 可以在pickle库的源代码中找到。
由这些opcode我们可以得到一些需要注意的地方:
- 编写opcode时要想象栈中的数据,以正确使用每种opcode。
- 在理解时注意与python本身的操作对照(比如python列表的
append
对应a
、extend
对应e
;字典的update
对应u
)。 c
操作符会尝试import
库,所以在pickle.loads
时不需要漏洞代码中先引入系统库。- pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c
、i
。而如何查值也是CTF的一个重要考点。 s
、u
、b
操作符可以构造并赋值原来没有的属性、键值对。
使用 pickletools
的 dis
函数可以展示更易识别的序列化后的内容。
利用 opcode 我们可以更精细化地做事情。
拼接 opcode
仅需把第一个 pickle 流结尾表示结束的 .
去掉后直接拼接第二个 pickle 流即可。
全局变量覆盖
1 | import secret |
opcode 解析:
c__main__\nsecret\n
,读取__main__
中的secret
模块(
,标记一个 MARKS'name'\n
,生成字符串'name'
入栈(并是标记位)S'qsdz'\n
,生成字符串'qsdz'
入栈d
,将栈-2和栈-1位置的值按key-value
对组成字典,然后入栈字典b
,将key-value
对以key=value
的形式赋值属性.
,结束反序列化
函数执行
能进行函数执行的 opcode
有三个,R
、i
、o
,所以有三种方向构造
R
1 | b'''cos |
i
1 | b'''(S'whoami' |
o
1 | b'''(cos |
实例化对象
1 | import pickle |
其他
为了解决pickle反序列化的问题,官方给出了使用改写
Unpickler.find_class()
方法,引入白名单的方式来解决,并且给出警告:对于允许反序列化的对象必须要保持警惕。对于开发者而言,如果实在要给用户反序列化的权限,最好使用双白名单限制
module
和 name
并充分考虑到白名单中的各模块和各函数是否有危险。
由于 pickle
“只能赋值,不能查值”的特性,唯一能够根据键值查询的操作就是
find_class
函数,即 c
、i
等
opcode,如何根据特有的魔术方法、属性等找到突破口是关键;此外,在利用过程中,往往会借助
getattr
、get
等函数。
yaml 反序列化
1 | !!python/object/new:tuple |