#!/bin/bash
# Indicator of Compromise Scanner for CVE-2019-19781 (Citrix ADC)
# Copyright 2020 FireEye, Inc. and Citrix Systems, Inc.
#
# Usage:
#
#    bash ioc-scanner-CVE-2019-19781.sh [-v|--verbose] [root path, optional, default: /]
#
# Must be run as root when running against a live device.
# Writes status to STDERR.
# Writes results to STDOUT.
# Non-zero status upon failure.
#
# ----------------------------------------------------
# Note, please keep the above in-sync with `build.sh`.

# unset variables are errors
set -o nounset;
# any failed commands are errors
set -o errexit;

# temp directory for intermediate stages of report
#  (removed at end of script)
readonly tmpdir="/tmp/tmp_$(date +%s)";
mkdir "$tmpdir";

# initialize summary flags
found_evidence_compromise="No";
found_evidence_scanning="N/A - Script Executed in Default Mode";
found_evidence_failed_exploitation="N/A - Script Executed in Default Mode";

panic() {
    echo "[erro]: $@" >&2;
    exit 1;
}

info() {
    echo "[info]: $@" >&2;
}

verbose=false;
debug() {
    if "$verbose"; then
        echo "[debu]: $@" >&2;
    fi
}

repfile="$tmpdir/repbody.txt";
report() {
    echo "$@" >> "$repfile";
}

report_encoded_file_name() {
    echo "$1" | perl -lne 'print(pack("H*", $_))' >> "$repfile";
}

report_encoded_file_name_and_contents() {
    report_encoded_file_name "$1";
    echo "contents:" >> "$repfile";
    echo "$1" | perl -0ne 'print(pack("H*", $_))' | tr -d '\240' | xargs -0 cat >> "$repfile";
}

# format a key/value pair ($1, $2) with column width $3
fmt_key_val() {
    printf "%-$3s : %s\n" "$1" "$2"
}

# record a match - that a compromise was detected.
report_match() {
    found_evidence_compromise="Yes"
    report "";
    report "**********************************************************************";
    report "MATCH: $@";
    report "Found evidence of potential compromise.                               ";
    report "You should consider performing a forensic investigation of the system.";
    report "**********************************************************************";
}

# record a match - that successful scanning was detected.
report_scanning() {
    found_evidence_scanning="Yes"
    report "";
    report "**********************************************************************";
    report "SCANNING: $@";
    report "Found evidence of successful scanning.                                ";
    report "The device was probably vulnerable during the period of scanning.     ";
    report "There is a strong likelihood of compromise.                           ";
    report "**********************************************************************";
}

# record a match - that failed exploitation was detected.
report_failed_exploitation() {
    found_evidence_failed_exploitation="Yes"
    report "";
    report "**********************************************************************";
    report "FAILED EXPLOITATION: $@";
    report "Found evidence of failed exploitation.                                ";
    report "An actor attempted to exploit the device; however, it failed.         ";
    report "This may be due to any of many things, including:                     ";
    report "  - the device was patched correctly                                  ";
    report "  - the exploit had a bug                                             ";
    report "  - another actor (such as NOTROBIN) blocked it                       ";
    report "**********************************************************************";
}

# parse args.
# if we find -h or --help, print usage and quit (status: 0).
# if we find -v or --verbose, enable the verbose flag.
# otherwise, capture `$arg_root_directory` as positional argument, if present.
# there should really only be one (optional) positional argument,
#  but we don't check this right now.
arg_root_directory="";
while test $# -gt 0; do
    case "$1" in
        -h|--help)
            echo "Indicator of Compromise Scanner for CVE-2019-19781          "; 
            echo "Copyright 2020 FireEye, Inc. and Citrix Systems, Inc.       ";
            echo "                                                            ";
            echo "Usage:                                                      ";
            echo "                                                            ";
            echo "    bash $0 [-v|--verbose] [root path, optional, default: /]";
            echo "                                                            ";
            echo "Must be run as root when running against a live device.     ";
            echo "Writes status to STDERR.                                    ";
            echo "Writes results to STDOUT.                                   ";
            echo "Non-zero status upon failure.                               ";
            echo "                                                            ";
            echo "Please see additional documentation here:                   ";
            echo "                                                            ";
            echo "    https://github.com/fireeye/ioc-scanner-CVE-2019-19781   ";
            echo "                                                            ";
            exit 0;
            ;;
        -v|--verbose)
            verbose=true;
            shift;
            ;;
        *)
            # this will overwrite existing variable, so it doesn't handle 
            # the case when multiple args are provided (incorrectly).
            arg_root_directory="$1";
            shift;
            ;;
    esac
