#!/bin/bash

#### Description: Search for log4shell exploits
#### Written by: Cedric Courlet - ccourlet@siteperso.info on 2012/12/17
####
#### DONE:
#### * Inspects ear, war, class and jar files
#### * 3 modes: default / suspect / paranoid to inspect log4j-core * .jar, * log4j.jar or * .jar
#### * Inspect jar files contained in the war files
#### * Inspects META-INF files contained in ear files
#### * Possibility to change the search path
#### * Event filtering: infos, warnings, errors, statistics
#### * Statistics
####
#### TODO:
#### * test function
#### * format output function
#### * Unify pom search
#### * csv output
####
#### CHANLOG:
#### 2021/12/19 Optimizing stats (no more find)
#### 2021/12/19 Multiple /log4j-core/pom.properties on ear file
#### 2021/12/19 Search JndiLookup.class on ear file
#### 2021/12/19 Adding no-colors option
#### 2021/12/19 Fixing $AVOID* by ${AVOID*}
#### 2021/12/19 Inspecting /log4j-core/pom.properties on jar


START_TIME=$(date +%s.%N)
SCRIPT_RELEASE="1.1"
SCRIPT_NAME="check_log4shell"

##################################################
#     ERRORLEVELS (^2 = sum() compliant)         #
##################################################

ERR_UNRECOGNIZED_OPTION=1
ERR_UNZIP_NOT_FOUND=2
ERR_TOO_MANY_AVOIDS=4
ERR_PATH_NOT_FOUND=8
ERR_TEMP_PATH_NOT_FOUND=16

##################################################
#     DISPLAY USAGE                              #
##################################################

usage()
{
    echo -e ""
    echo -e "${SCRIPT_NAME} - Release ${SCRIPT_RELEASE} - 2021 Dec 17, written by Cedric Courlet"
    echo -e ""
    echo -e "The script checks for the presence of log4shell (CVE-2021-44228)"
    echo -e "Default focus is *.ear, *.war, log4j-core*.jar, JndiLookup.class; Insensitive"
    echo -e "Default path is '/'"
    echo -e "Write permission to the temporary folder is required"
    echo -e ""
    echo -e "Usage : ${SCRIPT_NAME} {-h | --help} {-s | --suspicious} {-p | --paranoid} \\"
    echo -e "                        {-t | --test } {--path 'newpath'} \\"
    echo -e "                        {--no-infos} {--no-warnings} {--no-errors} {--no-stats}"
    echo -e ""
    echo -e "  -h, --help             Display help and exit"
    echo -e "  -s, --suspicious       Check '*log4j*.jar' instead of 'log4j-core*.jar' for jar & war files"
    echo -e "  -p, --paranoid         Check every single file '*' for jar files"
    echo -e "  -t, --test             Perform Java test (require Java 8 & log4j-core >= 2.16.0)"
    echo -e "  --path 'newpath'       Default search path is '/'"
    echo -e "  --no-infos             Avoid infos output"
    echo -e "  --no-warnings          Avoid warnings output"
    echo -e "  --no-errors            Avoid errors output"
    echo -e "  --no-stats             Avoid statistics output"
    echo -e "  --no-colors            Avoid colored display"
    echo -e ""
    echo -e ""
}

##################################################
#     ERROR CATCH                                #
##################################################

catch()
{
    usage
    echo -e "Exit with error #${2} [${BOLD}${1}${END}]"
    echo
    exit #${2}
}

##################################################
#     COMPARE VERSIONS                           #
##################################################
#         0) op='=';;                            #
#         1) op='>';;                            #
#         2) op='<';;                            #
##################################################

