Java反序列化漏洞
Java反序列化漏洞
Apache Commons是Apache開源的Java通用類項目在Java中項目中被廣泛的使用,Apache Commons當中有一個組件叫做Apache Commons Collections,主要封裝了Java的Collection(集合)相關類對象。攻擊者利用存在漏洞版本的Apache Commons Collections庫的反序列化包發送到服務器端進行反序列化操作就會導致服務器被非法入侵。
1. RMI服務
為了便于測試反序列化漏洞,這里采用RMI服務作為示例,演示如何攻擊一個RMI后端服務,因為RMI的通訊方式就是對象序列化/反序列化。
添加commons-collections依賴:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
RMITestInterface.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMITestInterface extends Remote {
String test() throws RemoteException;
}
RMITestImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {
private static final long serialVersionUID = 1L;
protected RMITestImpl() throws RemoteException {
super();
}
public String test() throws RemoteException {
return "Hello RMI~";
}
}
RMI服務端:
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServerTest {
// RMI服務器IP地址
public static final String RMI_HOST = "127.0.0.1";
// RMI服務端口
public static final int RMI_PORT = 9527;
// RMI服務名稱
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
public static void main(String[] args) {
try {
// 注冊RMI端口
LocateRegistry.createRegistry(RMI_PORT);
// 綁定Remote對象
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服務啟動成功,服務地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序運行結果:RMI服務啟動成功,服務地址
RMI客戶端:
package com.anbai.sec.rmi;
import java.rmi.Naming;
public class RMIClientTest {
public static void main(String[] args) {
try {
// 查找遠程RMI服務
RMITestInterface rt = (RMITestInterface) Naming.lookup("rmi://127.0.0.1:9527/test");
// 調用遠程接口RMITestInterface類的test方法
String result = rt.test();
// 輸出RMI方法調用結果
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序運行結果:Hello RMI~

上述示例演示了一個業務邏輯絕對的簡單且安全RMI服務的正常業務流程,但是漏洞并不是出現在業務本身,而是Java的RMI服務和反序列化機制。
2. 反序列化攻擊
攻擊者可以借助RMI協議,發送帶有Apache Commons Collections反序列化攻擊Payload的請求到RMI服務端,服務端一旦反序列化RMI客戶端的請求就會觸發攻擊鏈,最終實現在遠程的RMI服務器上執行任意系統命令。

發送在遠程服務器上執行open -a Calculator.app(打開計算器)命令的攻擊Payload:

請求成功后在RMI服務端成功的彈出了計算器,攻擊成功。
3. 反序列化攻擊防御
修復反序列化漏洞的常規方法是升級第三方依賴庫和JDK版本,或者修改java.io.ObjectInputStream類的resolveClass和resolveProxyClass方法,檢測傳入的類名是否合法。
3.1 升級JDK版本
從JDK6u141、JDK7u131、JDK 8u121開始引入了JEP 290,JEP 290: Filter Incoming Serialization Data限制了RMI類反序列化,添加了安全過濾機制,在一定程度上阻止了反序列化攻擊。

ObjectInputStream在序列化對象時是會調用java.io.ObjectInputStream#filterCheck->sun.rmi.registry.RegistryImpl#registryFilter,檢測合法性:

當攻擊者向一個實現了JEP 290的服務端JDK發送反序列化對象時會攻擊失敗并拋出:java.io.InvalidClassException: filter status: REJECTED異常。

JDK9中ObjectInputStream可以設置ObjectInputFilter,可實現自定義對象過濾器,如下:
ObjectInputStream ois = new ObjectInputStream(bis);
ois.setObjectInputFilter(new ObjectInputFilter() {
@Override
public Status checkInput(FilterInfo filterInfo) {
// 序列化類名稱
String className = filterInfo.serialClass().getName();
// 類名檢測邏輯
return ALLOWED;
}
});
除此之外,還可以添加JVM啟動參數:-Djdk.serialFilter過濾危險的類,參考:JDK approach to address deserialization Vulnerability
3.2 重寫ObjectInputStream類resolveClass
https://github.com/ikkisoft/SerialKiller是一個非常簡單的反序列化攻擊檢測工具,利用的是繼承ObjectInputStream重寫resolveClass方法,為了便于理解,這里把SerialKiller改成了直接讀取規則的方式檢測反序列化的類名。
示例 - SerialKiller:
/*
* 修改自:https://github.com/ikkisoft/SerialKiller
*/
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ObjectInputStreamFilter extends ObjectInputStream {
// 定義禁止反序列化的類黑名單正則表達式
private static final String[] REGEXPS = new String[]{
"bsh\\.XThis$", "bsh\\.Interpreter$",
"com\\.mchange\\.v2\\.c3p0\\.impl\\.PoolBackedDataSourceBase$",
"org\\.apache\\.commons\\.beanutils\\.BeanComparator$",
"org\\.apache\\.commons\\.collections\\.Transformer$",
"org\\.apache\\.commons\\.collections\\.functors\\.InvokerTransformer$",
"org\\.apache\\.commons\\.collections\\.functors\\.ChainedTransformer$",
"org\\.apache\\.commons\\.collections\\.functors\\.ConstantTransformer$",
"org\\.apache\\.commons\\.collections\\.functors\\.InstantiateTransformer$",
"org\\.apache\\.commons\\.collections4\\.functors\\.InvokerTransformer$",
"org\\.apache\\.commons\\.collections4\\.functors\\.ChainedTransformer$",
"org\\.apache\\.commons\\.collections4\\.functors\\.ConstantTransformer$",
"org\\.apache\\.commons\\.collections4\\.functors\\.InstantiateTransformer$",
"org\\.apache\\.commons\\.collections4\\.comparators\\.TransformingComparator$",
"org\\.apache\\.commons\\.fileupload\\.disk\\.DiskFileItem$",
"org\\.apache\\.wicket\\.util\\.upload\\.DiskFileItem$",
"org\\.codehaus\\.groovy\\.runtime\\.ConvertedClosure$",
"org\\.codehaus\\.groovy\\.runtime\\.MethodClosure$",
"org\\.hibernate\\.engine\\.spi\\.TypedValue$",
"org\\.hibernate\\.tuple\\.component\\.AbstractComponentTuplizer$",
"org\\.hibernate\\.tuple\\.component\\.PojoComponentTuplizer$",
"org\\.hibernate\\.type\\.AbstractType$", "org\\.hibernate\\.type\\.ComponentType$",
"org\\.hibernate\\.type\\.Type$", "com\\.sun\\.rowset\\.JdbcRowSetImpl$",
"org\\.jboss\\.(weld\\.)?interceptor\\.builder\\.InterceptionModelBuilder$",
"org\\.jboss\\.(weld\\.)?interceptor\\.builder\\.MethodReference$",
"org\\.jboss\\.(weld\\.)?interceptor\\.proxy\\.DefaultInvocationContextFactory$",
"org\\.jboss\\.(weld\\.)?interceptor\\.proxy\\.InterceptorMethodHandler$",
"org\\.jboss\\.(weld\\.)?interceptor\\.reader\\.ClassMetadataInterceptorReference$",
"org\\.jboss\\.(weld\\.)?interceptor\\.reader\\.DefaultMethodMetadata$",
"org\\.jboss\\.(weld\\.)?interceptor\\.reader\\.ReflectiveClassMetadata$",
"org\\.jboss\\.(weld\\.)?interceptor\\.reader\\.SimpleInterceptorMetadata$",
"org\\.jboss\\.(weld\\.)?interceptor\\.spi\\.instance\\.InterceptorInstantiator$",
"org\\.jboss\\.(weld\\.)?interceptor\\.spi\\.metadata\\.InterceptorReference$",
"org\\.jboss\\.(weld\\.)?interceptor\\.spi\\.metadata\\.MethodMetadata$",
"org\\.jboss\\.(weld\\.)?interceptor\\.spi\\.model\\.InterceptionModel$",
"org\\.jboss\\.(weld\\.)?interceptor\\.spi\\.model\\.InterceptionType$",
"java\\.rmi\\.registry\\.Registry$", "java\\.rmi\\.server\\.ObjID$",
"java\\.rmi\\.server\\.RemoteObjectInvocationHandler$",
"net\\.sf\\.json\\.JSONObject$", "javax\\.xml\\.transform\\.Templates$",
"org\\.python\\.core\\.PyObject$", "org\\.python\\.core\\.PyBytecode$",
"org\\.python\\.core\\.PyFunction$", "org\\.mozilla\\.javascript\\..*$",
"org\\.apache\\.myfaces\\.context\\.servlet\\.FacesContextImpl$",
"org\\.apache\\.myfaces\\.context\\.servlet\\.FacesContextImplBase$",
"org\\.apache\\.myfaces\\.el\\.CompositeELResolver$",
"org\\.apache\\.myfaces\\.el\\.unified\\.FacesELContext$",
"org\\.apache\\.myfaces\\.view\\.facelets\\.el\\.ValueExpressionMethodExpression$",
"com\\.sun\\.syndication\\.feed\\.impl\\.ObjectBean$",
"org\\.springframework\\.beans\\.factory\\.ObjectFactory$",
"org\\.springframework\\.core\\.SerializableTypeWrapper\\$MethodInvokeTypeProvider$",
"org\\.springframework\\.aop\\.framework\\.AdvisedSupport$",
"org\\.springframework\\.aop\\.target\\.SingletonTargetSource$",
"org\\.springframework\\.aop\\.framework\\.JdkDynamicAopProxy$",
"org\\.springframework\\.core\\.SerializableTypeWrapper\\$TypeProvider$",
"java\\.util\\.PriorityQueue$", "java\\.lang\\.reflect\\.Proxy$",
"javax\\.management\\.MBeanServerInvocationHandler$",
"javax\\.management\\.openmbean\\.CompositeDataInvocationHandler$",
"org\\.springframework\\.aop\\.framework\\.JdkDynamicAopProxy$",
"java\\.beans\\.EventHandler$", "java\\.util\\.Comparator$",
"org\\.reflections\\.Reflections$"
};
public ObjectInputStreamFilter(final InputStream inputStream) throws IOException {
super(inputStream);
}
@Override
protected Class<?> resolveClass(final ObjectStreamClass serialInput) throws IOException, ClassNotFoundException {
classNameFilter(new String[]{serialInput.getName()});
return super.resolveClass(serialInput);
}
@Override
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
classNameFilter(interfaces);
return super.resolveProxyClass(interfaces);
}
private void classNameFilter(String[] classNames) throws InvalidClassException {
for (String className : classNames) {
for (String regexp : REGEXPS) {
Matcher blackMatcher = Pattern.compile(regexp).matcher(className);
if (blackMatcher.find()) {
throw new InvalidClassException("禁止反序列化的類:" + className);
}
}
}
}
}
調用方式:
// 存在惡意攻擊的反序列化類輸入流
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
// 包裝原來的ObjectInputStream,校驗反序列化類
ObjectInputStream ois = new ObjectInputStreamFilter(bis);
// ObjectInputStream ois = new ObjectInputStream(bis);
// 反序列化
ois.readObject();
反序列化包含惡意Payload的輸入流時會拋出異常:

重寫ObjectInputStream類方法雖然靈活,但是必須修改每一個需要反序列化輸入流的實現類,比較繁瑣。
3.3 RASP防御反序列化攻擊
RASP可以利用動態編輯類字節碼的優勢,直接編輯ObjectInputStream類的resolveClass/resolveProxyClass方法字節碼,動態插入RASP類代碼,從而實現檢測反序列化腳本攻擊。
package java.io;
public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants {
// .. 省略其他代碼
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
// 插入RASP檢測代碼,檢測ObjectStreamClass反序列化的類名是否合法
}
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
// 插入RASP檢測代碼,檢測動態代理類接口類名是否合法
}
}
RASP防御反序列化攻擊流程圖:

使用RASP檢測反序列化攻擊,可以不用受制于請求協議、服務、框架等,檢測規則可實時更新,從而程度上實現反序列化攻擊防御。
示例 - whoami.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.commons.codec.binary.Base64" %>
<%@ page import="java.io.ByteArrayInputStream" %>
<%@ page import="java.io.ObjectInputStream" %>
<%
// 定義一個使用ysoserial生成的執行本地系統命令的Payload:java -jar ysoserial-0.0.6.jar CommonsCollections5 "whoami" |base64
byte[] classBuffer = Base64.decodeBase64("rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQABndob2FtaXQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=");
ObjectInputStream bis = new ObjectInputStream(new ByteArrayInputStream(classBuffer));
bis.readObject();
%>
在使用RASP防御的情況下請求示例程序后Java會執行Runtime.getRuntime().exec("whoami");,如下圖:

當啟動RASP后再次請求示例程序后會發現示例程序已無法正常訪問,因為當RASP發現正在反序列化的類存在惡意攻擊時候會立即阻斷反序列化行為,如下圖:

Java Web安全
推薦文章: