| 1 | # Copyright (C) 2005 Insecure.Com LLC. |
|---|
| 2 | # |
|---|
| 3 | # Author: Adriano Monteiro Marques <py.adriano@gmail.com> |
|---|
| 4 | # |
|---|
| 5 | # This program is free software; you can redistribute it and/or modify |
|---|
| 6 | # it under the terms of the GNU General Public License as published by |
|---|
| 7 | # the Free Software Foundation; either version 2 of the License, or |
|---|
| 8 | # (at your option) any later version. |
|---|
| 9 | # |
|---|
| 10 | # This program is distributed in the hope that it will be useful, |
|---|
| 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 13 | # GNU General Public License for more details. |
|---|
| 14 | # |
|---|
| 15 | # You should have received a copy of the GNU General Public License |
|---|
| 16 | # along with this program; if not, write to the Free Software |
|---|
| 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|---|
| 18 | |
|---|
| 19 | import sys |
|---|
| 20 | import os |
|---|
| 21 | import re |
|---|
| 22 | import threading |
|---|
| 23 | |
|---|
| 24 | from tempfile import mktemp |
|---|
| 25 | from types import StringTypes |
|---|
| 26 | from subprocess import Popen, PIPE |
|---|
| 27 | |
|---|
| 28 | from umitCore.NmapOptions import NmapOptions |
|---|
| 29 | from umitCore.Paths import Path |
|---|
| 30 | from umitCore.Logging import log |
|---|
| 31 | |
|---|
| 32 | option_xml = Path.options |
|---|
| 33 | |
|---|
| 34 | # shell_state = True avoids python to open a terminal to execute nmap.exe |
|---|
| 35 | # shell_state = False is needed to run correctly at Linux |
|---|
| 36 | shell_state = (sys.platform == "win32") |
|---|
| 37 | |
|---|
| 38 | nmap_command_path = "nmap" |
|---|
| 39 | # Don't need the line below anymore |
|---|
| 40 | #if sys.platform == "win32": |
|---|
| 41 | # nmap_command_path = os.path.join(os.path.split(os.path.abspath(sys.executable))[0], "Nmap", "nmap.exe") |
|---|
| 42 | |
|---|
| 43 | log.debug(">>> Platform: %s" % sys.platform) |
|---|
| 44 | log.debug(">>> Nmap command path: %s" % nmap_command_path) |
|---|
| 45 | |
|---|
| 46 | class NmapCommand(object): |
|---|
| 47 | def __init__(self, command=None): |
|---|
| 48 | self.xml_output = mktemp() |
|---|
| 49 | self.normal_output = mktemp() |
|---|
| 50 | self.stdout_output = mktemp() |
|---|
| 51 | self.stderr_output = mktemp() |
|---|
| 52 | |
|---|
| 53 | # Creating files. Avoid troubles while running at Windows |
|---|
| 54 | open(self.xml_output,'w').close() |
|---|
| 55 | open(self.normal_output,'w').close() |
|---|
| 56 | open(self.stdout_output,'w').close() |
|---|
| 57 | open(self.stderr_output,'w').close() |
|---|
| 58 | |
|---|
| 59 | self.command_process = None |
|---|
| 60 | self.command_buffer = "" |
|---|
| 61 | self.command_stderr = "" |
|---|
| 62 | |
|---|
| 63 | if command: |
|---|
| 64 | self.command = command |
|---|
| 65 | |
|---|
| 66 | def get_command(self): |
|---|
| 67 | if type(self._command) == type(""): |
|---|
| 68 | return self._command.split() |
|---|
| 69 | return self._command |
|---|
| 70 | |
|---|
| 71 | def set_command(self, command): |
|---|
| 72 | self._command = self._verify(command) |
|---|
| 73 | |
|---|
| 74 | def _verify(self, command): |
|---|
| 75 | command = self._remove_double_space(command) |
|---|
| 76 | command = self._verify_output_options(command) |
|---|
| 77 | command[0] = nmap_command_path |
|---|
| 78 | |
|---|
| 79 | return command |
|---|
| 80 | |
|---|
| 81 | def _verify_output_options(self, command): |
|---|
| 82 | if type(command) == type([]): |
|---|
| 83 | command = " ".join(command) |
|---|
| 84 | |
|---|
| 85 | # Removing comments from command |
|---|
| 86 | for comment in re.findall('(#.*)', command): |
|---|
| 87 | command = command.replace(comment, '') |
|---|
| 88 | |
|---|
| 89 | # Removing output options that user may have set away from command |
|---|
| 90 | found = re.findall('(-o[XGASN]{1}) {0,1}', command) |
|---|
| 91 | |
|---|
| 92 | # Adequating double quoted options |
|---|
| 93 | splited = [x.replace("\"", "") for x in re.findall('([\w\-\,\./]+|".*")', command)] |
|---|
| 94 | #splited = command.split(' ') |
|---|
| 95 | |
|---|
| 96 | if found: |
|---|
| 97 | for option in found: |
|---|
| 98 | pos = splited.index(option) |
|---|
| 99 | del(splited[pos+1]) |
|---|
| 100 | del(splited[pos]) |
|---|
| 101 | |
|---|
| 102 | # Saving the XML output to a temporary file |
|---|
| 103 | splited.append('-oX') |
|---|
| 104 | splited.append('%s' % self.xml_output) |
|---|
| 105 | |
|---|
| 106 | # Saving the Normal output to a temporary file |
|---|
| 107 | splited.append('-oN') |
|---|
| 108 | splited.append('%s' % self.normal_output) |
|---|
| 109 | |
|---|
| 110 | # Disable runtime interaction feature |
|---|
| 111 | #splited.append("--noninteractive") |
|---|
| 112 | |
|---|
| 113 | |
|---|
| 114 | # Redirecting output |
|---|
| 115 | #splited.append('>') |
|---|
| 116 | #splited.append('%s' % self.stdout_output) |
|---|
| 117 | |
|---|
| 118 | return splited |
|---|
| 119 | |
|---|
| 120 | def _remove_double_space(self, command): |
|---|
| 121 | if type(command) == type([]): |
|---|
| 122 | command = " ".join(command) |
|---|
| 123 | |
|---|
| 124 | ## Found a better solution for this problem |
|---|
| 125 | #while re.findall('( )', command): |
|---|
| 126 | # command = command.replace(' ', ' ') |
|---|
| 127 | |
|---|
| 128 | |
|---|
| 129 | # The first join + split ensures to remove double spaces on lists like this: |
|---|
| 130 | # ["nmap ", "-T4", ...] |
|---|
| 131 | # And them, we must return a list of the command, that's why we have the second split |
|---|
| 132 | return " ".join(command.split()).split() |
|---|
| 133 | |
|---|
| 134 | def close(self): |
|---|
| 135 | # Remove temporary files created |
|---|
| 136 | self._stdout_handler.close() |
|---|
| 137 | self._stderr_handler.close() |
|---|
| 138 | |
|---|
| 139 | os.remove(self.xml_output) |
|---|
| 140 | os.remove(self.normal_output) |
|---|
| 141 | os.remove(self.stdout_output) |
|---|
| 142 | |
|---|
| 143 | def kill(self): |
|---|
| 144 | log.debug(">>> Killing scan process %s" % self.command_process.pid) |
|---|
| 145 | |
|---|
| 146 | if sys.platform != "win32": |
|---|
| 147 | try: |
|---|
| 148 | from signal import SIGKILL |
|---|
| 149 | os.kill(self.command_process.pid, SIGKILL) |
|---|
| 150 | except: |
|---|
| 151 | pass |
|---|
| 152 | else: |
|---|
| 153 | # Try to find a function at Windows that kills the process |
|---|
| 154 | pass |
|---|
| 155 | |
|---|
| 156 | def run_scan(self): |
|---|
| 157 | if self.command: |
|---|
| 158 | #self.command_process = Popen(self.command, bufsize=1, stdin=PIPE, |
|---|
| 159 | # stdout=PIPE, stderr=PIPE) |
|---|
| 160 | |
|---|
| 161 | # Because of problems with Windows, I passed only the file descriptors to \ |
|---|
| 162 | # Popen and set stdin to PIPE |
|---|
| 163 | # Python problems... Cross-platform execution of process should be improved |
|---|
| 164 | |
|---|
| 165 | self._stdout_handler = open(self.stdout_output, "w+") |
|---|
| 166 | self._stderr_handler = open(self.stderr_output, "w+") |
|---|
| 167 | |
|---|
| 168 | self.command_process = Popen(self.command, bufsize=1, |
|---|
| 169 | stdin=PIPE, |
|---|
| 170 | stdout=self._stdout_handler.fileno(), |
|---|
| 171 | stderr=self._stderr_handler.fileno(), |
|---|
| 172 | shell=shell_state) |
|---|
| 173 | else: |
|---|
| 174 | raise Exception("You have no command to run! Please, set the command \ |
|---|
| 175 | before trying to start scan!") |
|---|
| 176 | |
|---|
| 177 | def scan_state(self): |
|---|
| 178 | if self.command_process == None: |
|---|
| 179 | raise Exception("Scan is not running yet!") |
|---|
| 180 | |
|---|
| 181 | state = self.command_process.poll() |
|---|
| 182 | |
|---|
| 183 | ### Buffer is not been used anymore |
|---|
| 184 | ## This line blocks the GUI execution, once the read method waits until a |
|---|
| 185 | ## new content come to be buffered |
|---|
| 186 | #self.command_buffer += self.command_process.stdout.read() |
|---|
| 187 | ### |
|---|
| 188 | |
|---|
| 189 | if state == None: |
|---|
| 190 | return True # True means that the process is still running |
|---|
| 191 | elif state == 0: |
|---|
| 192 | return False # False means that the process had a successful exit |
|---|
| 193 | else: |
|---|
| 194 | self.command_stderr = self.get_error() |
|---|
| 195 | |
|---|
| 196 | log.critical("An error occourried during the scan execution!") |
|---|
| 197 | log.critical('%s' % self.command_stderr) |
|---|
| 198 | log.critical("Command that raised the exception: '%s'" % " ".join(self.command)) |
|---|
| 199 | |
|---|
| 200 | raise Exception("An error occourried during the scan execution!\n'%s'" % \ |
|---|
| 201 | self.command_stderr) |
|---|
| 202 | |
|---|
| 203 | def scan_progress(self): |
|---|
| 204 | """Should return a tuple with the stage and status of the scan execution progress. |
|---|
| 205 | Will work only when the runtime interaction problem is solved. |
|---|
| 206 | """ |
|---|
| 207 | pass |
|---|
| 208 | |
|---|
| 209 | def get_raw_output(self): |
|---|
| 210 | raw_desc = open(self.stdout_output, "r") |
|---|
| 211 | raw_output = raw_desc.readlines() |
|---|
| 212 | |
|---|
| 213 | raw_desc.close() |
|---|
| 214 | return "\\n".join(raw_output) |
|---|
| 215 | |
|---|
| 216 | def get_output(self): |
|---|
| 217 | output_desc = open(self.stdout_output, "r") |
|---|
| 218 | output = output_desc.read() |
|---|
| 219 | |
|---|
| 220 | output_desc.close() |
|---|
| 221 | return output |
|---|
| 222 | |
|---|
| 223 | def get_output_file(self): |
|---|
| 224 | return self.stdout_output |
|---|
| 225 | |
|---|
| 226 | def get_normal_output(self): |
|---|
| 227 | normal_desc = open(self.normal_output, "r") |
|---|
| 228 | normal = normal_desc.read() |
|---|
| 229 | |
|---|
| 230 | normal_desc.close() |
|---|
| 231 | return normal |
|---|
| 232 | |
|---|
| 233 | def get_xml_output(self): |
|---|
| 234 | xml_desc = open(self.xml_output, "r") |
|---|
| 235 | xml = xml_desc.read() |
|---|
| 236 | |
|---|
| 237 | xml_desc.close() |
|---|
| 238 | return xml |
|---|
| 239 | |
|---|
| 240 | def get_xml_output_file(self): |
|---|
| 241 | return self.xml_output |
|---|
| 242 | |
|---|
| 243 | def get_normal_output_file(self): |
|---|
| 244 | return self.normal_output |
|---|
| 245 | |
|---|
| 246 | def get_error(self): |
|---|
| 247 | error_desc = open(self.stderr_output, "r") |
|---|
| 248 | error = error_desc.read() |
|---|
| 249 | |
|---|
| 250 | error_desc.close() |
|---|
| 251 | return error |
|---|
| 252 | |
|---|
| 253 | command = property(get_command, set_command) |
|---|
| 254 | _command = None |
|---|
| 255 | |
|---|
| 256 | |
|---|
| 257 | class CommandConstructor: |
|---|
| 258 | def __init__(self, profile=None): |
|---|
| 259 | self.option_profile = NmapOptions(option_xml) |
|---|
| 260 | self.options = {} |
|---|
| 261 | |
|---|
| 262 | def add_option(self, option_name, args=[], level=False): |
|---|
| 263 | option = self.option_profile.get_option(option_name) |
|---|
| 264 | |
|---|
| 265 | if type(args) in StringTypes: |
|---|
| 266 | args = [args] |
|---|
| 267 | |
|---|
| 268 | if level: |
|---|
| 269 | self.options[option_name] = (option['option']+' ')*level |
|---|
| 270 | elif args: |
|---|
| 271 | args = tuple (args) |
|---|
| 272 | self.options[option_name] = option['option'] % args[0] |
|---|
| 273 | else: |
|---|
| 274 | self.options[option_name] = option['option'] |
|---|
| 275 | |
|---|
| 276 | def remove_option(self, option_name): |
|---|
| 277 | if option_name in self.options.keys(): |
|---|
| 278 | del(self.options[option_name]) |
|---|
| 279 | |
|---|
| 280 | def get_command(self, target): |
|---|
| 281 | splited = ['%s' % nmap_command_path] |
|---|
| 282 | for option in self.options.values(): |
|---|
| 283 | splited.append(option) |
|---|
| 284 | |
|---|
| 285 | splited.append(target) |
|---|
| 286 | return ' '.join(splited) |
|---|
| 287 | |
|---|
| 288 | def __remove_double_space(self, command): |
|---|
| 289 | while re.findall('( )', command): |
|---|
| 290 | command = command.replace(' ', ' ') |
|---|
| 291 | return command |
|---|
| 292 | |
|---|
| 293 | class CommandThread(threading.Thread): |
|---|
| 294 | def __init__(self, command): |
|---|
| 295 | self._stop_event = threading.Event() |
|---|
| 296 | self._sleep = 1.0 |
|---|
| 297 | threading.Thread.__init__(self) |
|---|
| 298 | self.command = command |
|---|
| 299 | |
|---|
| 300 | def run(self): |
|---|
| 301 | #self.command_result = os.popen3(self.command) |
|---|
| 302 | self.command_result = os.system(self.command) |
|---|
| 303 | |
|---|
| 304 | def join(self, timeout=None): |
|---|
| 305 | self._stop_event.set() |
|---|
| 306 | threading.Thread.join(self, timeout) |
|---|
| 307 | |
|---|
| 308 | |
|---|
| 309 | ############## |
|---|
| 310 | # Exceptions # |
|---|
| 311 | ############## |
|---|
| 312 | |
|---|
| 313 | class WrongCommandType(Exception): |
|---|
| 314 | def __init__(self, command): |
|---|
| 315 | self.command = command |
|---|
| 316 | |
|---|
| 317 | def __str__(self): |
|---|
| 318 | print "Command must be of type string! Got %s instead." % str(type(self.command)) |
|---|
| 319 | |
|---|
| 320 | class OptionDependency(Exception): |
|---|
| 321 | def __init__(self, option, dependency): |
|---|
| 322 | self.option = option |
|---|
| 323 | self.dependency = dependency |
|---|
| 324 | |
|---|
| 325 | def __str__(self): |
|---|
| 326 | return "The given option '%s' has a dependency not commited: %s" %\ |
|---|
| 327 | (self.option, self.dependency) |
|---|
| 328 | |
|---|
| 329 | class OptionConflict(Exception): |
|---|
| 330 | def __init__(self, option, option_conflict): |
|---|
| 331 | self.option = option |
|---|
| 332 | self.option_conflict = option_conflict |
|---|
| 333 | |
|---|
| 334 | def __str__(self): |
|---|
| 335 | return "The given option '%s' is conflicting with '%s'" %\ |
|---|
| 336 | (self.option, self.option_conflict) |
|---|
| 337 | |
|---|
| 338 | class NmapCommandError(Exception): |
|---|
| 339 | def __init__(self, command, error): |
|---|
| 340 | self.error = error |
|---|
| 341 | self.command = command |
|---|
| 342 | |
|---|
| 343 | def __str__(self): |
|---|
| 344 | return """An error occouried while trying to execute nmap command. |
|---|
| 345 | |
|---|
| 346 | ERROR: %s |
|---|
| 347 | COMMAND: %s |
|---|
| 348 | """ % (self.error, self.command) |
|---|
| 349 | |
|---|
| 350 | |
|---|
| 351 | |
|---|
| 352 | |
|---|
| 353 | # Testing module functionality! ;-) |
|---|
| 354 | if __name__ == '__main__': |
|---|
| 355 | #command = CommandConstructor ('option_profile.uop') |
|---|
| 356 | #print 'Aggressive options:', command.add_option ('Aggressive Options') |
|---|
| 357 | #print 'UDP Scan:', command.add_option ('Version Detection') |
|---|
| 358 | #print 'UDP Scan:', command.add_option ('UDP Scan') |
|---|
| 359 | #command.add_option ('Idle Scan', ['10.0.0.138']) |
|---|
| 360 | #command.add_option ('UDP Scan') |
|---|
| 361 | #command.add_option ('ACK scan') |
|---|
| 362 | #command.remove_option ('Idle Scannn') |
|---|
| 363 | |
|---|
| 364 | #print command.get_command ('localhost') |
|---|
| 365 | #print command.get_command ('localhost') |
|---|
| 366 | #print command.get_command ('localhost') |
|---|
| 367 | |
|---|
| 368 | #from time import sleep |
|---|
| 369 | |
|---|
| 370 | #nmap = NmapCommand (command) |
|---|
| 371 | #executando = nmap.execute_nmap_command () |
|---|
| 372 | #print nmap.command |
|---|
| 373 | #while executando[0].isAlive (): |
|---|
| 374 | # print open(executando[3]).read() |
|---|
| 375 | # sleep (1) |
|---|
| 376 | #print open(executando[3]).read() |
|---|
| 377 | |
|---|
| 378 | scan = NmapCommand('%s -T4 -iL "/home/adriano/umit/test/targets\ teste"' % nmap_command_path) |
|---|
| 379 | scan.run_scan() |
|---|
| 380 | |
|---|
| 381 | while scan.scan_state(): |
|---|
| 382 | print ">>>", scan.get_normal_output() |
|---|
| 383 | print "Scan is finished!" |
|---|