compareversions () {
    if [[ $1 == $2 ]]
    then
        return 0
    fi
    local IFS=.
    local i ver1=($1) ver2=($2)
    # fill empty fields in ver1 with zeros
    for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
    do
        ver1[i]=0
    done
    for ((i=0; i<${#ver1[@]}; i++))
    do
        if [[ -z ${ver2[i]} ]]
        then
            # fill empty fields in ver2 with zeros
            ver2[i]=0
        fi
        if ((10#${ver1[i]} > 10#${ver2[i]}))
        then
            return 1
        fi
        if ((10#${ver1[i]} < 10#${ver2[i]}))
        then
            return 2
        fi
    done
    return 0
}

##################################################
#     REQUIRED RUNTIMES                          #
##################################################

if ! [ -x "$(command -v unzip)" ]; then
  catch "unzip is not installed" ${ERR_UNZIP_NOT_FOUND}
fi

UNZIP="$(which unzip)"
GETOPT="$(which getopt)"

LOG4J_FILTER="log4j-core-*.jar"
LOG4J_FILTER_NAME="default"
SEARCH_PATH="/"
AVOID=0

NB_WAR_FILES=0
NB_JAR_FILES=0
NB_EAR_FILES=0
NB_DEFLATED_JAR_FILES=0
NB_POM_FILES=0

##################################################
#     CHECKING PARAMETERS                        #
##################################################

getopt_results=$(${GETOPT} -s bash -o h,s,p,t --long help,suspicious,paranoid,test,path:,no-infos,no-warnings,no-errors,no-stats,no-colors -- "$@" 2>/dev/null)
if test $? != 0
then
    catch "Unrecognized parameter(s)" ${ERR_UNRECOGNIZED_OPTION}
fi

eval set -- "${getopt_results}"
while true
do
    case "${1}" in
        -h|--help)
            usage;
            exit 0
            ;;
        -s|--suspicious)
            LOG4J_FILTER="*log4j*.jar";
            LOG4J_FILTER_NAME="suspicious";
            shift 1;
            ;;
        -p|--paranoid)
            LOG4J_FILTER="*.jar";
            LOG4J_FILTER_NAME="paranoid";
            shift 1;
            ;;
        --path)
            SEARCH_PATH="$(echo ${2})";
            shift 2;
            ;;
        --no-infos)
            AVOID_INFOS=1;
            let "AVOID++";
            shift
            ;;
        --no-warnings)
            AVOID_WARNINGS=1;
            let "AVOID++";
            shift
            ;;
        --no-errors)
            AVOID_ERRORS=1;
            let "AVOID++";
            shift
            ;;
        --no-stats)
            AVOID_STATS=1;
            shift
            ;;
        --no-colors)
            AVOID_COLORS=1;
            shift
            ;;
        --)
            shift
            break
            ;;
        *)
            catch "Unparseable option" ${ERR_UNPARSEABLE_PARAMETER}
            ;;
    esac
done


##################################################
#     ADD SOME COLORS WHEN XTERM                 #
##################################################

if [ "${TERM}" = "xterm" ] && [[ ${AVOID_COLORS} -ne 1 ]]; then
       BOLD=$(echo -e "\e[1m")
        END=$(echo -e "\e[0m")
        RED=$(echo -e "\e[31m")
      GREEN=$(echo -e "\e[32m")
     YELLOW=$(echo -e "\e[33m")
    REVERSE=$(echo -e "\e[7m")
else
       BOLD=""
        END=""
        RED=""
      GREEN=""
     YELLOW=""
    REVERSE=""
fi

##################################################
#     CATCH SOME ERRORS                          #
##################################################

if [ $AVOID -ge 3 ]; then
    catch "You have to display something" ${ERR_TOO_MANY_AVOIDS}
fi

if [ ! -d "${SEARCH_PATH}" ]; then
    catch "Directory ${SEARCH_PATH} does not exists." ${ERR_PATH_NOT_FOUND}
fi

TEMP_PATH=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
if [ -d "${TEMP_PATH}" ]; then
    trap "rm -fr ${TEMP_PATH}; exit" SIGHUP SIGINT SIGTERM EXIT
else
    catch "You don't have permissions to create ${TEMP_PATH}" ${ERR_TEMP_PATH_NOT_FOUND}
fi

##################################################
#     CHECKING JAR FILE                          #
##################################################

