flask session 安全问题 和 python 格式化字符串漏洞
前言
ctf题中遇到了伪造session和python的格式化字符串漏洞 这里做个小结
1、flask session 安全问题
flask 是非常轻量级的 Web框架 其 session 存储在客户端中(可以通过HTTP请求头Cookie字段的session获取)
1、flask对session的防护
flask对session的防护如下
- 新建了
URLSafeTimedSerializer
类 ,用它的dumps
方法将类型为字典的session
对象序列化成字符串,然后用response.set_cookie
将最后的内容保存在cookie
中 - json.dumps 将对象转换成json字符串,作为数据
- 如果数据压缩后长度更短,则用zlib库进行压缩
- 将数据用base64编码
- 通过hmac算法计算数据的签名,将签名附在数据后,用“.”分割
这就解决了用户篡改session的问题,在不知道secret_key的情况下,是无法伪造签名的
代码语言:javascript复制class SecureCookieSessionInterface(SessionInterface):
"""The default session interface that stores sessions in signed cookies
through the :mod:`itsdangerous` module.
"""
#: the salt that should be applied on top of the secret key for the
#: signing of cookie based sessions.
salt = 'cookie-session'
#: the hash function to use for the signature. The default is sha1
digest_method = staticmethod(hashlib.sha1)
#: the name of the itsdangerous supported key derivation. The default
#: is hmac.
key_derivation = 'hmac'
#: A python serializer for the payload. The default is a compact
#: JSON derived serializer with support for some extra Python types
#: such as datetime objects or tuples.
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)
# Delete case. If there is no session we bail early.
# If the session was modified to be empty we remove the
# whole cookie.
if not session:
if session.modified:
response.delete_cookie(app.session_cookie_name,
domain=domain, path=path)
return
# Modification case. There are upsides and downsides to
# emitting a set-cookie header each request. The behavior
# is controlled by the :meth:`should_set_cookie` method
# which performs a quick check to figure out if the cookie
# should be set or not. This is controlled by the
# SESSION_REFRESH_EACH_REQUEST config flag as well as
# the permanent flag on the session itself.
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)
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
2、安全问题
但问题也来了 flask仅对 session 进行了签名,缺少数据防篡改实现,这便很容易存在安全漏洞
假设现在我们有一串 session 值为: eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY
那么我们可以通过如下代码对其进行解密:
from itsdangerous import *
s = "eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY"
data,timestamp,secret = s.split('.')
int.from_bytes(base64_decode(timestamp),byteorder='big')
P师傅的对session的解密脚本
代码语言:javascript复制#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
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__':
print(decryption(sys.argv[1].encode()))
可能危害:
- 敏感信息泄露
- 验证码绕过漏洞
- session伪造及对象注入漏洞
作为一个开发者,如果我们使用的web框架或web语言的session是存储在客户端中,那就必须牢记下面几点:
- 没有加密时,用户可以看到完整的session对象
- 加密/签名不完善或密钥泄露的情况下,用户可以修改任意session
- 使用强健的加密及签名算法,而不是自己造(反例discuz)
2、python的格式化字符串漏洞
在 python 中,提供了 4种 主要的格式化字符串方式,分别如下:
1、%
操作符
%
操作符沿袭C语言中printf语句的风格
>>> name = 'Bob'
>>> 'Hello, %s' % name
"Hello, Bob"
2、string.Template
使用标准库中的模板字符串类进行字符串格式化
代码语言:javascript复制>>> name = 'Bob'
>>> from string import Template
>>> t = Template('Hey, $name!')
>>> t.substitute(name=name)
'Hey, Bob!'
3、调用format
方法
python3后引入的新版格式化字符串写法,但是这种写法存在安全隐患
代码语言:javascript复制#直接格式化字符串
>>> 'My name is {}'.format('Hu3sky')
'My name is Hu3sky'
#指定位置
>>> 'Hello {0} {1}'.format('World','Hacker')
'Hello World Hacker'
>>> 'Hello {1} {0}'.format('World','Hacker')
'Hello Hacker World'
#设置参数
>>> 'Hello {name} {age}'.format(name='Hacker',age='17')
'Hello Hacker 17'
#百分比格式
>>> 'We have {:.2%}'.format(0.25)
'We have 25.00%'
#获取数组的键值
>>> '{arr[2]}'.format(arr=[1,2,3,4,5])
'3'
存在安全隐患的事例代码:
代码语言:javascript复制>>> config = {'SECRET_KEY': '12345'}
>>> class User(object):
... def __init__(self, name):
... self.name = name
...
>>> user = User('joe')
>>> '{0.__class__.__init__.__globals__[config]}'.format(user)
"{'SECRET_KEY': '12345'}"
从上面的例子中,我们可以发现:如果用来格式化的字符串可以被控制,攻击者就可以通过注入特殊变量,带出敏感数据
主要语句user.__class__.__init__.__globals__
4、f-Strings
这是python3.6之后新增的一种格式化字符串方式,其功能十分强大 可以执行字符串中包含的python表达式,安全隐患可想而知
代码语言:javascript复制>>> a , b = 5 , 10
>>> f'Five plus ten is {a b} and not {2 * (a b)}.'
'Five plus ten is 15 and not 30.'
>>> f'{__import__("os").system("id")}'
uid=0(root) gid=0(root) groups=0(root)
'0'
结语
这俩通常会一起出现 然后通过格式化字符串漏洞获取secret_key 再用secret_key伪造session 从而获取admin权限
参考
- 客户端 session 导致的安全问题
- Python 格式化字符串漏洞(Django为例)
- 从两道CTF实例看python格式化字符串漏洞
- Python Web之flask session&格式化字符串漏洞
红客突击队于2019年由队长k龙牵头,联合国内多位顶尖高校研究生成立。其团队从成立至今多次参加国际网络安全竞赛并取得良好成绩,积累了丰富的竞赛经验。团队现有三十多位正式成员及若干预备人员,下属联合分队数支。红客突击队始终秉承先做人后技术的宗旨,旨在打造国际顶尖网络安全团队。