news 2026/10/20 14:18:15

Flask session伪造与SECRET_KEY弱密钥爆破实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flask session伪造与SECRET_KEY弱密钥爆破实战指南

玩 CTF Web 的朋友应该都见过这么一种题:进去是个平平无奇的登录页,注册个账号、登录之后,访问/admin被拦下来,返回 403。随手把 Cookie 里的 session 解码,里面明文写着{"username":"guest"}。这时候题目在暗示什么?明摆着告诉你:Flask session 是可以伪造的,你得想办法把guest变成admin。

这类题目在 CTF 里出现频率极高,核心就三个词:Flask、session、伪造。它考的不是什么高深漏洞,而是你有没有真正理解 Flask 会话机制的底层实现。很多人看到SECRET_KEY就懵,拿着字典爆破半天没结果,还有人在手工构造 cookie 时因为盐、序列化、签名参数对不上而反复失败。这篇我把整套思路从头到尾捋一遍,从 session 存储原理讲到弱密钥爆破,再到手工伪造 admin cookie,附带我踩过的坑和排查方法,给正在刷题的你一份能直接照着操作的完整流程。


1. 先搞清楚 Flask 的 session 到底是什么

1.1 客户端 session 到底存哪了?

Flask 默认的 session 机制和传统服务端 session 完全不同。传统 PHP、Java 的 session 数据存在服务器内存或数据库里,客户端只拿一个 session id;而 Flask 默认把整个 session 对象序列化之后塞进浏览器 Cookie,所以叫“客户端 session”。

也就是说,当你登录后浏览器里那个很长一串的eyJ1c2VybmFtZSI6Imd1ZXN0In0.Z...,本质上就是{"username":"guest"}经过 Base64 编码再加了一段签名后的产物。把eyJ开头那一段丢进 Base64 解码工具,马上就能看到明文的 session 内容。这意味着任何拿到你 cookie 的人都能直接读出 session 里的所有字段,这就是 Flask session 的第一个特点:透明可见,不保密。

这个设计带来的问题很明显:用户能看见自己的 session 内容,就会想“我能不能改一下”。改很容易,但改完得让服务器认账,于是 Flask 在写入 cookie 前会对序列化后的数据进行签名。签名的作用是保证数据没有被篡改,而不是加密。

理解这点很关键。很多新手以为 Flask session 做了加密,安全得很,于是看到 cookie 里是密文和签名就放弃伪造了。实际上,你只需要拿到签名用的密钥,就能自己生成任何内容的合法 cookie,服务器完全分辨不出来。

1.2 签名不是加密,签名的意义和缺口

Flask 的签名机制用的是itsdangerous库,具体流程可以简化成三步:

  1. 把 session 字典序列化成 JSON 字符串,例如{"username":"admin"}。
  2. 对这个字符串做签名,签名过程中混入了SECRET_KEY、盐、时间戳、摘要算法等参数。
  3. 把“内容 + 时间戳 + 签名”一起 Base64 编码,拼成最终 cookie。

服务器收到 cookie 后,用同样的SECRET_KEY和参数重新计算签名,如果一致就信任里面的内容,再反序列化成 Python 字典作为 session。

这里有个关键点:整个验证过程只依赖 SECRET_KEY 这一个秘密。只要拿到了 SECRET_KEY,你就能伪造出任何 session 内容,包括把普通用户改成 admin、绕过任何只校验 session 的权限逻辑。

签名的缺口不止在于密钥泄露,还有另一个容易被忽略的点:签名算法只保证“内容没被改”,不保证“内容不透明”。所以即使没有密钥,攻击者也能读出 session 里的敏感信息。CTF 题目里有时会把密码、地址、密钥碎片等直接放进 session,造成信息泄露,这也算 session 机制附带的一类考点。

1.3 为什么 CTF 里特别爱考这个

