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