done

# root_directory is the directory from which to begin scanning.
# comes from first positional CLI argument, or default: "/".
# this supports:
#  - running against mounted forensic images.
#  - running against test cases.
if [ -n "$arg_root_directory" ]; then
    readonly root_directory="$arg_root_directory";
else
    readonly root_directory="/";
fi

# live_mode is true when the tool is running on and inspecting a live system.
# live_mode is false when the tool is inspecting a forensic image.
if [ "$root_directory" == "/" ]; then
    readonly live_mode=true;
else
    readonly live_mode=false;
fi

# return md5 hash for $1
echo_hash() {
    # OpenBSD/NetScaler doesn't have md5sum, but it has md5.
    # If it doesn't exist, assume `md5sum` is available (Linux).
    if [ -f "/sbin/md5" ]; then
        md5 -q "$1"
    else
        md5sum "$1" | cut -d" " -f1
    fi
}

info "**********************************************************************"
info "* Indicator of Compromise Scanner for CVE-2019-19781                 *"
info "* Copyright 2020 FireEye, Inc. and Citrix Systems, Inc.              *"
info "*                                                                    *"
info "* https://github.com/fireeye/ioc-scanner-CVE-2019-19781              *"
info "**********************************************************************"
info ""
readonly scripthash=$(echo_hash "$0")
report "this script: $0 $scripthash"
report "root_directory: $root_directory";
if [ ! -d "$root_directory" ]; then
    panic "$root_directory does not exist";
fi

# when running in live mode, must be super user
# this is so we can access all files, list ports, enumerate processes, etc.
if $live_mode; then
    if [[ $UID -ne 0 || $EUID -ne 0 ]]; then
        panic "$0 must be run as root in live mode";
    fi
fi

# collect some basic stats about the system
readonly startdate=$(date)
if $live_mode; then
    info ""
    info "Since you are running the tool in live mode,"
    info " the tool will now emit metadata, such as timestamp and hostname."
    info ""

    report "whoami: $(whoami)"
    report "uname: $(uname -a)"
    report "hostname: $(hostname)"
    report "date: $startdate"
fi

info ""
info "Next, the tool will emit first and last entries from select log files."
info "This will show the time ranges that the logs cover."
info "If the logs have rolled since scanning and exploitation began (~Jan 7),"
info " then the tool may not identify all compromise."
info "Likewise, if the logs have been modified by an attacker,"
info " then this tool may not recognize compromise."
info ""
readonly log_patterns=(
    "/var/log/bash.log"
    "/var/log/notice.log"
    "/var/log/httpaccess.log"
    "/var/log/httperror.log"
);
for logpattern in "${log_patterns[@]}"; do
    if ! compgen -G "$root_directory$logpattern*" >/dev/null; then
        continue
    fi

    # sorry for the indentation here...
    # because we're looping over ls results, which may include paths with spaces,
    # then we need be careful to split on newlines, not spaces.
    IFSBAK="$IFS"
IFS='
'

    # use `ls | sort` to find all available log files.
    # order from old to new (-r).
    for logfile in $(ls "$root_directory$logpattern"* | sort -r); do
        report "log file: $logfile";
        report "  first entry: $(zgrep ":" "$logfile" | head -n 1)";
        report "  last entry: $(zgrep ":" "$logfile" | tail -n 1)";
        report "  metadata: $(ls -lah $logfile)";
        report "  md5: $(echo_hash $logfile)":
    done
    IFS="$IFSBAK"
done

# current_directory is the path to the directory containing this script.
# ref: https://stackoverflow.com/a/4774063/87207
readonly current_directory="$( cd "$(dirname "$0")" ; pwd -P )"

readonly version_file="$current_directory/version.sh";
if [ ! -f "$version_file" ]; then
    panic "missing script: $version_file";
fi
source "$version_file";

# scanners is the list of scanners that we'll load as dependencies.
# register new scanners here. this loads the code and makes it accessible
# invoke the exported functions manually below.
declare -a scanners;
# FreeBSD/NetScaler bash doesn't support array declaration shortcut
# so we create the array by hand... I'm sorry.
scanners[0]="scanners/fs-paths.sh";
scanners[1]="scanners/ports.sh";
scanners[2]="scanners/processes.sh";
scanners[3]="scanners/crontab.sh";
scanners[4]="scanners/access-logs.sh";
scanners[5]="scanners/error-logs.sh";
scanners[6]="scanners/netscaler-content.sh";
scanners[7]="scanners/shell-history.sh";
scanners[8]="scanners/cron-history.sh";
scanners[9]="scanners/successful-scanning.sh";
scanners[10]="scanners/failed-exploitation.sh";

