Merge pull request #1 from caggle/working-scanner
First iteration, working scanner
This commit is contained in:
Коммит
02e5ef818a
|
@ -0,0 +1,2 @@
|
|||
TENABLEIO_ACCESS_KEY=
|
||||
TENABLEIO_SECRET_KEY=
|
|
@ -108,3 +108,5 @@ venv.bak/
|
|||
# results files
|
||||
results/*
|
||||
|
||||
# License files
|
||||
LICENSE
|
|
@ -0,0 +1,74 @@
|
|||
FROM ruby
|
||||
MAINTAINER Cag
|
||||
|
||||
# This is to be able to talk to Tenable API
|
||||
ARG TENABLEIO_ACCESS_KEY
|
||||
ARG TENABLEIO_SECRET_KEY
|
||||
|
||||
ENV TENABLEIO_ACCESS_KEY "$TENABLEIO_ACCESS_KEY"
|
||||
ENV TENABLEIO_SECRET_KEY "$TENABLEIO_SECRET_KEY"
|
||||
|
||||
# Make a landing location for results
|
||||
RUN mkdir -p /app/results && \
|
||||
mkdir -p /app/vendor && \
|
||||
mkdir -p /app/vautomator
|
||||
|
||||
# Update deps and install make utils for compiling tools
|
||||
# and clean up in the end in the same layer
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends unzip \
|
||||
dos2unix build-essential make curl \
|
||||
nmap software-properties-common && \
|
||||
apt-get install -y python3-pip && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN cd /app/vendor && \
|
||||
wget -nv https://bootstrap.pypa.io/get-pip.py && \
|
||||
python2 get-pip.py
|
||||
|
||||
# Install and compile dirb
|
||||
COPY ./vendor/dirb222.tar.gz /app/vendor/dirb222.tar.gz
|
||||
RUN tar -xvf /app/vendor/dirb222.tar.gz -C /app/vendor/ && \
|
||||
chmod -R 777 /app/vendor/dirb222 && \
|
||||
chown -R root /app/vendor/dirb222 && \
|
||||
cd /app/vendor/dirb222/ && ./configure && \
|
||||
make
|
||||
COPY ./vendor/gobuster-master.zip /app/vendor/gobuster-master.zip
|
||||
RUN unzip /app/vendor/gobuster-master.zip -d /app/vendor/ && \
|
||||
chmod -R 777 /app/vendor/gobuster-master && \
|
||||
chown -R root /app/vendor/gobuster-master
|
||||
|
||||
# Install ssh_scan
|
||||
RUN gem install ssh_scan
|
||||
|
||||
# Install HTTP Observatory tool
|
||||
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash -
|
||||
RUN apt-get install -y nodejs && \
|
||||
npm install -g observatory-cli
|
||||
|
||||
# Install TLS Observatory tool
|
||||
# First build Go from master
|
||||
# TODO: Change hard-coded Go version
|
||||
RUN cd /tmp && \
|
||||
wget -nv https://dl.google.com/go/go1.11.2.linux-amd64.tar.gz && \
|
||||
tar -C /app/vendor/ -xzf /tmp/go1.11.2.linux-amd64.tar.gz
|
||||
|
||||
ENV GOPATH /app/vendor/go/bin
|
||||
ENV PATH $GOPATH:$PATH
|
||||
ENV PATH $GOPATH/bin:$PATH
|
||||
|
||||
RUN go get github.com/mozilla/tls-observatory/tlsobs
|
||||
RUN wget -nv http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip && \
|
||||
wget -nv http://s3.amazonaws.com/alexa-static/top-1m.csv.zip -O alexa-top-1m.csv.zip && \
|
||||
mkdir -p /etc/tls-observatory && \
|
||||
unzip top-1m.csv.zip && \
|
||||
mv top-1m.csv /etc/tls-observatory/cisco-top-1m.csv && \
|
||||
unzip alexa-top-1m.csv.zip && \
|
||||
mv top-1m.csv /etc/tls-observatory/alexa-top-1m.csv && \
|
||||
rm top-1m.csv.zip && rm alexa-top-1m.csv.zip && \
|
||||
dos2unix /etc/tls-observatory/cisco-top-1m.csv && dos2unix /etc/tls-observatory/alexa-top-1m.csv
|
||||
|
||||
COPY . /app/vautomator
|
||||
RUN pip3 install -r /app/vautomator/requirements.txt && \
|
||||
chmod +x /app/vautomator/run.py
|
||||
WORKDIR /app/vautomator
|
89
Readme.md
89
Readme.md
|
@ -0,0 +1,89 @@
|
|||
# vautomator-standalone
|
||||
Iterative automation of common VA tasks using OOP.
|
||||
|
||||
If you'd like to contribute, please reach out to [me](https://mozillians.org/en-US/u/Cag/) and I'd be happy to add you as a contributor.
|
||||
|
||||
## Install & Running
|
||||
|
||||
1. First, download the repo: `git clone https://github.com/caggle/vautomator-standalone.git && cd vautomator-standalone`
|
||||
2. Build the Docker image: `docker-compose build vautomator`
|
||||
3. Run it!: `docker run -v ${PWD}/results:/app/results -it vautomator:latest ./run.py <target>`
|
||||
4. You can review tool results in the ./results folder while vautomator does it's thing
|
||||
|
||||
Example run:
|
||||
```
|
||||
$ docker run -v ${PWD}/results:/app/results -it vautomator:latest ./run.py http://192.168.0.1
|
||||
[f2769b83b62b] 2019-01-21 06:23:51 AM UTC INFO [+] Running all the scans now. This may take a while...
|
||||
[f2769b83b62b] 2019-01-21 06:24:23 AM UTC WARNING [!] The target has recently been scanned by Tenable.io, retrieving results...
|
||||
[f2769b83b62b] 2019-01-21 06:24:30 AM UTC INFO [+] Running nmap port scans...
|
||||
[f2769b83b62b] 2019-01-21 06:26:54 AM UTC INFO [+] Nmap port scan(s) successfully ran.
|
||||
[f2769b83b62b] 2019-01-21 06:26:54 AM UTC INFO [+] Running ssh_scan...
|
||||
[f2769b83b62b] 2019-01-21 06:26:58 AM UTC INFO [+] SSH scan successfully ran.
|
||||
[f2769b83b62b] 2019-01-21 06:26:58 AM UTC INFO [+] Running TLS Observatory scan...
|
||||
[f2769b83b62b] 2019-01-21 06:27:19 AM UTC INFO [+] TLS Observatory scan successfully ran.
|
||||
[f2769b83b62b] 2019-01-21 06:27:19 AM UTC INFO [+] Running dirb scan...
|
||||
[f2769b83b62b] 2019-01-21 06:31:48 AM UTC INFO [+] Directory brute scan successfully ran.
|
||||
[f2769b83b62b] 2019-01-21 06:31:49 AM UTC INFO [+] All done. Tool output from the scan can be found at /app/results/192.168.0.1/
|
||||
|
||||
====== SCAN SUMMARY ======
|
||||
INFO [+] [\o/] nmap scan completed successfully!
|
||||
INFO [+] [\o/] dirbrute scan completed successfully!
|
||||
INFO [+] [\o/] sshscan scan completed successfully!
|
||||
INFO [+] [\o/] tlsobs scan completed successfully!
|
||||
INFO [+] [\o/] nessus scan completed successfully!
|
||||
WARNING [!] [ :| ] httpobs scan skipped as not applicable to the target.
|
||||
====== END OF SCAN =======
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
Using **Python 3**, it runs a bunch of tools against a URL/FQDN/IPv4 address on a Docker image of its own, and saves tool outputs for later analysis, as a part of a vulnerability assessment.
|
||||
|
||||
### What it actually does
|
||||
|
||||
* Determines if the the target is a URL, an IPv4 address or a hostname/FQDN
|
||||
* If URL *(note: it could be a URL with FQDN or IPv4 address)* it will run:
|
||||
* An nmap UDP scan for about 25 selected UDP services
|
||||
* An nmap TCP scan for top 1000 services
|
||||
* ssh_scan (if an SSH service is identified)
|
||||
* A Nessus (Tenable.io) "Basic Network Scan" (provided if you have valid Tenable.io API keys)
|
||||
* HTTP Observatory scan
|
||||
* TLS Observatory scan
|
||||
* Directory bruteforcing against a wordlist
|
||||
|
||||
* If IP address, it will only run:
|
||||
* An nmap UDP scan for about 25 selected UDP services
|
||||
* An nmap TCP scan for top 1000 services
|
||||
* ssh_scan (if an SSH service is identified)
|
||||
* A Nessus (Tenable.io) "Basic Network Scan" (provided if you have valid Tenable.io API keys)
|
||||
|
||||
In the current implementation these tasks are performed sequentially with the intent being "run and forget" for a couple of hours, while you are doing other important work.
|
||||
|
||||
#### Port scans
|
||||
|
||||
For TCP and UDP port scans, [python-nmap](https://pypi.org/project/python-nmap/) is used.
|
||||
|
||||
##### SSH scan
|
||||
|
||||
For SSH scan, [ssh_scan](https://github.com/mozilla/ssh_scan) is used.
|
||||
|
||||
#### Nessus scan
|
||||
|
||||
Nessus scans will fail unless you have a pair of valid Tenable.io API keys *with administrative permissions*. If you do, populate the .env file with them in the below form building the Docker image:
|
||||
|
||||
```
|
||||
TENABLEIO_ACCESS_KEY=<ACCESS_KEY>
|
||||
TENABLEIO_SECRET_KEY=<SECRET_KEY>
|
||||
```
|
||||
|
||||
Technically admin permissions is not required to initiate a Tenable.io scan with API. This is required in the code because the tool checks if the target had been scanned in the last 15 days before launching a scan (and that requires admin perms). If it had, then the results are retrieved.
|
||||
You are OK to not provide API keys if you wish, and the tool will simply not run a Tenable.io scan in that case.
|
||||
|
||||
#### Web App scans
|
||||
|
||||
If you are running the tool against a URL, a number of additional external tools will be utilised. These will be installed in the Docker container when you build it.
|
||||
* [HTTP Observatory](https://github.com/mozilla/http-observatory) is used as a Python module.
|
||||
* [TLS Observatory](https://github.com/mozilla/tls-observatory), by means of `tlsobs` client.
|
||||
* For directory brute-forcing:
|
||||
* By default, `dirb` will be used with the common wordlist.
|
||||
* `gobuster` will also be installed in the Docker container, however a command line switch to use it instead is not available yet (you would have to modify the code).
|
|
@ -0,0 +1,13 @@
|
|||
version: '3'
|
||||
services:
|
||||
vautomator:
|
||||
# This does not work for some reason
|
||||
# sysctls:
|
||||
# - net.ipv6.conf.all.disable_ipv6=0
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
args:
|
||||
- TENABLEIO_ACCESS_KEY=${TENABLEIO_ACCESS_KEY}
|
||||
- TENABLEIO_SECRET_KEY=${TENABLEIO_SECRET_KEY}
|
||||
image: vautomator
|
|
@ -0,0 +1,162 @@
|
|||
import socket
|
||||
import logging
|
||||
import coloredlogs
|
||||
import datetime
|
||||
from netaddr import valid_ipv4
|
||||
from urllib.parse import urlparse
|
||||
from lib import task
|
||||
|
||||
# Logging in UTC
|
||||
logger = logging.getLogger(__name__)
|
||||
coloredlogs.install(level='INFO', logger=logger, reconfigure=True,
|
||||
fmt='[%(hostname)s] %(asctime)s %(levelname)-8s %(message)s',
|
||||
datefmt="%Y-%m-%d %I:%M:%S %p %Z")
|
||||
|
||||
|
||||
class Target:
|
||||
|
||||
# Here, tasklist is a list of Task objects
|
||||
def __init__(self, target, results_dict):
|
||||
self.targetname = target
|
||||
self.targetdomain = ""
|
||||
self.type = ""
|
||||
self.tasklist = []
|
||||
self.resultsdict = results_dict
|
||||
|
||||
def isValid(self):
|
||||
# A target can be 4 things:
|
||||
# 1. FQDN
|
||||
# 2. IPv4 address
|
||||
# 3. URL with a hostname
|
||||
# 4. URL with IPv4 address
|
||||
|
||||
if not isinstance(self.targetname, str):
|
||||
return False
|
||||
|
||||
starts_with_anti_patterns = [
|
||||
'127.0.0',
|
||||
'10.',
|
||||
'172.',
|
||||
'192.168',
|
||||
'169.254.169.254'
|
||||
]
|
||||
|
||||
for pattern in starts_with_anti_patterns:
|
||||
if self.targetname.startswith(pattern):
|
||||
return False
|
||||
|
||||
if self.valid_ip():
|
||||
self.type = "IPv4"
|
||||
self.targetdomain = self.targetname
|
||||
return True
|
||||
elif self.valid_fqdn():
|
||||
self.type = "FQDN"
|
||||
self.targetdomain = self.targetname
|
||||
return True
|
||||
else:
|
||||
if "http" in self.targetname:
|
||||
orig_target = self.targetname
|
||||
self.targetname = urlparse(self.targetname).netloc
|
||||
if self.valid_ip():
|
||||
self.type = "IPv4|URL"
|
||||
self.targetdomain = self.targetname
|
||||
self.targetname = orig_target
|
||||
elif self.valid_fqdn():
|
||||
self.type = "FQDN|URL"
|
||||
self.targetdomain = self.targetname
|
||||
self.targetname = orig_target
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def getType(self):
|
||||
return self.type
|
||||
|
||||
def valid_ip(self):
|
||||
if valid_ipv4(self.targetname):
|
||||
self.type = "IPv4"
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def valid_fqdn(self):
|
||||
try:
|
||||
socket.gethostbyname(self.targetname)
|
||||
self.type = "FQDN"
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def addTask(self, new_task):
|
||||
# This is a hacky way pf running ssh_scan
|
||||
# right after nmap port scan
|
||||
if isinstance(new_task, task.SSHScanTask):
|
||||
self.tasklist.insert(2, new_task)
|
||||
else:
|
||||
self.tasklist.insert(len(self.tasklist), new_task)
|
||||
|
||||
def runTasks(self):
|
||||
|
||||
fresh_nessus = None
|
||||
|
||||
for one_task in self.tasklist:
|
||||
|
||||
if isinstance(one_task, task.NmapTask):
|
||||
nmap_results = one_task.runNmapScan()
|
||||
if nmap_results:
|
||||
logger.info("[+] Nmap port scan(s) successfully ran.")
|
||||
self.resultsdict.update({'nmap': True})
|
||||
|
||||
elif isinstance(one_task, task.NessusTask):
|
||||
nessus_results = one_task.runNessusScan()
|
||||
if (nessus_results):
|
||||
# TODO: Need to be more precise about this time check
|
||||
epoch_cdate = nessus_results.histories()[0].creation_date
|
||||
cdate = datetime.datetime.fromtimestamp(float(epoch_cdate))
|
||||
if(cdate.date() < datetime.date.today()):
|
||||
self.resultsdict.update({'nessus': "OLD"})
|
||||
else:
|
||||
logger.info("[+] Tenable.io scan kicked off.")
|
||||
self.resultsdict.update({'nessus': True})
|
||||
fresh_nessus = nessus_results
|
||||
|
||||
elif isinstance(one_task, task.MozillaTLSObservatoryTask):
|
||||
tlsobs_results = one_task.runTLSObsScan()
|
||||
if (tlsobs_results and tlsobs_results.returncode == 0):
|
||||
logger.info("[+] TLS Observatory scan successfully ran.")
|
||||
self.resultsdict.update({'tlsobs': True})
|
||||
|
||||
elif isinstance(one_task, task.MozillaHTTPObservatoryTask):
|
||||
httpobs_results = one_task.runHttpObsScan()
|
||||
# 0 is the returncode for successful execution
|
||||
if (httpobs_results and httpobs_results.returncode == 0):
|
||||
logger.info("[+] HTTP Observatory scan successfully ran.")
|
||||
self.resultsdict.update({'httpobs': True})
|
||||
|
||||
elif isinstance(one_task, task.SSHScanTask):
|
||||
sshscan_results = one_task.runSSHScan()
|
||||
if (sshscan_results and sshscan_results.returncode == 0):
|
||||
logger.info("[+] SSH scan successfully ran.")
|
||||
self.resultsdict.update({'sshscan': True})
|
||||
|
||||
elif isinstance(one_task, task.DirectoryBruteTask):
|
||||
dirbrute_results = one_task.runDirectoryBruteScan()
|
||||
if (dirbrute_results and dirbrute_results.returncode == 0):
|
||||
logger.info("[+] Directory brute scan successfully ran.")
|
||||
self.resultsdict.update({'dirbrute': True})
|
||||
|
||||
else:
|
||||
logger.error("[-] No or unidentified task specified!")
|
||||
return False
|
||||
|
||||
# Need to check if the current Nessus scan is complete
|
||||
if (self.resultsdict['nessus'] and self.resultsdict['nessus'] is not "OLD"):
|
||||
if (task.NessusTask(self.targetname).checkScanStatus(fresh_nessus) == "COMPLETE"):
|
||||
task.NessusTask(self.targetname).downloadReport(fresh_nessus)
|
||||
else:
|
||||
logger.warning("[!] Tenable scan for target is still underway, check the TIO console manually for results.")
|
||||
|
||||
return self.resultsdict
|
|
@ -0,0 +1,366 @@
|
|||
import os
|
||||
import logging
|
||||
import coloredlogs
|
||||
import json
|
||||
import nmap
|
||||
import subprocess
|
||||
from lib import target, utils
|
||||
from tenable_io.client import TenableIOClient
|
||||
from tenable_io.exceptions import TenableIOApiException
|
||||
from tenable_io.api.scans import ScanExportRequest
|
||||
from tenable_io.api.models import Scan
|
||||
from distutils.spawn import find_executable
|
||||
|
||||
# Logging in UTC
|
||||
logger = logging.getLogger(__name__)
|
||||
coloredlogs.install(level='INFO', logger=logger, reconfigure=True,
|
||||
fmt='[%(hostname)s] %(asctime)s %(levelname)-8s %(message)s',
|
||||
datefmt="%Y-%m-%d %I:%M:%S %p %Z")
|
||||
|
||||
|
||||
class Task:
|
||||
# One target will have at least one task
|
||||
# One task will have one target at a time
|
||||
# self.tasktarget here is a Target object
|
||||
def __init__(self, target_obj):
|
||||
self.tasktarget = target_obj
|
||||
|
||||
|
||||
class NmapTask(Task):
|
||||
|
||||
def __init__(self, target_obj, scan_type="full"):
|
||||
super().__init__(target_obj)
|
||||
self.portscan_type = scan_type
|
||||
|
||||
def checkForSSH(self, port_scan_results):
|
||||
# We need to check if SSH service is available within port scan results
|
||||
if (port_scan_results["".join(port_scan_results.all_hosts())].has_tcp(22)):
|
||||
# Port 22/tcp is open, perform ssh_scan scan
|
||||
self.tasktarget.addTask(SSHScanTask(self.tasktarget, 22))
|
||||
|
||||
else:
|
||||
# Need to find the actual SSH port, in case it is not 22
|
||||
# Magic happens here...
|
||||
# Ref: https://bitbucket.org/xael/python-nmap/src/2b493f71a26f63a01c155c073fbf0211a3219ff2/nmap/nmap.py?at=default&fileviewer=file-view-default#nmap.py-436:465
|
||||
for ssh_port in port_scan_results["".join(port_scan_results.all_hosts())].all_tcp():
|
||||
if 'script' in port_scan_results["".join(port_scan_results.all_hosts())]['tcp'][ssh_port].keys():
|
||||
if 'ssh' in "".join(port_scan_results["".join(port_scan_results.all_hosts())]['tcp'][ssh_port]['script'].values()).lower():
|
||||
# We have SSH service on a non-standard port, perform scan
|
||||
self.tasktarget.addTask(SSHScanTask(self.tasktarget, ssh_port))
|
||||
return
|
||||
|
||||
def runNmapScan(self):
|
||||
|
||||
# Note, python-nmap relies on nmap being installed
|
||||
# Need to ensure nmap is installed via Dockerfile
|
||||
# We are NOT using subprocess calls here
|
||||
|
||||
logger.info("[+] Running nmap port scans...")
|
||||
|
||||
nm = nmap.PortScanner()
|
||||
isSudo = False
|
||||
udp_ports = ("17,19,53,67,68,123,137,138,139,"
|
||||
"161,162,500,520,646,1900,3784,3785,5353,27015,"
|
||||
"27016,27017,27018,27019,27020,27960")
|
||||
if self.portscan_type == "tcp":
|
||||
nmap_arguments = '-v -Pn -sT -sV --script=banner --top-ports 1000 --open -T4 --system-dns'
|
||||
results = nm.scan(self.tasktarget.targetdomain, arguments=nmap_arguments, sudo=isSudo)
|
||||
|
||||
elif self.portscan_type == "udp":
|
||||
nmap_arguments = '-v -Pn -sU -sV --open -T4 --system-dns'
|
||||
isSudo = True
|
||||
results = nm.scan(self.tasktarget.targetdomain, ports=udp_ports, arguments=nmap_arguments, sudo=isSudo)
|
||||
|
||||
else:
|
||||
# Need to run both UDP and TCP scans
|
||||
# This looks rather ugly however it's a current way to
|
||||
# specify different ports for TCP and UDP scans in a
|
||||
# single nmap command: https://seclists.org/nmap-dev/2011/q2/365
|
||||
# TODO: Perhaps read this from an environment variable or config
|
||||
tcp_top1000_ports = ("1,3-4,6-7,9,13,17,19-26,30,32-33,"
|
||||
"37,42-43,49,53,70,79-85,88-90,99-100,106,109-111,"
|
||||
"113,119,125,135,139,143-144,146,161,163,179,199,"
|
||||
"211-212,222,254-256,259,264,280,301,306,311,340,"
|
||||
"366,389,406-407,416-417,425,427,443-445,458,464-465,"
|
||||
"481,497,500,512-515,524,541,543-545,548,554-555,563,"
|
||||
"587,593,616-617,625,631,636,646,648,666-668,683,687,"
|
||||
"691,700,705,711,714,720,722,726,749,765,777,783,787,"
|
||||
"800-801,808,843,873,880,888,898,900-903,911-912,981,"
|
||||
"987,990,992-993,995,999-1002,1007,1009-1011,1021-1100,"
|
||||
"1102,1104-1108,1110-1114,1117,1119,1121-1124,1126,"
|
||||
"1130-1132,1137-1138,1141,1145,1147-1149,1151-1152,1154,"
|
||||
"1163-1166,1169,1174-1175,1183,1185-1187,1192,1198-1199,"
|
||||
"1201,1213,1216-1218,1233-1234,1236,1244,1247-1248,1259,"
|
||||
"1271-1272,1277,1287,1296,1300-1301,1309-1311,1322,1328,"
|
||||
"1334,1352,1417,1433-1434,1443,1455,1461,1494,1500-1501,"
|
||||
"1503,1521,1524,1533,1556,1580,1583,1594,1600,1641,1658,"
|
||||
"1666,1687-1688,1700,1717-1721,1723,1755,1761,1782-1783,"
|
||||
"1801,1805,1812,1839-1840,1862-1864,1875,1900,1914,1935,"
|
||||
"1947,1971-1972,1974,1984,1998-2010,2013,2020-2022,2030,"
|
||||
"2033-2035,2038,2040-2043,2045-2049,2065,2068,2099-2100,"
|
||||
"2103,2105-2107,2111,2119,2121,2126,2135,2144,2160-2161,"
|
||||
"2170,2179,2190-2191,2196,2200,2222,2251,2260,2288,2301,"
|
||||
"2323,2366,2381-2383,2393-2394,2399,2401,2492,2500,2522,"
|
||||
"2525,2557,2601-2602,2604-2605,2607-2608,2638,2701-2702,"
|
||||
"2710,2717-2718,2725,2800,2809,2811,2869,2875,2909-2910,"
|
||||
"2920,2967-2968,2998,3000-3001,3003,3005-3007,3011,3013,"
|
||||
"3017,3030-3031,3052,3071,3077,3128,3168,3211,3221,3260-3261,"
|
||||
"3268-3269,3283,3300-3301,3306,3322-3325,3333,3351,3367,"
|
||||
"3369-3372,3389-3390,3404,3476,3493,3517,3527,3546,3551,"
|
||||
"3580,3659,3689-3690,3703,3737,3766,3784,3800-3801,3809,"
|
||||
"3814,3826-3828,3851,3869,3871,3878,3880,3889,3905,3914,"
|
||||
"3918,3920,3945,3971,3986,3995,3998,4000-4006,4045,4111,"
|
||||
"4125-4126,4129,4224,4242,4279,4321,4343,4443-4446,4449,"
|
||||
"4550,4567,4662,4848,4899-4900,4998,5000-5004,5009,5030,"
|
||||
"5033,5050-5051,5054,5060-5061,5080,5087,5100-5102,5120,"
|
||||
"5190,5200,5214,5221-5222,5225-5226,5269,5280,5298,5357,"
|
||||
"5405,5414,5431-5432,5440,5500,5510,5544,5550,5555,5560,"
|
||||
"5566,5631,5633,5666,5678-5679,5718,5730,5800-5802,5810-5811,"
|
||||
"5815,5822,5825,5850,5859,5862,5877,5900-5904,5906-5907,"
|
||||
"5910-5911,5915,5922,5925,5950,5952,5959-5963,5987-5989,"
|
||||
"5998-6007,6009,6025,6059,6100-6101,6106,6112,6123,6129,"
|
||||
"6156,6346,6389,6502,6510,6543,6547,6565-6567,6580,6646,"
|
||||
"6666-6669,6689,6692,6699,6779,6788-6789,6792,6839,6881,"
|
||||
"6901,6969,7000-7002,7004,7007,7019,7025,7070,7100,7103,"
|
||||
"7106,7200-7201,7402,7435,7443,7496,7512,7625,7627,7676,"
|
||||
"7741,7777-7778,7800,7911,7920-7921,7937-7938,7999-8002,"
|
||||
"8007-8011,8021-8022,8031,8042,8045,8080-8090,8093,8099-8100,"
|
||||
"8180-8181,8192-8194,8200,8222,8254,8290-8292,8300,8333,"
|
||||
"8383,8400,8402,8443,8500,8600,8649,8651-8652,8654,8701,"
|
||||
"8800,8873,8888,8899,8994,9000-9003,9009-9011,9040,9050,"
|
||||
"9071,9080-9081,9090-9091,9099-9103,9110-9111,9200,9207,"
|
||||
"9220,9290,9415,9418,9485,9500,9502-9503,9535,9575,9593-9595,"
|
||||
"9618,9666,9876-9878,9898,9900,9917,9929,9943-9944,9968,"
|
||||
"9998-10004,10009-10010,10012,10024-10025,10082,10180,10215,"
|
||||
"10243,10566,10616-10617,10621,10626,10628-10629,10778,"
|
||||
"11110-11111,11967,12000,12174,12265,12345,13456,13722,"
|
||||
"13782-13783,14000,14238,14441-14442,15000,15002-15004,"
|
||||
"15660,15742,16000-16001,16012,16016,16018,16080,16113,"
|
||||
"16992-16993,17877,17988,18040,18101,18988,19101,19283,"
|
||||
"19315,19350,19780,19801,19842,20000,20005,20031,20221-20222,"
|
||||
"20828,21571,22939,23502,24444,24800,25734-25735,26214,"
|
||||
"27000,27352-27353,27355-27356,27715,28201,30000,30718,"
|
||||
"30951,31038,31337,32768-32785,33354,33899,34571-34573,"
|
||||
"35500,38292,40193,40911,41511,42510,44176,44442-44443,"
|
||||
"44501,45100,48080,49152-49161,49163,49165,49167,49175-49176,"
|
||||
"49400,49999-50003,50006,50300,50389,50500,50636,50800,"
|
||||
"51103,51493,52673,52822,52848,52869,54045,54328,55055-55056,"
|
||||
"55555,55600,56737-56738,57294,57797,58080,60020,60443,"
|
||||
"61532,61900,62078,63331,64623,64680,65000,65129,65389")
|
||||
nmap_arguments = '-v -Pn -sTU -sV --script=banner -p T:' + tcp_top1000_ports + ',U:' + udp_ports + ' --open -T4 --system-dns'
|
||||
isSudo = True
|
||||
results = nm.scan(self.tasktarget.targetdomain, arguments=nmap_arguments, sudo=False)
|
||||
|
||||
self.checkForSSH(nm)
|
||||
|
||||
if results:
|
||||
try:
|
||||
nmap_output = open("/app/results/" + self.tasktarget.targetdomain + "/"
|
||||
+ "nmap_tcp.json", "w+")
|
||||
nmap_output.write(json.dumps(results))
|
||||
return True
|
||||
except Exception:
|
||||
logger.error("[-] Could not open file for nmap output!")
|
||||
return False
|
||||
|
||||
|
||||
class SSHScanTask(Task):
|
||||
|
||||
def __init__(self, target_obj, sshport=22):
|
||||
super().__init__(target_obj)
|
||||
self.ssh_port = sshport
|
||||
|
||||
def runSSHScan(self):
|
||||
if find_executable('ssh_scan'):
|
||||
# Found in path, run the command
|
||||
logger.info("[+] Running ssh_scan...")
|
||||
cmd = "ssh_scan -t " + self.tasktarget.targetdomain + " -p " \
|
||||
+ str(self.ssh_port) + " -o /app/results/" + self.tasktarget.targetdomain \
|
||||
+ "/ssh_scan.txt"
|
||||
sshscan_cmd = utils.sanitise_shell_command(cmd)
|
||||
p = subprocess.Popen(sshscan_cmd, stdout=subprocess.PIPE, shell=True)
|
||||
p.wait()
|
||||
return p
|
||||
else:
|
||||
logger.error("[-] ssh_scan not found in Docker image!")
|
||||
return False
|
||||
|
||||
|
||||
class NessusTask(Task):
|
||||
|
||||
def __init__(self, target_obj):
|
||||
super().__init__(target_obj)
|
||||
# According to documentation TenableIO client can be initialised
|
||||
# in a number of ways. I choose here the environment variable option.
|
||||
self.tio_access_key = os.getenv('TENABLEIO_ACCESS_KEY')
|
||||
self.tio_secret_key = os.getenv('TENABLEIO_SECRET_KEY')
|
||||
|
||||
def runNessusScan(self):
|
||||
|
||||
# First, check to see if we are provided with API keys
|
||||
if (self.tio_access_key == "" or self.tio_secret_key == ""):
|
||||
logger.warning("[!] Tenable.io API key(s) not provided, skipping "
|
||||
"Tenable.io scan. Perform the scan manually.")
|
||||
return False
|
||||
else:
|
||||
self.client = TenableIOClient(access_key=self.tio_access_key, secret_key=self.tio_secret_key)
|
||||
|
||||
# Reference: https://github.com/tenable/Tenable.io-SDK-for-Python/blob/master/examples/scans.py
|
||||
# Note no subprocess call is required here
|
||||
try:
|
||||
# Run a basic network scan on the target
|
||||
# Need to check if a recent scan was fired recently
|
||||
scan_name = "VA for " + self.tasktarget.targetdomain
|
||||
# We will check with both host IP and FQDN
|
||||
activities = self.client.scan_helper.activities(targets=self.tasktarget.targetdomain, date_range=15)
|
||||
if (len(activities) > 0):
|
||||
logger.warning("[!] The target has recently been scanned by Tenable.io, retrieving results...")
|
||||
old_nscans = self.client.scan_helper.scans(name=scan_name)
|
||||
for old in old_nscans:
|
||||
if old.status() == Scan.STATUS_COMPLETED:
|
||||
self.downloadReport(old)
|
||||
break
|
||||
return old
|
||||
else:
|
||||
# This target was not scanned before, scan it
|
||||
# We don't want this blocking, so don't wait
|
||||
new_nscan = self.client.scan_helper.create(name=scan_name, text_targets=self.tasktarget.targetdomain, template='basic')
|
||||
new_nscan.launch(wait=False)
|
||||
return new_nscan
|
||||
|
||||
except TenableIOApiException as TIOException:
|
||||
logger.error("[-] Tenable.io scan failed: ".format(TIOException))
|
||||
return False
|
||||
|
||||
def downloadReport(self, nscan, reportformat="html", style="assets"):
|
||||
report_path = "/app/results/" + self.tasktarget.targetdomain + "/Scan_for_" + self.tasktarget.targetdomain
|
||||
|
||||
if reportformat == "html":
|
||||
fmt = ScanExportRequest.FORMAT_HTML
|
||||
elif reportformat == "pdf":
|
||||
fmt = ScanExportRequest.FORMAT_PDF
|
||||
elif reportformat == "csv":
|
||||
fmt = ScanExportRequest.FORMAT_CSV
|
||||
elif reportformat == "nessus":
|
||||
fmt = ScanExportRequest.FORMAT_NESSUS
|
||||
elif reportformat == "db":
|
||||
fmt = ScanExportRequest.FORMAT_DB
|
||||
else:
|
||||
return False
|
||||
|
||||
if style == "assets":
|
||||
reportoutline = ScanExportRequest.CHAPTER_CUSTOM_VULN_BY_HOST
|
||||
elif style == "exec":
|
||||
reportoutline = ScanExportRequest.CHAPTER_EXECUTIVE_SUMMARY
|
||||
elif style == "plugins":
|
||||
reportoutline = ScanExportRequest.CHAPTER_CUSTOM_VULN_BY_PLUGIN
|
||||
else:
|
||||
return False
|
||||
|
||||
nscan.download(report_path, format=fmt, chapter=reportoutline)
|
||||
|
||||
def checkScanStatus(self, nscan):
|
||||
# Query Tenable API to check if the scan is finished
|
||||
status = nscan.status(nscan.id)
|
||||
|
||||
if status == nscan.STATUS_COMPLETED:
|
||||
return "COMPLETE"
|
||||
elif status == nscan.STATUS_ABORTED:
|
||||
return "ABORTED"
|
||||
elif status == nscan.STATUS_INITIALIZING:
|
||||
return "INITIALIZING"
|
||||
elif status == nscan.STATUS_PENDING:
|
||||
return "PENDING"
|
||||
elif status == nscan.STATUS_RUNNING:
|
||||
return "RUNNING"
|
||||
else:
|
||||
logger.error("[-] Something is wrong with Tenable.io scan. Check the TIO console manually.")
|
||||
return False
|
||||
|
||||
|
||||
class MozillaHTTPObservatoryTask(Task):
|
||||
|
||||
def __init__(self, target_obj):
|
||||
super().__init__(target_obj)
|
||||
|
||||
def runHttpObsScan(self):
|
||||
if "IPv4" in self.tasktarget.type:
|
||||
# HTTP Obs only accepts FQDN
|
||||
return False
|
||||
|
||||
if find_executable('observatory'):
|
||||
# Found in path, run the command
|
||||
logger.info("[+] Running HTTP Observatory scan...")
|
||||
cmd = "observatory --format json -z --rescan " \
|
||||
+ self.tasktarget.targetdomain + " > /app/results/" \
|
||||
+ self.tasktarget.targetdomain + "/httpobs_scan.json"
|
||||
observatory_cmd = utils.sanitise_shell_command(cmd)
|
||||
p = subprocess.Popen(observatory_cmd, stdout=subprocess.PIPE, shell=True)
|
||||
p.wait()
|
||||
return p
|
||||
|
||||
else:
|
||||
logger.error("[-] HTTP Observatory not found in Docker image!")
|
||||
return False
|
||||
|
||||
|
||||
class MozillaTLSObservatoryTask(Task):
|
||||
|
||||
def __init__(self, target_obj):
|
||||
super().__init__(target_obj)
|
||||
|
||||
def runTLSObsScan(self):
|
||||
if find_executable('tlsobs'):
|
||||
# Found in path, run the command
|
||||
logger.info("[+] Running TLS Observatory scan...")
|
||||
cmd = "tlsobs -r -raw " + self.tasktarget.targetname \
|
||||
+ " > /app/results/" + self.tasktarget.targetdomain \
|
||||
+ "/tlsobs_scan.txt"
|
||||
tlsobs_cmd = utils.sanitise_shell_command(cmd)
|
||||
p = subprocess.Popen(tlsobs_cmd, stdout=subprocess.PIPE, shell=True)
|
||||
p.wait()
|
||||
return p
|
||||
else:
|
||||
logger.error("[-] TLS Observatory not found in Docker image!")
|
||||
return False
|
||||
|
||||
|
||||
class DirectoryBruteTask(Task):
|
||||
|
||||
def __init__(self, target_obj, tool="dirb"):
|
||||
super().__init__(target_obj)
|
||||
self.toolToRun = tool
|
||||
|
||||
def runDirectoryBruteScan(self):
|
||||
if (self.toolToRun == "dirb"):
|
||||
# dirb is compiled from source, won't be in the PATH
|
||||
# Also defaulting to HTTPS URL here
|
||||
logger.info("[+] Running dirb scan...")
|
||||
if "URL" in self.tasktarget.getType():
|
||||
cmd = "/app/vendor/dirb222/dirb " + self.tasktarget.targetname \
|
||||
+ "/ /app/vendor/dirb222/wordlists/small.txt -o /app/results/" \
|
||||
+ self.tasktarget.targetdomain + "/https_dirb_common.txt -f -w -S -r"
|
||||
else:
|
||||
cmd = "/app/vendor/dirb222/dirb https://" + self.tasktarget.targetdomain \
|
||||
+ "/ /app/vendor/dirb222/wordlists/small.txt -o /app/results/" \
|
||||
+ self.tasktarget.targetdomain + "/https_dirb_common.txt -f -w -S -r"
|
||||
|
||||
dirbscan_cmd = utils.sanitise_shell_command(cmd)
|
||||
p = subprocess.Popen(dirbscan_cmd, stdout=subprocess.PIPE, shell=True)
|
||||
p.wait()
|
||||
return p
|
||||
elif (self.toolToRun == "gobuster"):
|
||||
logger.info("[+] Running gobuster scan...")
|
||||
if "URL" in self.tasktarget.getType():
|
||||
cmd = "go run /app/vendor/gobuster-master/main.go " + self.tasktarget.targetname \
|
||||
+ " -w /app/vendor/dirb222/wordlists/common.txt -v -l -o /app/results/" \
|
||||
+ self.tasktarget.targetdomain + "/gobuster_common.txt"
|
||||
else:
|
||||
cmd = "go run /app/vendor/gobuster-master/main.go https://" + self.tasktarget.targetdomain \
|
||||
+ " -w /app/vendor/dirb222/wordlists/common.txt -v -l -o /app/results/" \
|
||||
+ self.tasktarget.targetdomain + "/gobuster_common.txt"
|
||||
|
||||
gobuster_cmd = utils.sanitise_shell_command(cmd)
|
||||
p = subprocess.Popen(gobuster_cmd, stdout=subprocess.PIPE, shell=True)
|
||||
p.wait()
|
||||
return p
|
|
@ -0,0 +1,23 @@
|
|||
import shlex
|
||||
import subprocess
|
||||
|
||||
# Minimise likelihood of OS command injection
|
||||
# into subprocess.popen calls
|
||||
# TODO: Get rid of this later for the sake of
|
||||
# better subprocess calls (i.e. shell=False)
|
||||
|
||||
|
||||
def sanitise_shell_command(command):
|
||||
return shlex.split(shlex.quote(command))
|
||||
|
||||
|
||||
def package_results(output_dir):
|
||||
# Do reporting (take all the output from
|
||||
# the prior runs, zip it up
|
||||
tarfile = output_dir.split('/')
|
||||
cmd = "tar --warning=no-all -zcf " + output_dir + tarfile[3] + ".tar.gz -C " \
|
||||
+ output_dir + " . --exclude=" + output_dir + tarfile[3] + ".tar.gz"
|
||||
tar_cmd = sanitise_shell_command(cmd)
|
||||
p = subprocess.Popen(tar_cmd, shell=True)
|
||||
p.wait()
|
||||
return p
|
|
@ -0,0 +1,4 @@
|
|||
tenable_io==1.3.0
|
||||
python_nmap==0.6.1
|
||||
netaddr==0.7.19
|
||||
coloredlogs==10.0
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import coloredlogs
|
||||
from urllib.parse import urlparse
|
||||
from lib import target, task, utils
|
||||
|
||||
# Logging in UTC
|
||||
logger = logging.getLogger(__name__)
|
||||
coloredlogs.install(level='INFO', logger=logger, reconfigure=True,
|
||||
fmt='[%(hostname)s] %(asctime)s %(levelname)-8s %(message)s',
|
||||
datefmt="%Y-%m-%d %I:%M:%S %p %Z")
|
||||
|
||||
|
||||
def setupVA(va_target):
|
||||
|
||||
# Regardless of the type of target, we will run:
|
||||
# 1. Nessus scan
|
||||
# 2. Nmap scan
|
||||
# Also kicking of Nessus scan as the first task as it takes time
|
||||
va_target.addTask(task.NessusTask(va_target))
|
||||
va_target.addTask(task.NmapTask(va_target))
|
||||
|
||||
if "URL" in va_target.getType():
|
||||
# We have a URL, means HTTP Obs, TLS Obs,
|
||||
# and directory brute scans are a go
|
||||
if va_target.getType() == "FQDN|URL":
|
||||
# We can run all tools/tasks
|
||||
va_target.addTask(task.MozillaHTTPObservatoryTask(va_target))
|
||||
va_target.addTask(task.MozillaTLSObservatoryTask(va_target))
|
||||
va_target.addTask(task.DirectoryBruteTask(va_target, tool="dirb"))
|
||||
else:
|
||||
va_target.addTask(task.MozillaTLSObservatoryTask(va_target))
|
||||
va_target.addTask(task.DirectoryBruteTask(va_target, tool="dirb"))
|
||||
# HTTP Observatory does not like IPs as a target, skipping
|
||||
va_target.resultsdict.update({'httpobs': "PASS"})
|
||||
elif va_target.getType() == "IPv4":
|
||||
va_target.addTask(task.MozillaTLSObservatoryTask(va_target))
|
||||
va_target.addTask(task.DirectoryBruteTask(va_target, tool="dirb"))
|
||||
# Again, HTTP Observatory does not like IPs as a target, skipping
|
||||
va_target.resultsdict.update({'httpobs': "PASS"})
|
||||
else:
|
||||
# FQDN, we can run all tools/tasks
|
||||
va_target.addTask(task.MozillaHTTPObservatoryTask(va_target))
|
||||
va_target.addTask(task.MozillaTLSObservatoryTask(va_target))
|
||||
va_target.addTask(task.DirectoryBruteTask(va_target, tool="dirb"))
|
||||
|
||||
return va_target
|
||||
|
||||
|
||||
def showScanSummary(result_dictionary):
|
||||
|
||||
coloredlogs.install(level='INFO', logger=logger, reconfigure=True,
|
||||
fmt='%(levelname)-10s %(message)s')
|
||||
|
||||
print("\n====== SCAN SUMMARY ======")
|
||||
for one_task, status in result_dictionary.items():
|
||||
if status:
|
||||
if status == "PASS":
|
||||
logger.warning("[!] [ :| ] " + one_task + " scan skipped as not applicable to the target.")
|
||||
else:
|
||||
logger.info("[+] [\o/] " + one_task + " scan completed successfully!")
|
||||
else:
|
||||
logger.error("[-] [ :( ] " + one_task + " scan failed to run. Please investigate or run manually.")
|
||||
|
||||
print("====== END OF SCAN =======\n")
|
||||
|
||||
|
||||
def runVA(scan_with_tasks, outpath):
|
||||
logger.info("[+] Running all the scans now. This may take a while...")
|
||||
results = scan_with_tasks.runTasks()
|
||||
# results here is a dict
|
||||
time.sleep(1)
|
||||
# Return code check is a bit hacky,
|
||||
# basically we are ignoring warnings from tar
|
||||
if utils.package_results(outpath).returncode is not 127:
|
||||
logger.info("[+] All done. Tool output from the scan can be found at " + outpath)
|
||||
else:
|
||||
logger.warning("[!] There was a problem compressing tool output. Check " + outpath + " manually.")
|
||||
time.sleep(1)
|
||||
showScanSummary(results)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
results = {'nmap': False, 'nessus': False, 'tlsobs': False, 'httpobs': False, 'sshscan': False, 'dirbrute': False}
|
||||
destination = sys.argv[1]
|
||||
output_path = "/app/results/" + destination + "/"
|
||||
va_target = target.Target(destination, results)
|
||||
|
||||
if va_target.isValid():
|
||||
# We have a valid target, what is it?
|
||||
if "URL" in va_target.getType():
|
||||
domain = urlparse(va_target.targetname).netloc
|
||||
output_path = "/app/results/" + domain + "/"
|
||||
else:
|
||||
logger.error("[-] Invalid target, please use an FQDN or a URL.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Create a location to store our outputs
|
||||
try:
|
||||
os.stat(output_path)
|
||||
except Exception:
|
||||
os.mkdir(output_path)
|
||||
|
||||
va_scan = setupVA(va_target)
|
||||
runVA(va_scan, output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Двоичный файл не отображается.
Двоичный файл не отображается.
Загрузка…
Ссылка в новой задаче