DoubleTrouble
This repository offers insights and a proof-of-concept tool to exploit two significant deserialization vulnerabilities in Inductive Automation's Ignition software.
Install / Use
/learn @TecR0c/DoubleTroubleREADME
DoubleTrouble
This repository details the exploitation of deserialization vulnerabilities in Inductive Automation's Ignition software, specifically CVE-2023-39475 and CVE-2023-39476. These critical vulnerabilities enable remote attackers to execute arbitrary code without requiring authentication. The document offers an in-depth look at the vulnerabilities, exploitation methods, and instructions for using the DoubleTrouble PoC exploit.
Table of Contents
- Overview
- Affected Versions
- Vulnerability Summary
- Additional Insights
- Detailed Vulnerability Analysis
- Exploit Methodology
- Exploit Gadget Chain
- Building DoubleTrouble
- Running DoubleTrouble
- Usage Examples
- References and Credits
Overview
- ParameterVersionJavaSerializationCodec Deserialization RCE Vulnerability (CVE-2023-39475)
- JavaSerializationCodec Deserialization RCE Vulnerability (CVE-2023-39476)
These vulnerabilities, critically rated with a CVSS score of 9.8, pose a significant threat to systems using affected Ignition software versions. They allow remote attackers to execute arbitrary code without requiring authentication, potentially leading to complete system compromise, data theft, and unauthorized system control.
DoubleTrouble serves as a proof-of-concept to illustrate the exploitation process of these vulnerabilities, providing insights into their mechanics and the potential risks they pose. The tool targets Ignition versions 8.1.22 to 8.1.24, which are confirmed to be vulnerable.
Both vulnerabilities are critically rated with a CVSS score of 9.8, indicating their potential for severe impact (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
Affected Versions
The vulnerabilities affect the following versions of Inductive Automation's Ignition software:
- 8.1.22
- 8.1.23
- 8.1.24
Vulnerability Summary
These deserialization vulnerabilities in the JavaSerializationCodec and ParameterVersionJavaSerializationCodec classes of Inductive Automation's Ignition version 8.1.24 and below allow remote attackers to execute arbitrary code on affected systems without requiring authentication.
The specific flaws exist within each of the implemented decode methods, which lack proper input validation on untrusted data. These vulnerabilities allow attackers to inject malicious code into the targeted system that will execute with NT AUTHORITY/SYSTEM privileges.
Inductive Automation has been made aware of these vulnerabilities and it is recommended that users of Ignition version <= 8.1.24 update to the latest version as soon as possible to protect against potential exploitation. A proof-of-concept exploit with our RCE gadget has been developed to demonstrate how these vulnerabilities can be exploited in practice.
Additional Insights
Pwn2Own Miami 2023
These vulnerabilities were discovered and exploited in preperation for Pwn2Own Miami 2023, unfortunately the rules changed on January 4th rendering our work useless for the competition. Please see: https://web.archive.org/web/20230101043715/https://www.zerodayinitiative.com/Pwn2OwnMiami2023Rules.html
Network Configuration Requirements
The vulnerabilities require the gateway network to be configured, which is a common configuration: Gateway Network. This was same vector as used in Pwn2Own Miami 2020, so it appears that vendors don't learn.
SSL Considerations on Gateway
SSL needs to be disabled in the network gateway unless HTTPS is enabled on the server, in which case the attacker can set the SSL flag in the exploit to true to have the exploit work over HTTPS.

Detailed Vulnerability Analysis
Starting from the com.inductiveautomation.metro.impl.protocol.websocket.servlet.DataChannelServlet code:
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
InputStream inputStream = req.getInputStream();
OutputStream outputStream = resp.getOutputStream();
ProtocolHeader header = null;
try {
header = ProtocolHeader.decode(inputStream);
} catch (LocalException e) {
getLogger().error("onDataReceived", "Could not process protocol header from incoming data channel message", e);
}
if (header != null) {
String connectionId = header.getSenderId();
startMdc(connectionId);
Optional<WebSocketConnection> optConnection = getFactory().getIncomingBySystemName(connectionId);
if (optConnection.isEmpty()) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("doPost",
String.format("Data channel error: connection id '%s' was not found on this server. The web socket may be trying to reconnect", new Object[] { connectionId }), null);
}
} else {
if (getLogger().isTraceEnabled())
getLogger().trace("doPost", String.format("Received data message [%d] from %s at %s", new Object[] {
Short.valueOf(header.getMessageId()), header
.getSenderId(), header
.getSenderURL()
}));
((WebSocketConnection)optConnection.get()).onDataReceived(header, inputStream, outputStream); // 1
}
clearMdc();
}
}
We can reach onDataReceived of the WebSocketConnection class with attacker input:
public void onDataReceived(ProtocolHeader header, InputStream inputStream, OutputStream outputStream) {
//...
try {
try {
reserveCapacity();
acquired = true;
TransportMessage msg = TransportMessage.createFrom(new MeterTrackedInputStream(inputStream, this.incomingMeter, true)); // 2
CompletableFuture<Void> routeFuture = null;
Instant routingStartTime = Instant.now();
try {
setSecurityContextInfo();
Instant start = Instant.now();
routeFuture = forward(header.getTargetAddress(), msg); // 3
We can setup an arb TransportMessage and call forward with it...
protected CompletableFuture<Void> forward(String targetAddress, TransportMessage msg) { return this.receiveHandler.handle(targetAddress, msg); }
The receiveHandler will be the ConnectionWatcher:
public CompletableFuture<Void> handle(String targetAddress, TransportMessage data) {
CompletableFuture<Void> ret = new CompletableFuture<Void>();
ServerId serverId = ServerId.fromString(targetAddress);
try { ServerMessage sm = ServerMessage.createFrom(data); // 4
ServerId sendingServer = ServerId.fromString((String)sm.getHeaderValues().get("_source_"));
MDC.MDCCloseable ignored = MDC.putCloseable("gan-remote-gateway-name", sendingServer
.toDescriptiveString());
try { updateSecurityContext(sm);
if (this.centralManager.isEndOfRoute(serverId)) {
Exception errResult = null;
if (sm.getIntentName().startsWith("_conn_")) {
handleConnectionMessage(sm); // 5
The code calls handleConnectionMessage
protected void handleConnectionMessage(ServerMessage message) throws Exception {
if (this.conn != null) {
boolean available; String intentName = message.getIntentName();
if ("_conn_init".equalsIgnoreCase(intentName)) {
setRemoteServerAddress(ServerId.fromString(message.getHeaderValue("_source_")));
ConnectionEvent.ConnectStatus stat = this.conn.getStatus();
info(String.format("Connection successfully established. Remote server name: %s. Connection status: %s", new Object[] { this.remoteServerAddress, stat }));
}
else if ("_conn_svr".equalsIgnoreCase(intentName)) { // 6
setRemoteServerAddress(ServerId.fromString(message.getHeaderValue("_source_")));
available = "true".equals(message.getHeaderValue("replyrequested"));
ServerRouteDetails[] routes = (ServerRouteDetails[])message.decodePayload(); // 7
We set the intentName name to '_conn_svr' to reach [7], decodePayload:
public <T> T decodePayload() throws Exception {
MessageCodec codec = MessageCodecFactory.get().getCodec(getCodecName()); // 8
return (T)codec.decode(getSourceStream()); //9
}
We can set the codec name to two different MessageCodec classes:
JavaSerializationCodec(js)
public class JavaSerializationCodec
implements MessageCodec
{
public static final String ID = "_js_";
protected static final Logger logger = Logger.getLogger("metro.Codecs.JavaSerializationCodec");
//...
public Object decode(InputStream inputStream) throws Exception {
in = null;
try {
in = createObjectInputStream(inputStream);
return in.readObject(); // 10
} finally {
IOUtils.closeQuietly(in);
}
}
ParameterVersionJavaSerializationCodec(_js_tps_v3)
protected static class ParameterVersionJavaSerializationCodec
implements MessageCodec
{
public static final String ID = "_js_tps_v3";
protected static final Logger logger = Logger.getLogger("metro.Codecs.JavaSerializationCodec");
public String getId() { return "_js_tps_v3"; }
public Object decode(InputStream inputStream) throws Exception {
in = null;
try {
in = createObjectInputStream(inputStream);
return in.readObject(); // 11
} finally {
IOUtils.closeQuietly(in);
}
}
At [10] we reach the first unprotected pre-auth deserialization v