因为考题非常好设计。开发者只需要在登录逻辑里把用户名存入 session,再在/admin路由里判断session.get("username") == "admin",一道题就成型了。难度高低完全取决于 SECRET_KEY 藏得好不好:

  • 最简单:SECRET_KEY = "secret",一眼可见,爆破一秒出结果。
  • 中等:密钥藏在源码备份文件里,需要先找到www.zip或者.git泄露。
  • 偏难:密钥不直接在源码里,而是通过模板注入、文件读取、报错信息、环境变量等方式才能拿到。

考这个点本质上是在测试你对“客户端 session 信任边界”的理解。服务器信任了 cookie 里携带的数据,却没保护好签名密钥,于是攻击者拿到了任意伪造的权力。这个逻辑放在真实业务里同样成立,很多 Flask 应用如果密钥写死、写在代码仓库里,或者用的是弱口令,就存在同样的安全隐患。


2. 伪造 admin 之前,先判断这道题到底能不能打

2.1 信息收集三步:拿源码、解 cookie、找入口

拿到题目先别急着爆破,我做题的顺序一般是这样:

第一,看有没有源码泄露。目录扫描扫一下.git、www.zip、app.py、flag.py、docker-compose.yml这些常见文件。很多题目故意把源码放在备份文件里,或者直接允许访问某些静态文件。源码里可能会有 SECRET_KEY、路由逻辑、session 处理方式,这些都是判断可打性的关键信息。

第二,解码当前 cookie。把浏览器里的 session cookie 复制出来,用flask-unsign --decode或者随便一个 Base64 解码工具看看里面的内容。有的题目 cookie 里除了username还会带role、is_admin、user_id之类的字段,看清楚再决定伪造目标。如果 session 本身有多个字段,伪造时尽量保持原有字段结构,只改需要的字段,避免服务器因为缺少某个键而报错。

第三,找到权限校验入口。访问/admin、/flag、/console、/manage等路径,看返回什么。注意观察是纯前端隐藏还是后端判断。如果页面显示“403 Forbidden”,基本就是后端校验 session;如果只是前端不显示菜单,那改个 cookie 刷新就能看到隐藏内容。做题时不要漏掉接口文档、注释里提到的路径。

有个容易被忽略的点:题目可能根本不用admin字符串,而是用user_id=1或者role=root来控制权限。伪造之前一定先读源码或者多试几个字段名。

2.2 常用的三种密钥泄露途径

拿到 SECRET_KEY 是伪造的核心。CTF 里常见途径我分成三类:

源码硬编码。最直接。app.secret_key = "s3cr3t"写在代码里,找得到源码就等于拿到了密钥。有些题目还喜欢把密钥放在config.py、settings.py甚至 Dockerfile 的环境变量里,翻一翻就有。

文件读取 / 备份文件泄露。通过任意文件读取漏洞读/etc/passwd、/proc/self/environ,或者下载.git、.bak、*.swp等备份文件。这类路径非常考验目录扫描能力,字典里要包含app.py.bak、config.py.swp、Dockerfile、.env这些文件。

模板注入(SSTI)读配置。如果页面存在模板注入,可以直接用{{ config.SECRET_KEY }}或{{ config }}读出 Flask 配置对象里的密钥。这类题目等于把密钥喂给你,下一步就是直接伪造。

还有其他一些偏门路径,比如登录接口报错时把密钥带进日志、调试模式下/console需要 PIN 但可以从上下文拿密钥,或者 session 本身里居然存了SECRET_KEY(开发者调试时留下的)。永远不要假设密钥藏得有多深,测试环境里一切皆有可能。

2.3 弱密钥爆破的可行性判断

如果上述泄露途径都没有,也别急着放弃。很多题目为了降低难度,故意把 SECRET_KEY 设成一个弱字符串,比如"secret"、"admin"、"123456"、"password"、"ctf"、"test"、"key"这类。这时候可以上字典爆破。

