最近做了 ph 牛的 code-breaking,在做 picklecode 这一题时,没有搞懂那个 django 的 signed_cookies 引擎对 session 的操作,就 debug 了一下,做个总结,算是做了个代码审计吧
题目 : https://github.com/phith0n/code-breaking/tree/master/2018/picklecode
django 使用的 SESSION_ENGINE 为 django.contrib.sessions.backends.signed_cookies
pycharm 开启 debug 模式,username 为 peri0d,password 为 123456
入口文件在 views.py
,第 34 行新建了用户并对密码进行了加密。第 35 行调用 auth_login()
函数,跳转到 auth\__init__.py
的 login()
方法
第 97 行,调用 user
类的 get_session_auth_hash()
方法来获取 session_auth_hash
的值,跟进 get_session_auth_hash()
给 key_salt
赋值后调用 salted_hmac(key_salt, self.password)
生成 session_auth_hash
,这里的 password
是经过加密的,跟进 salted_hmac()
在第 39 行对 key_salt + secret
进行 sha1 加密并以 byte 类型返回给 key
。这里的 value
是经过加密后的 password
。然后调用 hmac.new()
返回一个 sha1 模式的 hmac
对象
流程梳理
key_salt = '***' # SECRET_KEY secret = '******' key = hashlib.sha1(key_salt + secret).digest() sha1_obj = hmac.new(key, msg=password_enc, digestmod=hashlib.sha1) session_auth_hash = sha1_obj.hexdigest()
获取 session_auth_hash
后,单步调试,进入 base.py
执行 __contains__()
函数,参数为 _auth_user_id
单步调试,然后执行 _get_session()
函数,返回缓存 session,是一个空字典
在第 108 行执行 cycle_key()
,使用新密钥保存相同的数据,调用 save()
,它在请求结束时自动保存一个带有新密钥的 cookie 。
跟进 save()
,在第 41 行执行 _get_session_key()
,生成一个 base64 编码后的字符串作为 session key
,继续跟进,它又调用了 signing.dumps()
然后单步调试进入到 _get_session()
方法获取 self._session
,从缓存中加载 session,此时为一个空字典,即 self._session == {}
然后分别给 compress,salt,serializer 赋值,然后调用 signing.dumps()
,继续跟进,传入的参数 obj = {}, salt = 'django.contrib.sessions.backends.signed_cookies', compress = True
在 signing.dumps()
中对序列化之后的数据进行压缩,然后进行 base64 编码,再 decode()
为一个 Unicode 的 base64d
,其值为 'gAN9cQAu'
,最后调用 TimestampSigner
类的 sign()
方法,继续跟进
TimestampSigner
类继承自 Signer
类,先调用它的 __init__
方法进行初始化,key = 'zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm', sep = ':', salt = 'django.contrib.sessions.backends.signed_cookies'
然后调用 TimestampSigner
类的 sign()
方法,根据 value='gAN9cQAu'
,sep
和 timestamp()
对 value
进行重新赋值,其值为 'gAN9cQAu:1i5q6e'
,然后再次在 Signer.sign()
中重新赋值,得到最后结果 'gAN9cQAu:1i5q6e:wjJR2MUONx_wmPA3m8zYqTj5uCQ
回到 save()
,继续单步调试,调用了 base.py
中第 170 行的 _set_session_key()
方法,将 value
赋值给 session_key
和 _session_key
回到 save()
,完成赋值,回到 cycle_key()
,再回到 auth\__init__.py
的 login()
方法的第 108 行,这时可以在变量列表看到设置的 session 信息了
后面的代码是 django 对用户的持久化处理以及对 CSRF token 的验证等等,值得注意的是在第 126 行到 128 行,进行了 session 设置
流程梳理
_session = {} # SECRET_KEY secret = '******' salt='****' data = serializer().dumps(_session) compressed = zlib.compress(data) base64d = b64_encode(data).decode() session_key = TimestampSigner(SECRET_KEY, salt=salt).sign(base64d)
然后看它如何在 response 中设置 cookie 的,继续调试,在 contrib\sessions\middleware.py
中发现其对 cookie 的操作,从 44 行开始是设置 cookie 的存活时间,在第 58 行看到了 save()
函数,进行 cookie 的保存,单步调试进入
在 save() 函数中,调用 _get_session_key()
函数,剩下的反序列化和前面的相同,只是 session 的值发生了改变,从空字典变为含有 3 个元素的字典,然后就是将 cookie 设置在返回包中,这就完成了 cookie 设置的分析
总结一下,它对 session 处理的核心机制在于 django.core.signing.dumps()
函数,其具体代码如下,可以看到,data
为 pickle 序列化之后的 byte 对象,我们只要将 data
改为构造好的 evil pickle code 即能实现任意的代码执行
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False): data = serializer().dumps(obj) is_compressed = False if compress: compressed = zlib.compress(data) if len(compressed) < (len(data) - 1): data = compressed is_compressed = True base64d = b64_encode(data).decode() if is_compressed: base64d = '.' + base64d return TimestampSigner(key, salt=salt).sign(base64d)