《Apache Dubbo: All roads lead to RCE》算是最早公开利用CodeQL 挖掘漏洞的文章,能够半自动化漏洞挖掘,难怪pwntester 祖师爷的产出能够如此高效。
0x01 Dubbo背景
Dubbo 的漏洞原理可以参考上一篇文章。
环境:
- Dubbo 2.7.8
- Codeql 2.13.1
Codeql 一直在增加框架的支持,pwntester的这篇文章发布在2021年9月,在2023年5月2.13.1版本中,已经增加了对于Netty的Source支持。
0x02 原文复现
参考[1]
2.1 Netty Source
by pwntester
CodeQL v2.13.1 default
内置的Netty Source已经更全面了。
1 | /** |
- 注意这里的s.deserialize(url, is) 是由isAdditionalTaintStep手动串起来的
后续的不赘述了,参考pwntester的文章。
但是,如上面的例子,pwntester给的Additional很突兀,后续的例子也需要对Dubbo漏洞有很深的了解,才能写出对应的QL。
如何在不需要这么多背景知识情况下,用更通用的QL发现Dubbo的这些漏洞呢?
0x03 GHSL-2021-036
GHSL-2021-036
GHSL-2021-036 Dubbo中数据流有很多途径到达hessian#readObject,pwntester首次在这里提出这条链readUTF->readString->expect->readObject,这个和后续的CVE-2021-43297异曲同工,而且更短
3.1 Sink
sink 点初步感觉应该是Hessian2Input#readObject
1 | override predicate isSink(DataFlow::Node sink) { |
实际不止如此,引用上一篇文章原理一节
核心的逻辑在于Hessian2#readString,如果发现tag字段类型非String,会调用expect进行Exception告警,其中
- 尝试readObject
- 将obj 进行“+” 拼接
从而触发obj.toString,造成反序列化问题
此处的sink点已经不是readObject了,鉴于CodeQL不能跟进hessian 内部,所以这里只能将readString也作为sink点。
所以最终的sink逻辑如下:
1 | override predicate isSink(DataFlow::Node sink) { |
- 这里pwntester能够发现Hessian的readUTF这个链,说明不单单是分析Dubbo了,至少还有去单独分析Hessain
3.2 Additional
java/ql/lib/ext/com.caucho.hessian.model.yml
1 | extensions: |
3.3 QL Result
pwntester 发现的链有这么多
- HeartBeat Request-> decodeHeartbeatData -> decodeEventData -> in.readEvent -> in.readObject
- Event Request-> decodeEventData -> in.readEvent -> in.readObject
- OK Response-> DecodeableRpcResult.decode() -> handleValue -> readObject
- OK Response-> DecodeableRpcResult.decode() -> handleException leads tBo readThrowable which leads to readObject
- OK Response-> DecodeableRpcResult.decode() -> handleAttachment leads to readAttachments which leads to
readObject
- OK HeartBeat Response-> decodeHeartbeatData -> decodeEventData -> in.readEvent -> in.readObject
- OK Event Response-> decodeEventData -> in.readEvent -> in.readObject
- NOK Response-> in.readUTF (in Hessian, readUTF can lead to readObject)
3.3.1 遗漏
3.3.1.1 ecodeableRpcResult#readAttachments
DecodeableRpcResult.java
1 | public class DecodeableRpcResult extends AppResponse implements Codec, Decodeable { |
1 | package org.apache.dubbo.common.serialize; |
看起来DecodeableRpcResult实际上是有readAttachments 到Sink的路径的,但是实际ql结果中没有,只有handleValue/handleException
而且有DecodeableRpcInvocation->readAttachments 的路径,但是却没有DecodeableRpcResult->readAttachments 的,神奇
猜测:CodeQL具有相似路径,或者更短路径,就不再重复计算?
加个黑名单过滤看看
1 | override predicate isSanitizer(DataFlow::Node node) { |
- OK Response-> DecodeableRpcResult.decode() -> handleAttachment leads to readAttachments which leads to
readObject
结论:CodeQL 污点分析并不会遍历所有的路径,可能更偏向于更短/更优的路径。
3.3.1.2 Codec2
1 | final public class NettyCodecAdapter { |
大致逻辑
1 | NettyCodecAdapter |
此处的Codec2,在最终路径中只有CodecAdapter,没有其他的
- ExchangeCodec
- TelnetCodec
Try Sanitizer
是否和以上问题类似?
注释掉CodecAdapter,让污点流量不经过CodecAdapter试试
1 | override predicate isSanitizer(DataFlow::Node node) { |
再次运行发现并无结果,看来并不是3.3.1.1的类似原因
结论
实际并不是的,注意此刻message 的类型,是一个NettyBackedChannelBuffer,ExchangeCodec 并不接受此类型的InputStream,Exchange对应的是NativeJavaSerialization反序列化
- Event Request-> decodeEventData -> in.readEvent -> in.readObject
- OK Event Response-> decodeEventData -> in.readEvent -> in.readObject
Final Sink
1 | override predicate isSink(DataFlow::Node sink) { |
3.3.2 补充
0x04 GHSL-2021-035
CVE-2021-25641
GHSL-2021-035 Dubbo除了支持hessian协议,还支持其他总共14协议,这些协议里面也有反序列化问题。
1 | 2 -> "hessian2" |
这些里面,部分是存在反序列化漏洞
4.1 Sink
1 | override predicate isSink(DataFlow::Node sink) { |
4.2 Additional
1 | - addsTo: |
1 | extensions: |
4.3 QL Result
0x05 GHSL-2021-037/GHSL-2021-038
CVE-2021-30179
GHSL-2021-037/GHSL-2021-038 Dubbo支持动态调用,有几个特殊的函数:$invoke$invokeAsync$echo,这几个的逻辑在GenericFilter 中,这几个函数支持特定格式的参数,在还原和处理这几个函数时,支持pojo、bean、javanative等方式,会有setter/反序列化问题
调用栈
1 | toObjectImpl:35, JndiConverter (org.apache.xbean.propertyeditor) |
5.1 Source
CodeQL 默认添加了io.netty相关的传播逻辑,但是Dubbo 2.7.8用的是org.jboss.netty,org.jboss.netty已经迁移到了io.netty,参考【3】
5.1.1 jboss netty
1 | extensions: |
尝试之后会发现污点可以直接到中间节点DubboProtocol$1#received
但是这只是巧合,
5.1.2 巧合
这个case很特殊,真实的流程如下
ChannelEventRunnable.java
1 | import java.lang.Runnable |
再往上一层 AllChannelHandler.java
1 | public class AllChannelHandler extends WrappedChannelHandler { |
继续往上到Netty Server服务
1 | public class NettyHandler extends SimpleChannelHandler { |
真实的调用栈应该如下
1 | NettyHandler#messageReceived |
5.1.3 Additional ExecutorService
CodeQL 中提供的Additional 接口是针对污点到污点的,没有增加Call流程的,该怎么处理?
TODO: 如何把implements Runnable中的<init> 和run 关联起来?
1 | ChannelEventRunnable#<init> |
5.2 Sink
5.2.1 javanative
这个就是ObjectInputStream#readObject
1 | exists( MethodAccess ma| |
5.2.2 JavaBean
可以抽象出规则
- newInstance Object
- setter/getter Method
- call Method.invoke(Object, args)
5.2.3 Pojo
5.3 QL Result
5.3.1 匿名内部内
1 | invoke:81, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol) |
DubboProtocol.java
1 | private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() { |
ProtocolFilterWrapper.java
1 | private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) { |
结论:CodeQL 支持匿名内部内的跟踪
5.3.2 强制类型转化
实际上污点可以传播值GenericFilter,但是到这里污点类型已经只是普通的Object,导致后续各种逻辑无法继续
疑问,inv 有强制类型转换为Invocation,但是为何污点信息还是个Object?
TODO
0x06 GHSL-2021-039
CVE-2021-32824
GHSL-2021-039 Dubbo支持Telnet,包括invoke 调用,原理和CVE-2021-30179 PojoUtils 利用类似
6.1 Source
6.2 Sink
6.3 QL Result
0x07 GHSL-2021-040/041/043
CVE-2021-30180
- GHSL-2021-040/GHSL-2021-041 Dubbo的路由实现支持多种,例如Tag路由、Condition路由,它两都支持动态配置,具体实现是以yml格式写入kafka配置中,再交由consumer去解析,consumer采用的是snakeyaml,存在反序列化漏洞。
- GHSL-2021-043: Dubbo还支持动态配置,原理与router config类似,不过是由provider来加载,最终调用snakeyaml实现反序列化。
7.1 Source
7.1.1 zookeeper source
注意:此处Source不全,未覆盖forPath的用法,后续还有针对forPath的用法
1 | class CuratorSource extends RemoteFlowSource { |
7.1.2 sourceModel != RemoteFlowSource ?
注意在yml文件中配置的sourceModel并没有自动加入RemoteFlowSource
1 | private class ExternalRemoteFlowSource extends RemoteFlowSource { |
原因为kind字段必须为remote的才会自动加载为ExternalRemoteFlowSource,否则不会,其实是自己笔误,拷贝了summaryModel 的kind =’taint’,改为’remote’之后ok
7.2 Sink
7.2.1 Yaml 反序列化
1 | override predicate isSink(DataFlow::Node sink) { |
7.3 QL Result
- TagRuleParser
- ConfigParser
- ConditionRuleParser
0x08 GHSL-2021-042
CVE-2021-30181
GHSL-2021-042 与上一个漏洞类似,还支持Script路由,Script引擎用的Nashorn,存在命令执行
与以往的漏洞都是攻击provider 不同,这个漏洞攻击的是consumer
8.1 Sink
Sink CodeQL 点实际默认也有
8.1.1 ScriptEngine
1 | extensions: |
Sink点没问题,按漏洞原理来讲Source也应该是Zookeeper,但是实际跑来,并没有结果
8.2 Source
但是从调用栈上看不出来何时读取的zookeeper
1 | eval:511, NashornScriptEngine$3 (jdk.nashorn.api.scripting) |
定位发现,是zookeeper forPath()
1 | public List<String> addTargetChildListener(String path, CuratorWatcherImpl listener) { |
8.2.1 Kafaka forPath Source
1 | ( |
TODO:这种整个methodAccess 作为taint的该怎么写yml SourceModule?
8.3 Addintional
8.3.1 computeIfAbsent
org.apache.dubbo.registry.support.AbstractRegistry#notify
1 | Iterator var5 = urls.iterator(); |
这里有一个computeIfAbsent 的相关逻辑
1 | - ["java.util", "Map", True, "computeIfAbsent", "", "", "Argument[this].MapValue", "ReturnValue", "value", "manual"] |
但是显然不能够将result value和u 污点关联起来,因为categoryList类似于引用调用,污点传播在后,可以传播到categoryList.add(u);
这里的u
,这应该是CodeQL的通用问题。
引用类型的无解
8.4 QL Result
目前认为无解
TODO,computeIfAbsent这种写法还挺多,暂时考虑手动串起来
0x09 GHSL-2021-094
CVE-2021-36162
GHSL-2021-094 Dubbo在2.7.10中修复了snakeYaml的反序列化问题,但是3.0新Router机制Mesh中依然存在snakeYaml反序列化
9.1 Source
1 | Yaml yaml = new Yaml(); |
用的还不是Yaml#load 接口,而是Yaml#loadAll
1 | override predicate isSink(DataFlow::Node sink) { |
9.2 QL Result
但是细看调用栈,会发现经过了notify#AbstractRegistry,和GHSL-2021-042一样,暂时无解
TODO:手动临时串起来
1 | next:547, Yaml$1 (org.yaml.snakeyaml) |
0x10 GHSL-2021-095
CVE-2021-36163
GHSL-2021-095 Dubbo还支持Hessian协议(并不是指反序列化),基于http,在HessianSkeleton 的处理中会对POST 的内容进行反序列化
1 | lookup:417, InitialContext (javax.naming) |
10.1 Source
调用栈可以看出是Serverlet Source
1 | import javax.servlet.http.HttpServlet; |
1 | override predicate isSource(DataFlow::Node source) { |
10.1.1 Where is Source Module HttpServletRequest#getInputStream Defined?
为什么在FlowSources.qll 中没有extends RemoteFlowSource的相关Servlet,为何以上的ql能直接定位到
1 | private class ExternalRemoteFlowSource extends RemoteFlowSource { |
在java/ql/lib/ext/javax.servlet.model.yml 中
1 | - ["javax.servlet", "ServletRequest", False, "getInputStream", "()", "", "ReturnValue", "remote", "manual"] |
疑问:注意这里subteyps=Flase,为什么HttpServletRequest 也能够适用呢
在java/ql/lib/ext/javax.servlet.http.model.yml 中,并无HttpServletRequest#getInputStream 的记录
具体是在哪里添加的getInputStream 这个Source点呢?
注释掉javax.servlet.model.yml 中的ServletRequest getInputStream一行,发现RemoteFlowSource的确结果中没有了
subteyps
查看HttpServletRequest发现并未重写getInputStream,因此subteyps实际还用的是ServletRequest getInputStream,注意这点:subyptes=False并不是表示”子类不适用这条规则”
引用师傅的结论:
subtypes:布尔类型,可以取值为true或false. 当为true时, 会寻找子类重写的类型成员name(使用
overridesOrInstantiates 谓词得到). 否则寻找当前类类型成员name
师傅看的真仔细。
ExternalFlow.qll
1 | exists(Member m | |
疑问:result = m,CodelQL 父类的member也是子类的Member吗
1 | from Member m |
并没有getInputStream
结论:不是
注意此处的and or 逻辑,与传统的语言顺序执行不同,这里的实际逻辑如下:
result = m
or (subtypes = true and result.(SrcMethod).overridesOrInstantiates+(m))
Member.qll
1 | predicate overridesOrInstantiates(Method m) { |
overridesOrInstantiates的逻辑为重写或者实现
个人理解subtypes的逻辑是针对Method member的:
- 如果为false
- 那么只对当前method member有效
- 如果为true
- 当前method member
- 所有重写/实现了该method的method
疑问:HttpServletRequest extends ServletRequest,其本身也是个interface,为什么也有?
想想trap 的生成逻辑,JavaCompiler Visitor,在编译的时候,HttpServletRequest#getInputStream(),HttpServletRequest并没有getInputStream,实际上是父类的getInputStream,因此即使subtypes=false,如果没有重写method,那么子类调用该method,还是可以认为该规则生效。
- 子类subclass 重写/实现了method,针对subclass#method的MethodCallExpr,subtypes=True 时该条规则才会命中
- 子类subclass 没有重写/实现method,一直生效,和subtypes无关
10.2 Sink
Sink点不是普通的Hessian2反序列化Hessian2Input#readObject, 而是HessianSkeleton#invoke
1 | - ["com.caucho.hessian.server", "HessianSkeleton", False, "invoke", "(InputStream,OutputStream)", "", "Argument[0]", "deserilization", "manual"] |
注意:(InputStream,OutputStream) 中间无空格
10.3 QL Result
0x11 GHSL-2021-096
GHSL-2021-096
GHSL-2021-096 Dubbo还支持rmi协议,Dobbo的Provider类似RMI的Register和Provider,可以用Client攻击Register和Provider的方式进行攻击
11.1 Sink
逻辑在org.apache.dubbo.rpc.protocol.rmi.RmiProtocol 内,用的org.springframework.remoting.rmi.RmiServiceExporter 实现RMI Register
1 | import org.springframework.remoting.rmi.RmiServiceExporter; |
这种无法写污点分析,没有污点源,只要启动了rmi server就有问题
从main 到 RmiServiceExporter getEnclosingCallable ?是不是可以为污点分析
11.2 QL Result
11.2.1 DataFlow::Node Tip
注: DataFlow::Node 可能指的是最小节点或者深度为1的节点,比如最下层的MethodAccess,如果嵌套则不包含上层
1 | override predicate isSink(DataFlow::Node sink) { |
例如以上的ql 命中不了setRegistryPort
1 | override predicate isSink(DataFlow::Node sink) { |
11.2.2 Final QL
1 | override predicate isSink(DataFlow::Node sink) { |
0x12 GHSL-2021-096
CVE-2021-37579
GHSL-2021-097 CodecSupport#checkSerialization bypass,该安全策略用于判断dubbo数据包指定的序列化方式是否是provider配置的配置,当version不存在时,会绕过检测漏洞
个人认为CodeQL不适合做bypass工作,暂时忽略
0x13 CVE-2021-43297
hessian-lite 作为dubbo内置的hessian版本,在处理异常的时候,会通过”+”进行字符串拼接,导致其中obj的toString 隐式调用,形成反序列化(也可以认为是CVE-2021-30179的补充)
部分重点的调用链
1 | readString:1853, Hessian2Input (com.alibaba.com.caucho.hessian.io) |
其实在GHSL-2021-096中就有这条链
0x14 CVE-2023-23638
CVE-2021-30179的绕过,可以利用setter设置全局静态配置变量,绕过安全规则
个人认为CodeQL不适合做bypass工作,暂时忽略
0x15 小结
遗留问题:
- 5.1.3 GHSL-2021-037/GHSL-2021-038一节中,如何把implements Runnable中的<init> 和run 关联起来?
- 5.3.2 中,即使存在强制类型,污点仍然为Object
- zookeeper source forPath怎么转换成yml sourceModule?
- computeIfAbsent 这种引用传播,污点如何串起来
- netInstance & setter/getter ql实现