判断可不可爆破,先看密钥特征。如果题目是动态 Docker 环境,密钥通常是通过secrets.token_hex(32)之类生成的强随机值,爆破没有意义;如果是从代码里简单赋值,比如app.secret_key = os.getenv("SECRET_KEY", "default123"),那大概率可以从默认值入手。实在不确定,先拿几十万词的常见字典跑一遍,成本很低,跑不出来再考虑其他路线。

还需要注意:爆破用的 session cookie 必须是你当前会话有效的原始 cookie。因为验证签名时要用到“时间戳 + 签名”的匹配关系,随便拿一个修改过内容的 cookie 去比对,就算密钥对了也验证不出来。我一般直接复制登录后的原始 session,这个 cookie 里的数据和签名是配套的,适合做签名验证。


3. 实操:从普通用户到 admin 的完整伪造流程

3.1 工具准备与安装

推荐直接使用flask-unsign,这是专门针对 Flask session 的解码、爆破、签名工具,命令行操作,Python 环境装一下就行:

pip install flask-unsign

装好之后先验证能不能用:

flask-unsign --decode --cookie 'eyJ1c2VybmFtZSI6Imd1ZXN0In0.Z...'

成功的话会直接输出 session 明文,比如{'username': 'guest'}。这一步能快速确认目标到底是不是 Flask,以及当前 cookie 里存了哪些字段。

如果不想装额外工具,也可以用 Python 的itsdangerous库,但需要自己处理盐、签名算法等细节,后面我会单独演示。新手建议先用flask-unsign走通流程,再回头学手工实现。

3.2 用 flask-unsign 爆破出 SECRET_KEY

爆破命令非常简单:

flask-unsign --unsign --cookie 'eyJ1c2VybmFtZSI6Imd1ZXN0In0.Z...' --wordlist /path/to/wordlist.txt

参数说明:

  • --cookie传入原始 session cookie,注意 cookie 值可能很长,建议用单引号包裹,防止 shell 转义。
  • --wordlist指定密码字典路径。CTF 常用的字典有rockyou.txt、flask-secret-key.txt这类针对性字典,也可以自己收集一些 CTF 出题常用弱密码。

爆破的原理不复杂。flask-unsign拿到 cookie 后,先把内容、时间戳、签名拆开,然后用字典里的每个候选密钥重新计算签名,和原签名比对。如果一致,就打印出匹配的密钥。因为 HMAC 计算非常快,几十万条字典几分钟内就能跑完。

成功后的输出类似:

[*] Server returned: 200 [*] Found secret key: 's3cr3t'

如果没跑出来,输出是[*] No secret key found。这时候可以换大字典、加一些目标相关的词,或者回到源码泄露和模板注入的路子继续找。爆破不是终点,它只是众多拿到密钥的手段之一。

3.3 手工构造 admin cookie 的两种方式

拿到 SECRET_KEY 之后,伪造 admin cookie 有两种做法。

第一种,还是用flask-unsign一条命令搞定:

flask-unsign --sign --cookie "{'username': 'admin'}" --secret 's3cr3t'

注意这里--cookie参数里传入的是要构造的 session 字典,Python 字典格式,键值都要用引号。--secret跟上一步爆出来的密钥。执行后工具会生成一个新的 cookie,直接替换浏览器里的旧 cookie 即可。

第二种,用 Python 脚本手工生成。如果要自己掌控盐和参数,可以参考 Flask 内部的签名实现。下面是一个兼容原生 Flask session 的脚本:

from flask.sessions import SecureCookieSessionInterface from itsdangerous import URLSafeTimedSerializer secret_key = "s3cr3t" class FakeApp: secret_key = secret_key serializer = SecureCookieSessionInterface().get_signing_serializer(FakeApp()) session_dict = {"username": "admin"} cookie = serializer.dumps(session_dict) print(cookie)

