Path: blob/master/modules/auxiliary/scanner/discovery/udp_probe.rb
19567 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'English'6require 'openssl'78class MetasploitModule < Msf::Auxiliary9include Msf::Auxiliary::Report10include Msf::Auxiliary::Scanner1112def initialize13super(14'Name' => 'UDP Service Prober',15'Description' => 'Detect common UDP services using sequential probes.',16'Author' => 'hdm',17'License' => MSF_LICENSE,18'Notes' => {19'Stability' => [CRASH_SAFE],20'SideEffects' => [IOC_IN_LOGS],21'Reliability' => []22}23)2425register_options(26[27Opt::CHOST,28]29)3031register_advanced_options(32[33OptBool.new('RANDOMIZE_PORTS', [false, 'Randomize the order the ports are probed', true])34]35)3637# Initialize the probes array38@probes = []3940# Add the UDP probe method names41@probes << 'probe_pkt_dns'42@probes << 'probe_pkt_netbios'43@probes << 'probe_pkt_portmap'44@probes << 'probe_pkt_mssql'45@probes << 'probe_pkt_ntp'46@probes << 'probe_pkt_snmp1'47@probes << 'probe_pkt_snmp2'48@probes << 'probe_pkt_sentinel'49@probes << 'probe_pkt_db2disco'50@probes << 'probe_pkt_citrix'51@probes << 'probe_pkt_pca_st'52@probes << 'probe_pkt_pca_nq'53@probes << 'probe_chargen'54end5556def setup57super5859if datastore['RANDOMIZE_PORTS']60@probes = @probes.sort_by { rand }61end62end6364# Fingerprint a single host65def run_host(ip)66@results = {}67@thost = ip6869begin70udp_sock = nil7172@probes.each do |probe|73# Send each probe to each host7475data, port = send(probe, ip)76@tport = port7778# Create an unbound UDP socket if no CHOST is specified, otherwise79# create a UDP socket bound to CHOST (in order to avail of pivoting)80udp_sock = Rex::Socket::Udp.create({81'LocalHost' => datastore['CHOST'] || nil,82'PeerHost' => ip, 'PeerPort' => port,83'Context' => { 'Msf' => framework, 'MsfExploit' => self }84})8586udp_sock.put(data)8788r = udp_sock.recvfrom(65535, 0.1) and r[1]89parse_reply(r) if r90rescue ::Interrupt91raise $ERROR_INFO92rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused, ::IOError93nil94ensure95udp_sock.close if udp_sock96end97rescue ::Interrupt98raise $ERROR_INFO99rescue StandardError => e100print_error("Unknown error: #{@thost}:#{@tport} #{e.class} #{e} #{e.backtrace}")101end102103@results.each_key do |k|104next if !@results[k].respond_to?('keys')105106data = @results[k]107108next unless inside_workspace_boundary?(data[:host])109110conf = {111host: data[:host],112port: data[:port],113proto: 'udp',114name: data[:app],115info: data[:info]116}117118if data[:hname]119conf[:host_name] = data[:hname].downcase120end121122if data[:mac]123conf[:mac] = data[:mac].downcase124end125126report_service(conf)127print_good("Discovered #{data[:app]} on #{k} (#{data[:info]})")128end129end130131#132# The response parsers133#134def parse_reply(pkt)135# Ignore "empty" packets136return if !pkt[1]137138if (pkt[1] =~ /^::ffff:/)139pkt[1] = pkt[1].sub(/^::ffff:/, '')140end141142app = 'unknown'143inf = ''144maddr = nil145hname = nil146147hkey = "#{pkt[1]}:#{pkt[2]}"148149# Work with protocols that return different data in different packets150# These are reported at the end of the scanning loop to build state151case pkt[2]152when 5632153154@results[hkey] ||= {}155data = @results[hkey]156data[:app] = 'pcAnywhere_stat'157data[:port] = pkt[2]158data[:host] = pkt[1]159160case pkt[0]161162when /^NR(........................)(........)/163name = ::Regexp.last_match(1).dup164caps = ::Regexp.last_match(2).dup165name = name.gsub(/_+$/, '').gsub("\x00", '').strip166caps = caps.gsub(/_+$/, '').gsub("\x00", '').strip167data[:name] = name168data[:caps] = caps169170when /^ST(.+)/171buff = ::Regexp.last_match(1).dup172stat = 'Unknown'173174if buff[2, 1].unpack('C')[0] == 67175stat = 'Available'176end177178if buff[2, 1].unpack('C')[0] == 11179stat = 'Busy'180end181182data[:stat] = stat183end184185if data[:name]186inf << "Name: #{data[:name]} "187end188189if data[:stat]190inf << "- #{data[:stat]} "191end192193if data[:caps]194inf << "( #{data[:caps]} ) "195end196data[:info] = inf197end198199# Ignore duplicates for the protocols below200return if @results[hkey]201202case pkt[2]203204when 19205app = 'chargen'206return unless chargen_parse(pkt[0])207208@results[hkey] = true209210when 53211app = 'DNS'212ver = nil213214if !ver && pkt[0] =~ %r{([6789]\.[\w.\-_:()\[\]/=+|{}]+)}i215ver = 'BIND ' + ::Regexp.last_match(1)216end217218ver = 'Microsoft DNS' if !ver && (pkt[0][2, 4] == "\x81\x04\x00\x01")219ver = 'TinyDNS' if !ver && (pkt[0][2, 4] == "\x81\x81\x00\x01")220221ver = pkt[0].unpack('H*')[0] if !ver222inf = ver if ver223@results[hkey] = true224225when 137226app = 'NetBIOS'227228data = pkt[0]229230head = data.slice!(0, 12)231232_, _, quests, answers, = head.unpack('n6')233return if quests != 0234return if answers == 0235236data.slice!(0, 34)237rtype, _, _, rlen = data.slice!(0, 10).unpack('nnNn')238buff = data.slice!(0, rlen)239240names = []241242case rtype243when 0x21244rcnt = buff.slice!(0, 1).unpack('C')[0]2451.upto(rcnt) do246tname = buff.slice!(0, 15).gsub(/\x00.*/, '').strip247ttype = buff.slice!(0, 1).unpack('C')[0]248tflag = buff.slice!(0, 2).unpack('n')[0]249names << [ tname, ttype, tflag ]250end251maddr = buff.slice!(0, 6).unpack('C*').map { |c| '%.2x' % c }.join(':')252253names.each do |n|254inf << n[0]255inf << ':<%.2x>' % n[1]256if (n[2] & 0x8000 == 0)257inf << ':U :'258else259inf << ':G :'260end261end262inf << maddr263264if !names.empty?265hname = names[0][0]266end267end268269@results[hkey] = true270271when 111272app = 'Portmap'273buf = pkt[0]274inf = ''275buf.slice!(0, 24)276svc = []277while (buf.length >= 20)278rec = buf.slice!(0, 20).unpack('N5')279svc << "#{rec[1]} v#{rec[2]} #{rec[3] == 0x06 ? 'TCP' : 'UDP'}(#{rec[4]})"280report_service(281host: pkt[1],282port: rec[4],283proto: (rec[3] == 0x06 ? 'tcp' : 'udp'),284name: 'sunrpc',285info: "#{rec[1]} v#{rec[2]}"286)287end288inf = svc.join(', ')289@results[hkey] = true290291when 123292app = 'NTP'293ver = pkt[0].unpack('H*')[0]294ver = 'NTP v3' if (ver =~ /^1c06|^1c05/)295ver = 'NTP v4' if (ver =~ /^240304/)296ver = 'NTP v4 (unsynchronized)' if (ver =~ /^e40/)297ver = 'Microsoft NTP' if (ver =~ /^dc00|^dc0f/)298inf = ver if ver299@results[hkey] = true300301when 1434302app = 'MSSQL'303mssql_ping_parse(pkt[0]).each_pair do |k, v|304inf += k + '=' + v + ' '305end306@results[hkey] = true307308when 161309app = 'SNMP'310311asn = begin312OpenSSL::ASN1.decode(pkt[0])313rescue StandardError314nil315end316return if !asn317318snmp_error = begin319asn.value[0].value320rescue StandardError321nil322end323snmp_comm = begin324asn.value[1].value325rescue StandardError326nil327end328snmp_data = begin329asn.value[2].value[3].value[0]330rescue StandardError331nil332end333snmp_oid = begin334snmp_data.value[0].value335rescue StandardError336nil337end338snmp_info = begin339snmp_data.value[1].value340rescue StandardError341nil342end343344return if !(snmp_error && snmp_comm && snmp_data && snmp_oid && snmp_info)345346snmp_info = snmp_info.to_s.gsub(/\s+/, ' ')347348inf = snmp_info349@results[hkey] = true350351when 5093352app = 'Sentinel'353@results[hkey] = true354355when 523356app = 'ibm-db2'357inf = db2disco_parse(pkt[0])358@results[hkey] = true359360when 1604361app = 'citrix-ica'362return unless citrix_parse(pkt[0])363364@results[hkey] = true365366end367368return unless inside_workspace_boundary?(pkt[1])369370report_service(371host: pkt[1],372mac: (maddr && (maddr != '00:00:00:00:00:00')) ? maddr : nil,373host_name: hname ? hname.downcase : nil,374port: pkt[2],375proto: 'udp',376name: app,377info: inf378)379380print_good("Discovered #{app} on #{pkt[1]}:#{pkt[2]} (#{inf})")381end382383#384# Parse a db2disco packet.385#386def db2disco_parse(data)387res = data.split("\x00")388"#{res[2]}_#{res[1]}"389end390391#392# Validate a chargen packet.393#394def chargen_parse(data)395data =~ /ABCDEFGHIJKLMNOPQRSTUVWXYZ|0123456789/i396end397398#399# Validate this is truly Citrix ICA; returns true or false.400#401def citrix_parse(data)402server_response = "\x30\x00\x02\x31\x02\xfd\xa8\xe3\x02\x00\x06\x44" # Server hello response403data =~ /^#{server_response}/404end405406#407# Parse a 'ping' response and format as a hash408#409def mssql_ping_parse(data)410res = {}411var = nil412idx = data.index('ServerName')413return res if !idx414415data[idx, data.length - idx].split(';').each do |d|416if !var417var = d418elsif !var.empty?419res[var] = d420var = nil421end422end423424return res425end426427#428# The probe definitions429#430431def probe_chargen(_ip)432pkt = Rex::Text.rand_text_alpha_lower(1)433return [pkt, 19]434end435436def probe_pkt_dns(_ip)437data = [rand(0xffff)].pack('n') +438"\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" \439"\x07" + 'VERSION' \440"\x04" + 'BIND' \441"\x00\x00\x10\x00\x03"442443return [data, 53]444end445446def probe_pkt_netbios(_ip)447data =448[rand(0xffff)].pack('n') +449"\x00\x00\x00\x01\x00\x00\x00\x00" \450"\x00\x00\x20\x43\x4b\x41\x41\x41" \451"\x41\x41\x41\x41\x41\x41\x41\x41" \452"\x41\x41\x41\x41\x41\x41\x41\x41" \453"\x41\x41\x41\x41\x41\x41\x41\x41" \454"\x41\x41\x41\x00\x00\x21\x00\x01"455456return [data, 137]457end458459def probe_pkt_portmap(_ip)460data =461[462rand(0xffffffff), # XID4630, # Type4642, # RPC Version465100000, # Program ID4662, # Program Version4674, # Procedure4680, 0, # Credentials4690, 0, # Verifier470].pack('N*')471472return [data, 111]473end474475def probe_pkt_mssql(_ip)476return ["\x02", 1434]477end478479def probe_pkt_ntp(_ip)480data =481"\xe3\x00\x04\xfa\x00\x01\x00\x00\x00\x01\x00\x00\x00" \482"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \483"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \484"\x00\xc5\x4f\x23\x4b\x71\xb1\x52\xf3"485return [data, 123]486end487488def probe_pkt_sentinel(_ip)489return ["\x7a\x00\x00\x00\x00\x00", 5093]490end491492def probe_pkt_snmp1(_ip)493version = 1494data = OpenSSL::ASN1::Sequence([495OpenSSL::ASN1::Integer(version - 1),496OpenSSL::ASN1::OctetString('public'),497OpenSSL::ASN1::Set.new([498OpenSSL::ASN1::Integer(rand(0x80000000)),499OpenSSL::ASN1::Integer(0),500OpenSSL::ASN1::Integer(0),501OpenSSL::ASN1::Sequence([502OpenSSL::ASN1::Sequence([503OpenSSL::ASN1.ObjectId('1.3.6.1.2.1.1.1.0'),504OpenSSL::ASN1.Null(nil)505])506]),507], 0, :IMPLICIT)508]).to_der509[data, 161]510end511512def probe_pkt_snmp2(_ip)513version = 2514data = OpenSSL::ASN1::Sequence([515OpenSSL::ASN1::Integer(version - 1),516OpenSSL::ASN1::OctetString('public'),517OpenSSL::ASN1::Set.new([518OpenSSL::ASN1::Integer(rand(0x80000000)),519OpenSSL::ASN1::Integer(0),520OpenSSL::ASN1::Integer(0),521OpenSSL::ASN1::Sequence([522OpenSSL::ASN1::Sequence([523OpenSSL::ASN1.ObjectId('1.3.6.1.2.1.1.1.0'),524OpenSSL::ASN1.Null(nil)525])526]),527], 0, :IMPLICIT)528]).to_der529[data, 161]530end531532def probe_pkt_db2disco(_ip)533data = "DB2GETADDR\x00SQL05000\x00"534[data, 523]535end536537# Server hello packet from citrix_published_bruteforce538def probe_pkt_citrix(_ip)539data =540"\x1e\x00\x01\x30\x02\xfd\xa8\xe3\x00\x00\x00\x00\x00" \541"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \542"\x00\x00\x00\x00"543return [data, 1604]544end545546def probe_pkt_pca_st(_ip)547return ['ST', 5632]548end549550def probe_pkt_pca_nq(_ip)551return ['NQ', 5632]552end553end554555556