wget https://repo.huaweicloud.com/jenkins/redhat-stable/jenkins-2.190.3-1.1.noarch.rpm rpm -ivh jenkins-2.190.3-1.1.noarch.rpm
# 启动jenkins服务 systemctl start jenkins # 查看jenkins状态 systemctl status jenkins
记得关掉防火墙
查看密码
CVE-2017-1000353 是一个与 Jenkins CI(持续集成工具)相关的漏洞,该漏洞可能导致远程代码执行。
由于我之前自己配制的jenkins环境有问题,所以后期我选择了vulhub靶场进行复现
具体攻击手法我就不再多赘述了,网上的复现都烂大街了,我就从它的源代码层面来分析一下这个漏洞的形成原因
Jenkins有一个专门进行命令执行的模块
而该反序列化漏洞就是出现在jenkins利用http协议进行双向通信的过程中,在该快代码中发生的
大致历程就是这样的:
创建双向channel->启动Reader Thread->读取command对象->反序列化漏洞执行cmd
双向通道入口函数位于
jenkins-2.46.1/core/src/main/java/hudson/cli/CLIAction.java
@Extension @Symbol("cli") @Restricted(NoExternalUse.class) public class CLIAction implements UnprotectedRootAction, StaplerProxy { private transient final Map<UUID,FullDuplexHttpChannel> duplexChannels = new HashMap<UUID, FullDuplexHttpChannel>(); ...... @Override public Object getTarget() { StaplerRequest req = Stapler.getCurrentRequest(); if (req.getRestOfPath().length()==0 && "POST".equals(req.getMethod())) { // CLI connection request throw new CliEndpointResponse(); } else { return this; } } private class CliEndpointResponse extends HttpResponseException { @Override public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { try { // do not require any permission to establish a CLI connection // the actual authentication for the connecting Channel is done by CLICommand UUID uuid = UUID.fromString(req.getHeader("Session")); rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know FullDuplexHttpChannel server; if(req.getHeader("Side").equals("download")) { duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) { @Override protected void main(Channel channel) throws IOException, InterruptedException { // capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator() channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication()); channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); } }); try { server.download(req,rsp); } finally { duplexChannels.remove(uuid); } } else { duplexChannels.get(uuid).upload(req,rsp); } } catch (InterruptedException e) { throw new IOException(e); } } }
分析这段代码可以看出:
CliEndpointResponse
是一个内部类,继承自HttpResponseException
。它重写了generateResponse()
方法,用于处理CLI连接的建立和数据传输。
在generateResponse()
方法中,通过请求头中的Session标识符和Side标识符来处理下载和上传两种不同的请求。
如果是下载请求(Side为"download"),则根据Session标识符创建一个FullDuplexHttpChannel
实例,并进行下载操作。下载完成后,从duplexChannels
中移除对应的通道。
如果是上传请求,则根据Session标识符获取对应的FullDuplexHttpChannel
实例,并进行上传操作。
jenkins-2.46.1/core/src/main/java/hudson/model/FullDuplexHttpChannel.java
public synchronized void download(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException { ...... {// wait until we have the other channel long end = System.currentTimeMillis() + CONNECTION_TIMEOUT; while (upload == null && System.currentTimeMillis()<end) wait(1000); if (upload==null) throw new IOException("HTTP full-duplex channel timeout: "+uuid); } try { channel = new Channel("HTTP full-duplex channel " + uuid, Computer.threadPoolForRemoting, Mode.BINARY, upload, out, null, restricted); ...... } finally { // publish that we are done completed=true; notify(); } } public synchronized void upload(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException { rsp.setStatus(HttpServletResponse.SC_OK); InputStream in = req.getInputStream(); if(DIY_CHUNKING) in = new ChunkedInputStream(in); // publish the upload channel upload = in; notify(); // wait until we are done while (!completed) wait(); }
在 download()
方法中,通过循环等待来获取上传通道。它会不断检查 upload
是否为 null
,直到超过设定的连接超时时间(CONNECTION_TIMEOUT
)或者成功获取到上传通道。如果超时仍未获取到上传通道,则抛出 IOException
异常。
然后,通过创建一个新的 Channel
对象来建立全双工通道。这个通道使用上传通道和输出流作为输入参数,并指定了线程池和模式。
最后,在 finally
块中,设置 completed
为 true
,并调用 notify()
方法通知其他等待线程,表示下载操作已完成。
在 upload()
方法中,首先设置响应状态为 HttpServletResponse.SC_OK
,表示上传操作已经准备就绪。
然后,通过 req.getInputStream()
获取请求中的输入流,并进行可选的分块处理。
接下来,将上传通道设置为获取到的输入流,并调用 notify()
方法通知其他等待线程。
最后,在循环中等待直到 completed
为 true
,表示下载操作已完成。在等待过程中,其他线程可以通过调用 notify()
方法来通知上传操作已完成。
这是双向通道建立的基本过程
ReaderThread是由Channel对象来启动。在原代码中,channel对象被upload请求作为输入流来实例化
channel类的构造链:
最终调用的构造方法为Channel(ChannelBuilder settings, CommandTransport transport)
,
该构造方法的transport参数,由ChannelBuilder类的negotiate()方法获得。
protected CommandTransport negotiate(final InputStream is, final OutputStream os) throws IOException { ...... {// read the input until we hit preamble Mode[] modes={Mode.BINARY,Mode.TEXT}; byte[][] preambles = new byte[][]{Mode.BINARY.preamble, Mode.TEXT.preamble, Capability.PREAMBLE}; int[] ptr=new int[3]; Capability cap = new Capability(0); // remote capacity that we obtained. If we don't hear from remote, assume no capability while(true) { int ch = is.read(); ...... for(int i=0;i<preambles.length;i++) { byte[] preamble = preambles[i]; if(preamble[ptr[i]]==ch) { if(++ptr[i]==preamble.length) { switch (i) { case 0: case 1: ...... return makeTransport(is, os, mode, cap); case 2: cap = Capability.read(is);
在这个方法中,首先定义了两个数组 modes
和 preambles
。modes
数组包含了两种通信模式,分别是 Mode.BINARY
和 Mode.TEXT
。preambles
数组包含了对应通信模式的标志序列,以及一个表示能力的标志序列。
然后,通过循环不断读取输入流 is
的字节,直到遇到标志序列的起始字节。
在每次读取字节后,会遍历 preambles
数组,检查当前字节是否与某个标志序列的下一个字节匹配。如果匹配成功,则将指针 ptr[i]
自增,并检查是否已经匹配到了整个标志序列。
如果匹配到了 Mode.BINARY
或 Mode.TEXT
的标志序列,则根据匹配的模式创建相应的传输对象,并返回该对象。
如果匹配到了能力的标志序列,则通过 Capability.read(is)
方法从输入流中读取远程能力信息,并将其存储在 cap
对象中。
protected CommandTransport makeTransport(InputStream is, OutputStream os, Mode mode, Capability cap) throws IOException { FlightRecorderInputStream fis = new FlightRecorderInputStream(is); if (cap.supportsChunking()) return new ChunkedCommandTransport(cap, mode.wrap(fis), mode.wrap(os), os); else { ObjectOutputStream oos = new ObjectOutputStream(mode.wrap(os)); oos.flush(); // make sure that stream preamble is sent to the other end. avoids dead-lock return new ClassicCommandTransport( new ObjectInputStreamEx(mode.wrap(fis),getBaseLoader(),getClassFilter()), oos,fis,os,cap); } }
在该方法中,首先使用 FlightRecorderInputStream
对输入流 is
进行包装,以记录输入流的操作。
然后,根据远程能力 cap
是否支持分块传输进行判断。如果支持分块传输,则创建一个 ChunkedCommandTransport
对象,该对象使用支持分块传输的模式包装输入输出流。
如果不支持分块传输,则创建一个 ClassicCommandTransport
对象。在创建该对象之前,先使用 ObjectOutputStream
对输出流 os
进行包装,并调用 flush()
方法确保流的前导信息被发送到对端,以避免死锁。然后,分别使用 ObjectInputStreamEx
对输入流 fis
和 ObjectOutputStream
对输出流 oos
进行包装,创建 ClassicCommandTransport
对象。
最终,根据远程能力的支持情况,选择合适的传输方式并返回相应的 CommandTransport
对象。
negotiate()会检查输入(upload请求)的前导码, 所有发往Jenkins CLI的命令中都包含某种格式的前导码(preamble),前导码格式通常为:<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAH4=
, 该前导码包含一个经过base64编码的序列化对象。“Capability”类型的序列化对象的功能是告诉服务器客户端具备哪些具体功能(比如HTTP分块编码功能)。
现在github上流行的POC发送数据包之后返回的是ClassicCommandTransport
对象
继承关系如下所示
ClassicCommandTransport -> SynchronousCommandTransport -> CommandTransport
Channel构造函数Channel(ChannelBuilder settings, CommandTransport transport)
中, transport.setup()调用SynchronousCommandTransport类的setup()方法来启动一个ReaderThread线程。
public void setup(Channel channel, CommandReceiver receiver) { this.channel = channel; new ReaderThread(receiver).start(); }
在该类的构造函数中,接收一个 CommandReceiver
对象作为参数,并通过调用父类 Thread
的构造函数来设置线程的名称。
在 run()
方法中,使用一个循环来读取通道中的命令,直到通道被关闭为止。在每次循环迭代中,通过调用 read()
方法来读取命令。
//通过上面的ReaderThread.start()方法启动一个线程,ReaderThread为SynchronousCommandTransport类的内部类,在run()方法中,调用ClassicCommandTransport类的read()方法读取输入,read()方法实际是调用Command类的readFrom()方法读取,通过反序列化输入返回一个Command对象。
private final class ReaderThread extends Thread { ...... public ReaderThread(CommandReceiver receiver) { super("Channel reader thread: "+channel.getName()); this.receiver = receiver; } @Override public void run() { final String name =channel.getName(); try { while(!channel.isInClosed()) { Command cmd = null; try { cmd = read(); public final Command read() throws IOException, ClassNotFoundException { try { Command cmd = Command.readFrom(channel, ois);
在反序列化输入返回一个Command对象时就执行了cmd命令,
而不是通过正常的回调handle()方法执行cmd命令