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
wrapper.java.additional.3=-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:8000
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
Phân tích CVE-2020-10644
CVE này được mô tả rằng
Cấu hình mặc định có thể bị khai thác bởi kẻ tấn công chưa được xác thực, có thể đạt được việc thực thi mã từ xa dưới dạng SYSTEM trên cài đặt Windows hoặc root trong 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 một số lượng lớn cổng TCP và UDP, vì nó phải xử lý một số giao thức SCADA và chức năng của chính nó. Các cổng chính là 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), cũng như xử lý giao tiếp giữa các thành phần khác nhau 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
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:
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
Đ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()
AbstractGatewayFunction.invoke()
thực hiện các công việc sau:
- Phân tích request nhận được
- Xác định function cần được gọi
- 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
- Thực hiện gọi function với các args đã được decode
- 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ừ
classWhitelist = Sets.newHashSet(SaferObjectInputStream.DEFAULT_WHITELIST);
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.
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()
Gửi request như sau và hit break point
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>
Tóm lại, chain sẽ như thế này
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