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. Inductive 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
Hàm này được gọi trong com.inductiveautomation.metro.impl.transport.ServerMessage#decodePayload
Và được gọi từ com.inductiveautomation.metro.impl.ConnectionWatcher#handleConnectionMessage
nếu intentName
khác _conn_init
Trong com.inductiveautomation.metro.impl.ConnectionWatcher#handle
, nếu intentName
bắt đầu bằngg _conn_
thì sẽ được gọi:
Được gọi từ com.inductiveautomation.metro.impl.protocol.websocket.WebSocketConnection#forward
Từ com.inductiveautomation.metro.impl.protocol.websocket.WebSocketConnection#onDataReceived
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
Servlet này có endpoint uri là /ws-datachannel-servlet
Tuy nhiên đây không phải là endpoint thực
Dynamic route đã được khai báo trong com.inductiveautomation.ignition.gateway.gan.WSChannelManager#restartChannels(Optional<ServerId>)
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
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.
Nếu đổi thành https hoặc sử dụng port 8060 là port https của nó thì sẽ bị BAD_SSL
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
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à
Đi vào lib, mình thấy Inductive 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ụ
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
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ụ:
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:
Ví dụ:
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
.
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
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
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 đó.
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ả:
Nhưng đời không như là mơ
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
.
Đâ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
All rights reserved