关键点在于SecureCookieSessionInterface内部使用的盐是cookie-session,摘要算法是 SHA1,密钥派生方式是 HMAC。get_signing_serializer会自动配好这些参数,所以直接用 Flask 自带的类最保险,比自己拼URLSafeTimedSerializer参数要少踩很多坑。

不过要注意,不同版本的 Flask 在序列化方式上可能有细微差异。如果你用的是 Flask 2.x/3.x,上面的脚本基本通用;如果是老版本遇到问题,优先用flask-unsign,它会处理好版本兼容。

3.4 验证结果:带着新 cookie 访问 admin 页面

伪造完 cookie,下一步就是验证。打开浏览器开发者工具,找到当前站点对应的 session cookie,把生成的新 cookie 值粘进去,替换掉原来的值,然后刷新/admin页面。

有几种情况值得注意:

  • 页面从 403 变成正常内容或者直接输出 flag,说明伪造成功。
  • 页面跳回登录页,说明 cookie 的时间戳可能有问题,或者服务器对 session 还有额外校验,比如校验 IP、UA 等。这时候需要重新登录一次,拿到最新环境下的原始 cookie 再伪造。
  • 页面报告签名无效,说明密钥不对,或者flask-unsign生成时用的序列化参数和目标 Flask 版本不一致。排查方向就是对比原始 cookie 和伪造 cookie 的长度、结构,确认盐、摘要算法、时间戳格式一致。

还有一种常见情况:生成 cookie 时没错,但浏览器里设置不了那么长的 cookie,或者中间多复制了一个引号。遇到这种情况,把 cookie 值重新复制,去掉首尾多余字符,再试一次。别小看这些细节,实际做题时很多时间都耗在复制粘贴上。


4. 进阶场景:不是简单弱密钥怎么办?

4.1 通过模板注入读配置拿密钥

如果题目里存在 SSTI(服务端模板注入),读取 SECRET_KEY 往往比爆破更省事。利用点在 Flask 的模板上下文默认包含config,或者在可访问的对象链上能找到config。

最常见的 payload:

{{ config.SECRET_KEY }}

如果过滤严格,可以试试:

{{ config }} {{ self.__init__.__globals__['app'].config['SECRET_KEY'] }} {{ get_flashed_messages.__globals__['current_app'].config['SECRET_KEY'] }}

拿到密钥后,再用前面说的方法伪造 admin cookie。这种题目的本质是“先拿密钥,再伪造 session”,密钥本身只是中间目标,最终目标始终是提升权限到 admin。

实操中要留意:有些环境会自定义 Jinja2 的配置,把config从模板上下文里删掉,或者开启了 sandboxed 环境。这时可以尝试通过url_for.__globals__['current_app'].config一类的链子,因为 Flask 内置函数对象往往保存着对当前应用上下文的引用,利用空间很大。

4.2 源码泄露与备份文件排查

源码泄露是另一条拿密钥的高频路径。除了.git、www.zip,还可以看这几个:

  • .env:环境变量文件,经常存SECRET_KEY=xxx
  • config.py或settings.py:直接定义了 Flask 配置
  • requirements.txt:先确认目标 Flask 版本,再针对性构造伪造参数
  • Dockerfile:可能通过ENV SECRET_KEY设置密钥
  • docker-compose.yml:环境变量注入的地方
  • .bak、.swp、~后缀文件:编辑器产生的备份,可能存有源码

目录扫描的字典要够全,不能只扫常见路径。CTF 出题人经常故意留下backup.zip、code.tar.gz、tmp.zip这类模糊名称,扫描工具可能因为字典里没有就漏掉。手动测试时可以把常见备份扩展名挨个试一遍,成本不高,回报率却很高。

找到源码后先打开找SECRET_KEY,再找 session 相关的处理逻辑。有时候密钥是多个字符串拼接的,比如app.secret_key = a + b + c,你得把变量值都找齐才能在本地还原出完整的密钥。

4.3 处理动态盐、自定义序列化等改版题型