info ""
info "Next, the tool will load supporting scanning modules."
info "It will report the MD5 hashes of these files"
info " so that we can recover exact versions if you have bugs to report."
info ""
for scanner_name in "${scanners[@]}"; do
    scanner_path="$current_directory/$scanner_name";
    if [ ! -f "$scanner_path" ]; then
        panic "missing script: $scanner_name";
    fi
    report "loading: $scanner_name ($(echo_hash "$scanner_path"))";

    source "$scanner_path";
done

# manually invoke scanner routines, loaded from above.
# do shell history first, cause its most likely to be overwritten.

info "";
info "The tool will now scan shell history logs for post-exploitation.";
info "If it identifies evidence of compromise, it will emit like this:";
info "";
info "example:  ****************************************************";
info "example:  MATCH: description-of-evidence-here                 ";
info "example:  Found evidence of potential compromise.             ";
info "example:  You should consider performing a                    ";
info "example:    forensic investigation of the system.             ";
info "example:  ****************************************************";
info "";
info "here we go...";
info "";
scan_shell_history;

info "";
info "The tool will now look for unexpected cron history entries.";
info "";
scan_cron_history;

info "";
info "The tool will now look for known paths to malware files.";
info "";
scan_fs_known_paths;

info "";
info "The tool will now scan for unexpected listening ports.";
info "";
scan_ports;

info "";
info "The tool will now scan for unexpected processes.";
info "";
scan_processes;

info "";
info "The tool will now scan for unexpected crontab entries.";
info "";
scan_crontab;

info "";
info "The tool will now search web server logs for exploitation.";
info "";
scan_access_logs;

info "";
info "The tool will now search error logs for post-exploitation.";
info "";
scan_error_logs;

info "";
info "The tool will now scan NetScaler directories for unexpected content.";
info "";
scan_netscaler_content;

if "$verbose"; then
    info "";
    info "The tool will now search web server logs for successful scanning activity.";
    info "";
    found_evidence_scanning="No";
    scan_successful_scanning;
fi

if "$verbose"; then
    info "";
    info "The tool will now search web server logs for failed exploitation attempts.";
    info "";
    found_evidence_failed_exploitation="No";
    scan_failed_exploitation;
fi

# save the report text we've gathered to here
report "";
report "end of report.";

# the remaining information is placed at the top of the report
repfile="$tmpdir/rephead.txt";

# print a summary of the findings
report "";
report "**********************************************************************";
report "SUMMARY:"
colwidth="37";
report "$(fmt_key_val "Date" "$startdate" "$colwidth")";
# Hostname
if [ -f "$root_directory/nsconfig/ns.conf" ]; then
    readonly hostname=$(grep 'set ns hostName' "$root_directory/nsconfig/ns.conf" | awk -F ' ' '{print $4}');
    report "$(fmt_key_val "Hostname" "$hostname" "$colwidth")";
fi
# IP address
if [ -f "$root_directory/nsconfig/ns.conf" ]; then
    readonly ipstr=$(grep 'ns config \-IPAddress' "$root_directory/nsconfig/ns.conf" | awk -F ' ' '{print $5}');
    report "$(fmt_key_val "IP" "$ipstr" "$colwidth")";
fi
# NS version
if [ -f "$root_directory/flash/boot/loader.conf" ]; then
    # grab the version string from kernel image filename
    ns_verstr="$(grep "kernel=" "$root_directory/flash/boot/loader.conf" | sed 's/["/]//g' | awk -F '=' '{print $2}')";
    # strip leading "ns-"
    if [[ $ns_verstr == "ns-"* ]]; then
        ns_verstr=${ns_verstr:3};
    fi
    report "$(fmt_key_val "NS version" "$ns_verstr" "$colwidth")";
fi
report "$(fmt_key_val "Scanner version" "$git_tag" "$colwidth")";
if "$verbose"; then
    runmode="Verbose";
else
    runmode="Default";
fi
report "$(fmt_key_val "Scanner run mode" "$runmode" "$colwidth")";
report "$(fmt_key_val "Evidence of compromise found" "$found_evidence_compromise" "$colwidth")";
report "$(fmt_key_val "Evidence of scanning found" "$found_evidence_scanning" "$colwidth")";
report "$(fmt_key_val "Evidence of failed exploitation found" "$found_evidence_failed_exploitation" "$colwidth")";
report "**********************************************************************";
report "";

cat "$tmpdir/rephead.txt";
cat "$tmpdir/repbody.txt";

info "done.";
rm -rf "$tmpdir";
exit 0;
