前言
某日,华为大佬突然说apache druid有0day,并且发送了一个github的链接长亭出的一道CTF题目,下载github的项目,打开发现,是一个docker,里面只有一个druid,所以,该CTF真的就是挖出druid的0day,google搜索了下Apache Druid的docs,文档介绍说Apache Druid 是一个实时分析数据库,专为对大型数据集进行快速切片和切块分析, 大多数情况下,Druid 支持实时摄取、快速查询性能和高正常运行时间很重要的用例.数据库这个关键字,当时我就猜想,是JDBC的JNDI注入,其实,这个注入早就被发现了,我没关注过,不知道druid修复了.后面,大佬提醒我是kafka的JNDI,有了方向,就开始尝试挖掘
尝试挖掘
光速下载了Apache Druid的源码,搜索了一下lookup,没有!心里凉了半截,源码没有,那就只能从Resource开始阅读源码,一边debug一边看,启动ctf的docker,先黑盒看看ctf的druid有啥功能没,结果ctf的druid没几个功能正常,这个按下不表,为了调试方便,我又装了一个本机的druid,同时还装了一个kafka做测试的数据源,启动本机的druid,黑盒看了它的功能,因为没接触过大数据相关的知识,我只能浅显的理解为druid可以从一些数据源中获取数据,然后整合保存成一份tables,后续从tables中查询数据,在我的理解中,能发起JNDI请求的操作,明显是获取数据的时候,因为后续都是druid自身实现的功能,如果是查询阶段发起的JNDI注入,那在源码里应该能搜索到,并且,在文档中,也没搜不到能发起JNDI的功能,那按照挖掘的顺序,肯定优先挖掘获取数据的时候.
druid的Server端是guice+jersey,不是普通的ssm架构,通过黑盒的抓包信息,发现URL为/druid/indexer/v1/sampler?for=connect,搜索该URL对应的Resource
在SamplerResource打下断点,查看数据流到Resource前有无经过高危操作,看不出来,从功能来讲,能有高危操作,就在拦截器和过滤器中,跟进filter,同样未发现有高危操作
只能跟进sample方法,简单浏览和debug走了一下,简单概括就是初始化kafkaConsumer,通过KafkaRecordSupplier操作kafkaConsumer和kafka Server链接,后续将数据打包处理之后返回给前端,通过不停的来回debug,大致了解到代码运行到这里时,数据已经返回了,后续的代码是处理数据的阶段,有大段的算法,暂时放弃,并且,该阶段主要由自身的代码实现,如果有JNDI,在之前的代码搜索中就能发现,主要精力放到初始化kafkaConsumer开始审计,一直到发起链接为止,开始的思路是链接kafka和从kafka读取数据的时候,是否会出问题?先从输入源开始追踪,之后来来回回的debug过程中,感觉非常的难熬,时不时就会跳到另一个线程中,不停的有干扰的断点出现,很多lambda 表达式和链式调用给我阅读体验非常差!
开始从初始化kafkaConsumer进行跟踪,进入kafka client之后,明显代码就好阅读很多了,从debug过程中可以发现config可以由外部控制,传入的bootstrap.servers由我们传入,第一时间关注这个servers的传参
跟踪servers一直往下走,进入ClientUtils.parseAndValidateAddresses方法,其实看到返回值就能明白,kafka client与server端的链接是采用socket直接链接,没有调用JNDI的地方,心里非常凉,觉得已经没希望了
这个时候,又不甘心,心里想可能追错了,回去docker的durid又看了一遍,docker的durid非常奇怪,其他的功能都是502,只有设置数据源可以访问,如果这是CTF的提示,其实也很明显了,就是设置数据源有问题,其中kafka有两个设置数据源的地方,之前我只看了第一个,后面又看到了第二个设置kafka链接的选项,并且,进入之后,发现一个非常有趣的参数sasl.jaas.config
sasl是一种java默认提供的认证机制,肯定支持LDAP的方式进行认证,google一下相关的文档,woc,瞬间觉得看到希望了,回去跟踪kafka client的sasl认证,从docs继续搜索kafka的sasl认证,发现sasl存在多种认证,并且支持ldap,稳啦!?并没有!
sasl有多种认证方式,从docs中可以发现传入不同的sasl.jaas.config,他走的认证也不同,当我查看ldap的认证的时候,发现传入的参数竟然和plain一样,赶紧去看看org.apache.kafka.common.serurity.plain.PlainLoginModule,一眼看完,没有高危操作,只能跟踪一下,这个类怎么调用
重新进入断点,在ClientUtils.createChannelBuilder(config, this.time, logContext);方法中获取了sasl的相关参数,跟进
最后跟进create方法,这个方法会根据serurityProtocol选择不同的ChannelBuilder(如果显示在前端的不是SASL,那估计还要花更多时间来挖掘,也是运气哈哈),config传入3个参数,根据config的参数走SASL分支,代码也比较清晰明了,主要获取构造SaslChannelBuilder的参数,其中,最主要的是jaasContext,跟进JaasContext.loadClientContext(configs)方法
从docs中可以发现主要是sasl.jaas.config控制了认证登陆的逻辑,并且,传入的参数是一个类!跟进到load,此时我传入的参数是javax.naming.initialContext,成功设置到jaasConfig,说明在设置属性时,不会对传入的参数进行校验,往下跟进sasl的认证过程
之后就是所以传入JaasContext的方法都跟踪一遍,后面的代码对JaasContext的调用没啥分支,可以一路跟到发起认证的调用点
一直跟进到javax.security.auth.login.LoginContext#invoke方法,中途没有检查javax.naming.initialContext的合法性,但是invoke方法里对method的invoke有明显的要求,先invoke initialize方法,之后再invoke login方法,一般这种写法都是继承某一个统一的接口
回到org.apache.kafka.common.serurity.plain.PlainLoginModule,他继承自LoginModule,查看LoginModule的继承者
bingo!发现一个JNDI,根据上面的数据流跟踪,全程是没有校验认证的Module是否合法
构造PoC
根据JndiLoginModule的代码构造一个JNDI
1 | POST /druid/indexer/v1/sampler?for=connect HTTP/1.1 |
其他漏洞
中途还挖了2个黑盒就能发现洞
任意文件读取
SSRF
该漏洞druid同样已知晓,但是未修复