From 02e41c27e70f70809ae6c592f72c89e7d5b347fe Mon Sep 17 00:00:00 2001 From: Jon Hart Date: Fri, 18 Jul 2014 10:29:14 -0700 Subject: [PATCH] Split SIP response parsing out on its own, add unit tests. Passes rspec but fails in framework. WIP. --- lib/rex/proto/sip.rb | 1 + lib/rex/proto/sip/response.rb | 35 +++++++++ lib/rex/proto/sip/util.rb | 79 ++++++++++++-------- modules/auxiliary/scanner/sip/options.rb | 4 +- modules/auxiliary/scanner/sip/options_tcp.rb | 18 +++-- spec/lib/rex/proto/sip/response_spec.rb | 37 +++++++++ spec/lib/rex/proto/sip/util_spec.rb | 19 +++++ 7 files changed, 152 insertions(+), 41 deletions(-) create mode 100644 lib/rex/proto/sip/response.rb create mode 100644 spec/lib/rex/proto/sip/response_spec.rb create mode 100644 spec/lib/rex/proto/sip/util_spec.rb diff --git a/lib/rex/proto/sip.rb b/lib/rex/proto/sip.rb index 9fd3fa65ec..165565872d 100644 --- a/lib/rex/proto/sip.rb +++ b/lib/rex/proto/sip.rb @@ -1,4 +1,5 @@ # encoding: UTF-8 # SIP protocol support +require 'rex/proto/sip/response' require 'rex/proto/sip/util' diff --git a/lib/rex/proto/sip/response.rb b/lib/rex/proto/sip/response.rb new file mode 100644 index 0000000000..a817115b43 --- /dev/null +++ b/lib/rex/proto/sip/response.rb @@ -0,0 +1,35 @@ +# encoding: UTF-8 + +require 'rex/proto/sip/util' + +module Rex + module Proto + # SIP protocol support + module SIP + SIP_STATUS_REGEX = /^SIP\/(\d\.\d) (\d{3})\s*(.*)$/ + + # Represents a SIP response message + class Response + attr_accessor :version, :code, :message, :headers + + def header(name) + @headers.select { |k, _| k.downcase == name.downcase }.last + end + + def self.parse(data) + response = Response.new + # do some basic sanity checking on this response to ensure that it is SIP + status_line = data.split(/\r\n/)[0] + unless status_line && status_line =~ SIP_STATUS_REGEX + fail(ArgumentError, 'Does not start with a valid SIP status line') + end + response.version = Regexp.last_match(1) + response.code = Regexp.last_match(2) + response.message = Regexp.last_match(3) + response.headers = ::Rex::Proto::SIP.extract_headers(data) + response + end + end + end + end +end diff --git a/lib/rex/proto/sip/util.rb b/lib/rex/proto/sip/util.rb index a7d90a8f0f..969a668e83 100644 --- a/lib/rex/proto/sip/util.rb +++ b/lib/rex/proto/sip/util.rb @@ -1,32 +1,35 @@ # encoding: UTF-8 +require 'rex/proto/sip/response' + module Rex module Proto # SIP protocol support module SIP - # Returns a list of the values for all instances of header_name from the - # response, or nil if that header was not found - def extract_header_values(resp, header_name) - values = resp.scan(/^#{header_name}:\s*(.*)$/i) - return nil if values.empty? - values.flatten.map { |v| v.strip }.sort + # Returns a hash of header name to values mapping + # from the provided message, or nil if no headers + # are found + def 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 - # Parses +resp+, extracts useful metdata and then reports on it - def parse_reply(resp, proto) - rcode = resp.split(/\s+/)[0] - # Extract the interesting headers - metadata = { - 'agent' => extract_header_values(resp, 'User-Agent'), - 'verbs' => extract_header_values(resp, 'Allow'), - 'server' => extract_header_values(resp, 'Server'), - 'proxy' => extract_header_values(resp, 'Proxy-Require') - } - # Drop any that we don't retrieve - metadata.delete_if { |_, v| v.nil? } - - print_status("#{rhost} #{rcode} #{metadata}") + # Parses +response+, extracts useful metdata and then reports on it + def parse_response(response, proto, desired_headers = %w(User-Agent Server)) + endpoint = "#{rhost}:#{rport}/#{proto}" + begin + options_response = Rex::Proto::SIP::Response.parse(response) + rescue ArgumentError => e + vprint_error("#{endpoint} is not SIP: #{e}") + end + # We know it is SIP, so report report_service( host: rhost, port: rport, @@ -34,17 +37,33 @@ module Rex name: 'sip' ) - report_note( - host: rhost, - type: 'sip_useragent', - data: metadata['agent'] - ) if metadata['agent'] + # Do header extraction as necessary + extracted_headers = {} + unless desired_headers.nil? || desired_headers.empty? + options_response.headers.select { |k, _| desired_headers.any? { |h| h.downcase == k.downcase } }.each do |header| + name = header.first.downcase + values = header.last + extracted_headers[name] ||= [] + extracted_headers[name] << values + end - report_note( - host: rhost, - type: 'sip_server', - data: metadata['server'] - ) if metadata['server'] + # report on any extracted headers + extracted_headers.each do |k, v| + report_note( + host: rhost, + port: rport, + proto: proto, + type: "sip_#{k}", + data: v + ) + end + end + + if extracted_headers.empty? + print_status("#{endpoint} #{version} #{status}") + else + print_status("#{endpoint} #{version} #{status}: #{extracted_headers}") + end end def create_probe(ip, proto) diff --git a/modules/auxiliary/scanner/sip/options.rb b/modules/auxiliary/scanner/sip/options.rb index d1eb21be51..5d899f7711 100644 --- a/modules/auxiliary/scanner/sip/options.rb +++ b/modules/auxiliary/scanner/sip/options.rb @@ -42,11 +42,9 @@ class Metasploit3 < Msf::Auxiliary # 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| @@ -91,6 +89,6 @@ class Metasploit3 < Msf::Auxiliary pkt[1].sub!(/^::ffff:/, '') resp = pkt[0].split(/\s+/)[1] - parse_reply(resp, 'udp') + parse_response(resp, 'udp') end end diff --git a/modules/auxiliary/scanner/sip/options_tcp.rb b/modules/auxiliary/scanner/sip/options_tcp.rb index 550fb576f8..11da563dcd 100644 --- a/modules/auxiliary/scanner/sip/options_tcp.rb +++ b/modules/auxiliary/scanner/sip/options_tcp.rb @@ -31,13 +31,15 @@ class Metasploit3 < Msf::Auxiliary # Operate on a single system at a time def run_host(ip) - connect - sock.put(create_probe(ip, 'TCP')) - res = sock.get_once(-1, 5) - parse_reply(res, 'tcp') if res - rescue ::Interrupt - raise $ERROR_INFO - ensure - disconnect + begin + connect + sock.put(create_probe(ip, 'TCP')) + res = sock.get_once(-1, 5) + parse_response(res, 'tcp') if res + rescue ::Interrupt + raise $ERROR_INFO + ensure + disconnect + end 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..4a3fbc1949 --- /dev/null +++ b/spec/lib/rex/proto/sip/response_spec.rb @@ -0,0 +1,37 @@ +# encoding: UTF-8 + +require 'rex/proto/sip/response' +include Rex::Proto::SIP + +describe 'Rex::Proto::SIP::Response parsing' do + describe 'Parses vaild responses correctly' do + specify do + r = Response.parse('SIP/1.0 123 Sure, OK') + 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 + r = Response.parse("SIP/2.0 200 OK\r\nFoo: bar\r\nBlah: 0\r\n") + 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)) + 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 { Response.parse(r) }.to raise_error(ArgumentError, /status/) + end + end + end +end diff --git a/spec/lib/rex/proto/sip/util_spec.rb b/spec/lib/rex/proto/sip/util_spec.rb new file mode 100644 index 0000000000..5e655995dd --- /dev/null +++ b/spec/lib/rex/proto/sip/util_spec.rb @@ -0,0 +1,19 @@ +# encoding: UTF-8 + +require 'rex/proto/sip/util' +include Rex::Proto::SIP + +describe 'Rex::Proto::SIP SIP utility methods' do + describe 'Extracts headers correctly' do + headerless_response = 'Look, no headers' + specify { extract_headers(headerless_response).should be_nil } + response_with_headers = < %w(v1), 'H2' => %w(v2 v21), 'H3' => %w(v3) } + specify { extract_headers(response_with_headers).should == expected_headers } + end +end