├── requirements.txt ├── filename-iocs.txt ├── demo ├── mal-content.asp └── evil.jsp ├── screens ├── stat1.png ├── fenrir1.png ├── fenrir2.png ├── fenrir3.png ├── fenrir4.png └── fenrir5.png ├── .gitignore ├── .travis.yml ├── c2-iocs.txt ├── string-iocs.txt ├── LICENSE ├── ansible └── fenrir-ansible-playbook.yml ├── README.md ├── hash-iocs.txt └── fenrir.sh /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /filename-iocs.txt: -------------------------------------------------------------------------------- 1 | log4j-core 2 | # END - DO NOT REMOVE -------------------------------------------------------------------------------- /demo/mal-content.asp: -------------------------------------------------------------------------------- 1 | # THIS 2 | <%eval request("ice")%> 3 | # END 4 | -------------------------------------------------------------------------------- /demo/evil.jsp: -------------------------------------------------------------------------------- 1 | # This is not en evil file - just for demonstration purposes 2 | -------------------------------------------------------------------------------- /screens/stat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/Fenrir/master/screens/stat1.png -------------------------------------------------------------------------------- /screens/fenrir1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/Fenrir/master/screens/fenrir1.png -------------------------------------------------------------------------------- /screens/fenrir2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/Fenrir/master/screens/fenrir2.png -------------------------------------------------------------------------------- /screens/fenrir3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/Fenrir/master/screens/fenrir3.png -------------------------------------------------------------------------------- /screens/fenrir4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/Fenrir/master/screens/fenrir4.png -------------------------------------------------------------------------------- /screens/fenrir5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/Fenrir/master/screens/fenrir5.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/* 2 | /fenrir_client8/* 3 | fenrir_backup.sh 4 | 5 | fenrir.log 6 | 7 | *.log 8 | 9 | output/audit.txt 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | script: 4 | - bash ./fenrir.sh ./demo 5 | 6 | notifications: 7 | email: 8 | recipients: 9 | - venom14@gmail.com 10 | on_success: never 11 | on_failure: always 12 | -------------------------------------------------------------------------------- /c2-iocs.txt: -------------------------------------------------------------------------------- 1 | bingsearchlib.com 2 | 34.198.182.201 3 | canarytokens.com 4 | 52.18.63.80 5 | log4j.binaryedge.io 6 | 134.209.163.248 7 | leakix.net 8 | 167.71.13.196 9 | dnslog.cn 10 | 47.244.138.18 11 | kryptoslogic-cve-2021-44228.com 12 | 167.99.86.185 13 | interact.sh 14 | 46.101.25.250 15 | # END -------------------------------------------------------------------------------- /string-iocs.txt: -------------------------------------------------------------------------------- 1 | ${jndi:ldap:/ 2 | ${jndi:rmi:/ 3 | ${jndi:ldaps:/ 4 | ${jndi:dns:/ 5 | /$%7bjndi: 6 | %24%7bjndi: 7 | $%7Bjndi: 8 | %2524%257Bjndi 9 | %2F%252524%25257Bjndi%3A 10 | ${jndi:${lower: 11 | ${::-j}${ 12 | ${jndi:nis 13 | ${jndi:nds 14 | ${jndi:corba 15 | ${jndi:iiop 16 | ${${env:BARFOO:-j} 17 | ${::-l}${::-d}${::-a}${::-p} 18 | ${base64:JHtqbmRp 19 | /Basic/Command/Base64/ 20 | * * * wget 21 | * * * curl 22 | bingsearchlib.com 23 | 34.198.182.201 24 | canarytokens.com 25 | 52.18.63.80 26 | log4j.binaryedge.io 27 | 134.209.163.248 28 | leakix.net 29 | 167.71.13.196 30 | dnslog.cn 31 | 47.244.138.18 32 | kryptoslogic-cve-2021-44228.com 33 | 167.99.86.185 34 | interact.sh 35 | 46.101.25.250 36 | # END - DO NOT REMOVE - contents passed to grep - double escape square brackets -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Florian Roth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ansible/fenrir-ansible-playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | 4 | tasks: 5 | - name: Fenrir 6 | block: 7 | - name: Create folder for temporary RAM drive 8 | ansible.builtin.file: 9 | path: /mnt/temp_ram 10 | state: directory 11 | owner: root 12 | group: root 13 | mode: "u=rwx,g=,o=" 14 | 15 | - name: Create Fenrir RAM drive on target 16 | ansible.posix.mount: 17 | path: /mnt/temp_ram/ 18 | src: ramfs 19 | fstype: ramfs 20 | opts: size=30M 21 | state: mounted 22 | 23 | # copy is slow, but synchronize doesn't work with `become: yes` 24 | # I suppose using `become: no` is an option, if /mnt/temp_ram was owned 25 | # by {{ ansible_user }} 26 | - name: Copy Fenrir to RAM drive 27 | ansible.builtin.copy: 28 | src: "{{ playbook_dir }}/.." 29 | dest: /mnt/temp_ram/ 30 | owner: root 31 | group: root 32 | 33 | - name: Make Fenrir Executeable 34 | ansible.builtin.file: 35 | path: /mnt/temp_ram/fenrir.sh 36 | state: touch 37 | mode: "u=rx,g=rx,o=rx" 38 | 39 | - name: Execute Fenrir 40 | shell: 41 | cmd: /mnt/temp_ram/fenrir.sh / > /mnt/temp_ram/fenrir.log 42 | 43 | - name: Fetch Log file 44 | ansible.builtin.fetch: 45 | src: /mnt/temp_ram/fenrir.log 46 | dest: ./ 47 | 48 | - name: Unmount temporary RAM drive 49 | ansible.posix.mount: 50 | path: /mnt/temp_ram/ 51 | state: unmounted 52 | 53 | - name: Delete folder for temporary RAM drive 54 | ansible.builtin.file: 55 | path: /mnt/temp_ram/ 56 | state: absent 57 | become: yes 58 | when: ansible_system == "Linux" 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Active Development](https://img.shields.io/badge/Maintenance%20Level-Actively%20Developed-brightgreen.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d) 2 | 3 | # Fenrir 4 | Simple Bash IOC Scanner 5 | 6 | Fenrir is a simple IOC scanner bash script. It allows scanning Linux/Unix/OSX systems for the following Indicators of Compromise (IOCs): 7 | 8 | - Hashes 9 | 10 | MD5, SHA1 and SHA256 (using md5sum, sha1sum, sha -a 256) 11 | 12 | - File Names 13 | 14 | string - checked for substring of the full path, e.g. "temp/p.exe" in "/var/temp/p.exe" 15 | 16 | - Strings 17 | 18 | grep in files 19 | 20 | - C2 Server 21 | 22 | checking for C2 server strings in 'lsof -i' and 'lsof -i -n' output 23 | 24 | - Hot Time Frame 25 | 26 | using stat in different modes - define min and max epoch time stamp and get all files that have been created in between 27 | 28 | Basic characteristics: 29 | - Bash Script 30 | - No installation or agent needed 31 | - Uses common tools to extract attributes (e.g. md5sum, grep, stat in different modes) 32 | - Intended to run on any Linux / Unix / OS X with Bash 33 | - Low footprint - Ansible playbook with RAM drive solution 34 | - Smart exclusions (file size, extension, certain directories) speeds up the scan process 35 | 36 | # Why Fenrir? 37 | FENRIR is the 3rd tool after THOR and LOKI. [THOR](http://www.bsk-consulting.de/apt-scanner-thor/) is our full featured APT Scanner with many modules and export types for corporate customers. [LOKI](https://github.com/Neo23x0/Loki) is a free and open IOC scanner that uses [YARA](https://plusvic.github.io/yara/) as signature format. 38 | 39 | The problem with both predecessors is that both have certain requirements on the Linux platform. We build THOR for a certain Linux version in order to match the correct libc that is required by the YARA module. LOKI requires Python and YARA installed on Linux to run. 40 | 41 | We faced the problem of checking more than 100 different Linux systems for certain Indicators of Compromise (IOCs) without installing an agent or software packages. We already had an [Ansible](http://www.ansible.com/) playbook for the distribution of THOR on a defined set of Linux remote systems. This playbook creates a RAM drive on the remote system, copies the local program binary to the remote system, runs it and retrieves the logs afterwards. This ensures that the program's footprint on the remote system is minimal. I adapted the Ansible playbook for Fenrir. (it is still untested) 42 | 43 | Fenrir is still 'testing'. Please report back errors (and solutions) via the "Issues" section here on github. 44 | 45 | If you find a better / more solid / less error-prone solution to the evaluations in the script, please report them back. I am not a full-time bash programmer so I'd expect some room for improvement. 46 | 47 | # Usage 48 | 49 | ``` 50 | Usage: ./fenrir.sh DIRECTORY 51 | 52 | DIRECTORY - Start point of the recursive scan 53 | ``` 54 | 55 | All settings can be configured in the header of the script. 56 | 57 | ![Settings](./screens/fenrir2.png) 58 | 59 | # Step by Step 60 | 61 | What Fenrir does is: 62 | - Reads the IOC files 63 | - Takes a parameter as starting directory for the recursive walk 64 | - Checks C2 servers in lsof output 65 | - Checks for directory exclusions (configurable in the script header) 66 | - Checks for certain file extensions to check (configurable in the script header) 67 | - Checks the file name (full path) for matches in IOC files 68 | - Checks for file size exclusions (configurable in the script header) 69 | - Checks for certain strings in the file (via grep) 70 | - Checks for certain hash values 71 | - Checks for change/creation time stamp 72 | 73 | # Screenshots 74 | 75 | Scan Run showing the different match types on a demo directory. 76 | 77 | ![Scan Run](./screens/fenrir1.png) 78 | 79 | Detect C2 connections 80 | 81 | ![Ansible Playbook](./screens/fenrir4.png) 82 | 83 | Detect strings in GZIP packed log files 84 | 85 | ![Strings in GZP Logs](./screens/fenrir5.png) 86 | 87 | Configuration 88 | 89 | ![Configuration](./screens/fenrir2.png) 90 | 91 | Ansible Playbook 92 | 93 | ![Ansible Playbook](./screens/fenrir3.png) 94 | 95 | Stat issue (regarding the CREATED file stamp on Linux file systems) 96 | 97 | ![Stat issue](./screens/stat1.png) 98 | 99 | # Known Issues 100 | 101 | # Contact 102 | 103 | via Twitter @Cyb3rOps 104 | -------------------------------------------------------------------------------- /hash-iocs.txt: -------------------------------------------------------------------------------- 1 | 2addabe2ceca2145955c02a6182f7fc5;Vulnerable Log4j version ./apache-log4j-2.0-alpha2-bin/log4j-core-2.0-alpha2.jar 2 | 5b1d4e4eea828a724c8b0237326829b3;Vulnerable Log4j version ./apache-log4j-2.0-beta1-bin/log4j-core-2.0-beta1.jar 3 | ce9e9a27c2a5caa47754999eb9c549b8;Vulnerable Log4j version ./apache-log4j-2.0-beta2-bin/log4j-core-2.0-beta2.jar 4 | 1538d8c342e3e2a31cd16e01e3865276;Vulnerable Log4j version ./apache-log4j-2.0-beta3-bin/log4j-core-2.0-beta3.jar 5 | 9cb138881a317a7f49c74c3e462f35f4;Vulnerable Log4j version ./apache-log4j-2.0-beta4-bin/log4j-core-2.0-beta4.jar 6 | 578ffc5bcccb29f6be2d23176c0425e0;Vulnerable Log4j version ./apache-log4j-2.0-beta5-bin/log4j-core-2.0-beta5.jar 7 | 5b73a0ad257c57e7441778edee4620a7;Vulnerable Log4j version ./apache-log4j-2.0-beta6-bin/log4j-core-2.0-beta6.jar 8 | e32489039dab38637557882cca0653d7;Vulnerable Log4j version ./apache-log4j-2.0-beta7-bin/log4j-core-2.0-beta7.jar 9 | db025370dbe801ac623382edb2336ede;Vulnerable Log4j version ./apache-log4j-2.0-beta8-bin/log4j-core-2.0-beta8.jar 10 | 152ecb3ce094ac5bc9ea39d6122e2814;Vulnerable Log4j version ./apache-log4j-2.0-beta9-bin/log4j-core-2.0-beta9.jar 11 | cd70a1888ecdd311c1990e784867ce1e;Vulnerable Log4j version ./apache-log4j-2.0-bin/log4j-core-2.0.jar 12 | 088df113ad249ab72bf19b7f00b863d5;Vulnerable Log4j version ./apache-log4j-2.0-rc1-bin/log4j-core-2.0-rc1.jar 13 | de8d01cc15fd0c74fea8bbb668e289f5;Vulnerable Log4j version ./apache-log4j-2.0-rc2-bin/log4j-core-2.0-rc2.jar 14 | fbfa5f33ab4b29a6fdd52473ee7b834d;Vulnerable Log4j version ./apache-log4j-2.0.1-bin/log4j-core-2.0.1.jar 15 | 8c0cf3eb047154a4f8e16daf5a209319;Vulnerable Log4j version ./apache-log4j-2.0.2-bin/log4j-core-2.0.2.jar 16 | 8d331544b2e7b20ad166debca2550d73;Vulnerable Log4j version ./apache-log4j-2.1-bin/log4j-core-2.1.jar 17 | 5e4bca5ed20b94ab19bb65836da93f96;Vulnerable Log4j version ./apache-log4j-2.2-bin/log4j-core-2.2.jar 18 | 110ab3e3e4f3780921e8ee5dde3373ad;Vulnerable Log4j version ./apache-log4j-2.3-bin/log4j-core-2.3.jar 19 | 0079c907230659968f0fc0e41a6abcf9;Vulnerable Log4j version ./apache-log4j-2.4-bin/log4j-core-2.4.jar 20 | f0c43adaca2afc71c6cc80f851b38818;Vulnerable Log4j version ./apache-log4j-2.4.1-bin/log4j-core-2.4.1.jar 21 | dd0e3e0b404083ec69618aabb50b8ac0;Vulnerable Log4j version ./apache-log4j-2.5-bin/log4j-core-2.5.jar 22 | 5523f144faef2bfca08a3ca8b2becd6a;Vulnerable Log4j version ./apache-log4j-2.6-bin/log4j-core-2.6.jar 23 | 48f7f3cda53030a87e8c387d8d1e4265;Vulnerable Log4j version ./apache-log4j-2.6.1-bin/log4j-core-2.6.1.jar 24 | 472c8e1fbaa0e61520e025c255b5d168;Vulnerable Log4j version ./apache-log4j-2.6.2-bin/log4j-core-2.6.2.jar 25 | 2b63e0e5063fdaccf669a1e26384f3fd;Vulnerable Log4j version ./apache-log4j-2.7-bin/log4j-core-2.7.jar 26 | c6d233bc8e9cfe5da690059d27d9f88f;Vulnerable Log4j version ./apache-log4j-2.8-bin/log4j-core-2.8.jar 27 | 547bb3ed2deb856d0e3bbd77c27b9625;Vulnerable Log4j version ./apache-log4j-2.8.1-bin/log4j-core-2.8.1.jar 28 | 4a5177a172764bda6f4472b94ba17ccb;Vulnerable Log4j version ./apache-log4j-2.8.2-bin/log4j-core-2.8.2.jar 29 | a27e67868b69b7223576d6e8511659dd;Vulnerable Log4j version ./apache-log4j-2.9.0-bin/log4j-core-2.9.0.jar 30 | a3a6bc23ffc5615efcb637e9fd8be7ec;Vulnerable Log4j version ./apache-log4j-2.9.1-bin/log4j-core-2.9.1.jar 31 | 0042e7de635dc1c6c0c5a1ebd2c1c416;Vulnerable Log4j version ./apache-log4j-2.10.0-bin/log4j-core-2.10.0.jar 32 | 90c12763ac2a49966dbb9a6d98be361d;Vulnerable Log4j version ./apache-log4j-2.11.0-bin/log4j-core-2.11.0.jar 33 | 71d3394226547d81d1bf6373a5b0e53a;Vulnerable Log4j version ./apache-log4j-2.11.1-bin/log4j-core-2.11.1.jar 34 | 8da9b75725fb3357cb9872adf7711f9f;Vulnerable Log4j version ./apache-log4j-2.11.2-bin/log4j-core-2.11.2.jar 35 | 7943c49b634b404144557181f550a59c;Vulnerable Log4j version ./apache-log4j-2.12.0-bin/log4j-core-2.12.0.jar 36 | df949e7d73479ab717e5770814de0ae9;Vulnerable Log4j version ./apache-log4j-2.12.1-bin/log4j-core-2.12.1.jar 37 | 2803991d51c98421be35d2db4ed3c2ac;Vulnerable Log4j version ./apache-log4j-2.13.0-bin/log4j-core-2.13.0.jar 38 | 5ff1dab00c278ab8c7d46aadc60b4074;Vulnerable Log4j version ./apache-log4j-2.13.1-bin/log4j-core-2.13.1.jar 39 | b8e0d2779abbf38586b869f8b8e2eb46;Vulnerable Log4j version ./apache-log4j-2.13.2-bin/log4j-core-2.13.2.jar 40 | 46e660d79456e6f751c22b94976f6ad5;Vulnerable Log4j version ./apache-log4j-2.13.3-bin/log4j-core-2.13.3.jar 41 | 62ad26fbfb783183663ba5bfdbfb5ace;Vulnerable Log4j version ./apache-log4j-2.14.0-bin/log4j-core-2.14.0.jar 42 | 3570d00d9ceb3ca645d6927f15c03a62;Vulnerable Log4j version ./apache-log4j-2.14.1-bin/log4j-core-2.14.1.jar 43 | f5e2d2a9543ee3c4339b6f90b6cb01fc;Vulnerable Log4j version ./log4j-2.0-alpha1/log4j-core-2.0-alpha1.jar -------------------------------------------------------------------------------- /fenrir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # FENRIR 4 | # Simple Bash IOC Checker 5 | # Florian Roth 6 | 7 | VERSION="0.9.0-log4shell" 8 | 9 | # Settings ------------------------------------------------------------ 10 | SYSTEM_NAME=$(uname -n | tr -d "\n") 11 | TS_CONDENSED=$(date +%Y%m%d) 12 | 13 | # IOCs 14 | HASH_IOC_FILE="./hash-iocs.txt" 15 | STRING_IOCS="./string-iocs.txt" 16 | FILENAME_IOCS="./filename-iocs.txt" 17 | C2_IOCS="./c2-iocs.txt" 18 | 19 | # Log 20 | LOGFILE="./FENRIR_${SYSTEM_NAME}_${TS_CONDENSED}.log" 21 | LOG_TO_FILE=1 22 | LOG_TO_SYSLOG=0 # Log to syslog is set to 'off' by default > false positives 23 | LOG_TO_CMDLINE=1 24 | SYSLOG_FACILITY=local4 25 | 26 | # Enable / Disable Checks 27 | ENABLE_C2_CHECK=1 28 | ENABLE_TYPE_CHECK=1 29 | ENABLE_HASH_CHECK=1 30 | 31 | # Exclusions 32 | MAX_FILE_SIZE=8000 # max file size to check in kilobyte, default 2 MB 33 | CHECK_ONLY_RELEVANT_EXTENSIONS=1 # ELF binaries get always checked 34 | declare -a RELEVANT_EXTENSIONS=('jsp' 'jspx' 'txt' 'tmp' 'pl' 'war' 'sh' 'log' 'jar'); # use lower-case 35 | # files in these directories will be checked with string grep 36 | # regradless of their size and extension 37 | declare -a EXCLUDED_DIRS=('/proc/' '/initctl/' '/dev/' '/media/'); 38 | # Force Checks 39 | declare -a FORCED_STRING_MATCH_DIRS=('/var/log/' '/etc/hosts' '/etc/crontab'); 40 | # Exclude all output lines that contain these strings 41 | declare -a EXCLUDE_STRINGS=('iocs.txt' 'fenrir'); 42 | 43 | # global var for passing pseudo hash 44 | declare -i pseudo_h 45 | 46 | # Hot Time Frame Check 47 | MIN_HOT_EPOCH=1444163570 # minimum Unix epoch for hot time frame e.g. 1444160522 48 | MAX_HOT_EPOCH=1444163590 # maximum Unix epoch for hot time frame e.g. 1444160619 49 | CHECK_FOR_HOT_TIMEFRAME=0 50 | 51 | # Debug 52 | DEBUG=0 53 | 54 | # Code ---------------------------------------------------------------- 55 | 56 | # Global vars 57 | declare -a hash_iocs 58 | declare -a pseudo_hash_iocs 59 | declare -a hash_ioc_description 60 | declare -a string_iocs 61 | declare -a check_strings 62 | declare -a filename_iocs 63 | declare -a c2_iocs 64 | # declare grep_strings 65 | 66 | function scan_dirs 67 | { 68 | # Scan Dir 69 | scandir=$1 70 | 71 | # Debug Output -------------------------------------------- 72 | log debug "Scanning $scandir ..." 73 | 74 | # Cleanup trailing "/" in the most compatible way 75 | if [ "${scandir: -1}" == "/" ] && [ "${#scandir}" -gt 1 ]; then 76 | scandir="${scandir:0:${#scandir}-1}" 77 | fi 78 | 79 | # Loop through files 80 | find "$scandir" -type f 2> /dev/null | while read -r file_path 81 | do 82 | if [ -f "${file_path}" ]; then 83 | 84 | # Debug Output -------------------------------------------- 85 | log debug "Scanning $file_path ..." 86 | 87 | # Marker -------------------------------------------------- 88 | DO_STRING_CHECK=1 89 | DO_HASH_CHECK=1 90 | DO_DATE_CHECK=1 91 | DO_FILENAME_CHECK=1 92 | 93 | # Evaluations --------------------------------------------- 94 | file_name=$(basename "$file_path") 95 | extension="${file_name##*.}" 96 | 97 | # Checks to disable modules ------------------------------- 98 | 99 | # Excluded Directories 100 | result=$(check_dir "$file_path") 101 | if [ "${result}" -eq 1 ]; then 102 | log debug "Skipping $file_path due to exclusion ..." 103 | DO_STRING_CHECK=0 104 | DO_HASH_CHECK=0 105 | DO_DATE_CHECK=0 106 | DO_FILENAME_CHECK=0 107 | fi 108 | 109 | # Check if relevant type 110 | if [ $ENABLE_TYPE_CHECK -eq 1 ]; then 111 | relevant_type=$(file "$file_path" | grep -F "ELF") 112 | fi 113 | 114 | # Exclude Extensions 115 | if [ $CHECK_ONLY_RELEVANT_EXTENSIONS -eq 1 ] && [ "$relevant_type" == "" ]; then 116 | result=$(check_extension "$extension") 117 | if [ "${result}" -ne 1 ]; then 118 | log debug "Deactivating some checks on $file_path due to irrelevant extension ..." 119 | DO_STRING_CHECK=0 120 | DO_HASH_CHECK=0 121 | fi 122 | fi 123 | 124 | # Check Size 125 | filesize=$(du -k "$file_path" | cut -f1) 126 | if [ "${filesize}" -gt $MAX_FILE_SIZE ]; then 127 | log debug "Deactivating some checks on $file_path due to size" 128 | DO_STRING_CHECK=0 129 | DO_HASH_CHECK=0 130 | fi 131 | 132 | # Checks to include modules ------------------------------- 133 | 134 | # Forced string check directory 135 | for fsm_dir in "${FORCED_STRING_MATCH_DIRS[@]}"; 136 | do 137 | # echo "Checking if $ex_dir is in $dir" 138 | # The following check matches when $fsm_dir is ANYWHERE in the 139 | # $file_path, not only at the beginning. As we're just doing 140 | # more checks in that case, we don't care 141 | if [ "${file_path/$fsm_dir}" != "$file_path" ]; then 142 | DO_STRING_CHECK=1 143 | log debug "Activating string check on $file_name" 144 | fi 145 | done 146 | 147 | # Checks -------------------------------------------------- 148 | 149 | # File Name Check 150 | if [ $DO_FILENAME_CHECK -eq 1 ]; then 151 | check_filename "$file_path" 152 | fi 153 | 154 | # String Check 155 | if [ $DO_STRING_CHECK -eq 1 ]; then 156 | check_string "$file_path" "$extension" 157 | fi 158 | 159 | # Hash Check 160 | if [ $DO_HASH_CHECK -eq 1 ] && [ $ENABLE_HASH_CHECK -eq 1 ]; then 161 | md5=$(md5sum "$file_path" 2> /dev/null | cut -f1 -d' ') 162 | #md5tmp=$(md5sum "$file_path" 2> /dev/null) 163 | #md5=${md5tmp%% *} 164 | sha1=$(sha1sum "$file_path" 2> /dev/null | cut -f1 -d' ') 165 | sha256=$(sha256sum "$file_path" 2> /dev/null | cut -f1 -d' ') 166 | log debug "Checking hashes of file $file_path : $md5" 167 | check_pseudo_hashes "$md5" "$sha1" "$sha256" "$file_path" 168 | fi 169 | 170 | # Date Check 171 | if [ $CHECK_FOR_HOT_TIMEFRAME -eq 1 ] && [ $DO_DATE_CHECK -eq 1 ]; then 172 | check_date "$file_path" 173 | fi 174 | fi 175 | 176 | 177 | done 178 | IFS=$oldIFS 179 | } 180 | 181 | # Check Functions ----------------------------------------------------- 182 | 183 | function check_pseudo_hashes 184 | { 185 | local md5=$1 186 | local sha1=$2 187 | local sha256=$3 188 | local filepath=$4 189 | 190 | hashes=(${md5} ${sha1} ${sha256}) 191 | for hash in "${hashes[@]}"; 192 | do 193 | pseudo_hash "$hash" 194 | if [ -n "${pseudo_hash_iocs[$pseudo_h]}" ]; then 195 | # TODO change to "log debug" 196 | log debug "[+] Pseudo hash match on $file_path pseudo hash: $pseudo_h real hash will be checked now: $hash" 197 | 198 | check_hashes "$hash" "$file_path" 199 | fi 200 | done 201 | } 202 | 203 | function check_hashes 204 | { 205 | local index=0 206 | local check_hash=$1 207 | #local md5=$1 208 | #local sha1=$2 209 | #local sha256=$3 210 | local filepath=$2 211 | 212 | for hash in "${hash_iocs[@]}"; 213 | do 214 | #echo "Comparing $hash with $md5" 215 | if [ "$check_hash" == "$hash" ]; then 216 | description=${hash_ioc_description[$index]} 217 | log warning "[!] Hash match found FILE: $filepath HASH: $hash DESCRIPTION: $description" 218 | fi 219 | #if [ "$sha1" == "$hash" ]; then 220 | #description=${hash_ioc_description[$index]} 221 | #log warning "[!] Hash match found FILE: $filepath HASH: $hash DESCRIPTION: $description" 222 | #fi 223 | #if [ "$sha256" == "$hash" ]; then 224 | #description=${hash_ioc_description[$index]} 225 | #log warning "[!] Hash match found FILE: $filepath HASH: $hash DESCRIPTION: $description" 226 | #fi 227 | index=$((index+1)) 228 | done 229 | } 230 | 231 | function check_string 232 | { 233 | local filepath=$1 234 | local extension=$2 235 | local varlog="/var/log" 236 | 237 | # echo "Greping $string in $1" 238 | match=$(grep -F "$check_strings" "$filepath" 2> /dev/null) 239 | # Cut big matches (fixes buges in super long web shell lines without line breaks) 240 | match=$(echo "$match" |cut -c1-100) 241 | 242 | # if [[ ! -z "${match// }" ]] ; then 243 | if [ "$match" != "" ]; then 244 | string=$(determine_stringmatch "$match") 245 | log warning "[!] String match found FILE: $filepath STRING: $string TYPE: plain MATCH: $match" 246 | fi 247 | # Try zgrep on gz files below /var/log 248 | if [ "$extension" == "gz" ] || [ "$extension" == "Z" ] || [ "$extension" == "zip" ]; then 249 | if [ "${filepath/$varlog}" != "$filepath" ]; then 250 | match=$(zgrep -F "$check_strings" "$filepath" 2> /dev/null) 251 | if [ "$match" != "" ]; then 252 | string=$(determine_stringmatch "$match") 253 | match_extract=$(echo "$match" |cut -c1-100) 254 | size_of_match=${#match} 255 | if [ "$size_of_match" -gt 100 ]; then 256 | match_extract="$match_extract ... (truncated)" 257 | fi 258 | log warning "[!] String match found FILE: $filepath STRING: $string TYPE: gzip MATCH: $match_extract" 259 | fi 260 | fi 261 | fi 262 | # Try bzgrep on bz files below /var/log 263 | if [ "$extension" == "bz" ] || [ "$extension" == "bz2" ]; then 264 | if [ "${filepath/$varlog}" != "$filepath" ]; then 265 | match=$(bzgrep -F "$check_strings" "$filepath" 2> /dev/null) 266 | if [ "$match" != "" ]; then 267 | string=$(determine_stringmatch "$match") 268 | match_extract=$(echo "$match" |cut -c1-100) 269 | size_of_match=${#match} 270 | if [ "$size_of_match" -gt 100 ]; then 271 | match_extract="$match_extract ... (truncated)" 272 | fi 273 | log warning "[!] String match found FILE: $filepath STRING: $string TYPE: bzip2 MATCH: $match_extract" 274 | fi 275 | fi 276 | fi 277 | } 278 | 279 | function determine_stringmatch 280 | { 281 | for string in "${string_iocs[@]}"; 282 | do 283 | if [ "${1/$string}" != "$1" ]; then 284 | echo "$string" 285 | return 0 286 | fi 287 | done 288 | for string in "${c2_iocs[@]}"; 289 | do 290 | if [ "${1/$string}" != "$1" ]; then 291 | echo "$string" 292 | return 0 293 | fi 294 | done 295 | echo "(binary match)" 296 | } 297 | 298 | function check_filename 299 | { 300 | for filename in "${filename_iocs[@]}"; 301 | do 302 | if [ "${1/$filename}" != "$1" ]; then 303 | log warning "[!] Filename match found FILE: $1 INDICATOR: $filename" 304 | fi 305 | done 306 | } 307 | 308 | function check_extension 309 | { 310 | extension=$(echo "$1" | tr '[:upper:]' '[:lower:]') 311 | result=0 312 | for ext in "${RELEVANT_EXTENSIONS[@]}"; 313 | do 314 | # echo "Comparing $extension with $ext" 315 | if [ "$extension" == "$ext" ]; then 316 | result=1 317 | fi 318 | done 319 | echo "$result" 320 | } 321 | 322 | function check_date 323 | { 324 | local filepath="$1" 325 | local file_epoch=123 # dummy value 326 | if [ "$stat_mode" -eq 1 ]; then 327 | file_epoch=$(stat -c '%Z' "$filepath") 328 | else 329 | local st_ctime="$file_epoch" 330 | eval "$(stat -s "$filepath")" 331 | file_epoch="$st_ctime" 332 | fi 333 | # echo "$file_epoch" 334 | if [ "$file_epoch" -gt "$MIN_HOT_EPOCH" ] && [ "$file_epoch" -lt "$MAX_HOT_EPOCH" ]; then 335 | log warning "[!] File changed/created in hot time frame FILE: $filepath EPOCH: $file_epoch" 336 | fi 337 | } 338 | 339 | function check_dir 340 | { 341 | dir=$1 342 | result=0 343 | for ex_dir in "${EXCLUDED_DIRS[@]}"; 344 | do 345 | # echo "Checking if $ex_dir is in $dir" 346 | if [ "${dir/$ex_dir}" != "$dir" ]; then 347 | if [ "${dir/#$ex_dir}" = "$dir" ];then 348 | log debug "Skipping $dir due to WRONG exclusion bc/ $ex_dir in the middle of the path..." 349 | fi 350 | result=1 351 | fi 352 | done 353 | echo $result 354 | } 355 | 356 | # Analysis -------------------------------------------------------------------- 357 | function scan_c2 358 | { 359 | oldIFS=$IFS 360 | IFS=$'\n' 361 | # Don't resolve names 362 | lsof_output=$(lsof -i -n) 363 | for lsof_line in ${lsof_output}; do 364 | for c2 in "${c2_iocs[@]}"; do 365 | # C2 check 366 | if [ "${lsof_line/$c2}" != "$lsof_line" ]; then 367 | log warning "[!] C2 server found in lsof output SERVER: $c2 LSOF_LINE: $lsof_line" 368 | fi 369 | done 370 | # Shell Check 371 | if [ "${lsof_line:0:5}" == "bash " ] || [ "${lsof_line:0:3}" == "sh " ]; then 372 | if [ "${lsof_line/127.0.0.1}" == "$lsof_line" ]; then 373 | log notice "[!] Shell found in lsof output - could be a back connect shell LSOF_LINE: $lsof_line" 374 | fi 375 | fi 376 | done 377 | # Resolve names 378 | lsof_output=$(lsof -i) 379 | for lsof_line in ${lsof_output}; do 380 | for c2 in "${c2_iocs[@]}"; do 381 | # echo "$lsof_line - $c2" 382 | if [ "${lsof_line/$c2}" != "$lsof_line" ]; then 383 | log warning "[!] C2 server found in lsof output SERVER: $c2 LSOF_LINE: $lsof_line" 384 | fi 385 | done 386 | done 387 | IFS=$oldIFS 388 | } 389 | 390 | # Helpers ------------------------------------------------------------- 391 | 392 | function evaluate_stat_mode 393 | { 394 | # Check if Linux mode works 395 | local result 396 | result=$(stat -c '%Z' "$0" 2>&1) 397 | local marker="illegal option" 398 | if [ "${result/$marker}" != "$result" ]; then 399 | log info "[+] Setting stat mode to Unix / OS X" 400 | stat_mode=2 401 | else 402 | log info "[+] Setting stat mode to Linux" 403 | stat_mode=1 404 | fi 405 | } 406 | 407 | function timestamp { 408 | date +%F_%T 409 | } 410 | 411 | function log { 412 | local type="$1" 413 | local message="$2" 414 | local ts 415 | ts=$(timestamp) 416 | 417 | # Only report debug messages if mode is enabled 418 | if [ "$type" == "debug" ] && [ $DEBUG -ne 1 ]; then 419 | return 0 420 | fi 421 | 422 | # Exclude certain strings (false positives) 423 | for ex_string in "${EXCLUDE_STRINGS[@]}"; 424 | do 425 | # echo "Checking if $ex_string is in $message" 426 | if [ "${message/$ex_string}" != "$message" ]; then 427 | return 0 428 | fi 429 | done 430 | 431 | # Remove line breaks 432 | message=$(echo "$message" | tr -d '\r' | tr '\n' ' ') 433 | 434 | # Remove prefix (e.g. [+]) 435 | if [[ "${message:0:1}" == "[" ]]; then 436 | message_cleaned="${message:4:${#message}}" 437 | else 438 | message_cleaned="$message" 439 | fi 440 | 441 | # Log to file 442 | if [[ $LOG_TO_FILE -eq 1 ]]; then 443 | echo "$ts $type $message_cleaned" >> "$LOGFILE" 444 | fi 445 | # Log to syslog 446 | if [[ $LOG_TO_SYSLOG -eq 1 ]]; then 447 | logger -p "$SYSLOG_FACILITY.$type" "$(basename "$0"): $message_cleaned" 448 | fi 449 | # Log to command line 450 | if [[ $LOG_TO_CMDLINE -eq 1 ]]; then 451 | echo "$message" >&2 452 | fi 453 | } 454 | 455 | # READ IOCS ----------------------------------------------------------- 456 | 457 | function pseudo_hash { 458 | local hash=$1 459 | #echo hash: $hash 460 | 461 | short_hash="0x${hash:0:8}" 462 | let pseudo_h=$(($short_hash)) 463 | 464 | # use global var to save the fork of /bin/echo 465 | #echo $pseudo_h 466 | } 467 | 468 | 469 | function read_hashes_iocs 470 | { 471 | # Save field separator 472 | oldIFS="$IFS" 473 | IFS=$'\n' 474 | local index=0 475 | while read -r line ; do 476 | #hash=$(echo "$line" | cut -f1 -d';') 477 | #description=$(echo "$line" | cut -f2 -d';') 478 | hash=${line%;*} 479 | if [[ -z "${hash// }" ]] ; then 480 | continue 481 | fi 482 | 483 | # Skip comments 484 | if [[ $line == \#* ]] ; then 485 | continue 486 | fi 487 | description=${line#*;} 488 | 489 | hash_iocs[$index]="$hash" 490 | hash_ioc_description[$index]="$description" 491 | 492 | # changes global var $pseudo_h 493 | pseudo_hash "$hash" 494 | # assigning the real hash value to the pseudo hash array. beware: there might be collisions so only the last one is in there! 495 | #echo $pseudo_h 496 | pseudo_hash_iocs[$pseudo_h]=$hash 497 | 498 | # echo "$hash $description" 499 | index=$((index+1)) 500 | done < $HASH_IOC_FILE 501 | IFS=$oldIFS 502 | } 503 | 504 | function read_string_iocs 505 | { 506 | # Save field separator 507 | oldIFS="$IFS" 508 | IFS=$'\n' 509 | local index=0 510 | while read -r line ; do 511 | # Skip empty values 512 | if [[ -z "${line// }" ]] ; then 513 | continue 514 | fi 515 | # Skip comments 516 | if [[ $line == \#* ]] ; then 517 | continue 518 | fi 519 | string_iocs[$index]="$line" 520 | # echo "$line" 521 | index=$((index+1)) 522 | done < $STRING_IOCS 523 | # Prepare grep strings - tried to concatenate a complete string, failed - todo 524 | # grep_strings=$(prepare_grep_strings) 525 | # echo $grep_strings 526 | IFS=$oldIFS 527 | } 528 | 529 | function read_filename_iocs 530 | { 531 | # Save field separator 532 | oldIFS="$IFS" 533 | IFS=$'\n' 534 | local index=0 535 | while read -r line ; do 536 | if [[ -z "${line// }" ]] ; then 537 | continue 538 | fi 539 | filename_iocs[$index]="$line" 540 | # echo "$line" 541 | index=$((index+1)) 542 | done < $FILENAME_IOCS 543 | IFS=$oldIFS 544 | } 545 | 546 | function read_c2_iocs 547 | { 548 | # Save field separator 549 | oldIFS="$IFS" 550 | IFS=$'\n' 551 | local index=0 552 | while read -r line ; do 553 | if [[ -z "${line// }" ]] ; then 554 | continue 555 | fi 556 | c2_iocs[$index]="$line" 557 | # echo "$line" 558 | index=$((index+1)) 559 | done < $C2_IOCS 560 | IFS=$oldIFS 561 | } 562 | 563 | function prepare_check_stings 564 | { 565 | # New method - create a string with values divided by new line for use in 'grep -F' 566 | check_strings=$( 567 | for string in "${string_iocs[@]}"; 568 | do 569 | echo "$string" 570 | done 571 | 572 | # Add C2 iocs if directory is log directory 573 | for string in "${c2_iocs[@]}"; 574 | do 575 | echo "$string" 576 | done 577 | ) 578 | } 579 | 580 | function check_req 581 | { 582 | log info "Checking the required utilities ..." 583 | file_avail=$(command -v file) 584 | if [[ -z $file_avail ]]; then 585 | log error "The 'file' command can't be found (disabling file type checks)" 586 | ENABLE_TYPE_CHECK=0 587 | fi 588 | lsof_avail=$(command -v lsof) 589 | if [[ -z $lsof_avail ]]; then 590 | log error "The 'lsof' command can't be found (disabling C2 checks)" 591 | ENABLE_C2_CHECK=0 592 | fi 593 | md5sum_avail=$(command -v md5sum) 594 | if [[ -z $md5sum_avail ]]; then 595 | log error "The 'md5sum' command can't be found (disabling hash checks)" 596 | ENABLE_HASH_CHECK=0 597 | fi 598 | } 599 | 600 | 601 | # Program ------------------------------------------------------------- 602 | 603 | echo "##############################################################" 604 | echo " ____ _ " 605 | echo " / __/__ ___ ____(_)___ " 606 | echo " / _// -_) _ \/ __/ / __/ " 607 | echo " /_/ \__/_//_/_/ /_/_/ " 608 | echo " v$VERSION" 609 | echo " " 610 | echo " Simple Bash IOC Checker" 611 | echo " Florian Roth, Dec 2021" 612 | echo "##############################################################" 613 | 614 | if [ "$#" -ne 1 ]; then 615 | echo " " 616 | echo "[E] Error - not enough parameters" 617 | echo " Usage: $0 DIRECTORY" >&2 618 | echo " " 619 | echo " DIRECTORY - Start point of the recursive scan" 620 | echo " " 621 | exit 1 622 | fi 623 | 624 | # Non-static global variables 625 | declare stat_mode=1 626 | 627 | log info "Started FENRIR Scan - version $VERSION" 628 | log info "Writing logfile to ${LOGFILE}" 629 | log info "HOSTNAME: ${SYSTEM_NAME}" 630 | 631 | IP_ADDRESS=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | tr '\n' ' ') 632 | OS_RELEASE=$(cat /etc/*release | sort -u | tr "\n" ";") 633 | OS_ISSUE=$(cat /etc/issue) 634 | OS_KERNEL=$(uname -a) 635 | 636 | log info "IP: $IP_ADDRESS" 637 | log info "OS: $OS_RELEASE" 638 | log info "ISSUE: $OS_ISSUE" 639 | log info "KERNEL: $OS_KERNEL" 640 | 641 | # Evaluate which stat mode to use 642 | evaluate_stat_mode 643 | 644 | # Check requirements 645 | check_req 646 | 647 | # Read all IOCs 648 | log info "[+] Reading Hash IOCs ..." 649 | read_hashes_iocs 650 | log info "[+] Reading String IOCs ..." 651 | read_string_iocs 652 | prepare_check_stings 653 | log info "[+] Reading Filename IOCs ..." 654 | read_filename_iocs 655 | log info "[+] Reading C2 IOCs ..." 656 | read_c2_iocs 657 | 658 | # Now scan the given first parameter 659 | if [ $ENABLE_C2_CHECK -eq 1 ]; then 660 | log info "[+] Scanning for C2 servers in 'lsof' output ..." 661 | scan_c2 662 | fi 663 | log info "[+] Scanning path $1 ..." 664 | scan_dirs "$1" 665 | log info "Finished FENRIR Scan" 666 | --------------------------------------------------------------------------------