Để khai thác lỗ hổng Deserialization, ngoài việc phải kiểm soát được giá trị đầu vào để đưa vào hàm thực hiện deserialize, ta cũng cần phải có một Object mà khi nó được deserialize, nó sẽ gọi tới một loạt các Object khác, tới cuối cùng gọi tới hàm đại loại như Runtime.exec
để thực thi lệnh do attacker điều khiển, người ta gọi đó là gadget chain.
Lúc mới biết tới lỗ hổng này, mình nghĩ rằng 1 Object chỉ cần được viết đại loại như sau:
import java.io.IOException;
public class CommandExecutor { public CommandExecutor(String command) { String command = "<command here>"; try { Process process = Runtime.getRuntime().exec(command); } } catch (IOException e) { e.printStackTrace(); } }
}
Sau đó, ta có 1 hàm Serialize(new CommandExecutor("whoami"));
và gửi cái Object thu được lên server là sẽ thành công, và mình luôn đi theo lối suy nghĩ đó, không xem các gadget ở trong tool ysoserial hoạt động như thế nào, do vậy lúc đó không hề khai thác được bất kì một lỗ hổng nào liên quan đến Deserialization.
Đến một ngày có một người anh khai sáng cho mình về lỗ hổng này thì mình mới bắt đầu tìm hiểu xem cách các gadget được xây dựng nên, do vậy mình sẽ phân tích một gadget tiêu biểu trong ysoserial để làm tư liệu cho các bạn mới tìm hiểu về lỗi này.
1. Phân tích
Đây là chain của gadget này, lấy từ ysoserial:
Để dễ dàng tìm hiểu, ta bắt đầu từ giữa của gadget, hàm LazyMap.get()
trước. Hãy tưởng tượng rằng ta có thể gọi đến hàm get của lớp LazyMap
với đầu vào là key
Điều gì sẽ xảy ra tiếp theo? Object tên key
là đầu vào của hàm get()
sẽ được đưa vào hàm transform()
của biến factory
- là một Object Transformer
Transformer
này lại chỉ là một interface, tức là lúc trước khi chương trình được chạy, ta không thể biết được biến factory
này thực sự là một instance của lớp nào
Do có tới 21 class khác đã implement Transformer
. Lúc này, khi chương trình thực tế chạy, factory
sẽ là instance của class nào hoàn toàn do giá trị key
quyết định.
Ở trong payload sử dụng ChainedTransformer
, hàm transform()
của nó trông như sau:
Theo mô tả thì ChainedTransformer
được sử dụng làm cầu nối để nối các Transformer
khác lại với nhau
Hàm transform()
nhận đầu vào là Object, tương ứng với giá trị key
phía trên, lại được đưa vào một hàm transform
của một mảng instance khác của Transformer
Gadget đã sử dụng 2 instance là ConstantTransformer
và InvokerTransformer
.
Và đây là đoạn mã khai thác từ ysoserial:
Đoạn mã này đã làm những gì? Đầu tiên nó tạo 1 mảng các Transformer
.
Với new ConstantTransformer(Runtime.class)
, ta sẽ có ConstantTransformer.transformer()
trả về Runtime.class
Với mỗi InvokeTransformer
tiếp theo, nó sẽ trả về một method tương ứng với class.
Ví dụ: new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
Thì một list các class là String, Class[]
, args là getRuntime
và method là getMethod
, do vậy kết quả return sẽ là getMethod.invoke(Runtime, "getRuntime")
, điều này bằng với Runtime.getMethod("getRuntime", null)
, đây là một kĩ thuật Reflection trong Java.
Tóm tắt lại:
1. ConstantTransformer.transform() --> Runtime
2. InvokerTransformer.transform() --> getMethod.invoke(Runtime, "getRuntime") == Runtime.getMethod("getRuntime",null)
3. InvokerTransformer.transform() --> invoke.invoke(Runtime.getMethod("getRuntime",null), null) == Runtime.getMethod("getRuntime",null).invoke(null, null)
4. InvokerTransformer.transform() --> exec.invoke(Runtime.getMethod("getRuntime",null).invoke(null, null), args) == Runtime.getMethod("getRuntime",null).invoke(null, null).exec(args)
Như vậy đến cuối cùng ta có thể chạy được lệnh tùy ý.
Trở lại phần phía trên, theo payload trong ysoserial, bắt đầu với AnnotationInvocationHandler.readObject()
, hàm này khi được gọi sẽ gọi tới hàm Map(Proxy).entrySet()
rồi lại gọi AnnotationInvocationHandler.invoke()
Điều này thực sự khá là lạ vì trong hàm entrySet()
không có cách nào có thể gọi tới hàm invoke
đó cả.
Thủ thuật ở đây rất thú vị
Tải trọng đã tạo một proxy, dưới đây là các hàm liên quan trong ysoserial
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception { return createProxy(createMemoizedInvocationHandler(map), iface, ifaces); } public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception { return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map); } public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) { final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1); allIfaces[ 0 ] = iface; if ( ifaces.length > 0 ) { System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length); } return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih)); }
với
Kết quả cuối cùng sẽ tạo thành một variable như sau:
Map proxyMap = (Map) Proxy.newProxyInstance(AValidClass.class.getClassLoader(), new Class[] { Map.class }, new AnnotationInvocationHandler(lazyMap));
Đi vào hàm Proxy.newProxyInstance()
, ta có thể thấy
Một dynamic proxy Object đã được khởi tạo từ LazyMap
với một AnnotationInvocationHandler
. Như vậy từ Map(Proxy).entrySet()
đã trở thành AnnotationInvocationHandler.invoke(mapProxy, "entrySet")
.
Và hàm invoke()
ở đây đã thực hiện gọi đến LazyMap.get()
(với việc vượt qua một số switch/case phía trước)
2. Các thông tin khác
Việc thực hiện Deserialization thường sẽ gây ra các Exception không mong muốn, nhưng thực tế các class đều đã được gọi nên cuối cùng thì command sẽ được thực thi trước khi Exception xảy ra.
Có thể chặn các gadget bằng cách blacklist 1 trong những class tham gia vào trong gadget. Điều này có thể bị bypass bằng cách attacker tìm con đường vòng, không thông qua class đó nhưng vẫn có thể gọi tới class phía sau.
Sử dụng whitelist có hạn chế là sẽ ảnh hưởng khá nhiều tới hệ thống và phải cẩn trọng trong việc sử dụng whitelist, tránh bị thiếu các class cần thiết.