1. 从一块饼干说起Flask Session的“甜蜜”陷阱大家好我是老张一个在Web安全和开发领域摸爬滚打了十多年的老码农。今天想和大家聊一个既经典又充满“甜味”的安全话题——Flask框架的Session机制。你可能觉得Session不就是服务器用来记住用户状态的小饼干Cookie吗能有什么大问题那我问你如果你知道用来给这块“饼干”签名的秘方就藏在一张公开的饼干菜单里你会怎么做这正是PicoCTF平台上那道名为“Most Cookie”的题目给我们出的难题。它用一个精巧的案例赤裸裸地展示了当Flask应用的secret_key秘密密钥不够“秘密”时整个会话系统会变得多么脆弱。我最初接触这道题时也像很多新手一样对着源码和那一长串饼干名字列表cookie_names发懵感觉无从下手。但一旦你理解了Flask Session的工作原理和其中那个关键的“签名”环节整个攻击路径就会变得清晰无比就像捅破一层窗户纸。简单来说这道题的核心逻辑是这样的网站的后台随机从一份公开的饼干名称列表中挑选一个作为加密和签名Session的secret_key。我们的目标是伪造一个Session告诉服务器“我是管理员admin”从而拿到最终的Flag。但你不能直接改Cookie值因为Flask会用密钥对Session内容进行签名篡改后签名对不上服务器一眼就能识破。所以破解的关键就在于如何从那份公开的列表中找出服务器当前到底用的是哪一个名字作为密钥。找到了它你就能自己当“糕点师”烤出任何你想要的、能被服务器认可的“饼干”了。这个过程本质上是一场针对弱密钥的“猜谜游戏”。对于开发者而言这是一个严肃的安全警示对于学习Web安全的朋友来说这是一次绝佳的实战演练。接下来我就带你一步步拆解这个“猜谜”过程不仅告诉你工具怎么用更要把背后的原理掰开揉碎讲清楚让你下次遇到类似问题能自己成为那个“破局者”。2. 庖丁解牛深入Most Cookie题目源码拿到任何一道CTF题目尤其是这种给了源码的第一件事绝不是急着运行工具而是静下心来读代码。理解程序的逻辑往往比盲目操作更重要。我们先把题目给的server.py核心部分拎出来看看它到底在玩什么花样。2.1 密钥的生成致命的“随机”选择我们看源码开头这几行cookie_names [snickerdoodle, chocolate chip, ... , white chocolate macadamia] # 一长串饼干名 app.secret_key random.choice(cookie_names)这里定义了一个包含28种饼干名称的列表cookie_names然后使用random.choice()从中随机选择一个赋值给app.secret_key。这里就是第一个也是最致命的安全漏洞密钥空间太小且可预测。secret_key在Flask中至关重要它用于对Session Cookie进行签名注意是签名不是加密这点后面细说防止数据被篡改。一个安全的密钥应该是足够长、足够随机、不可预测的字符串通常通过os.urandom()这类密码学安全的随机函数生成。而这里密钥被限制在仅仅28个可能的字符串中。攻击者完全可以暴力枚举这28种可能性总有一种能对上。我见过很多新手开发者的项目会把secret_key直接硬编码成dev_key_123或者flask_secret这种简单字符串然后上传到公开的代码仓库。这和把家门钥匙放在门口脚垫下面没什么区别。这道题只不过用一种更戏剧化的方式饼干菜单把这个问题暴露了出来。2.2 会话流转的逻辑迷宫理解了密钥的脆弱性我们再看看程序如何通过Session来控制用户流程。整个应用有三个关键路由/根目录、/search和/display。它们通过一个名为very_auth的Session变量来串联。访问根目录 (/): 程序检查你的Session里有没有very_auth这个键。如果没有就给你设置成blank然后让你看到首页index.html。首页就是一个简单的输入框让你输入一个饼干名。提交搜索 (/search): 你在首页输入一个名字并提交程序会检查这个名字是否在cookie_names列表里。如果在它就把你的session[very_auth]设置成你输入的这个饼干名然后把你重定向到/display。如果不在就报错并把你very_auth重置为blank。展示结果 (/display): 这是我们的目标。这个函数会检查session[very_auth]的值。只有当这个值等于字符串admin时它才会渲染出包含Flag的页面flag.html。如果是其他任何在cookie_names列表里的值它只会显示一个“这不是特殊饼干”的页面not-flag.html。逻辑漏洞点正常流程中用户根本无法让very_auth变成admin。因为/search路由只接受来自cookie_names列表的输入而这个列表里根本没有admin。所以想通过前端界面正常操作成为管理员是不可能的。那么出路在哪出路就在于我们能否直接伪造一个Session其中very_auth的值就是admin并且这个伪造的Session的签名是有效的能让服务器信以为真。这就回到了我们最初的问题你需要知道当前的secret_key是什么才能造出有效的签名。3. 核心原理Flask Session的签名机制不是加密这是很多人的一个误区包括早期的我。看到Session Cookie里一堆乱码就以为是加密的。其实不然Flask默认使用Werkzeug库的Session机制是“签名”而非“加密”。这有本质区别。一个典型的Flask Session Cookie值长这样eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.ZiYpOA.zmBpLFgcFfmlaoC3mMCEybH_tg0它由三个部分用点号.分隔第一部分Session DataeyJ2ZXJ5X2F1dGgiOiJibGFuayJ9这是经过压缩可选和Base64编码后的实际Session数据。你把这段解码就能得到明文{very_auth:blank}。是的明文所以Session数据本身在客户端是可见的并不保密。第二部分TimestampZiYpOA这是一个时间戳同样经过Base64编码。用于实现Session过期功能。第三部分Signature签名zmBpLFgcFfmlaoC3mMCEybH_tg0这是最关键的部分。它是服务器使用secret_key对“第一部分数据 第二部分时间戳”进行HMAC哈希消息认证码计算后再Base64编码得到的结果。签名的作用服务器收到Cookie后会用同样的secret_key对收到的前两部分数据时间戳重新计算一次HMAC生成一个新的签名。然后拿这个新签名和Cookie中的第三部分收到的签名进行比对。如果两者一致说明数据在传输过程中没有被篡改并且是由拥有正确secret_key的服务器颁发的。如果不一致服务器就会丢弃这个Session视为无效。攻击的突破口正因为数据部分是明文的我们可以随意修改它比如把{very_auth:blank}改成{very_auth:admin}。但是如果我们只改数据而不改签名服务器自己重新计算签名时发现对不上就会拒绝我们的请求。所以我们必须为修改后的数据生成一个新的、有效的签名。而生成新签名的唯一条件就是知道那个secret_key。在“Most Cookie”这道题里secret_key是28选1。我们只需要用工具拿着我们截获的一个合法Cookie比如very_auth为snickerdoodle时的Cookie对cookie_names列表里的每一个字符串进行尝试看用哪个作为密钥能验证通过这个Cookie的签名。一旦找到我们就拿到了通关的万能钥匙。4. 实战演练手把手破解Session密钥理论说了一堆现在我们来真刀真枪地操作。整个过程就像侦探破案一步步收集线索最终找到真相。4.1 第一步环境侦察与数据收集首先你需要访问题目提供的靶场地址。用浏览器打开后按F12打开开发者工具切换到“网络”Network标签页并勾选“保留日志”Preserve log。获取初始Session刷新页面或首次访问浏览器会向服务器发起请求。在网络请求中找到对根目录/的请求查看响应头Response Headers中的Set-Cookie字段。你会看到一个类似sessioneyJ...的字符串。这就是服务器发给你的、初始的Session Cookie其中very_auth被设置为了blank。把它完整地复制下来备用。获取一个有效Session在网页的输入框里随便输入一个cookie_names列表里的名字比如chocolate chip然后提交。页面会跳转到/display。此时再次查看网络请求找到跳转后对/display的请求查看它的请求头Request Headers中的Cookie字段。这里的session值已经变了因为/search路由将你的very_auth设置成了chocolate chip。这个Cookie是我们用来破解密钥的关键素材把它也复制下来。为什么不用初始的blank那个因为两个都可以原理上只要是一个由目标服务器用正确secret_key签发的合法Cookie就行。我们选择chocolate chip这个只是为了确保流程走通。4.2 第二步准备破解工具——flask-unsign工欲善其事必先利其器。我们要使用的神器叫做flask-unsign它是一个专门针对Flask Session进行解码、破解密钥、伪造签名的Python工具。安装非常简单打开你的终端命令行pip3 install flask-unsign如果安装速度慢或者遇到问题可以尝试使用国内的镜像源比如pip3 install flask-unsign -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后在命令行输入flask-unsign --help可以看到详细的帮助信息说明安装成功。4.3 第三步制作“密钥字典”文件题目给了我们一个包含28个候选密钥的列表cookie_names。我们需要把它制作成一个文本文件每行一个密钥供flask-unsign进行暴力枚举。在你的工作目录下新建一个文本文件命名为wordlist.txt。然后把题目源码里那个长长的列表每个饼干名单独一行粘贴进去。文件内容开头应该是这样的snickerdoodle chocolate chip oatmeal raisin gingersnap ...中间省略... white chocolate macadamia保存文件。注意有些饼干名中间有空格如chocolate chip这没关系工具能正确处理。4.4 第四步发起密钥破解攻击现在我们手里有一个有效的Session Cookie值来自/display请求。一个可能的密钥列表文件wordlist.txt。打开终端进入你存放wordlist.txt文件的目录。然后执行以下命令flask-unsign --unsign --cookie eyJ2ZXJ5X2F1dGgiOiJjaG9jb2xhdGUgY2hpcCJ9.ZiYqZQ.abcdefghijk... --wordlist wordlist.txt注意你需要将命令中的eyJ...部分替换成你实际抓取到的那个完整的Session Cookie值就是very_auth为chocolate chip或其他值时的那个长字符串。Cookie值要用单引号括起来防止命令行解析错误。敲下回车工具就会开始工作。它会依次尝试wordlist.txt中的每一个字符串作为secret_key去验证你提供的Cookie签名是否有效。这个过程非常快因为只有28种可能。成功的结果当工具尝试到正确的密钥时它会停止并输出类似下面的信息[*] Session decodes to: {very_auth: chocolate chip} [*] Starting brute-forcing with 28 threads... [] Found secret key after 5 attempts gingersnap看它找到了密钥gingersnap这只是个例子实际结果会是列表中的某一个。这意味着当前服务器使用的secret_key就是gingersnap。至此最关键的秘密已经被我们掌握了。4.5 第五步伪造管理员Session拿到了secret_key我们就可以为所欲为地签发任何Session了。我们的目标是生成一个very_auth为admin的有效Session。继续在终端使用flask-unsign执行签名命令flask-unsign --sign --cookie {very_auth: admin} --secret gingersnap重要提示这里的--cookie参数其值的格式是Python字典的字符串表示形式。注意它使用的是单引号包裹整个字符串字典的键和值用双引号。也就是{very_auth: admin}。如果你写成--cookie {very_auth: admin}双引号在外在某些环境下可能会被命令行错误解析导致生成错误的签名。保险起见使用上面那种格式。如果一种不行可以另一种也试试。命令执行后工具会输出一串新的、长长的Base64字符串这就是我们伪造的、带有有效签名的管理员Session Cookie。4.6 第六步替换Cookie夺取Flag最后一步就是让浏览器使用我们伪造的Cookie去访问网站。再次打开浏览器的开发者工具切换到“应用程序”Application或“存储”Storage标签页找到Cookies。找到当前网站的sessionCookie将其值修改为我们刚刚伪造生成的那一串新值。修改完成后直接刷新页面或者手动在地址栏访问/display路径。如果一切顺利你应该不会再看到“That is a cookie! Not very special though...”的提示而是直接看到了梦寐以求的Flag页面上面显示着本次挑战的Flag。5. 防御之道如何让你的Flask应用更安全通过这次实战我们亲身体验了弱secret_key带来的灾难性后果。那么作为一个开发者我们应该如何避免自己的应用成为下一个“Most Cookie”呢这里分享几个我多年实践中总结的关键点。5.1 密钥管理绝不用“弱密码”这是最根本的一条。生成强密钥永远不要使用可预测的、简短的字符串作为secret_key。应该使用操作系统提供的密码学安全随机数生成器。在Python中最推荐的方式是import os app.secret_key os.urandom(24) # 生成24字节192位的随机字节串os.urandom(24)会生成一个足够随机的字节序列直接赋值即可。它作为字节串在签名时会被使用。环境变量隔离绝对不要将secret_key硬编码在源码中尤其是计划公开或上传到版本控制系统如Git的代码。正确的做法是将其存储在环境变量中。import os app.secret_key os.environ.get(FLASK_SECRET_KEY) if not app.secret_key: raise ValueError(No FLASK_SECRET_KEY set for Flask application)这样你的代码库是安全的密钥配置在部署服务器的环境中。即使代码泄露攻击者也无法直接获得密钥。不同环境使用不同密钥开发、测试、生产环境必须使用完全不同的secret_key。防止在测试环境生成的Session被用于攻击生产环境。5.2 加固Session安全配置Flask Session有一些配置选项可以增强安全性。SESSION_COOKIE_SECURE True: 此设置确保Session Cookie只通过HTTPS协议传输防止在HTTP明文传输中被窃听。在生产环境中必须启用。SESSION_COOKIE_HTTPONLY True: 这是默认设置它阻止客户端JavaScript通过document.cookie访问Session Cookie可以有效缓解跨站脚本XSS攻击窃取Session的风险。你应该确保它被启用。SESSION_COOKIE_SAMESITE Lax或Strict: 这个设置可以帮助防范跨站请求伪造CSRF攻击。Strict最安全但可能影响用户体验比如从第三方链接点回你的网站会丢失Session。Lax是一个平衡的选择是现代浏览器的默认值。5.3 建立安全开发意识与流程技术手段之外人和流程同样重要。代码审计在项目上线前或定期进行代码安全审计重点检查密钥管理、身份验证、会话管理等敏感逻辑。像“Most Cookie”这种弱密钥问题在审计中很容易被发现。依赖项更新定期更新Flask及其依赖库如Werkzeug。安全漏洞的修复通常包含在新版本中。使用安全工具可以主动使用像flask-unsign、banditPython代码安全扫描工具这样的工具对自己的应用进行“攻击性”测试提前发现类似弱密钥这样的配置问题。最小权限原则确保Session中存储的信息尽可能少不要存放敏感信息如密码明文。Session应该只是一个用户身份的“凭据”具体的用户数据应从数据库根据这个凭据实时查询。6. 举一反三Flask Session安全问题的其他变体“Most Cookie”展示的是密钥可枚举这种最典型的情况。但在实际的安全研究和CTF比赛中Flask Session的玩法还有很多。6.1 密钥泄露的多种途径除了这种明显的弱密钥secret_key还可能通过其他方式泄露配置文件泄露例如config.py,.env文件被意外部署到Web目录可通过直接访问下载。版本控制历史在Git仓库的历史提交中可能曾经硬编码过密钥虽然当前版本删除了但通过.git目录泄露依然可以恢复出来。错误信息泄露在某些调试或错误配置下Flask的错误回显页面可能包含部分应用配置信息。依赖包漏洞如果使用了存在漏洞的第三方扩展攻击者可能利用该漏洞读取应用的内存或环境变量从而获取密钥。6.2 攻击已签名Cookie的其他思路即使密钥很强无法破解如果应用逻辑有缺陷攻击者依然可能利用签名机制。签名验证绕过理论如果应用的签名验证逻辑存在缺陷比如自己实现了不安全的比较函数可能存在定时攻击Timing Attack等绕过方式。不过Flask/Werkzeug默认的实现是安全的。Session固定攻击Session Fixation如果应用在用户登录后没有更换Session ID攻击者可以先获取一个自己的合法Session诱导受害者使用这个Session ID登录从而获得受害者的权限。防御方法是在用户登录成功后调用session.regenerate()或直接重新生成secret_key不推荐来创建全新的Session。数据结构混淆攻击Flask的Session是基于Pickle序列化的如果使用flask.session接口且未指定序列化器但默认的Werkzeug 0.15已改用JSON。在旧版本或特定配置下如果攻击者能够控制Session数据的一部分并利用Pickle反序列化漏洞可能造成远程代码执行RCE。务必确保使用JSON序列化器Flask默认。6.3 工具链的扩展使用flask-unsign的功能不止于此。除了我们用的--unsign破解和--sign签名它还有--decode直接解码Cookie查看明文数据无需密钥。这在分析Session结构时非常有用。flask-unsign --decode --cookie eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.ZiYpOA.zmBpLFgcFfmlaoC3mMCEybH_tg0--unsign结合大型字典对于不是28选1而是使用常见弱口令作为密钥的情况可以结合rockyou.txt这类大型密码字典进行爆破。集成到自动化脚本你可以用Python调用flask-unsign的库将其集成到自己的自动化测试或攻击脚本中。我在实际的安全评估项目中就曾遇到过开发团队将密钥设置为公司名称缩写加上“2023”的情况。利用一个简单的生日字典几分钟就破解成功。这比“Most Cookie”的28种可能多不了多少本质上是一样的脆弱。所以千万不要心存侥幸安全无小事往往就败在这些细节上。希望这次从PicoCTF一道题引发的深入探讨能让你真正理解Flask Session安全的核心并在今后的开发和学习中保持警惕。