0x00:关于Flask框架
Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合MVC模式进行开发,开发人员分工合作,小型团队在短时间内就可以完成功能丰富的中小型网站或Web服务的实现。另外,Flask还有很强的定制性,用户可以根据自己的需求来添加相应的功能,在保持核心功能简单的同时实现功能的丰富与扩展,其强大的插件库可以让用户实现个性化的网站定制,开发出功能强大的网站。
同时正是因为其轻量化特性,所以并没有像TP框架将session放置服务端、Django框架默认将session放置数据库,Flask对数据库操作的框架都没有,即选择将session放置在客户端的cookie中(数字签名加密后的)
0x01:客户端session
在解析 session 的实现之前,我们先介绍一下 session 怎么使用。session 可以看做是在不同的请求之间保存数据的方法,因为 HTTP 是无状态的协议,但是在业务应用上我们希望知道不同请求是否是同一个人发起的。比如购物网站在用户点击进入购物车的时候,服务器需要知道是哪个用户执行了这个操作。
对于我们熟悉的其他web环境中,大部分对session操作都是存入服务器本地文件中,用户看到的只是session的名称(一个随机字符串),其内容保存在服务端。这种就做到了用户无法读取到session,这种叫服务端session。
类似下面这样
那么客户端session顾名思义就是存储在客户端,和cookie一样,用户通过f12即可查看当前的sessionid
那么将session存储在客户端中,最重要的就是要做到解决session不能被篡改的问题
Flask处理session逻辑如下
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)
这里跟踪URLSafeTimedSerializer的代码逻辑
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
这里session被序列化,着重关注序列化的dump_payload和dumps函数
经过分析得知
将session对象转换为json字符串->base64编码->进行签名校验
那么此时只需得知签名的secret_key即可进行伪造签名
可以通过文件泄露、SSTI模板注入{{config}}进行读取secret_key,进行伪造签名
即使在无法读取到secret_key的情况下,也可以通过解密客户端的session进行分析(解密源码见0x02)
有时仍然可以通过密码找回页面进行漏洞利用。
0x02:漏洞利用
在CTF中 session伪造的考点多在源码泄露+session伪造或者SSTI+session伪造用户 等等。
接下来介绍两个脚本(解密和加密)
session解密脚本
#!/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()))
脚本利用效果如下??
可以看到在不知道secret_key前提下仍然可以得知一些session组成信息。往往可以在一些逻辑漏洞中展开深究
session重构加密脚本
https://github.com/noraj/flask-session-cookie-manager
0x03:总结
介于Flask的本身特性,就是为了轻便而生,往往可能并没有运行在带有数据库的服务器上,所以session保存在客户端不可避免。
那么开发者在使用Flask框架时使用强健的加密以及签名算法、防止ssti漏洞(做好服务器安全开发)、注意源码保密即可将风险降到可控范围内
原文:https://www.cnblogs.com/Tkitn/p/12803475.html