Land #3545, fix up sip scanners, msftidy, db services cmd
commit
4e19d9ade1
25
.rubocop.yml
25
.rubocop.yml
|
@ -39,6 +39,11 @@ Style/MethodLength:
|
|||
often exceed 200 lines.
|
||||
Max: 300
|
||||
|
||||
# Basically everything in metasploit needs binary encoding, not UTF-8.
|
||||
# Disable this here and enforce it through msftidy
|
||||
Style/Encoding:
|
||||
Enabled: false
|
||||
|
||||
Style/NumericLiterals:
|
||||
Enabled: false
|
||||
Description: 'This often hurts readability for exploit-ish code.'
|
||||
|
@ -53,4 +58,22 @@ Style/StringLiterals:
|
|||
|
||||
Style/WordArray:
|
||||
Enabled: false
|
||||
Description: 'Metasploit prefers consistent use of []'
|
||||
Description: 'Metasploit prefers consistent use of []'
|
||||
|
||||
Style/RedundantBegin:
|
||||
Exclude:
|
||||
# this pattern is very common and somewhat unavoidable
|
||||
# def run_host(ip)
|
||||
# begin
|
||||
# ...
|
||||
# rescue ...
|
||||
# ...
|
||||
# ensure
|
||||
# disconnect
|
||||
# end
|
||||
# end
|
||||
- 'modules/**/*'
|
||||
|
||||
Documentation:
|
||||
Exclude:
|
||||
- 'modules/**/*'
|
||||
|
|
|
@ -57,6 +57,7 @@ require 'msf/core/exploit/wdbrpc'
|
|||
require 'msf/core/exploit/wdbrpc_client'
|
||||
require 'msf/core/exploit/afp'
|
||||
require 'msf/core/exploit/realport'
|
||||
require 'msf/core/exploit/sip'
|
||||
|
||||
# Telephony
|
||||
require 'msf/core/exploit/dialup'
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/proto/sip/response'
|
||||
|
||||
module Msf
|
||||
# SIP protocol support
|
||||
module Exploit::Remote::SIP
|
||||
# Parses +response+, extracts useful metdata and then reports on it.
|
||||
# Returns true iff the response was a valid SIP response
|
||||
def report_response(response, rhost, proto, desired_headers = %w(User-Agent Server Allow))
|
||||
endpoint = "#{rhost}:#{rport} #{proto}"
|
||||
begin
|
||||
options_response = Rex::Proto::SIP::Response.parse(response)
|
||||
rescue ArgumentError => e
|
||||
vprint_error("#{endpoint} is not SIP: #{e}")
|
||||
return false
|
||||
end
|
||||
|
||||
# We know it is SIP, so report
|
||||
report_service(
|
||||
host: rhost,
|
||||
port: rport,
|
||||
proto: proto.downcase,
|
||||
name: 'sip'
|
||||
)
|
||||
|
||||
# Do header extraction as necessary
|
||||
extracted_headers = {}
|
||||
unless desired_headers.nil? || desired_headers.empty?
|
||||
desired_headers.each do |desired_header|
|
||||
next unless (found_header = options_response.header(desired_header))
|
||||
extracted_headers[desired_header] ||= []
|
||||
extracted_headers[desired_header] |= found_header
|
||||
end
|
||||
|
||||
# report on any extracted headers
|
||||
extracted_headers.each do |k, v|
|
||||
report_note(
|
||||
host: rhost,
|
||||
port: rport,
|
||||
proto: proto.downcase,
|
||||
type: "sip_header.#{k.gsub(/-/, '_').downcase}",
|
||||
data: v.join(',')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
status = "#{endpoint} #{options_response.status_line}"
|
||||
status += ": #{extracted_headers}" unless extracted_headers.empty?
|
||||
print_status(status)
|
||||
true
|
||||
end
|
||||
|
||||
def create_probe(ip, proto)
|
||||
suser = Rex::Text.rand_text_alphanumeric(rand(8) + 1)
|
||||
shost = Rex::Socket.source_address(ip)
|
||||
src = "#{shost}:#{datastore['RPORT']}"
|
||||
|
||||
data = "OPTIONS sip:#{datastore['TO']}@#{ip} SIP/2.0\r\n"
|
||||
data << "Via: SIP/2.0/#{proto.upcase} #{src};branch=z9hG4bK.#{format('%.8x', rand(0x100000000))};rport;alias\r\n"
|
||||
data << "From: sip:#{suser}@#{src};tag=70c00e8c\r\n"
|
||||
data << "To: sip:#{datastore['TO']}@#{ip}\r\n"
|
||||
data << "Call-ID: #{rand(0x100000000)}@#{shost}\r\n"
|
||||
data << "CSeq: 1 OPTIONS\r\n"
|
||||
data << "Contact: sip:#{suser}@#{src}\r\n"
|
||||
data << "Max-Forwards: 20\r\n"
|
||||
data << "User-Agent: #{suser}\r\n"
|
||||
data << "Accept: application/sdp\r\n"
|
||||
data << "Content-Length: 0\r\n"
|
||||
data << "\r\n"
|
||||
data
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1109,8 +1109,9 @@ class Db
|
|||
end
|
||||
end
|
||||
if (note.service)
|
||||
name = (note.service.name ? note.service.name : "#{note.service.port}/#{note.service.proto}")
|
||||
msg << " service=#{name}"
|
||||
msg << " service=#{note.service.name}" if note.service.name
|
||||
msg << " port=#{note.service.port}" if note.service.port
|
||||
msg << " protocol=#{note.service.proto}" if note.service.proto
|
||||
end
|
||||
msg << " type=#{note.ntype} data=#{note.data.inspect}"
|
||||
print_status(msg)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# encoding: binary
|
||||
|
||||
# SIP protocol support
|
||||
require 'rex/proto/sip/response'
|
|
@ -0,0 +1,61 @@
|
|||
# encoding: binary
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
# SIP protocol support
|
||||
module SIP
|
||||
SIP_STATUS_REGEX = /^SIP\/(\d\.\d) (\d{3})\s*(.*)$/
|
||||
|
||||
# Represents a generic SIP message
|
||||
class Message
|
||||
attr_accessor :headers
|
||||
|
||||
def initialize
|
||||
@headers = {}
|
||||
end
|
||||
|
||||
# Returns a list of all values from all +name+ headers, regardless of case,
|
||||
# or nil if no matching header is found
|
||||
def header(name)
|
||||
matches = @headers.select { |k, _| k.downcase == name.downcase }
|
||||
return nil if matches.empty?
|
||||
matches.values.flatten
|
||||
end
|
||||
|
||||
# Returns a hash of header name to values mapping
|
||||
# from the provided message, or nil if no headers
|
||||
# are found
|
||||
def self.extract_headers(message)
|
||||
pairs = message.scan(/^([^\s:]+):\s*(.*)$/)
|
||||
return nil if pairs.empty?
|
||||
headers = {}
|
||||
pairs.each do |pair|
|
||||
headers[pair.first] ||= []
|
||||
headers[pair.first] << pair.last.strip
|
||||
end
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
# Represents a SIP response message
|
||||
class Response < Message
|
||||
attr_accessor :code, :message, :status_line, :version
|
||||
|
||||
# Parses +data+, constructs and returns a Response
|
||||
def self.parse(data)
|
||||
response = Response.new
|
||||
# do some basic sanity checking on this response to ensure that it is SIP
|
||||
response.status_line = data.split(/\r\n/)[0]
|
||||
unless response.status_line && response.status_line =~ SIP_STATUS_REGEX
|
||||
fail(ArgumentError, "Invalid SIP status line: #{response.status_line}")
|
||||
end
|
||||
response.version = Regexp.last_match(1)
|
||||
response.code = Regexp.last_match(2)
|
||||
response.message = Regexp.last_match(3)
|
||||
response.headers = extract_headers(data)
|
||||
response
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,14 +3,13 @@
|
|||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::Udp
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Auxiliary::Scanner
|
||||
include Msf::Auxiliary::UDPScanner
|
||||
include Msf::Exploit::Remote::SIP
|
||||
|
||||
def initialize
|
||||
super(
|
||||
|
@ -22,139 +21,21 @@ class Metasploit3 < Msf::Auxiliary
|
|||
|
||||
register_options(
|
||||
[
|
||||
OptInt.new('BATCHSIZE', [true, 'The number of hosts to probe in each set', 256]),
|
||||
OptString.new('TO', [ false, "The destination username to probe at each host", "nobody"]),
|
||||
Opt::RPORT(5060),
|
||||
Opt::CHOST,
|
||||
Opt::CPORT(5060)
|
||||
OptString.new('TO', [false, 'The destination username to probe at each host', 'nobody']),
|
||||
Opt::RPORT(5060)
|
||||
], self.class)
|
||||
end
|
||||
|
||||
|
||||
# Define our batch size
|
||||
def run_batch_size
|
||||
datastore['BATCHSIZE'].to_i
|
||||
def scanner_prescan(batch)
|
||||
print_status("Sending SIP UDP OPTIONS requests to #{batch[0]}->#{batch[-1]} (#{batch.length} hosts)")
|
||||
@res = {}
|
||||
end
|
||||
|
||||
# Operate on an entire batch of hosts at once
|
||||
def run_batch(batch)
|
||||
|
||||
begin
|
||||
udp_sock = nil
|
||||
idx = 0
|
||||
|
||||
# Create an unbound UDP socket if no CHOST is specified, otherwise
|
||||
# create a UDP socket bound to CHOST (in order to avail of pivoting)
|
||||
udp_sock = Rex::Socket::Udp.create(
|
||||
{
|
||||
'LocalHost' => datastore['CHOST'] || nil,
|
||||
'LocalPort' => datastore['CPORT'].to_i,
|
||||
'Context' => {'Msf' => framework, 'MsfExploit' => self}
|
||||
}
|
||||
)
|
||||
add_socket(udp_sock)
|
||||
|
||||
batch.each do |ip|
|
||||
data = create_probe(ip)
|
||||
|
||||
begin
|
||||
udp_sock.sendto(data, ip, datastore['RPORT'].to_i, 0)
|
||||
rescue ::Interrupt
|
||||
raise $!
|
||||
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
|
||||
nil
|
||||
end
|
||||
|
||||
if (idx % 10 == 0)
|
||||
while (r = udp_sock.recvfrom(65535, 0.01) and r[1])
|
||||
parse_reply(r)
|
||||
end
|
||||
end
|
||||
|
||||
idx += 1
|
||||
end
|
||||
|
||||
while (r = udp_sock.recvfrom(65535, 3) and r[1])
|
||||
parse_reply(r)
|
||||
end
|
||||
|
||||
rescue ::Interrupt
|
||||
raise $!
|
||||
rescue ::Exception => e
|
||||
print_error("Unknown error: #{e.class} #{e}")
|
||||
ensure
|
||||
udp_sock.close if udp_sock
|
||||
end
|
||||
def scan_host(ip)
|
||||
scanner_send(create_probe(ip, 'udp'), ip, datastore['RPORT'])
|
||||
end
|
||||
|
||||
#
|
||||
# The response parsers
|
||||
#
|
||||
def parse_reply(pkt)
|
||||
|
||||
return if not pkt[1]
|
||||
|
||||
if(pkt[1] =~ /^::ffff:/)
|
||||
pkt[1] = pkt[1].sub(/^::ffff:/, '')
|
||||
end
|
||||
|
||||
resp = pkt[0].split(/\s+/)[1]
|
||||
agent = ''
|
||||
verbs = ''
|
||||
serv = ''
|
||||
prox = ''
|
||||
|
||||
if(pkt[0] =~ /^User-Agent:\s*(.*)$/i)
|
||||
agent = "agent='#{$1.strip}' "
|
||||
end
|
||||
|
||||
if(pkt[0] =~ /^Allow:\s+(.*)$/i)
|
||||
verbs = "verbs='#{$1.strip}' "
|
||||
end
|
||||
|
||||
if(pkt[0] =~ /^Server:\s+(.*)$/)
|
||||
serv = "server='#{$1.strip}' "
|
||||
end
|
||||
|
||||
if(pkt[0] =~ /^Proxy-Require:\s+(.*)$/)
|
||||
serv = "proxy-required='#{$1.strip}' "
|
||||
end
|
||||
|
||||
print_status("#{pkt[1]} #{resp} #{agent}#{serv}#{prox}#{verbs}")
|
||||
|
||||
report_service(
|
||||
:host => pkt[1],
|
||||
:port => pkt[2],
|
||||
:proto => 'udp',
|
||||
:name => 'sip'
|
||||
)
|
||||
|
||||
if(not agent.empty?)
|
||||
report_note(
|
||||
:host => pkt[1],
|
||||
:type => 'sip_useragent',
|
||||
:data => agent
|
||||
)
|
||||
end
|
||||
def scanner_process(data, shost, _)
|
||||
report_response(data, shost, 'udp')
|
||||
end
|
||||
|
||||
def create_probe(ip)
|
||||
suser = Rex::Text.rand_text_alphanumeric(rand(8)+1)
|
||||
shost = Rex::Socket.source_address(ip)
|
||||
src = "#{shost}:#{datastore['CPORT']}"
|
||||
|
||||
data = "OPTIONS sip:#{datastore['TO']}@#{ip} SIP/2.0\r\n"
|
||||
data << "Via: SIP/2.0/UDP #{src};branch=z9hG4bK.#{"%.8x" % rand(0x100000000)};rport;alias\r\n"
|
||||
data << "From: sip:#{suser}@#{src};tag=70c00e8c\r\n"
|
||||
data << "To: sip:#{datastore['TO']}@#{ip}\r\n"
|
||||
data << "Call-ID: #{rand(0x100000000)}@#{shost}\r\n"
|
||||
data << "CSeq: 1 OPTIONS\r\n"
|
||||
data << "Contact: sip:#{suser}@#{src}\r\n"
|
||||
data << "Content-Length: 0\r\n"
|
||||
data << "Max-Forwards: 20\r\n"
|
||||
data << "User-Agent: #{suser}\r\n"
|
||||
data << "Accept: text/plain\r\n"
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Auxiliary::Scanner
|
||||
include Msf::Exploit::Remote::SIP
|
||||
|
||||
def initialize
|
||||
super(
|
||||
|
@ -21,94 +21,22 @@ class Metasploit3 < Msf::Auxiliary
|
|||
|
||||
register_options(
|
||||
[
|
||||
OptInt.new('BATCHSIZE', [true, 'The number of hosts to probe in each set', 256]),
|
||||
OptString.new('TO', [ false, "The destination username to probe at each host", "nobody"]),
|
||||
OptString.new('TO', [false, 'The destination username to probe at each host', 'nobody']),
|
||||
Opt::RPORT(5060)
|
||||
], self.class)
|
||||
end
|
||||
|
||||
# Operate on a single system at a time
|
||||
def run_host(ip)
|
||||
|
||||
begin
|
||||
idx = 0
|
||||
|
||||
connect
|
||||
sock.put(create_probe(ip))
|
||||
sock.put(create_probe(ip, 'tcp'))
|
||||
res = sock.get_once(-1, 5)
|
||||
parse_reply(res) if res
|
||||
|
||||
report_response(res, rhost, 'tcp') if res
|
||||
rescue ::Interrupt
|
||||
raise $!
|
||||
raise $ERROR_INFO
|
||||
ensure
|
||||
disconnect
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# The response parser
|
||||
#
|
||||
def parse_reply(resp)
|
||||
|
||||
rcode = resp.split(/\s+/)[0]
|
||||
agent = ''
|
||||
verbs = ''
|
||||
serv = ''
|
||||
prox = ''
|
||||
|
||||
if(resp =~ /^User-Agent:\s*(.*)$/i)
|
||||
agent = "agent='#{$1.strip}' "
|
||||
end
|
||||
|
||||
if(resp =~ /^Allow:\s+(.*)$/i)
|
||||
verbs = "verbs='#{$1.strip}' "
|
||||
end
|
||||
|
||||
if(resp =~ /^Server:\s+(.*)$/)
|
||||
serv = "server='#{$1.strip}' "
|
||||
end
|
||||
|
||||
if(resp =~ /^Proxy-Require:\s+(.*)$/)
|
||||
serv = "proxy-required='#{$1.strip}' "
|
||||
end
|
||||
|
||||
print_status("#{rhost} #{rcode} #{agent}#{serv}#{prox}#{verbs}")
|
||||
|
||||
report_service(
|
||||
:host => rhost,
|
||||
:port => rport,
|
||||
:proto => 'tcp',
|
||||
:name => 'sip'
|
||||
)
|
||||
|
||||
if(not agent.empty?)
|
||||
report_note(
|
||||
:host => rhost,
|
||||
:type => 'sip_useragent',
|
||||
:data => agent
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def create_probe(ip)
|
||||
suser = Rex::Text.rand_text_alphanumeric(rand(8)+1)
|
||||
shost = Rex::Socket.source_address(ip)
|
||||
src = "#{shost}:#{datastore['RPORT']}"
|
||||
|
||||
data = "OPTIONS sip:#{datastore['TO']}@#{ip} SIP/2.0\r\n"
|
||||
data << "Via: SIP/2.0/TCP #{src};branch=z9hG4bK.#{"%.8x" % rand(0x100000000)};rport;alias\r\n"
|
||||
data << "From: sip:#{suser}@#{src};tag=70c00e8c\r\n"
|
||||
data << "To: sip:#{datastore['TO']}@#{ip}\r\n"
|
||||
data << "Call-ID: #{rand(0x100000000)}@#{shost}\r\n"
|
||||
data << "CSeq: 1 OPTIONS\r\n"
|
||||
data << "Contact: sip:#{suser}@#{src}\r\n"
|
||||
data << "Max-Forwards: 20\r\n"
|
||||
data << "User-Agent: #{suser}\r\n"
|
||||
data << "Accept: text/plain\r\n"
|
||||
data << "Content-Length: 0\r\n"
|
||||
data << "\r\n"
|
||||
data
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/proto/sip/response'
|
||||
|
||||
describe 'Rex::Proto::SIP::Response parsing' do
|
||||
describe 'Parses vaild responses correctly' do
|
||||
specify do
|
||||
resp = 'SIP/1.0 123 Sure, OK'
|
||||
r = ::Rex::Proto::SIP::Response.parse(resp)
|
||||
r.status_line.should eq(resp)
|
||||
r.version.should eq('1.0')
|
||||
r.code.should eq('123')
|
||||
r.message.should eq('Sure, OK')
|
||||
r.headers.should be_nil
|
||||
end
|
||||
|
||||
specify do
|
||||
resp = "SIP/2.0 200 OK\r\nFoo: bar\r\nBlah: 0\r\nFoO: blaf\r\n"
|
||||
r = ::Rex::Proto::SIP::Response.parse(resp)
|
||||
r.status_line.should eq('SIP/2.0 200 OK')
|
||||
r.version.should eq('2.0')
|
||||
r.code.should eq('200')
|
||||
r.message.should eq('OK')
|
||||
r.headers.should eq('Foo' => %w(bar), 'Blah' => %w(0), 'FoO' => %w(blaf))
|
||||
r.header('Foo').should eq %w(bar blaf)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Parses invalid responses correctly' do
|
||||
[
|
||||
'',
|
||||
'aldkjfakdjfasdf',
|
||||
'SIP/foo 200 OK',
|
||||
'SIP/2.0 foo OK'
|
||||
].each do |r|
|
||||
it 'Should fail to parse an invalid response' do
|
||||
expect { ::Rex::Proto::SIP::Response.parse(r) }.to raise_error(ArgumentError, /status/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ require 'time'
|
|||
|
||||
CHECK_OLD_RUBIES = !!ENV['MSF_CHECK_OLD_RUBIES']
|
||||
SUPPRESS_INFO_MESSAGES = !!ENV['MSF_SUPPRESS_INFO_MESSAGES']
|
||||
ENCODING_REGEX = /^# (?:\-\*\- )?encoding:\s*(\S+)/
|
||||
|
||||
if CHECK_OLD_RUBIES
|
||||
require 'rvm'
|
||||
|
@ -109,6 +110,27 @@ class Msftidy
|
|||
end
|
||||
end
|
||||
|
||||
# Check that modules don't have any encoding comment and that
|
||||
# non-modules have an explicity binary encoding comment
|
||||
def check_encoding
|
||||
# coding/encoding lines must be the first or second line if present
|
||||
encoding_lines = @source.lines.to_a[0,2].select { |l| l =~ ENCODING_REGEX }
|
||||
if @full_filepath =~ /(?:^|\/)modules\//
|
||||
warn('Modules do not need an encoding comment') unless encoding_lines.empty?
|
||||
else
|
||||
if encoding_lines.empty?
|
||||
warn('Non-modules must have an encoding comment')
|
||||
else
|
||||
encoding_line = encoding_lines.first
|
||||
encoding_line =~ ENCODING_REGEX
|
||||
encoding_type = Regexp.last_match(1)
|
||||
unless encoding_type == 'binary'
|
||||
warn("Non-modules must have a binary encoding comment, not #{encoding_type}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_shebang
|
||||
if @source.lines.first =~ /^#!/
|
||||
warn("Module should not have a #! line")
|
||||
|
@ -583,6 +605,7 @@ def run_checks(full_filepath)
|
|||
tidy = Msftidy.new(full_filepath)
|
||||
tidy.check_mode
|
||||
tidy.check_shebang
|
||||
tidy.check_encoding
|
||||
tidy.check_nokogiri
|
||||
tidy.check_rubygems
|
||||
tidy.check_ref_identifiers
|
||||
|
|
Loading…
Reference in New Issue