Land #3545, fix up sip scanners, msftidy, db services cmd

bug/bundler_fix
HD Moore 2014-08-26 13:59:29 -05:00
commit 4e19d9ade1
10 changed files with 248 additions and 211 deletions

View File

@ -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/**/*'

View File

@ -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'

View File

@ -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

View File

@ -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)

4
lib/rex/proto/sip.rb Normal file
View File

@ -0,0 +1,4 @@
# encoding: binary
# SIP protocol support
require 'rex/proto/sip/response'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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