标准 Flask session 的盐是固定的"cookie-session",但有些题会自定义SecureCookieSessionInterface的子类,覆盖salt、digest_method、key_derivation甚至serializer。如果直接拿flask-unsign默认参数去爆破,可能永远跑不出密钥,因为签名参数对不上。

遇到这种题,排查重点是:源码里是否重写了 session 接口。如果看到类似:

class MySessionInterface(SecureCookieSessionInterface): salt = "my_salt" digest_method = staticmethod(hashlib.sha256)

那么伪造时也要用相同参数。手工脚本里就要改成:

from itsdangerous import URLSafeTimedSerializer from flask.sessions import TaggedJSONSerializer serializer = URLSafeTimedSerializer( secret_key, salt="my_salt", serializer=TaggedJSONSerializer(), signer_kwargs={ "key_derivation": "hmac", "digest_method": hashlib.sha256 } ) cookie = serializer.dumps({"username": "admin"})

另一个变种是禁用时间戳。有的题目出于稳定考虑会用URLSafeSerializer而不是URLSafeTimedSerializer,这时伪造出的 cookie 长度可能比原来更短,因为少了时间戳部分。正常情况下不需要刻意处理,但如果伪造后签名无效,就要回头检查对方到底用的是不是定时签名的方式。

自定义序列化也不少见,比如把 session 改成 pickle 序列化。一旦变成 pickle,伪造的后果就从权限提升直接升级到反序列化漏洞,甚至可以 RCE。这类题需要单独分析,不能照搬 JSON session 的做法。判断方法是解码 cookie,如果看不到 JSON 的键值结构,而是出现乱码或pickle特征,立刻改变思路。


5. 常见踩坑记录与修复建议

5.1 我实际操作中遇到的 5 个典型坑

第一个坑:用修改后的 cookie 去爆破密钥。前面提过,爆破验证签名时,原始 cookie 的签名和内容是配套的。如果你先把guest改成admin再去跑字典,因为签名已经不匹配,爆破算法无法通过签名比对来验证密钥,结果自然是找不出来。正确做法永远是:先复制原始登录 cookie,用它爆破密钥,再用密钥重新签名目标内容。

第二个坑:命令行里--cookie参数引号处理出错。在 bash 里写--cookie "{'username':'admin'}"没问题,但有些人写成--cookie "{\"username\": \"admin\"}",或者漏了外层单引号,shell 会把花括号和引号吃掉,导致工具报错或者生成一个错误的 cookie。我的经验是:直接把整段 cookie 复制到引号里,不要手动去改里面的引号格式。

第三个坑:伪造成功但页面仍然 403。这时候先别怀疑签名,去看看目标路由到底校验的是什么字段。有的/admin校验的是session["role"] == "admin",有的校验的是session.get("user_id") == 1,还有的是把用户名存到数据库里,session 里只有user_id,你单纯把username改成admin根本没用。必须结合源码看清楚校验逻辑,再决定改哪个字段。

第四点:时间戳过期。Flask 默认的 session 签名带时间戳,生成 cookie 之后在一定时间内有效。如果你伪造完放了一晚上再去提交,签名验证会因为过期失败。解决办法是重新生成一个带当前时间戳的 cookie,或者干脆登录一次、立刻伪造、立刻访问。

第五个坑:浏览器缓存干扰。有时候新的 cookie 已经设置成功,但页面还是旧内容,因为浏览器缓存了 403 响应。按 Ctrl+F5 强制刷新,或者用无痕窗口重新访问。这个小问题不仔细排查真的很浪费时间。

5.2 从防御视角反推:这样修才不容易被伪造

写 CTF 题的人会故意留漏洞,但真实业务里可不能这么干。从防御端看,防止 Flask session 被伪造的关键就几个原则。

