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