diff --git a/.rubocop.yml b/.rubocop.yml index 3ae05511be..c9ba4d1bb3 100644 --- a/.rubocop.yml +++ b/.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 []' \ No newline at end of file + 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/**/*' diff --git a/lib/msf/core/exploit/mixins.rb b/lib/msf/core/exploit/mixins.rb index 9f2fdd9669..c5e2b0fc1b 100644 --- a/lib/msf/core/exploit/mixins.rb +++ b/lib/msf/core/exploit/mixins.rb @@ -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' diff --git a/lib/msf/core/exploit/sip.rb b/lib/msf/core/exploit/sip.rb new file mode 100644 index 0000000000..093f47221c --- /dev/null +++ b/lib/msf/core/exploit/sip.rb @@ -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 diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 598d64ef8d..25d8590752 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -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) diff --git a/lib/rex/proto/sip.rb b/lib/rex/proto/sip.rb new file mode 100644 index 0000000000..3c0098cb54 --- /dev/null +++ b/lib/rex/proto/sip.rb @@ -0,0 +1,4 @@ +# encoding: binary + +# SIP protocol support +require 'rex/proto/sip/response' diff --git a/lib/rex/proto/sip/response.rb b/lib/rex/proto/sip/response.rb new file mode 100644 index 0000000000..3135052471 --- /dev/null +++ b/lib/rex/proto/sip/response.rb @@ -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 diff --git a/modules/auxiliary/scanner/sip/options.rb b/modules/auxiliary/scanner/sip/options.rb index b8cd0d8a44..ebf466ff4c 100644 --- a/modules/auxiliary/scanner/sip/options.rb +++ b/modules/auxiliary/scanner/sip/options.rb @@ -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 diff --git a/modules/auxiliary/scanner/sip/options_tcp.rb b/modules/auxiliary/scanner/sip/options_tcp.rb index d4fb1f0b56..60746418be 100644 --- a/modules/auxiliary/scanner/sip/options_tcp.rb +++ b/modules/auxiliary/scanner/sip/options_tcp.rb @@ -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 diff --git a/spec/lib/rex/proto/sip/response_spec.rb b/spec/lib/rex/proto/sip/response_spec.rb new file mode 100644 index 0000000000..2f9715bd93 --- /dev/null +++ b/spec/lib/rex/proto/sip/response_spec.rb @@ -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 diff --git a/tools/msftidy.rb b/tools/msftidy.rb index cee49583d0..ff6affc211 100755 --- a/tools/msftidy.rb +++ b/tools/msftidy.rb @@ -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