Phân tích CVE-2023-39476 Inductive Automation Ignition Deserialization và vấn đề với Gadget Jython2

Một CVE mới từ một pháp sư trung hoa nào đó. Mình sẽ viết lại vì quá trình reproduce nó khá hay và trắc trở. Trong đó trung tâm bài này là Gadget Jython. Automation Ignition sử dụng jython-ia là library riêng thay vì jython-standalone trong ysoserial. https://github.com/frohoff/ysoserial/pull/200/commits/93fad0750ae075544d4df5f9a965ffe9724be939

Link bài writeup tại đây, các bạn có thể tham khảo.

Các bước install và debug đã được nêu rõ trong bài gốc, vì vậy mình đi luôn vào phần phân tích.

1. Vị trí gây lỗi

Nơi Object được serialize là com.inductiveautomation.metro.impl.codecs.JavaSerializationCodec#decode

image.png

Hàm này được gọi trong com.inductiveautomation.metro.impl.transport.ServerMessage#decodePayload

image.png

Và được gọi từ com.inductiveautomation.metro.impl.ConnectionWatcher#handleConnectionMessage nếu intentName khác _conn_init

image.png

Trong com.inductiveautomation.metro.impl.ConnectionWatcher#handle, nếu intentName bắt đầu bằngg _conn_ thì sẽ được gọi:

image.png

Được gọi từ com.inductiveautomation.metro.impl.protocol.websocket.WebSocketConnection#forward

image.png

Từ com.inductiveautomation.metro.impl.protocol.websocket.WebSocketConnection#onDataReceived

image.png

Tuy nhiên để có thể gọi được đến đây, address phải từ một nguồn không bị access denied.

com.inductiveautomation.metro.impl.protocol.websocket.servlet.DataChannelServlet#doPost đã gọi đến hàm onDataReceived

image.png

Servlet này có endpoint uri là /ws-datachannel-servlet

image.png

Tuy nhiên đây không phải là endpoint thực

image.png

Dynamic route đã được khai báo trong com.inductiveautomation.ignition.gateway.gan.WSChannelManager#restartChannels(Optional<ServerId>)

image.png

Như vậy endpoint là /system/ws-datachannel-servlet. Vấn đề ở chỗ nếu ta truy cập trực tiếp thì sẽ trả về 403

image.png

Debug tại com.inductiveautomation.metro.impl.protocol.websocket.servlet.DataChannelServlet#service ta thấy lí do là vì SSL=true nhưng ta truy cập từ giao thức http không thỏa mãn.

image.png

Nếu đổi thành https hoặc sử dụng port 8060 là port https của nó thì sẽ bị BAD_SSL

image.png

Như vậy, để thực hiện được khai thác, ta cần 2 setup Disable SSL và add WhitelistIP (hoặc để Unstricted) trong Gateway setting

image.png

image.png

2. Gadget

Gadget được tác giả sử dụng là Jython, tuy nhiên khi mình dựng gadget này (sử dụng POC từ bài writeup) đã gặp lỗi đó là

image.png

Đi vào lib, mình thấy Automation Ignition sử dụng library riêng là jython-ia-2.7.2.1 chứ không phải jython-standalone như trong gadget của ysoserial. Nếu mình sử dụng jython-standalone thì gadget hoạt động bình thường. Ta hãy cùng phân tích root cause.

Về sink của gadget, nó sẽ thực thi Py.eval(). Vì vậy command truyền vào sẽ là 1 dạng của hàm eval trong Python. Chi tiết hơn về cách thực thi code trong hàm eval, các bạn có thể tham khảo thêm tại đây. Trong file gadget, tác giả cũng đã note rằng nó thực thi hàm eval của python2. Nếu như server cài đặt Python2 thì nó sẽ có module os. Ví dụ

image.png

image.png

Dựa trên kết quả debug của server ta thấy đã có thể import os.

Tuy nhiên khi trên server là Python3 thì kết quả vẫn vậy =)). Nên mình xác nhận nó vẫn hoạt động trên Python3. Nhưng nó sẽ không hoạt động khi chúng ta cài đặt Python không thêm Python vào path env. Khi ấy, ta chỉ import được các default module. Trùng hợp thay mình đã làm điều đó và vô tình dính phải trường hợp này rồi mất thêm vài ngày nghiên cứu. Dưới đây là những gì mình thử trên máy Windows với việc cài đặt Python mà không thêm vào path env.

Khi lib này được load, class org.python.modules.Setup được load với các default module

image.png

Các module được khai báo theo dạng: name:class. Việc rút gọn như này có thể hỗ trợ khi import module. Ta có thể import với name hoặc import với class đều được mà không gặp lỗi. Như ta thấy, không có module os.

Ví dụ:

image.png

Như này có nghĩa là module _ast có tồn tại nhưng không tồn tại method hehe. Các method có thể thực thi được khai báo bên trong hàm classDictInit của từng module:

image.png

Ví dụ:

image.png

Vì vậy, nếu ta import java.lang.Runtime thì vẫn được nhưng thực thi getRuntime.exec thì không được, vì class kia không implement classDictInit.

image.png

Mình đã duyệt qua một lượt tất cả các method trong default module, không có method nào có thể lợi dụng để thực thi command. Chỉ có 1 module là PosixModule.getOSName() trông có vẻ hứa hẹn. Mudule này trả về tên của enum

image.png

Với Windows, nó sẽ trả về nt ( bằng "NT".toLowerCase()). Trong danh sách lib nt của Python thì có hàm system, execv, execve cho ta thực thi command

image.png

Tuy nhiên vẫn có một sự cay cú không hề nhẹ là trong ngữ cảnh của Jython-ia thì không có các hàm đó.

image.png

Mặc dù không thể sử dụng hàm system, chúng ta vẫn còn nhiều hàm khác có thể lợi dụng như mkdir, rmdir, open.

Như vậy ta có thể lợi dụng để viết 1 webshell. Thư mục webroot là webserver/webapps/main

Kết quả:

image.png

Nhưng đời không như là mơ

image.png

Lí do là vì Automation Ignition viết bằng spring và coi toàn bộ các file trong thư mục đều có content type là text. Công nhận cay.

Như vậy là trên Windows mình không nghĩ ra được cách gì khác là ghi thêm command vào file .bat để khi Automation Ignition khởi động lại sẽ tự thực thi trong trường hợp Python cài không theo mặc định.

Còn trên Linux các lib Python, path env luôn có sẵn nên nó có thể import os để thực thi command trực tiếp.

Trong Jython-standalone, nó có thêm folder lib riêng của nó nên có thể import os vào để run command, có lẽ vì thế nó mới có tên là standalone.

image.png

image.png

Đây chỉ là phần additional vì thực tế trường hợp không add Python vào path cũng khá hiếm xảy ra. Bạn đọc có cách làm hay hơn có thể để lại comment bên dưới.

Tài liệu tham khảo