301 lines
7.1 KiB
Ruby
301 lines
7.1 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Auxiliary::Scanner
|
|
include Msf::Exploit::Capture
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'URGENT/11 Scanner, Based on Detection Tool by Armis',
|
|
'Description' => %q{
|
|
This module detects VxWorks and the IPnet IP stack, along with devices
|
|
vulnerable to CVE-2019-12258.
|
|
},
|
|
'Author' => [
|
|
'Ben Seri', # Upstream tool
|
|
'Brent Cook', # Metasploit module
|
|
'wvu' # Metasploit module
|
|
],
|
|
'References' => [
|
|
['CVE', '2019-12258'],
|
|
['URL', 'https://armis.com/urgent11'],
|
|
['URL', 'https://github.com/ArmisSecurity/urgent11-detector']
|
|
],
|
|
'DisclosureDate' => '2019-08-09', # NVD entry publication
|
|
'License' => MSF_LICENSE,
|
|
'Notes' => {'Stability' => [CRASH_SAFE]}
|
|
))
|
|
|
|
register_options([
|
|
OptString.new('RPORTS', required: true, default: "21 22 23 80 443", desc: 'Target ports for TCP detections')
|
|
])
|
|
|
|
register_advanced_options([
|
|
OptInt.new('RetransmissionRate', required: true, default: 3, desc: 'Send n TCP packets')
|
|
])
|
|
|
|
deregister_options('PCAPFILE', 'FILTER')
|
|
end
|
|
|
|
#
|
|
# Utility methods
|
|
#
|
|
|
|
def rports
|
|
datastore['RPORTS'].split(/[\s,]/).collect{|i| (i.to_i.to_s == i) ? i.to_i : nil}.compact
|
|
end
|
|
|
|
def filter(ip)
|
|
"src host #{ip} and dst host #{Rex::Socket.source_address(ip)}"
|
|
end
|
|
|
|
#
|
|
# Scanner methods
|
|
#
|
|
|
|
def run_host(ip)
|
|
# XXX: Configuring Ethernet and IP headers sends a UDP packet!
|
|
@config = PacketFu::Utils.whoami?(target: ip)
|
|
|
|
open_pcap
|
|
capture.setfilter(filter(ip))
|
|
|
|
port_open = false
|
|
rports.each do |rport|
|
|
port_open |= run_detections(ip, rport)
|
|
end
|
|
raise RuntimeError.new("No ports open on #{ip} from #{datastore['RPORTS']}") if !port_open
|
|
rescue RuntimeError => e
|
|
fail_with(Failure::BadConfig, e.message)
|
|
ensure
|
|
close_pcap
|
|
end
|
|
|
|
def detections
|
|
%w[
|
|
tcp_dos_detection
|
|
tcp_malformed_options_detection
|
|
icmp_code_detection
|
|
icmp_timestamp_detection
|
|
]
|
|
end
|
|
|
|
def run_detections(ip, port)
|
|
print_status("#{ip}:#{port} being checked")
|
|
|
|
final_ipnet_score = 0
|
|
final_vxworks_score = 0
|
|
affected_vulnerabilities = []
|
|
|
|
begin
|
|
sock = Rex::Socket::Tcp.create(
|
|
'PeerHost' => ip,
|
|
'PeerPort' => port
|
|
)
|
|
rescue
|
|
vprint_bad("Could not connect to #{ip}:#{port}, cannot verify vulnerability")
|
|
return false
|
|
end
|
|
|
|
detections.each do |detection|
|
|
@ipnet_score = 0
|
|
@vxworks_score = 0
|
|
@vulnerable_cves = []
|
|
|
|
detection_name = detection.camelize
|
|
|
|
begin
|
|
send(detection, sock, ip, port)
|
|
rescue StandardError => e
|
|
vprint_error("#{detection_name} failed: #{e.message}")
|
|
next
|
|
end
|
|
|
|
vprint_status(
|
|
"\t#{detection_name.ljust(30)}" \
|
|
"\tVxWorks: #{@vxworks_score}" \
|
|
"\tIPnet: #{@ipnet_score}"
|
|
)
|
|
|
|
final_ipnet_score += @ipnet_score
|
|
final_vxworks_score += @vxworks_score
|
|
affected_vulnerabilities += @vulnerable_cves
|
|
end
|
|
|
|
sock.close
|
|
|
|
if final_ipnet_score > 0
|
|
vprint_good("#{ip}:#{port} detected as IPnet")
|
|
elsif final_ipnet_score < 0
|
|
vprint_error("#{ip}:#{port} detected as NOT IPnet")
|
|
end
|
|
|
|
if final_vxworks_score > 100
|
|
vprint_good("#{ip}:#{port} detected as VxWorks")
|
|
elsif final_vxworks_score < 0
|
|
vprint_error("#{ip}:#{port} detected as NOT VxWorks")
|
|
end
|
|
|
|
affected_vulnerabilities.each do |vuln|
|
|
msg = "#{ip}:#{port} affected by #{vuln}"
|
|
print_good(msg)
|
|
report_vuln(
|
|
host: ip,
|
|
name: name,
|
|
refs: references,
|
|
info: msg
|
|
)
|
|
end
|
|
true
|
|
end
|
|
|
|
#
|
|
# TCP detection methods
|
|
#
|
|
|
|
def tcp_malformed_options_detection(sock, ip, port)
|
|
pkt = PacketFu::TCPPacket.new(config: @config)
|
|
|
|
# IP destination address
|
|
pkt.ip_daddr = ip
|
|
|
|
# TCP SYN with malformed options
|
|
pkt.tcp_dst = port
|
|
pkt.tcp_flags.syn = 1
|
|
pkt.tcp_opts = [2, 4, 1460].pack('CCn') + # MSS
|
|
[1].pack('C') + # NOP
|
|
[3, 2].pack('CC') + # WSCALE with invalid length
|
|
[3, 3, 0].pack('CCC') # WSCALE with valid length
|
|
pkt.recalc
|
|
|
|
res = nil
|
|
|
|
datastore['RetransmissionRate'].times do
|
|
pkt.to_w
|
|
res = inject_reply(:tcp)
|
|
|
|
break unless res
|
|
end
|
|
|
|
unless res
|
|
return @vxworks_score = 0,
|
|
@ipnet_score = 50
|
|
end
|
|
|
|
if res.tcp_flags.rst == 1 &&
|
|
res.tcp_dst == pkt.tcp_src && res.tcp_dst == pkt.tcp_src
|
|
|
|
return @vxworks_score = 100,
|
|
@ipnet_score = 100
|
|
end
|
|
|
|
return @vxworks_score = -100,
|
|
@ipnet_score = -100
|
|
end
|
|
|
|
def tcp_dos_detection(sock, ip, port)
|
|
pkt = PacketFu::TCPPacket.new(config: @config)
|
|
|
|
# IP destination address
|
|
pkt.ip_daddr = ip
|
|
|
|
# TCP SYN with malformed (truncated) WS option
|
|
pkt.tcp_src = sock.getlocalname.last
|
|
pkt.tcp_dst = sock.peerport
|
|
pkt.tcp_seq = rand(0xffffffff + 1)
|
|
pkt.tcp_ack = rand(0xffffffff + 1)
|
|
pkt.tcp_flags.syn = 1
|
|
pkt.tcp_opts = [3, 2].pack('CC') + # WSCALE with invalid length
|
|
[1, 0].pack('CC') # NOP + EOL
|
|
pkt.recalc
|
|
|
|
res = nil
|
|
|
|
datastore['RetransmissionRate'].times do
|
|
pkt.to_w
|
|
res = inject_reply(:tcp)
|
|
|
|
break unless res
|
|
end
|
|
|
|
unless res
|
|
return @vxworks_score = 0,
|
|
@ipnet_score = 0
|
|
end
|
|
|
|
if res.tcp_flags.rst == 1 &&
|
|
res.tcp_dst == pkt.tcp_src && res.tcp_dst == pkt.tcp_src
|
|
|
|
return @vxworks_score = 100,
|
|
@ipnet_score = 100,
|
|
@vulnerable_cves = ['CVE-2019-12258']
|
|
end
|
|
|
|
return @vxworks_score = 0,
|
|
@ipnet_score = 0
|
|
end
|
|
|
|
#
|
|
# ICMP detection methods
|
|
#
|
|
|
|
def icmp_code_detection(sock, ip, _port = nil)
|
|
pkt = PacketFu::ICMPPacket.new(config: @config)
|
|
|
|
# IP destination address
|
|
pkt.ip_daddr = ip
|
|
|
|
# ICMP echo request with non-zero code
|
|
pkt.icmp_type = 8
|
|
pkt.icmp_code = rand(0x01..0xff)
|
|
pkt.payload = capture_icmp_echo_pack
|
|
pkt.recalc
|
|
|
|
pkt.to_w
|
|
res = inject_reply(:icmp)
|
|
|
|
unless res
|
|
return @ipnet_score = 0
|
|
end
|
|
|
|
# Echo reply with zeroed code
|
|
if res.icmp_type == 0 && res.icmp_code == 0
|
|
return @ipnet_score = 20
|
|
end
|
|
|
|
@ipnet_score = -20
|
|
end
|
|
|
|
def icmp_timestamp_detection(sock, ip, _port = nil)
|
|
pkt = PacketFu::ICMPPacket.new(config: @config)
|
|
|
|
# IP destination address
|
|
pkt.ip_daddr = ip
|
|
|
|
# Truncated ICMP timestamp request
|
|
pkt.icmp_type = 13
|
|
pkt.icmp_code = 0
|
|
pkt.payload = "\x00" * 4
|
|
pkt.recalc
|
|
|
|
pkt.to_w
|
|
res = inject_reply(:icmp)
|
|
|
|
unless res
|
|
return @ipnet_score = 0
|
|
end
|
|
|
|
# Timestamp reply
|
|
if res.icmp_type == 14
|
|
return @ipnet_score = 90
|
|
end
|
|
|
|
@ipnet_score = -30
|
|
end
|
|
|
|
end
|