check_jar () {
    filename=${1}
    [[ "${2}" == "deflated" ]] && let "NB_DEFLATED_JAR_FILES++" || let "NB_JAR_FILES++"
    basename=$(basename "${filename}")
    if [[ $(${UNZIP} -Z1 ${filename} 2>/dev/null | grep -i JndiLookup.class) ]]; then
        [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "jar   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} contains JndiLookup.class and could be affected"
        fileversion=$(echo -e "${basename}" | sed -n '/log4j-core-/s/.*log4j-core-\([[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+\)\.jar$/\1/p')
        if [[ ! -z "${fileversion}" ]]; then
            compareversions "${fileversion}" "2.16.0"
            case $? in
                0)
                    # op='=';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "jar   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${YELLOW} version is equal to 2.16.0";
                    ;;
                1)
                    # op='>';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "jar   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${YELLOW} version is greater than 2.16.0";
                    ;;
                2) 
                    # op='<';
                    [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "jar   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${filename}${END} version ${fileversion} is less than 2.16.0";
                    ;;
            esac
        else
            [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "jar   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${filename}${END} log4j-core version is unknown";
        fi
    else
        [[ ${AVOID_INFOS} -ne 1 ]] && echo -e "jar   |${REVERSE}${GREEN} [INFO]    ${END}| ${filename} does not contain JndiLookup.class"
    fi
    jarfiles2check=$(${UNZIP} -Z1 ${filename} 2>/dev/null | grep -i "/log4j-core/pom.properties")
    for jarfile2check in $(echo -e ${jarfiles2check}); do
        let "NB_POM_FILES++"
        fileversion=$(${UNZIP} -p ${filename} ${jarfile2check} | grep -i 'version' | awk -F'=' '{print $2}' | sed 's/\s//g')
        if [[ ! -z "${fileversion}" ]]; then
            [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "jar   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} contains ${jarfile2check} and could be affected"
            compareversions "${fileversion}" "2.16.0"
            case $? in
                0)
                    # op='=';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "jar   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} ${jarfile2check} version is equal to 2.16.0";
                    ;;
                1)
                    # op='>';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "jar   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} ${jarfile2check} version is greater than 2.16.0";
                    ;;
                2) 
                    # op='<';
                    [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "jar   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${filename}${END} ${jarfile2check} version ${fileversion} is less than 2.16.0";
                    ;;
            esac
        else
            [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "jar   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${filename}${END} ${jarfile2check} version is unknown";
        fi
    done
}

##################################################
#     CHECKING WAR FILE                          #
##################################################

check_war () {
    warfilename=${1}
    let "NB_WAR_FILES++"
    filter=$(echo -e "$LOG4J_FILTER" | sed 's/*/.*/g')
    warfiles2check=$(${UNZIP} -Z1 ${warfilename} 2>/dev/null | grep -i "$filter")
    for warfile2check in $(echo ${warfiles2check}); do
        basename=$(basename "${warfile2check}")
        fileversion=$(echo -e "${basename}" | sed -n '/log4j-core-/s/.*log4j-core-\([[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+\)\.jar$/\1/p')
        if [[ ! -z "${fileversion}" ]]; then
            [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "war   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${warfilename}${END} contains ${basename} and could be affected"
            compareversions "${fileversion}" "2.16.0"
            case $? in
                0)
                    # op='=';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "war   | ${YELLOW}[WARNING]${END} | ${YELLOW}${warfilename} -> ${basename}${YELLOW} version is equal to 2.16.0";
                    ;;
                1)
                    # op='>';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "war   | ${YELLOW}[WARNING]${END} | ${YELLOW}${warfilename} -> ${basename}${YELLOW} version is greater than 2.16.0";
                    ;;
                2)
                    # op='<';
                    [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "war   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${warfilename} -> ${basename}${END} version ${fileversion} is less than 2.16.0";
                    ;;
            esac
        else
            if [[ "${basename}" == "log4j-core"* ]]; then
                [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "war   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${warfile2check}${END} log4j-core version is unknown";
            fi
        fi
        ${UNZIP} -xo "${warfilename}" "${warfile2check}" -d "${TEMP_PATH}/" &> /dev/null
        check_jar "${TEMP_PATH}/${warfile2check}" "deflated"
        rm "${TEMP_PATH}/${warfile2check}"
    done
    warfiles2check=""
}

##################################################
#     CHECKING EAR FILE                          #
##################################################

check_ear() {
    filename=${1}
    let "NB_EAR_FILES++"
    basename=$(basename "${filename}")
    earfiles2check=$(${UNZIP} -Z1 ${filename} 2>/dev/null | grep -i "JndiLookup.class")
    for earfile2check in $(echo -e ${earfiles2check}); do
        [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "ear   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} contains ${earfile2check} and could be affected";
    done
    earfiles2check=$(${UNZIP} -Z1 ${filename} 2>/dev/null | grep -i "/log4j-core/pom.properties")
    for earfile2check in $(echo -e ${earfiles2check}); do
        let "NB_POM_FILES++"
        fileversion=$(${UNZIP} -p ${filename} ${earfile2check} | grep -i 'version' | awk -F'=' '{print $2}' | sed 's/\s//g')
        if [[ ! -z "${fileversion}" ]]; then
            [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "ear   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} contains ${earfile2check} and could be affected"
            compareversions "${fileversion}" "2.16.0"
            case $? in
                0)
                    # op='=';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "ear   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} ${earfile2check} version is equal to 2.16.0";
                    ;;
                1)
                    # op='>';
                    [[ ${AVOID_WARNINGS} -ne 1 ]] && echo -e "ear   |${REVERSE}${YELLOW} [WARNING] ${END}| ${YELLOW}${filename}${END} ${earfile2check} version is greater than 2.16.0";
                    ;;
                2) 
                    # op='<';
                    [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "ear   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${filename}${END} ${earfile2check} version ${fileversion} is less than 2.16.0";
                    ;;
            esac
        else
            [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "ear   |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${filename}${END} ${earfile2check} version is unknown";
        fi
    done
}

##################################################
#     CHECKING CLASS FILE                        #
##################################################

check_class() {
    let "NB_CLASS_FILES++"
    filename=${1}
    basename=$(basename "${filename}")
    [[ ${AVOID_ERRORS} -ne 1 ]] && echo -e "class |${REVERSE}${RED} [ERROR]   ${END}| ${RED}${filename}${END} could be affected"
}

##################################################
#     MAIN JOB                                   #
##################################################

echo -e ""
echo -e "${SCRIPT_NAME} - Release ${SCRIPT_RELEASE} - 2021 Dec 17, written by Cedric Courlet"
echo -e ""
echo -e "Running on ${LOG4J_FILTER_NAME} mode : '${LOG4J_FILTER}'"
echo -e "Search path is '${SEARCH_PATH}'"
echo -e ""
for filename in $(find ${SEARCH_PATH} -type f \( -name "${LOG4J_FILTER}" -o -name "*.ear" -o -name "*.war" -o -name JndiLookup.class \) 2>/dev/null); do
    extension="${filename##*.}"
    case ${extension} in
        jar)
            check_jar   "${filename}" "";;
        war)
            check_war   "${filename}";;
        ear)
            check_ear   "${filename}";;
        class)
            check_class "${filename}";;
    esac
done
echo -e ""

##################################################
#     DISPLAY STATISTICS                         #
##################################################

if [[ $AVOID_STATS -ne 1 ]]; then
    echo -e "Was inspected :"
    echo -e ""
    printf "%7d war file(s)\n" "${NB_WAR_FILES}"
    printf "%7d jar file(s)\n" "${NB_JAR_FILES}"
    printf "%7d ear file(s)\n" "${NB_EAR_FILES}"
    printf "%7d deflated jar file(s)\n" "${NB_DEFLATED_JAR_FILES}"
    printf "%7d pom.properties file(s)\n" "${NB_POM_FILES}"
    echo -e ""
    STOP_TIME=$(date +%s.%N)
    EXECUTION_TIME=$(echo -e "${STOP_TIME} - ${START_TIME}" | bc -l)
    printf "Executed in %0.2f seconds\n" "${EXECUTION_TIME}"
    echo -e ""
fi

exit 0