Remote code injection in Log4j
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
# Summary Log4j versions prior to 2.16.0 are subject to a remote code execution vulnerability via the ldap JNDI parser. As per [Apache's Log4j security guide](https://logging.apache.org/log4j/2.x/security.html): Apache Log4j2 <=2.14.1 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.16.0, this behavior has been disabled by default. Log4j version 2.15.0 contained an earlier fix for the vulnerability, but that patch did not disable attacker-controlled JNDI lookups in all situations. For more information, see the `Updated advice for version 2.16.0` section of this advisory. # Impact Logging untrusted or user controlled data with a vulnerable version of Log4J may result in Remote Code Execution (RCE) against your application. This includes untrusted data included in logged errors such as exception traces, authentication failures, and other unexpected vectors of user controlled input. # Affected versions Any Log4J version prior to v2.15.0 is affected to this specific issue. The v1 branch of Log4J which is considered End Of Life (EOL) is vulnerable to other RCE vectors so the recommendation is to still update to 2.16.0 where possible. ## Security releases Additional backports of this fix have been made available in versions 2.3.1, 2.12.2, and 2.12.3 ## Affected packages Only the `org.apache.logging.log4j:log4j-core` package is directly affected by this vulnerability. The `org.apache.logging.log4j:log4j-api` should be kept at the same version as the `org.apache.logging.log4j:log4j-core` package to ensure compatability if in use. # Remediation Advice ## Updated advice for version 2.16.0 The Apache Logging Services team provided updated mitigation advice upon the release of version 2.16.0, w
The fix
Restrict LDAP access via JNDI
log4j-core/pom.xml+5 −0
@@ -349,6 +349,11 @@<artifactId>awaitility</artifactId><scope>test</scope></dependency>+<dependency>+<groupId>org.zapodot</groupId>+<artifactId>embedded-ldap-junit</artifactId>+<scope>test</scope>+</dependency></dependencies><build><plugins>
log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java+29 −2
@@ -88,6 +88,12 @@ public static class Builder<B extends Builder<B>> extends AbstractAppender.Build@PluginBuilderAttributeprivate boolean immediateFail;+@PluginBuilderAttribute+private String allowedLdapClasses;++@PluginBuilderAttribute+private String allowedLdapHosts;+// Programmatic access only for now.private JmsManager jmsManager;@@ -100,8 +106,18 @@ public JmsAppender build() {JmsManager actualJmsManager = jmsManager;JmsManagerConfiguration configuration = null;if (actualJmsManager == null) {+Properties additionalProperties = null;+if (allowedLdapClasses != null || allowedLdapHosts != null) {+additionalProperties = new Properties();+if (allowedLdapHosts != null) {+additionalProperties.put(JndiManager.ALLOWED_HOSTS, allowedLdapHosts);+}+if (allowedLdapClasses != null) {+additionalProperties.put(JndiManager.ALLOWED_CLASSES, allowedLdapClasses);+}+}final Properties jndiProperties = JndiManager.createProperties(factoryName, providerUrl, urlPkgPrefixes,-securityPrincipalName, securityCredentials, null);+securityPrincipalName, securityCredentials, additionalProperties);configuration = new JmsManagerConfiguration(jndiProperties, factoryBindingName, destinationBindingName,userName, password, false, reconnectIntervalMillis);actualJmsManager = AbstractManager.getManager(getName(), JmsManager.FACTORY, configuration);@@ -202,6 +218,16 @@ public Builder setUserName(final String userName) {return this;}+public Builder setAllowedLdapClasses(final String allowedLdapClasses) {+this.allowedLdapClasses = allowedLdapClasses;+return this;+}++public Builder setAllowedLdapHosts(final String allowedLdapHosts) {+this.allowedLdapHosts = allowedLdapHosts;+return this;+}+/*** Does not include the password.*/@@ -212,7 +238,8 @@ public String toString() {+ ", securityCredentials=" + securityCredentials + ", factoryBindingName=" + factoryBindingName+ ", destinationBindingName=" + destinationBindingName + ", username=" + userName + ", layout="+ getLayout() + ", filter=" + getFilter() + ", ignoreExceptions=" + isIgnoreExceptions()-+ ", jmsManager=" + jmsManager + "]";++ ", jmsManager=" + jmsManager + ", allowedLdapClasses=" + allowedLdapClasses++ ", allowedLdapHosts=" + allowedLdapHosts + "]";}}
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/JndiLookup.java+5 −0
@@ -16,6 +16,11 @@*/package org.apache.logging.log4j.core.lookup;+import java.net.MalformedURLException;+import java.net.URI;+import java.net.URISyntaxException;+import java.util.ArrayList;+import java.util.List;import java.util.Objects;import javax.naming.NamingException;
log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java+100 −6
@@ -17,31 +17,76 @@package org.apache.logging.log4j.core.net;+import java.io.Serializable;+import java.net.URI;+import java.net.URISyntaxException;+import java.util.ArrayList;+import java.util.Arrays;+import java.util.List;import java.util.Properties;import java.util.concurrent.TimeUnit;import javax.naming.Context;-import javax.naming.InitialContext;+import javax.naming.NameClassPair;+import javax.naming.NamingEnumeration;import javax.naming.NamingException;+import javax.naming.Referenceable;+import javax.naming.directory.Attribute;+import javax.naming.directory.Attributes;+import javax.naming.directory.DirContext;+import javax.naming.directory.InitialDirContext;import org.apache.logging.log4j.core.appender.AbstractManager;import org.apache.logging.log4j.core.appender.ManagerFactory;import org.apache.logging.log4j.core.util.JndiCloser;+import org.apache.logging.log4j.core.util.NetUtils;+import org.apache.logging.log4j.util.PropertiesUtil;/**-* Manages a JNDI {@link javax.naming.Context}.+* Manages a JNDI {@link javax.naming.directory.DirContext}.** @since 2.1*/public class JndiManager extends AbstractManager {+public static final String ALLOWED_HOSTS = "allowedLdapHosts";+public static final String ALLOWED_CLASSES = "allowedLdapClasses";+private static final JndiManagerFactory FACTORY = new JndiManagerFactory();+private static final String PREFIX = "log4j2.";+private static final List<String> permanentAllowedHosts = new ArrayList<>();+private static final List<String> permanentAllowedClasses = new ArrayList<>();+private static final String LDAP = "ldap";+private static final String SERIALIZED_DATA = "javaserializeddata";+private static final String CLASS_NAME = "javaclassname";+private static final String REFERENCE_ADDRESS = "javareferenceaddress";+private static final String OBJECT_FACTORY = "javafactory";+private final List<String> allowedHosts;+private final List<String> allowedClasses;++static {+permanentAllowedHosts.addAll(NetUtils.getLocalIps());+permanentAllowedClasses.add(Boolean.class.getName());+permanentAllowedClasses.add(Byte.class.getName());+permanentAllowedClasses.add(Character.class.getName());+permanentAllowedClasses.add(Double.class.getName());+permanentAllowedClasses.add(Float.class.getName());+permanentAllowedClasses.add(Integer.class.getName());+permanentAllowedClasses.add(Long.class.getName());+permanentAllowedClasses.add(Number.class.getName());+permanentAllowedClasses.add(Short.class.getName());+permanentAllowedClasses.add(String.class.getName());+}-private final Context context;-private JndiManager(final String name, final Context context) {+private final DirContext context;++private JndiManager(final String name, final DirContext context, final List<String> allowedHosts,+final List<String> allowedClasses) {super(null, name);this.context = context;+this.allowedHosts = allowedHosts;+this.allowedClasses = allowedClasses;}/**@@ -168,7 +213,37 @@ protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) {* @throws NamingException if a naming exception is encountered*/@SuppressWarnings("unchecked")-public <T> T lookup(final String name) throws NamingException {+public synchronized <T> T lookup(final String name) throws NamingException {+try {+URI uri = new URI(name);+if (LDAP.equalsIgnoreCase(uri.getScheme())) {+if (!allowedHosts.contains(uri.getHost())) {+LOGGER.warn("Attempt to access ldap server not in allowed list");+return null;+}+Attributes attributes = this.context.getAttributes(name);+if (attributes != null) {+Attribute classNameAttr = attributes.get(CLASS_NAME);+if (attributes.get(SERIALIZED_DATA) != null) {+if (classNameAttr != null) {+String className = classNameAttr.get().toString();+if (!allowedClasses.contains(className)) {+LOGGER.warn("Deserialization of {} is not allowed", className);+return null;+}+} else {+LOGGER.warn("No class name provided for {}", name);+return null;+}+} else if (attributes.get(REFERENCE_ADDRESS) != null || attributes.get(OBJECT_FACTORY) != null){+LOGGER.warn("Referenceable class is not allowed for {}", name);+return null;+}+}+}+} catch (URISyntaxException ex) {+// This is OK.+}return (T) this.context.lookup(name);}@@ -176,13 +251,32 @@ private static class JndiManagerFactory implements ManagerFactory<JndiManager, P@Overridepublic JndiManager createManager(final String name, final Properties data) {+String hosts = data != null ? data.getProperty(ALLOWED_HOSTS) : null;+String classes = data != null ? data.getProperty(ALLOWED_CLASSES) : null;+List<String> allowedHosts = new ArrayList<>();+List<String> allowedClasses = new ArrayList<>();+addAll(hosts, allowedHosts, permanentAllowedHosts, ALLOWED_HOSTS, data);+addAll(classes, allowedClasses, permanentAllowedClasses, ALLOWED_CLASSES, data);try {-return new JndiManager(name, new InitialContext(data));+return new JndiManager(name, new InitialDirContext(data), allowedHosts, allowedClasses);} catch (final NamingException e) {LOGGER.error("Error creating JNDI InitialContext.", e);return null;}}++private void addAll(String toSplit, List<String> list, List<String> permanentList, String propertyName,+Properties data) {+if (toSplit != null) {+list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*")));+data.remove(propertyName);+}+toSplit = PropertiesUtil.getProperties().getStringProperty(PREFIX + propertyName);+if (toSplit != null) {+list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*")));+}+list.addAll(permanentList);+}}@Override
log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java+44 −0
@@ -17,6 +17,8 @@package org.apache.logging.log4j.core.util;import java.io.File;+import java.net.Inet4Address;+import java.net.Inet6Address;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.NetworkInterface;@@ -25,11 +27,14 @@import java.net.URISyntaxException;import java.net.URL;import java.net.UnknownHostException;+import java.util.ArrayList;import java.util.Arrays;import java.util.Enumeration;+import java.util.List;import org.apache.logging.log4j.Logger;import org.apache.logging.log4j.status.StatusLogger;+import org.apache.logging.log4j.util.Strings;/*** Networking-related convenience methods.@@ -79,6 +84,45 @@ public static String getLocalHostname() {}}+public static List<String> getLocalIps() {+List<String> localIps = new ArrayList<>();+localIps.add("localhost");+localIps.add("127.0.0.1");+try {+final InetAddress addr = Inet4Address.getLocalHost();+setHostName(addr, localIps);+} catch (final UnknownHostException ex) {+// Ignore this.+}+try {+final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();+if (interfaces != null) {+while (interfaces.hasMoreElements()) {+final NetworkInterface nic = interfaces.nextElement();+final Enumeration<InetAddress> addresses = nic.getInetAddresses();+while (addresses.hasMoreElements()) {+final InetAddress address = addresses.nextElement();+setHostName(address, localIps);+}+}+}+} catch (final SocketException se) {+// ignore.+}+return localIps;+}++private static void setHostName(InetAddress address, List<String> localIps) {+String[] parts = address.toString().split("\\s*/\\s*");+if (parts.length > 0) {+for (String part : parts) {+if (Strings.isNotBlank(part) && !localIps.contains(part)) {+localIps.add(part);+}+}+}+}+/*** Returns the local network interface's MAC address if possible. The local network interface is defined here as* the {@link java.net.NetworkInterface} that is both up and not a loopback interface.
log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/JndiExploit.java+36 −0
@@ -0,0 +1,36 @@+/*+* Licensed to the Apache Software Foundation (ASF) under one or more+* contributor license agreements. See the NOTICE file distributed with+* this work for additional information regarding copyright ownership.+* The ASF licenses this file to You under the Apache license, Version 2.0+* (the "License"); you may not use this file except in compliance with+* the License. You may obtain a copy of the License at+*+* http://www.apache.org/licenses/LICENSE-2.0+*+* Unless required by applicable law or agreed to in writing, software+* distributed under the License is distributed on an "AS IS" BASIS,+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+* See the license for the specific language governing permissions and+* limitations under the license.+*/+package org.apache.logging.log4j.core.lookup;++import javax.naming.Context;+import javax.naming.Name;+import javax.naming.spi.ObjectFactory;+import java.util.Hashtable;++import static org.junit.jupiter.api.Assertions.fail;++/**+* Test LDAP object+*/+public class JndiExploit implements ObjectFactory {+@Override+public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment)+throws Exception {+fail("getObjectInstance must not be allowed");+return null;+}+}
log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/JndiLdapLookupTest.java+124 −0
@@ -0,0 +1,124 @@+/*+* Licensed to the Apache Software Foundation (ASF) under one or more+* contributor license agreements. See the NOTICE file distributed with+* this work for additional information regarding copyright ownership.+* The ASF licenses this file to You under the Apache license, Version 2.0+* (the "License"); you may not use this file except in compliance with+* the License. You may obtain a copy of the License at+*+* http://www.apache.org/licenses/LICENSE-2.0+*+* Unless required by applicable law or agreed to in writing, software+* distributed under the License is distributed on an "AS IS" BASIS,+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+* See the license for the specific language governing permissions and+* limitations under the license.+*/+package org.apache.logging.log4j.core.lookup;++import javax.naming.Context;+import javax.naming.NamingException;+import javax.naming.Reference;+import javax.naming.Referenceable;+import javax.naming.StringRefAddr;++import org.apache.logging.log4j.Level;+import org.apache.logging.log4j.message.SimpleMessage;+import org.junit.BeforeClass;+import org.junit.Rule;+import org.junit.Test;+import org.zapodot.junit.ldap.EmbeddedLdapRule;+import org.zapodot.junit.ldap.EmbeddedLdapRuleBuilder;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.fail;++/**+* JndiLookupTest+*/+public class JndiLdapLookupTest {++private static final String LDAP_URL = "ldap://127.0.0.1:";+private static final String RESOURCE = "JndiExploit";+private static final String TEST_STRING = "TestString";+private static final String TEST_MESSAGE = "TestMessage";+private static final String LEVEL = "TestLevel";+public static final String DOMAIN_DSN = "dc=apache,dc=org";++@Rule+public EmbeddedLdapRule embeddedLdapRule = EmbeddedLdapRuleBuilder.newInstance().usingDomainDsn(DOMAIN_DSN)+.importingLdifs("java-import.ldif").build();++@BeforeClass+public static void beforeClass() {+System.setProperty("log4j2.allowedLdapClasses", Level.class.getName());+}++@Test+public void testReferenceLookup() throws Exception {+int port = embeddedLdapRule.embeddedServerPort();+Context context = embeddedLdapRule.context();+context.bind( "cn=" + RESOURCE +"," + DOMAIN_DSN, new Fruit("Test Message"));+final StrLookup lookup = new JndiLookup();+String result = lookup.lookup(LDAP_URL + port + "/" + "cn=" + RESOURCE + "," + DOMAIN_DSN);+if (result != null) {+fail("Lookup returned an object");+}+}++@Test+public void testSerializableLookup() throws Exception {+int port = embeddedLdapRule.embeddedServerPort();+Context context = embeddedLdapRule.context();+context.bind( "cn=" + TEST_STRING +"," + DOMAIN_DSN, "Test Message");+final StrLookup lookup = new JndiLookup();+String result = lookup.lookup(LDAP_URL + port + "/" + "cn=" + TEST_STRING + "," + DOMAIN_DSN);+if (result == null) {+fail("Lookup failed to return the test string");+}+assertEquals("Incorrect message returned", "Test Message", result);+}++@Test+public void testBadSerializableLookup() throws Exception {+int port = embeddedLdapRule.embeddedServerPort();+Context context = embeddedLdapRule.context();+context.bind( "cn=" + TEST_MESSAGE +"," + DOMAIN_DSN, new SimpleMessage("Test Message"));+final StrLookup lookup = new JndiLookup();+String result = lookup.lookup(LDAP_URL + port + "/" + "cn=" + TEST_MESSAGE + "," + DOMAIN_DSN);+if (result != null) {+fail("Lookup returned an object");+}+}++@Test+public void testSpecialSerializableLookup() throws Exception {+int port = embeddedLdapRule.embeddedServerPort();+Context context = embeddedLdapRule.context();+context.bind( "cn=" + LEVEL +"," + DOMAIN_DSN, Level.ERROR);+final StrLookup lookup = new JndiLookup();+String result = lookup.lookup(LDAP_URL + port + "/" + "cn=" + LEVEL + "," + DOMAIN_DSN);+if (result == null) {+fail("Lookup failed to return the level");+}+assertEquals("Incorrect level returned", Level.ERROR.toString(), result);+}++class Fruit implements Referenceable {+String fruit;+public Fruit(String f) {+fruit = f;+}++public Reference getReference() throws NamingException {++return new Reference(Fruit.class.getName(), new StringRefAddr("fruit",+fruit), JndiExploit.class.getName(), null); // factory location+}++public String toString() {+return fruit;+}+}++}
log4j-core/src/test/resources/java-import.ldif+4 −0
@@ -0,0 +1,4 @@+dn: dc=apache,dc=org+objectClass: domain+objectClass: top+dc: apache
Improve GHSA-jfh8-c2jp-5v3q
advisories/github-reviewed/2021/12/GHSA-jfh8-c2jp-5v3q/GHSA-jfh8-c2jp-5v3q.json+39 −20
@@ -1,7 +1,7 @@{"schema_version": "1.4.0","id": "GHSA-jfh8-c2jp-5v3q",-"modified": "2024-07-25T20:12:08Z",+"modified": "2024-07-25T20:12:11Z","published": "2021-12-10T00:40:56Z","aliases": ["CVE-2021-44228"@@ -34,25 +34,6 @@}]},-{-"package": {-"ecosystem": "Maven",-"name": "org.apache.logging.log4j:log4j-core"-},-"ranges": [-{-"type": "ECOSYSTEM",-"events": [-{-"introduced": "2.0-beta9"-},-{-"fixed": "2.3.1"-}-]-}-]-},{"package": {"ecosystem": "Maven",@@ -108,6 +89,44 @@"versions": ["2.6.3-CUSTOM"]+},+{+"package": {+"ecosystem": "Maven",+"name": "org.apache.logging.log4j:log4j-core"+},+"ranges": [+{+"type": "ECOSYSTEM",+"events": [+{+"introduced": "2.0-beta9"+},+{+"fixed": "2.3.1"+}+]+}+]+},+{+"package": {+"ecosystem": "Maven",+"name": "org.ops4j.pax.logging:pax-logging-log4j2"+},+"ranges": [+{+"type": "ECOSYSTEM",+"events": [+{+"introduced": "1.8.0"+},+{+"fixed": "2.0.11"+}+]+}+]}],"references": [
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2021-44228
- WEBhttps://github.com/apache/logging-log4j2/pull/608
- WEBhttps://github.com/github/advisory-database/pull/5501
- WEBhttps://cert-portal.siemens.com/productcert/pdf/ssa-397453.pdf
- WEBhttps://packetstormsecurity.com/files/165673/UniFi-Network-Application-Unauthenticated-Log4Shell-Remote-Code-Execution.html
- WEBhttps://packetstormsecurity.com/files/167794/Open-Xchange-App-Suite-7.10.x-Cross-Site-Scripting-Command-Injection.html
- WEBhttps://packetstormsecurity.com/files/167917/MobileIron-Log4Shell-Remote-Command-Execution.html
- WEBhttps://packetstormsecurity.com/files/171626/AD-Manager-Plus-7122-Remote-Code-Execution.html
- WEBhttps://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0032
- WEBhttps://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-log4j-qRuKNEbd
- WEBhttps://seclists.org/fulldisclosure/2022/Dec/2
- WEBhttps://seclists.org/fulldisclosure/2022/Jul/11