Dubbo Kryo & FST RCE

漏洞簡介
漏洞原理
Dubbo Provider即服務提供方默認使用dubbo協議來進行RPC通信,而dubbo協議默認是使用Hessian2序列化格式進行對象傳輸的,但是針對Hessian2序列化格式的對象傳輸可能會有黑白名單設置的限制,參考:https://github.com/apache/dubbo/pull/6378
針對這種場景,攻擊者可以通過更改dubbo協議的第三個flag位字節來更改為使用Kryo或FST序列化格式來進行Dubbo Provider反序列化攻擊從而繞過針對Hessian2反序列化相關的限制來達到RCE。
影響版本
- 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)
環境復現
安裝zookeeper和dubbo-samples,用idea打開dubbo-samples-api,然后修改其中的pom.xml如下:

注意,dubbo-common必須 ≤2.7.3版本。
在Dubbo<=2.7.3中fastjson的版本≤1.2.46 ,這也是我們這個洞的利用點,不過這里復現使用的更高版本所以需要添加依賴,
<dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.46version>dependency>
使用POC進行測試:


Dubbo的協議設計
由于Dubbo可以支持很多類型的反序列化協議,以滿足不同系統對RPC的需求,比如:
?跨語言的序列化協議:Protostuff、ProtoBuf、Thrift、Avro、MsgPack
? 針對Java語言的序列化方式:Kryo、FST
? 基于Json文本形式的反序列化方式:Json、Gson
Dubbo中對支持的協議做了一個編號,每個序列化協議都有一個對應的編號,以便在獲取TCP流量后,根據編號選擇相應的反序列化方法,因此這就是Dubbo支持這么多序列化協議的秘密,但同時也是危險所在。
org.apache.dubbo.common.serialize.Constants中可見每種序列化協議的編號:

而在Dubbo的RPC通信時,對流量的規定最前方為header,而header中通過指定SerializationID,確定客戶端和服務提供端通信過程使用的序列化協議。
Dubbo通信的具體數據包規定如下圖所示:

雖然Dubbo的provider默認使用hessian2協議,但我們可以自由的修改SerializationID,選定危險的(反)序列化協議,例如kryo和fst。
Dubbo RPC數據包格式

?Magic(魔術) - Magic High & Magic Low (16 bits)用值標識dubbo協議:0xdabb?Req/Res (1 bit)標識這是一個請求或響應。請求 : 1;響應 : 0。?2 Way (1 bit)Only useful when Req/Res is 1 (Request), expect for a return value from server or not. Set to 1 if need a return value from server.?Event (1 bit)Identifies an event message or not, for example, heartbeat event. Set to 1 if this is an event.?Serialization ID (5 bit)標識序列化類型:fastjson 的值為 6。?Status (8 bits)Only useful when Req/Res is 0 (Response), identifies the status of response?20 - OK–30 - CLIENT_TIMEOUT–31 - SERVER_TIMEOUT–40 - BAD_REQUEST–50 - BAD_RESPONSE–60 - SERVICE_NOT_FOUND–70 - SERVICE_ERROR–80 - SERVER_ERROR–90 - CLIENT_ERROR–100 - SERVER_THREADPOOL_EXHAUSTED_ERROR?Request ID (64 bits)Identifies an unique request. Numeric (long).?Data Length (32)序列化后內容(可變部分)的長度,以字節為單位。數字(整數)。?Variable PartEach part is a byte[] after serialization with specific serialization type, identifies by Serialization ID.Every part is a byte[] after serialization with specific serialization type, identifies by Serialization ID1.If the content is a Request (Req/Res = 1), each part consists of the content, in turn is:?Dubbo version–Service name–Service version–Method name–Method parameter types–Method arguments–Attachments1.If the content is a Response (Req/Res = 0), each part consists of the content, in turn is:?Return value type, identifies what kind of value returns from server side: RESPONSE_NULL_VALUE - 2, RESPONSE_VALUE - 1, RESPONSE_WITH_EXCEPTION - 0.–Return value, the real value returns from server.
注意:對于(Variable Part)變長部分,當前版本的dubbo框架使用json序列化時,在每部分內容間額外增加了換行符作為分隔,請選手在Variable Part的每個part后額外增加換行符,如:
Dubbo version bytes (換行符)Service name bytes (換行符)...
案例

漏洞分析
FTS反序列化
FTS反序列化發生在RPC協議反序列化。

org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
在上述方法中首先通過serializationType索引序列化器,當TypeId為8時使用Kryo,TypeId為9時使用Fst。


kryo和fst的調用鏈都比較類似,使用map序列化器反序列化時觸發,