密钥必须强随机、独立管理。不要再写app.secret_key = "secret"了。生产环境应该用os.urandom(32)生成,或者从环境变量、密钥管理系统读取。密钥不能提交进 Git,不能留在 Docker 镜像的环境变量里,不能出现在报错页面。即使是强密钥,也建议定期轮换,降低泄露后的影响。

敏感操作不要只信任 session 里的用户名。admin 权限校验最好同时校验数据库中的真实角色、用户状态、登录会话标识,不要只读 session 字典。因为 session 可读可伪造,单单靠一个username字段做管理员判断,就算密钥不泄露,也可能因为信息泄露被猜到内部字段结构,留下隐患。

能不用客户端 session 就不用。对安全性要求高的应用,切换到服务端 session,比如 Flask-Session + Redis。这样 session 数据只存在服务端,客户端拿到的只是随机 ID,即使 ID 泄露,也无法篡改里面的权限字段。当然,这种方式需要引入额外组件,适合对安全性敏感的场景。

给 cookie 加上安全属性。HttpOnly防止 JavaScript 读取 cookie,Secure保证只在 HTTPS 下传输,SameSite缓解跨站请求伪造。这些属性不能阻止密钥泄露,但可以加大攻击者拿到合法 cookie 的难度,属于基础加固。

这些防御手段单独看都很简单,组合起来却能从源头上切断伪造的可能。CTF 题里我们利用的是开发者的疏忽,而真实项目中,我们需要堵住的正是这些疏忽。


最后说点个人体会。我最早刷这类题时,也以为伪造 session 就是把字典改一改再编码回去,结果折腾了半天页面始终 403。后来耐下心把 Flask session 的签名流程读透,才发现问题都出在参数细节上:盐、摘要算法、密钥派生方式、有没有时间戳,每一项错了签名都对不上。从那之后我遇到 Flask session 题,第一反应不再是盲目爆破,而是先分析源码、确认 cookie 结构、判断校验字段,再决定伪造目标。这套流程走熟了,无论题目怎么换皮,核心思路都不会乱。如果你正卡在类似题目上,别急着怀疑工具不行,回头再读一遍 session 的签名机制,九成问题都能迎刃而解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 14:18:00

单周期CPU数据通路设计与十指令MIPS命令集实现全攻略

简介:面向计算机组成原理课程设计的实践资料包,适用于山东大学20级学生及相关专业学习者,围绕电路图设计与指令集实现展开,帮助将数据表示、运算器、控制器等理论落地为可运行的硬件方案。压缩包内共150个文件,以Quart…

作者头像 李华
网站建设 2026/10/20 14:17:39

TinyML入门实战:从微控制器到关键词唤醒的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 14:17:32

Looped Transformer:共享权重循环迭代,重塑深度模型效率边界

最近这段时间,Transformer 相关的研究方向出现了一个挺有意思的收敛趋势:大家不再一味把模型往“更深、更宽”的方向堆,而是开始回头思考一个非常基础的问题——同一组参数,能不能在推理时被反复使用多次?这个思路落到…

作者头像 李华
网站建设 2026/10/20 14:17:20

AA-Scan:可编程扫描仪行为建模工具

简介:AA-Scan是一款基于Arduino与Android平台的开源全自动3D扫描仪软硬件方案,面向嵌入式开发初学者、创客及三维建模爱好者,解决低成本、易组装、可复现的简易3D扫描设备搭建问题。资源包为完整项目源码,共5个文件:含…

作者头像 李华
网站建设 2026/10/20 14:17:20

多模态数据扩散模型做预测:原理、实操与工程落地全解析

打开扩散模型相关论文的时候,我第一反应也是“又一个做图像生成的”。直到我自己把扩散模型接到一个多模态预测项目上,才发现这条路的价值被严重低估了。传统的预测模型,不管是LSTM、Transformer还是各种树模型,本质上都在学“给定…

作者头像 李华
网站建设 2026/10/20 14:14:41

ESP32-S31固件烧录:Windows一键打包与边界处理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华