Land #5840, add LLMNR & mDNS modules
commit
9f9bbce034
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
require 'msf/core/auxiliary/mdns'
|
||||||
|
|
||||||
|
module Msf
|
||||||
|
# This module provides methods for working with LLMNR
|
||||||
|
module Auxiliary::LLMNR
|
||||||
|
include Auxiliary::MDNS
|
||||||
|
|
||||||
|
# Initializes an instance of an auxiliary module that uses LLMNR
|
||||||
|
def initialize(info = {})
|
||||||
|
super
|
||||||
|
register_options(
|
||||||
|
[
|
||||||
|
OptAddressRange.new('RHOSTS', [true, 'The multicast address or CIDR range of targets to query', '224.0.0.252']),
|
||||||
|
Opt::RPORT(5355),
|
||||||
|
# TODO: allow more than one
|
||||||
|
OptString.new('NAME', [true, 'The name to query', 'localhost']),
|
||||||
|
OptString.new('TYPE', [true, 'The query type (name, # or TYPE#)', 'A'])
|
||||||
|
],
|
||||||
|
self.class
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,108 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
require 'net/dns'
|
||||||
|
|
||||||
|
module Msf
|
||||||
|
# This module provides methods for working with mDNS
|
||||||
|
module Auxiliary::MDNS
|
||||||
|
# Initializes an instance of an auxiliary module that uses mDNS
|
||||||
|
def initialize(info = {})
|
||||||
|
super
|
||||||
|
register_options(
|
||||||
|
[
|
||||||
|
OptAddressRange.new('RHOSTS', [true, 'The multicast address or CIDR range of targets to query', '224.0.0.251']),
|
||||||
|
Opt::RPORT(5353),
|
||||||
|
OptString.new('NAME', [true, 'The name to query', '_services._dns-sd._udp.local']),
|
||||||
|
OptString.new('TYPE', [true, 'The query type (name, # or TYPE#)', 'PTR']),
|
||||||
|
OptString.new('CLASS', [true, 'The query class (name, # or CLASS#)', 'IN'])
|
||||||
|
],
|
||||||
|
self.class
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup
|
||||||
|
query_class_name
|
||||||
|
query_type_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_probe
|
||||||
|
@probe ||= ::Net::DNS::Packet.new(query_name, query_type_num, query_class_num).data
|
||||||
|
# TODO: support QU vs QM probes
|
||||||
|
# @probe[@probe.size-2] = [0x80].pack('C')
|
||||||
|
# @probe
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_class
|
||||||
|
if datastore['CLASS'] =~ /^\d+$/
|
||||||
|
datastore['CLASS'].to_i
|
||||||
|
else
|
||||||
|
datastore['CLASS'].upcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_class_name
|
||||||
|
Net::DNS::RR::Classes.new(query_class).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_class_num
|
||||||
|
Net::DNS::RR::Classes.new(query_class).to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_type
|
||||||
|
if datastore['TYPE'] =~ /^\d+$/
|
||||||
|
datastore['TYPE'].to_i
|
||||||
|
else
|
||||||
|
datastore['TYPE'].upcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_name
|
||||||
|
datastore['NAME']
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_type_name
|
||||||
|
Net::DNS::RR::Types.new(query_type).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_type_num
|
||||||
|
Net::DNS::RR::Types.new(query_type).to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def describe_response(response)
|
||||||
|
decoded = Resolv::DNS::Message.decode(response)
|
||||||
|
answers = decoded.answer
|
||||||
|
|
||||||
|
if answers.empty? # not sure this will ever happen...
|
||||||
|
"no answers"
|
||||||
|
else
|
||||||
|
# there are often many answers for the same RR, so group them
|
||||||
|
grouped_answers = answers.group_by { |name, _, _| name }
|
||||||
|
# now summarize each group by noting the resource type and the notable
|
||||||
|
# part(s) of that RR
|
||||||
|
summarized_answers = grouped_answers.map do |name, these_answers|
|
||||||
|
summarized_group = these_answers.map do |_, _, data|
|
||||||
|
case data
|
||||||
|
when Resolv::DNS::Resource::IN::A
|
||||||
|
"A #{data.address}"
|
||||||
|
when Resolv::DNS::Resource::IN::AAAA
|
||||||
|
"AAAA #{data.address}"
|
||||||
|
when Resolv::DNS::Resource::IN::PTR
|
||||||
|
"PTR #{data.name}"
|
||||||
|
when Resolv::DNS::Resource::IN::SRV
|
||||||
|
"SRV #{data.target}"
|
||||||
|
when Resolv::DNS::Resource::IN::TXT
|
||||||
|
"TXT #{data.strings.join(',')}"
|
||||||
|
else
|
||||||
|
data.inspect
|
||||||
|
end
|
||||||
|
end
|
||||||
|
"#{name}: (#{summarized_group.join(", ")})"
|
||||||
|
end
|
||||||
|
summarized_answers.join(', ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_info
|
||||||
|
"#{query_name} #{query_class}/#{query_type}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -20,6 +20,8 @@ require 'msf/core/auxiliary/login'
|
||||||
require 'msf/core/auxiliary/rservices'
|
require 'msf/core/auxiliary/rservices'
|
||||||
require 'msf/core/auxiliary/cisco'
|
require 'msf/core/auxiliary/cisco'
|
||||||
require 'msf/core/auxiliary/kademlia'
|
require 'msf/core/auxiliary/kademlia'
|
||||||
|
require 'msf/core/auxiliary/llmnr'
|
||||||
|
require 'msf/core/auxiliary/mdns'
|
||||||
require 'msf/core/auxiliary/nmap'
|
require 'msf/core/auxiliary/nmap'
|
||||||
require 'msf/core/auxiliary/natpmp'
|
require 'msf/core/auxiliary/natpmp'
|
||||||
require 'msf/core/auxiliary/iax2'
|
require 'msf/core/auxiliary/iax2'
|
||||||
|
|
|
@ -181,7 +181,9 @@ module Auxiliary::UDPScanner
|
||||||
end
|
end
|
||||||
|
|
||||||
# Called for each response packet
|
# Called for each response packet
|
||||||
def scanner_process(data, shost, sport)
|
def scanner_process(data, shost, _sport)
|
||||||
|
@results[shost] ||= []
|
||||||
|
@results[shost] << data
|
||||||
end
|
end
|
||||||
|
|
||||||
# Called before the scan block
|
# Called before the scan block
|
||||||
|
|
|
@ -242,7 +242,7 @@ module Msf
|
||||||
dev ||= datastore['INTERFACE']
|
dev ||= datastore['INTERFACE']
|
||||||
dst_mac, src_mac = lookup_eth(dhost, dev)
|
dst_mac, src_mac = lookup_eth(dhost, dev)
|
||||||
if dst_mac == nil and not bcast
|
if dst_mac == nil and not bcast
|
||||||
return false
|
raise RuntimeError, 'Unable to determine the destination MAC and bcast is false'
|
||||||
end
|
end
|
||||||
inject_eth(:payload => payload, :eth_daddr => dst_mac, :eth_saddr => src_mac)
|
inject_eth(:payload => payload, :eth_daddr => dst_mac, :eth_saddr => src_mac)
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ module Net # :nodoc:
|
||||||
when Fixnum
|
when Fixnum
|
||||||
return Classes.invert.has_key?(cls)
|
return Classes.invert.has_key?(cls)
|
||||||
else
|
else
|
||||||
raise ClassArgumentError, "Wrong cls class: #{cls.class}"
|
raise ClassArgumentError, "Wrong class: #{cls.class}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ module Net # :nodoc:
|
||||||
raise ClassArgumentError, "Unknown class number #{cls}"
|
raise ClassArgumentError, "Unknown class number #{cls}"
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
raise ClassArgumentError, "Wrong cls class: #{cls.class}"
|
raise ClassArgumentError, "Wrong class: #{cls.class}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ module Net # :nodoc:
|
||||||
@str = Classes.invert[@@default]
|
@str = Classes.invert[@@default]
|
||||||
@num = @@default
|
@num = @@default
|
||||||
else
|
else
|
||||||
raise ClassArgumentError, "Wrong cls class: #{cls.class}"
|
raise ClassArgumentError, "Wrong class: #{cls.class}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -89,15 +89,15 @@ module Net # :nodoc:
|
||||||
# *PRIVATE* method
|
# *PRIVATE* method
|
||||||
def new_from_string(cls)
|
def new_from_string(cls)
|
||||||
case cls
|
case cls
|
||||||
when /^CLASS\\d+/
|
when /^CLASS(\d+)$/
|
||||||
# TODO!!!
|
new_from_num(Regexp.last_match(1).to_i)
|
||||||
else
|
else
|
||||||
# String with name of class
|
# String with name of class
|
||||||
if Classes.has_key? cls
|
if Classes.has_key? cls
|
||||||
@str = cls
|
@str = cls
|
||||||
@num = Classes[cls]
|
@num = Classes[cls]
|
||||||
else
|
else
|
||||||
raise ClassesArgumentError, "Unknown cls #{cls}"
|
raise ClassArgumentError, "Unknown class #{cls}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -105,11 +105,13 @@ module Net # :nodoc:
|
||||||
# Contructor for numeric data class
|
# Contructor for numeric data class
|
||||||
# *PRIVATE* method
|
# *PRIVATE* method
|
||||||
def new_from_num(cls)
|
def new_from_num(cls)
|
||||||
|
raise ClassArgumentError, "Invalid class #{cls}" if cls < 0 || cls > 0xFFFF
|
||||||
if Classes.invert.has_key? cls
|
if Classes.invert.has_key? cls
|
||||||
@num = cls
|
@num = cls
|
||||||
@str = Classes.invert[cls]
|
@str = Classes.invert[cls]
|
||||||
else
|
else
|
||||||
raise ClassesArgumentError, "Unknown cls number #{cls}"
|
@num = cls
|
||||||
|
@str = "CLASS#{cls}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -167,8 +167,8 @@ module Net # :nodoc:
|
||||||
# *PRIVATE* method
|
# *PRIVATE* method
|
||||||
def new_from_string(type)
|
def new_from_string(type)
|
||||||
case type
|
case type
|
||||||
when /^TYPE\\d+/
|
when /^TYPE(\d+)$/
|
||||||
# TODO!!!
|
new_from_num(Regexp.last_match(1).to_i)
|
||||||
else
|
else
|
||||||
# String with name of type
|
# String with name of type
|
||||||
if Types.has_key? type
|
if Types.has_key? type
|
||||||
|
@ -183,11 +183,13 @@ module Net # :nodoc:
|
||||||
# Contructor for numeric data type
|
# Contructor for numeric data type
|
||||||
# *PRIVATE* method
|
# *PRIVATE* method
|
||||||
def new_from_num(type)
|
def new_from_num(type)
|
||||||
|
raise TypeArgumentError, "Invalid type #{type}" if type < 0 || type > 0xFFFF
|
||||||
if Types.invert.has_key? type
|
if Types.invert.has_key? type
|
||||||
@num = type
|
@num = type
|
||||||
@str = Types.invert[type]
|
@str = Types.invert[type]
|
||||||
else
|
else
|
||||||
raise TypeArgumentError, "Unknown type number #{type}"
|
@num = type
|
||||||
|
@str = "TYPE#{type}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
##
|
||||||
|
# This module requires Metasploit: http://metasploit.com/download
|
||||||
|
# Current source: https://github.com/rapid7/metasploit-framework
|
||||||
|
##
|
||||||
|
|
||||||
|
require 'msf/core'
|
||||||
|
|
||||||
|
class Metasploit3 < Msf::Auxiliary
|
||||||
|
include Msf::Auxiliary::Report
|
||||||
|
include Msf::Auxiliary::UDPScanner
|
||||||
|
include Msf::Auxiliary::LLMNR
|
||||||
|
|
||||||
|
def initialize(info = {})
|
||||||
|
super(
|
||||||
|
update_info(
|
||||||
|
info,
|
||||||
|
'Name' => 'LLMNR Query',
|
||||||
|
'Description' => %q(
|
||||||
|
This module sends LLMNR queries, which are really just normal UDP DNS
|
||||||
|
queries done (usually) over multicast on a different port, 5355.
|
||||||
|
Targets other than the default RHOSTS' 224.0.0.252 should not respond
|
||||||
|
but may anyway.
|
||||||
|
),
|
||||||
|
'Author' =>
|
||||||
|
[
|
||||||
|
'Jon Hart <jon_hart[at]rapid7.com>'
|
||||||
|
],
|
||||||
|
'License' => MSF_LICENSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def scanner_prescan(batch)
|
||||||
|
print_status("Sending LLMNR #{query_type_name}/#{query_class_name} queries for #{query_name} to #{batch[0]}->#{batch[-1]} port #{rport} (#{batch.length} hosts)")
|
||||||
|
@results = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def scanner_postscan(_batch)
|
||||||
|
found = {}
|
||||||
|
@results.each_pair do |peer, resps|
|
||||||
|
resps.each do |resp|
|
||||||
|
found[peer] ||= {}
|
||||||
|
next if found[peer][resp]
|
||||||
|
response_info = describe_response(resp)
|
||||||
|
print_good("#{peer} responded with #{response_info}")
|
||||||
|
report_service(host: peer, port: rport, proto: "udp", name: "llmnr", info: response_info)
|
||||||
|
found[peer][resp] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
##
|
||||||
|
# This module requires Metasploit: http://metasploit.com/download
|
||||||
|
# Current source: https://github.com/rapid7/metasploit-framework
|
||||||
|
##
|
||||||
|
|
||||||
|
require 'msf/core'
|
||||||
|
|
||||||
|
class Metasploit3 < Msf::Auxiliary
|
||||||
|
include Msf::Auxiliary::Report
|
||||||
|
include Msf::Auxiliary::UDPScanner
|
||||||
|
include Msf::Auxiliary::MDNS
|
||||||
|
|
||||||
|
def initialize(info = {})
|
||||||
|
super(
|
||||||
|
update_info(
|
||||||
|
info,
|
||||||
|
'Name' => 'mDNS Query',
|
||||||
|
'Description' => %q(
|
||||||
|
This module sends mDNS queries, which are really just normal UDP DNS
|
||||||
|
queries done (usually) over multicast on a different port, 5353.
|
||||||
|
),
|
||||||
|
'Author' =>
|
||||||
|
[
|
||||||
|
'Jon Hart <jon_hart[at]rapid7.com>'
|
||||||
|
],
|
||||||
|
'License' => MSF_LICENSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def scanner_prescan(batch)
|
||||||
|
print_status("Sending mDNS #{query_type_name} #{query_class_name} queries for " \
|
||||||
|
"#{query_name} to #{batch[0]}->#{batch[-1]} port #{rport} (#{batch.length} hosts)")
|
||||||
|
@results = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def scanner_postscan(_batch)
|
||||||
|
found = {}
|
||||||
|
@results.each_pair do |peer, resps|
|
||||||
|
resps.each do |resp|
|
||||||
|
found[peer] ||= {}
|
||||||
|
next if found[peer][resp]
|
||||||
|
response_info = describe_response(resp)
|
||||||
|
print_good("#{peer} responded with #{response_info}")
|
||||||
|
report_service(host: peer, port: rport, proto: "udp", name: "mdns", info: response_info)
|
||||||
|
found[peer][resp] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,62 @@
|
||||||
|
# -*- coding:binary -*-
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
require 'net/dns'
|
||||||
|
|
||||||
|
describe Net::DNS::RR::Classes do
|
||||||
|
|
||||||
|
subject do
|
||||||
|
described_class.new
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#initialize' do
|
||||||
|
subject(:rr_class) do
|
||||||
|
described_class.allocate
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when initialized with no args' do
|
||||||
|
expect { rr_class.send(:initialize) }.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'respects default RR class when initialized with a nil RR class' do
|
||||||
|
rr_class.send(:initialize, nil)
|
||||||
|
expect(rr_class.to_i).to eql(1)
|
||||||
|
expect(rr_class.to_s).to eql('IN')
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: figure out why this doesn't work
|
||||||
|
skip 'respects configured default RR class' do
|
||||||
|
rr_class.send(:default=, 'NONE')
|
||||||
|
expect(rr_class.to_i).to eql(254)
|
||||||
|
expect(rr_class.to_s).to eql('NONE')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'initializes with a valid RR class Fixnum argument' do
|
||||||
|
rr_class.send(:initialize, 4)
|
||||||
|
expect(rr_class.to_i).to eql(4)
|
||||||
|
expect(rr_class.to_s).to eql('HS')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when the supplied RR class Fixnum is invalid' do
|
||||||
|
expect { rr_class.send(:initialize, 123456) }.to raise_error(ClassArgumentError)
|
||||||
|
expect { rr_class.send(:initialize, -1) }.to raise_error(ClassArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'initializes with a valid RR class String argument' do
|
||||||
|
rr_class.send(:initialize, 'CH')
|
||||||
|
expect(rr_class.to_i).to eql(3)
|
||||||
|
expect(rr_class.to_s).to eql('CH')
|
||||||
|
rr_class.send(:initialize, 'CLASS9')
|
||||||
|
expect(rr_class.to_i).to eql(9)
|
||||||
|
expect(rr_class.to_s).to eql('CLASS9')
|
||||||
|
rr_class.send(:initialize, 'CLASS1')
|
||||||
|
expect(rr_class.to_i).to eql(1)
|
||||||
|
expect(rr_class.to_s).to eql('IN')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when the supplied RR class String is invalid' do
|
||||||
|
expect { rr_class.send(:initialize, 'cats') }.to raise_error(ClassArgumentError)
|
||||||
|
expect { rr_class.send(:initialize, 'CLASS123456') }.to raise_error(ClassArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,62 @@
|
||||||
|
# -*- coding:binary -*-
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
require 'net/dns'
|
||||||
|
|
||||||
|
describe Net::DNS::RR::Types do
|
||||||
|
|
||||||
|
subject do
|
||||||
|
described_class.new
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#initialize' do
|
||||||
|
subject(:rr_type) do
|
||||||
|
described_class.allocate
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when initialized with no args' do
|
||||||
|
expect { rr_type.send(:initialize) }.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'respects default RR type when initialized with a nil RR type' do
|
||||||
|
rr_type.send(:initialize, nil)
|
||||||
|
expect(rr_type.to_i).to eql(1)
|
||||||
|
expect(rr_type.to_s).to eql('A')
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: figure out why this doesn't work
|
||||||
|
skip 'respects configured default RR type' do
|
||||||
|
rr_type.send(:default=, 'CNAME')
|
||||||
|
expect(rr_type.to_i).to eql(5)
|
||||||
|
expect(rr_type.to_s).to eql('CNAME')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'initializes with a valid RR type Fixnum argument' do
|
||||||
|
rr_type.send(:initialize, 2)
|
||||||
|
expect(rr_type.to_i).to eql(2)
|
||||||
|
expect(rr_type.to_s).to eql('NS')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when the supplied RR type Fixnum is invalid' do
|
||||||
|
expect { rr_type.send(:initialize, 123456) }.to raise_error(TypeArgumentError)
|
||||||
|
expect { rr_type.send(:initialize, -1) }.to raise_error(TypeArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'initializes with a valid RR type String argument' do
|
||||||
|
rr_type.send(:initialize, 'SRV')
|
||||||
|
expect(rr_type.to_i).to eql(33)
|
||||||
|
expect(rr_type.to_s).to eql('SRV')
|
||||||
|
rr_type.send(:initialize, 'TYPE12')
|
||||||
|
expect(rr_type.to_i).to eql(12)
|
||||||
|
expect(rr_type.to_s).to eql('PTR')
|
||||||
|
rr_type.send(:initialize, 'TYPE123')
|
||||||
|
expect(rr_type.to_i).to eql(123)
|
||||||
|
expect(rr_type.to_s).to eql('TYPE123')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when the supplied RR type String is invalid' do
|
||||||
|
expect { rr_type.send(:initialize, 'cats') }.to raise_error(TypeArgumentError)
|
||||||
|
expect { rr_type.send(:initialize, 'TYPE123456') }.to raise_error(TypeArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue