影响版本
Jenkins主版本<=2.56版本)
Jenkins LTS<=2.46.1版本)
漏洞分析
漏洞发生在jenkins cli采用http方式进行通信的时候,处理url为http://127.0.0.1:8080/cli,其处理逻辑在hudson.cli.CLIAction中
jenkins采用的是Stapler框架,CLIAction实现了两个接口,分别是UnprotectedRootAction, StaplerProxy
UnprotectedRootAction接口
在Stapler框架中,UnprotectedRootAction是一个标记接口,用于表示一个可以不通过Jenkins安全认证就能访问的动作(或页面)。实现了UnprotectedRootAction接口的类可以被匿名用户访问,即使Jenkins实例配置为安全模式。简单说就是这个action不需要通过Jenkins安全认证就能访问
实现UnprotectedRootAction的类通常需要提供以下方法:
- getIconFileName():返回用于在Jenkins UI上显示的图标文件名,如果返回null,则不在UI上显示。
- getDisplayName():返回动作的显示名称,如果返回null,则不在UI上显示。
- getUrlName():返回动作的URL路径。这是动作可通过HTTP访问的入口点。
重点关注getUrlName(),这段代码表明如果CLI没有被禁用,可以直接通过访问<Jenkins服务器地址>/cli来与Jenkins的CLI功能交互。
StaplerProxy接口
StaplerProxy是Stapler框架中的一个接口,当一个对象实现了StaplerProxy接口,Stapler会调用该对象的getTarget()方法来获取实际处理请求的对象。
以下是hudson.cli.CLIAction#getTarget,92行判断当请求的路径是/cli,且使用的是POST方法时候,会进入94行的CliEndpointResponse内部类中
CliEndpointResponse内部类
CliEndpointResponse类继承了HttpResponseException,通过覆写的generateResponse方法来实现了自定义响应逻辑。当 CLI 请求通过 HTTP 到达 Jenkins 时,CliEndpointResponse 类中的 generateResponse 方法将响应这个请求
CliEndpointResponse 类的 generateResponse 方法实现了几个关键的逻辑点,主要用于处理Jenkins CLI通过HTTP方式的连接请求。这些关键逻辑包括:
- 会话标识处理:
- UUID.fromString(req.getHeader("Session")) 从请求头中获取会话标识(Session ID),将其转换为 UUID 对象。这个UUID值在之后的逻辑中,用于区分不同的会话通道。
- 设置响应头:
- rsp.setHeader("Hudson-Duplex", "") 设置响应头,告知客户端这是一个双向通信(full-duplex)通道。这个头部信息让客户端知道它可以通过这个通道进行双向通信。
- 通道管理:
- 根据请求头中的 "Side" 值(download或upload)来区分是 download 还是 upload 操作。根据http头部中session里面的uuid的值来区分不同的会话通道。
以下为download操作逻辑,会创建一个新的 FullDuplexHttpChannel 实例并将其存储在 duplexChannels 映射中,关联之前请求中提取的UUID。
而对于upload 上传请求,从 duplexChannels 中直接获取对应的通道实例并处理上传数据。
download操作逻辑
跟进download操作,在hudson.model.FullDuplexHttpChannel#download中,方法的整个逻辑大致如下:
- 设置响应状态和chunked 响应头:
- 将HTTP响应状态设置为 200 OK。
- 添加 Transfer-Encoding: chunked 响应头,启用HTTP分块传输编码,允许服务器发送一个尚未确定总大小的响应,通过将数据分为一系列的块(chunk)来传输。
- 发送初始数据:
- 向客户端发送一些初始数据("Starting HTTP duplex channel"),表示HTTP全双工通道的建立过程已经开始
- 等待upload上传通道建立:
- 进入循环,等待另一个通道(即上传通道)建立。如果在指定的超时时间内上传通道仍未建立,则抛出一个异常。
- 建立双向通道:
- 一旦上传通道建立,就创建一个 Channel 实例,代表两个Java虚拟机之间的双向通信通道。upload参数就是客户端通过HTTP请求以upload为标记传入的数据流
首先跟入new Channel到hudson.remoting.Channel#Channel(hudson.remoting.ChannelBuilder, java.io.InputStream, java.io.OutputStream)
中,调用构造函数,其中传入了settings.negotiate(is, os),输入流(is)就是客户端通过HTTP请求以upload为标记传入的数据流
跟入hudson.remoting.ChannelBuilder#negotiate的while逻辑,用于处理双向通信通道(Channel)的初始化过程,以下判断了前导码(preamble),通常以<=[JENKINS REMOTING CAPACITY]=>为开头
case 1中调用了makeTransport,用于协商传输机制
以下是makeTransport代码,通过cap确定是否支持分块传输(cap.supportsChunking()),返回ChunkedCommandTransport,或者ClassicCommandTransport对象
回到之前的Channel对象,继续跟入直到hudson.remoting.Channel#Channel(hudson.remoting.ChannelBuilder, hudson.remoting.CommandTransport)
方法参数中transport的值为之前hudson.remoting.ChannelBuilder#makeTransport中返回的ChunkedCommandTransport或者ClassicCommandTransport对象
第520行的处理接收到的命令逻辑,调用transport.setup,传入了一个CommandReceiver对象
关于transport.setup,有两种处理方式
ChunkedCommandTransport#setup
首先来看ChunkedCommandTransport#setup的逻辑ChunkedCommandTransport的setup方法使用的是其父类hudson.remoting.SynchronousCommandTransport#setup,其中第39行调用了ReaderThread()
ReaderThread类为一个内部类,继承自Thread,启动线程后会执行run方法中的逻辑,在其59行又调用了read方法
ChunkedCommandTransport中的read方法使用的是其父类的hudson.remoting.AbstractSynchronousByteArrayCommandTransport#read
跟入Command.readFrom,看到了反序列化操作
ClassicCommandTransport#setup
跟入ClassicCommandTransport#setup中,ClassicCommandTransport#setup方法调用的也是其父类SynchronousCommandTransport的setup
和之前ChunkedCommandTransport中setup逻辑一样,都用的同一个父类的setup,启动线程后会执行run方法中的逻辑,在其59行又调用了read方法,
这时候就是ClassicCommandTransport中的read方法了,以下为ClassicCommandTransport#read
跟入Command.readFrom(channel, ois),同样可以看到readObject反序列化操作
漏洞利用:
两种方式,利用分块传输,或者不分块传输都可以触发反序列化操作
这个漏洞也是基于上次补丁中黑名单的绕过,采用java.security.SignedObject类来二次反序列化,绕过黑名单中的CC链来触发漏洞
POC链接:https://github.com/vulhub/CVE-2017-1000353