instantiate:79, FSTMapSerializer (org.nustaq.serialization.serializers)instantiateAndReadWithSer:497, FSTObjectInput (org.nustaq.serialization)readObjectWithHeader:366, FSTObjectInput (org.nustaq.serialization)readObjectFields:708, FSTObjectInput (org.nustaq.serialization)instantiateAndReadNoSer:562, FSTObjectInput (org.nustaq.serialization)readObjectWithHeader:370, FSTObjectInput (org.nustaq.serialization)readObjectFields:708, FSTObjectInput (org.nustaq.serialization)instantiateAndReadNoSer:562, FSTObjectInput (org.nustaq.serialization)readObjectWithHeader:370, FSTObjectInput (org.nustaq.serialization)readObjectInternal:327, FSTObjectInput (org.nustaq.serialization)instantiate:77, 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)decode:73, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)decodeBody:132, DubboCodec (org.apache.dubbo.rpc.protocol.dubbo)
核心部分調用堆棧入手,根據調用堆棧可知獲取了FstSerialization解析器進行反序列化,在反序列化的過程中必然涉及到還原對象以及相關字段,而在還原HashMap 的時候對其成員進行還原時候會調用HashMap#put方法將鍵值對進行放入,而這個放入的過程就是觸發漏洞的關鍵點,在放入的過程中存在兩個口子,一個是hashCode,另一個是 equals。



這里調用equals口子進入toString方法,由此可知這里使用的:
org.springframework.aop.target.HotSwappableTargetSource#equals -> com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object) -> xxxx.toString()

這里再次觸發equals方法,參數是我們構造的惡意JSONObject對象,

com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)
在上述方法 中觸發了傳入的惡意JSONObject對象的toString方法:

fastjson中JSONObject是可以被序列化的,當其顯式或隱式被調用toString方法時,會觸發綁定對象的getter方法: 

這里將會觸發這個JSONObject中成員的值,也就是觸發了這個TemplatesImpl對象的get系列方法,而我們的Payload入口就是getOutProperties方法,至于后面就是常規的TemplatesImpl利用手法。
下圖是POC中對應的鏈的構造:



完整漏洞調用棧
exec:-1, Runtime (java.lang):-1, Pwner8957425893700 (ysoserial)newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)newInstance:-1, NativeConstructorAccessorImpl (sun.reflect)newInstance:-1, DelegatingConstructorAccessorImpl (sun.reflect)newInstance:-1, Constructor (java.lang.reflect)newInstance:-1, Class (java.lang)getTransletInstance:-1, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)newTransformer:-1, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)getOutputProperties:-1, 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:-1, XString (com.sun.org.apache.xpath.internal.objects)equals:104, HotSwappableTargetSource (org.springframework.aop.target)putVal:-1, HashMap (java.util)put:-1, 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)decode:73, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)decodeBody:132, DubboCodec (org.apache.dubbo.rpc.protocol.dubbo)decode:122, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)decode:82, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)decode:48, DubboCountCodec (org.apache.dubbo.rpc.protocol.dubbo)decode:90, NettyCodecAdapter$InternalDecoder (org.apache.dubbo.remoting.transport.netty4)decodeRemovalReentryProtection:508, ByteToMessageDecoder (io.netty.handler.codec)callDecode:447, ByteToMessageDecoder (io.netty.handler.codec)channelRead:276, ByteToMessageDecoder (io.netty.handler.codec)invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)channelRead:1410, DefaultChannelPipeline$HeadContext (io.netty.channel)invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)fireChannelRead:919, DefaultChannelPipeline (io.netty.channel)read:166, AbstractNioByteChannel$NioByteUnsafe (io.netty.channel.nio)processSelectedKey:719, NioEventLoop (io.netty.channel.nio)processSelectedKeysOptimized:655, NioEventLoop (io.netty.channel.nio)processSelectedKeys:581, NioEventLoop (io.netty.channel.nio)run:493, NioEventLoop (io.netty.channel.nio)run:989, SingleThreadEventExecutor$4 (io.netty.util.concurrent)run:74, ThreadExecutorMap$2 (io.netty.util.internal)run:30, FastThreadLocalRunnable (io.netty.util.concurrent)run:-1, Thread (java.lang)
Kryo反序列化
在DecodeableRpcInvocation類的decode()方法(該方法會在處理RPC請求時候調用)中,通過serializationType為8、獲取到反序列化器Kryo,然后調用readUTF()函數來讀取dubbo協議對應的字段信息如dubbo協議版本、服務名稱、服務版本、方法名、方法參數類型等:

提取方法參數類型為類數組后,再循環對參數進行Kryo反序列化。
從input中讀取解析到type為HashMap,因此會調用Kryo的MapSerializer序列化器來讀取input中的信息:


其中會將解析到的key和value都通過調用map.put()來放入HashMap對象中,發生在com.esotericsoftware.kryo.serializers.MapSerializer#read方法調用中,這里是有兩對鍵值對放進去了:

往下putVal()函數中會調用key即XString類的equals()函數來判斷兩個key值是否相等:

到這和前面FST反序列化基本上一樣了。
修復
dubbo-common 2.7.3的版本中存在kryo和fst的序列化需要的類,而在dubbo-common 2.7.4.1中,這三個包并不存在,需要自行導入。
在高版本中已將com.esotericsoftware:kryo依賴去掉了,在使用Kryo序列化器進行反序列化獲取KryoObjectInput對象時會報找不到KryoException類的錯誤,自帶的Fastjson版本為1.2.70,AutoType會自動攔截掉TemplatesImpl類。