CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/msf/core/auxiliary/nmap.rb
Views: 11784
1
# -*- coding: binary -*-
2
require 'open3'
3
4
module Msf
5
6
###
7
#
8
# This module provides methods for interacting with nmap.
9
# Modules that include this should define their own nmap_build_args()
10
# function, and usually should have some method for dealing with
11
# the data yielded from nmap_hosts(). See auxiliary/scanner/oracle/oracle_login
12
# for an example implementation.
13
#
14
###
15
16
module Auxiliary::Nmap
17
18
attr_accessor :nmap_args, :nmap_bin, :nmap_log
19
attr_reader :nmap_pid, :nmap_ver
20
21
def initialize(info = {})
22
super
23
24
register_options([
25
Opt::RHOSTS,
26
OptBool.new('NMAP_VERBOSE', [ false, 'Display nmap output', true]),
27
OptString.new('RPORTS', [ false, 'Ports to target']), # RPORT supersedes RPORTS
28
], Auxiliary::Nmap)
29
30
deregister_options("RPORT")
31
@nmap_args = []
32
@nmap_bin = nmap_binary_path
33
end
34
35
def rports
36
datastore['RPORTS']
37
end
38
39
def rport
40
datastore['RPORT']
41
end
42
43
def set_nmap_cmd
44
self.nmap_bin || (raise "Cannot locate nmap binary")
45
nmap_set_log
46
nmap_add_ports
47
nmap_cmd = [self.nmap_bin]
48
self.nmap_args.unshift("-oX #{self.nmap_log[1]}")
49
nmap_cmd << self.nmap_args.join(" ")
50
nmap_cmd << datastore['RHOSTS']
51
nmap_cmd.join(" ")
52
end
53
54
def get_nmap_ver
55
self.nmap_bin || (raise "Cannot locate nmap binary")
56
res = ""
57
nmap_cmd = [self.nmap_bin]
58
nmap_cmd << "--version"
59
res << %x{#{nmap_cmd.join(" ")}} rescue nil
60
res.gsub(/[\x0d\x0a]/n,"")
61
end
62
63
# Takes a version string in the form of Major.Minor and compares to
64
# the found version. It yells at you specifically if you try to
65
# compare a float b/c that's going to be a super common error.
66
# Comparing an Integer is okay, though.
67
def nmap_version_at_least?(test_ver=nil)
68
raise ArgumentError, "Cannot compare a Float, use a String or Integer" if test_ver.kind_of? Float
69
unless test_ver.to_s[/^([0-9]+(\x2e[0-9]+)?)/n]
70
raise ArgumentError, "Bad Nmap comparison version: #{test_ver.inspect}"
71
end
72
test_ver_str = test_ver.to_s
73
tnum_arr = $1.split(/\x2e/n)[0,2].map {|x| x.to_i}
74
installed_ver = get_nmap_ver()
75
vtag = installed_ver.split[2] # Should be ["Nmap", "version", "X.YZTAG", "(", "http..", ")"]
76
return false if (vtag.nil? || vtag.empty?)
77
return false unless (vtag =~ /^([0-9]+\x2e[0-9]+)/n) # Drop the tag.
78
inum_arr = $1.split(/\x2e/n)[0,2].map {|x| x.to_i}
79
return true if inum_arr[0] > tnum_arr[0]
80
return false if inum_arr[0] < tnum_arr[0]
81
inum_arr[1].to_i >= tnum_arr[1].to_i
82
end
83
84
def nmap_build_args
85
raise "nmap_build_args() not defined by #{self.refname}"
86
end
87
88
def nmap_run
89
nmap_cmd = set_nmap_cmd
90
begin
91
nmap_pipe = ::Open3::popen3(nmap_cmd)
92
@nmap_pid = nmap_pipe.last.pid
93
print_status "Nmap: Starting nmap with pid #{@nmap_pid}"
94
temp_nmap_threads = []
95
temp_nmap_threads << framework.threads.spawn("Module(#{self.refname})-NmapStdout", false, nmap_pipe[1]) do |np_1|
96
np_1.each_line do |nmap_out|
97
next if nmap_out.strip.empty?
98
print_status "Nmap: #{nmap_out.strip}" if datastore['NMAP_VERBOSE']
99
end
100
end
101
102
temp_nmap_threads << framework.threads.spawn("Module(#{self.refname})-NmapStderr", false, nmap_pipe[2]) do |np_2|
103
np_2.each_line do |nmap_err|
104
next if nmap_err.strip.empty?
105
print_status "Nmap: '#{nmap_err.strip}'"
106
end
107
end
108
109
temp_nmap_threads.map {|t| t.join rescue nil}
110
nmap_pipe.each {|p| p.close rescue nil}
111
if self.nmap_log[0].size.zero?
112
print_error "Nmap Warning: Output file is empty, no useful results can be processed."
113
end
114
rescue ::IOError
115
end
116
end
117
118
def nmap_binary_path
119
ret = Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe")
120
if ret
121
fullpath = ::File.expand_path(ret)
122
if fullpath =~ /\s/ # Thanks, "Program Files"
123
return "\"#{fullpath}\""
124
else
125
return fullpath
126
end
127
end
128
end
129
130
# Returns the [filehandle, pathname], and sets the same
131
# to self.nmap_log.
132
# Only supports XML format since that's the most useful.
133
def nmap_set_log
134
outfile = Rex::Quickfile.new("msf3-nmap-")
135
if Rex::Compat.is_cygwin and self.nmap_bin =~ /cygdrive/i
136
outfile_path = Rex::Compat.cygwin_to_win32(outfile.path)
137
else
138
outfile_path = outfile.path
139
end
140
self.nmap_log = [outfile,outfile_path]
141
end
142
143
def nmap_show_args
144
print_status self.nmap_args.join(" ")
145
end
146
147
def nmap_append_arg(str)
148
if nmap_validate_arg(str)
149
self.nmap_args << str
150
end
151
end
152
153
def nmap_reset_args
154
self.nmap_args = []
155
end
156
157
# A helper to add in rport or rports as a -p argument
158
def nmap_add_ports
159
if not nmap_validate_rports
160
raise "Cannot continue without a valid port list."
161
end
162
port_arg = "-p \"#{datastore['RPORT'] || rports}\""
163
if nmap_validate_arg(port_arg)
164
self.nmap_args << port_arg
165
else
166
raise "Argument is invalid"
167
end
168
end
169
170
# Validates the correctness of ports passed to nmap's -p
171
# option. Note that this will not validate named ports (like
172
# 'http'), nor will it validate when brackets are specified.
173
# The acceptable formats for this is:
174
#
175
# 80
176
# 80-90
177
# 22,23
178
# U:53,T:80
179
# and combinations thereof.
180
def nmap_validate_rports
181
# If there's an RPORT specified, use that instead.
182
if datastore['RPORT'] && (datastore['RPORT'].kind_of?(Integer) || !datastore['RPORT'].empty?)
183
return true
184
end
185
if rports.nil? || rports.empty?
186
print_error "Missing RPORTS"
187
return false
188
end
189
rports.split(/\s*,\s*/).each do |r|
190
if r =~ /^([TU]:)?[0-9]*-?[0-9]*$/
191
next
192
else
193
print_error "Malformed nmap port: #{r}"
194
return false
195
end
196
end
197
print_status "Using RPORTS range #{datastore['RPORTS']}"
198
return true
199
end
200
201
# Validates an argument to be passed on the command
202
# line to nmap. Most special characters aren't allowed,
203
# and commas in arguments are only allowed inside a
204
# quoted argument.
205
def nmap_validate_arg(str)
206
# Check for existence
207
if str.nil? || str.empty?
208
print_error "Missing nmap argument"
209
return false
210
end
211
# Check for quote balance
212
if !(str.scan(/'/).size % 2).zero? or !(str.scan(/"/).size % 2).zero?
213
print_error "Unbalanced quotes in nmap argument: #{str}"
214
return false
215
end
216
# Check for characters that enable badness
217
disallowed_characters = /([\x00-\x19\x21\x23-\x26\x28\x29\x3b\x3e\x60\x7b\x7c\x7d\x7e-\xff])/n
218
badchar = str[disallowed_characters]
219
if badchar
220
print_error "Malformed nmap arguments (contains '#{badchar}'): #{str}"
221
return false
222
end
223
# Check for commas outside of quoted arguments
224
quoted_22 = /\x22[^\x22]*\x22/n
225
requoted_str = str.tr('\'','"')
226
if requoted_str.split(quoted_22).join[/,/]
227
print_error "Malformed nmap arguments (unquoted comma): #{str}"
228
return false
229
end
230
return true
231
end
232
233
# Takes a block, and yields back the host object as discovered
234
# by the Rex::Parser::NmapXMLStreamParser. It's up to the
235
# module to ferret out whatever's interesting in this host
236
# object.
237
def nmap_hosts(&block)
238
@nmap_bin || (raise "Cannot locate the nmap binary.")
239
fh = self.nmap_log[0]
240
nmap_data = fh.read(fh.stat.size)
241
# fh.unlink
242
if Rex::Parser.nokogiri_loaded && framework.db.active
243
wspace = framework.db.find_workspace(datastore['WORKSPACE'])
244
wspace ||= framework.db.workspace
245
import_args = { :data => nmap_data, :workspace => wspace }
246
framework.db.import_nmap_noko_stream(import_args) { |type, data| yield type, data }
247
else
248
nmap_parser = Rex::Parser::NmapXMLStreamParser.new
249
nmap_parser.on_found_host = Proc.new { |h|
250
if (h["addrs"].has_key?("ipv4"))
251
addr = h["addrs"]["ipv4"]
252
elsif (h["addrs"].has_key?("ipv6"))
253
addr = h["addrs"]["ipv6"]
254
else
255
# Can't do much with it if it doesn't have an IP
256
next
257
end
258
yield h
259
}
260
REXML::Document.parse_stream(nmap_data, nmap_parser)
261
end
262
end
263
264
#Saves the data from the nmap scan to a file in the MSF::Config.local_directory
265
def nmap_save()
266
print_status "Nmap: saving nmap log file"
267
fh = self.nmap_log[0]
268
nmap_data = fh.read(fh.stat.size)
269
saved_path = store_local("nmap.scan.xml", "text/xml", nmap_data, "nmap_#{Time.now.utc.to_i}.xml")
270
print_status "Saved NMAP XML results to #{saved_path}"
271
end
272
273
end
274
end
275
276
277