Split SIP response parsing out on its own, add unit tests.

Passes rspec but fails in framework. WIP.
This commit is contained in:
Jon Hart 2014-07-18 10:29:14 -07:00
parent 69aa56d8d3
commit 02e41c27e7
7 changed files with 152 additions and 41 deletions

View File

@ -1,4 +1,5 @@
# encoding: UTF-8
# SIP protocol support
require 'rex/proto/sip/response'
require 'rex/proto/sip/util'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = <<EOF
H1: v1
H2: v2
H3: v3
H2: v21
EOF
expected_headers = { 'H1' => %w(v1), 'H2' => %w(v2 v21), 'H3' => %w(v3) }
specify { extract_headers(response_with_headers).should == expected_headers }
end
end