从代码层面分析Jenkins未授权访问CVE-2017-1000353
2023-7-19 16:30:0 Author: xz.aliyun.com(查看原文) 阅读量:22 收藏

首先是Jenkins环境的部署(都是烂大街的东西,就不写的太多了)

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 远程代码执行漏洞)

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实例,并进行上传操作。

跟进download函数

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 块中,设置 completedtrue,并调用 notify() 方法通知其他等待线程,表示下载操作已完成。

upload() 方法中,首先设置响应状态为 HttpServletResponse.SC_OK,表示上传操作已经准备就绪。

然后,通过 req.getInputStream() 获取请求中的输入流,并进行可选的分块处理。

接下来,将上传通道设置为获取到的输入流,并调用 notify() 方法通知其他等待线程。

最后,在循环中等待直到 completedtrue,表示下载操作已完成。在等待过程中,其他线程可以通过调用 notify() 方法来通知上传操作已完成。

这是双向通道建立的基本过程

启动ReaderThread

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);

在这个方法中,首先定义了两个数组 modespreamblesmodes 数组包含了两种通信模式,分别是 Mode.BINARYMode.TEXTpreambles 数组包含了对应通信模式的标志序列,以及一个表示能力的标志序列。

然后,通过循环不断读取输入流 is 的字节,直到遇到标志序列的起始字节。

在每次读取字节后,会遍历 preambles 数组,检查当前字节是否与某个标志序列的下一个字节匹配。如果匹配成功,则将指针 ptr[i] 自增,并检查是否已经匹配到了整个标志序列。

如果匹配到了 Mode.BINARYMode.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 对输入流 fisObjectOutputStream 对输出流 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();
    }

读取Command对象

在该类的构造函数中,接收一个 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命令


文章来源: https://xz.aliyun.com/t/12716
如有侵权请联系:admin#unsafe.sh